@evanp/activitypub-bot 0.45.20 → 0.46.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +19 -0
- package/README.md +6 -0
- package/lib/activitypubclient.js +9 -53
- package/lib/actorstorage.js +20 -0
- package/lib/app.js +10 -8
- package/lib/botcontext.js +23 -5
- package/lib/microsyntax.js +23 -3
- package/lib/migrations/012-actor-outbox-created-at-index.js +9 -0
- package/lib/routes/nodeinfo.js +6 -4
- package/lib/safefetcher.js +112 -0
- package/package.json +1 -1
- package/lib/safeagent.js +0 -52
package/CHANGELOG.md
CHANGED
|
@@ -9,6 +9,25 @@ and this project adheres to
|
|
|
9
9
|
|
|
10
10
|
## [Unreleased]
|
|
11
11
|
|
|
12
|
+
## [0.46.0] - 2026-05-25
|
|
13
|
+
|
|
14
|
+
### Added
|
|
15
|
+
|
|
16
|
+
- Support for nodeinfo 2.1 and 2.2.
|
|
17
|
+
- ActorStorage method for checking active users.
|
|
18
|
+
- nodeinfo properties for monthly active and half-yearly active users.
|
|
19
|
+
- SafeFetcher service class for all outbound HTTP requests
|
|
20
|
+
|
|
21
|
+
### Changed
|
|
22
|
+
|
|
23
|
+
- Name of package in nodeinfo from `activitypub-bot` to `activitypub-dot-bot` to
|
|
24
|
+
avoid a naming conflict with another package.
|
|
25
|
+
- Use URLFormatter in Transformer and BotContext to short-circuit Webfinger self-request
|
|
26
|
+
- Use SafeFetcher in ActivityPubClient
|
|
27
|
+
- Use SafeFetcher in Transformer
|
|
28
|
+
- Use SafeFetcher in BotContext
|
|
29
|
+
- Encode HTML entities in Transformer.transform()
|
|
30
|
+
|
|
12
31
|
## [0.45.20] - 2026-05-14
|
|
13
32
|
|
|
14
33
|
### Fixed
|
package/README.md
CHANGED
|
@@ -248,6 +248,12 @@ Alternatively, you can implement a whole class of bots with the [BotFactory](#bo
|
|
|
248
248
|
|
|
249
249
|
Bots will receive a [BotContext](#botcontext) object at initialization. The BotContext is the main way to access data or execute activities.
|
|
250
250
|
|
|
251
|
+
It's important to remember that the bot may run on many different physical
|
|
252
|
+
machines in a multi-server deployment. Don't keep volatile state for the bot in
|
|
253
|
+
memory; it will get out of sync between servers, and will be lost between server
|
|
254
|
+
restarts. Instead, use the `getData()`/`setData()` (see below) interfaces to
|
|
255
|
+
save named data items into the database.
|
|
256
|
+
|
|
251
257
|
### Bot
|
|
252
258
|
|
|
253
259
|
The Bot interface has the following methods.
|
package/lib/activitypubclient.js
CHANGED
|
@@ -2,10 +2,6 @@ import assert from 'node:assert'
|
|
|
2
2
|
import fs from 'node:fs'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { fileURLToPath } from 'node:url'
|
|
5
|
-
import dns from 'node:dns/promises'
|
|
6
|
-
|
|
7
|
-
import fetch from 'node-fetch'
|
|
8
|
-
import ipaddr from 'ipaddr.js'
|
|
9
5
|
|
|
10
6
|
import as2 from './activitystreams.js'
|
|
11
7
|
import { SignaturePolicyStorage } from './signaturepolicystorage.js'
|
|
@@ -52,14 +48,6 @@ export class ActivityPubClientError extends Error {
|
|
|
52
48
|
}
|
|
53
49
|
}
|
|
54
50
|
|
|
55
|
-
export class ProtocolError extends Error {
|
|
56
|
-
constructor (url) {
|
|
57
|
-
super(`URL ${url} uses a disallowed protocol`)
|
|
58
|
-
this.name = 'ProtocolError'
|
|
59
|
-
this.url = url
|
|
60
|
-
}
|
|
61
|
-
}
|
|
62
|
-
|
|
63
51
|
export class ActivityPubClient {
|
|
64
52
|
static #githubUrl = 'https://github.com/evanp/activitypub-bot'
|
|
65
53
|
static #userAgent = `activitypub.bot/${version} (${ActivityPubClient.#githubUrl})`
|
|
@@ -77,9 +65,9 @@ export class ActivityPubClient {
|
|
|
77
65
|
#cache
|
|
78
66
|
#messageSigner
|
|
79
67
|
#policyStorage
|
|
80
|
-
#
|
|
68
|
+
#fetcher
|
|
81
69
|
|
|
82
|
-
constructor (keyStorage, urlFormatter, signer, digester, logger, throttler, cache, messageSigner, policyStorage,
|
|
70
|
+
constructor (keyStorage, urlFormatter, signer, digester, logger, throttler, cache, messageSigner, policyStorage, fetcher) {
|
|
83
71
|
assert.strictEqual(typeof keyStorage, 'object')
|
|
84
72
|
assert.strictEqual(typeof urlFormatter, 'object')
|
|
85
73
|
assert.strictEqual(typeof signer, 'object')
|
|
@@ -89,6 +77,7 @@ export class ActivityPubClient {
|
|
|
89
77
|
assert.strictEqual(typeof cache, 'object')
|
|
90
78
|
assert.strictEqual(typeof messageSigner, 'object')
|
|
91
79
|
assert.strictEqual(typeof policyStorage, 'object')
|
|
80
|
+
assert.strictEqual(typeof fetcher, 'object')
|
|
92
81
|
this.#keyStorage = keyStorage
|
|
93
82
|
this.#urlFormatter = urlFormatter
|
|
94
83
|
this.#signer = signer
|
|
@@ -98,11 +87,11 @@ export class ActivityPubClient {
|
|
|
98
87
|
this.#cache = cache
|
|
99
88
|
this.#messageSigner = messageSigner
|
|
100
89
|
this.#policyStorage = policyStorage
|
|
101
|
-
this.#
|
|
90
|
+
this.#fetcher = fetcher
|
|
102
91
|
}
|
|
103
92
|
|
|
104
93
|
get allowPrivateNetworkRequests () {
|
|
105
|
-
return this.#
|
|
94
|
+
return this.#fetcher.allowPrivateNetworkRequests
|
|
106
95
|
}
|
|
107
96
|
|
|
108
97
|
async get (url, username = this.#urlFormatter.hostname) {
|
|
@@ -200,7 +189,7 @@ export class ActivityPubClient {
|
|
|
200
189
|
this.#logger.debug({ url: baseUrl, hostname }, 'Waiting for throttler')
|
|
201
190
|
await this.#throttler.throttle(hostname)
|
|
202
191
|
this.#logger.debug({ url: baseUrl }, 'Fetching with GET')
|
|
203
|
-
let res = await this.#fetch(baseUrl,
|
|
192
|
+
let res = await this.#fetcher.fetch(baseUrl,
|
|
204
193
|
{
|
|
205
194
|
method,
|
|
206
195
|
headers
|
|
@@ -218,7 +207,7 @@ export class ActivityPubClient {
|
|
|
218
207
|
delete headers['signature-input']
|
|
219
208
|
headers.signature =
|
|
220
209
|
await this.#sign({ username, url: baseUrl, method, headers })
|
|
221
|
-
res = await this.#fetch(baseUrl,
|
|
210
|
+
res = await this.#fetcher.fetch(baseUrl,
|
|
222
211
|
{
|
|
223
212
|
method,
|
|
224
213
|
headers
|
|
@@ -342,7 +331,7 @@ export class ActivityPubClient {
|
|
|
342
331
|
this.#logger.debug({ url, hostname }, 'Waiting for throttler')
|
|
343
332
|
await this.#throttler.throttle(hostname)
|
|
344
333
|
this.#logger.debug({ url }, 'Fetching POST')
|
|
345
|
-
let res = await this.#fetch(url,
|
|
334
|
+
let res = await this.#fetcher.fetch(url,
|
|
346
335
|
{
|
|
347
336
|
method,
|
|
348
337
|
headers,
|
|
@@ -363,7 +352,7 @@ export class ActivityPubClient {
|
|
|
363
352
|
}
|
|
364
353
|
headers.signature =
|
|
365
354
|
await this.#sign({ username, url, method, headers })
|
|
366
|
-
res = await this.#fetch(url,
|
|
355
|
+
res = await this.#fetcher.fetch(url,
|
|
367
356
|
{
|
|
368
357
|
method,
|
|
369
358
|
headers,
|
|
@@ -530,37 +519,4 @@ export class ActivityPubClient {
|
|
|
530
519
|
}
|
|
531
520
|
return results
|
|
532
521
|
}
|
|
533
|
-
|
|
534
|
-
async #fetch (url, options) {
|
|
535
|
-
if (!(await this.#checkProtocol(url))) {
|
|
536
|
-
throw new ProtocolError(url)
|
|
537
|
-
}
|
|
538
|
-
const fullOptions = {
|
|
539
|
-
...options,
|
|
540
|
-
agent: this.#agent ?? undefined,
|
|
541
|
-
signal: options?.signal ?? AbortSignal.timeout(10000),
|
|
542
|
-
size: 1024 * 1024,
|
|
543
|
-
follow: 10
|
|
544
|
-
}
|
|
545
|
-
return await fetch(url, fullOptions)
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
async #checkProtocol (url) {
|
|
549
|
-
const parsed = (new URL(url))
|
|
550
|
-
switch (parsed.protocol) {
|
|
551
|
-
case 'https:':
|
|
552
|
-
return true
|
|
553
|
-
case 'http:': {
|
|
554
|
-
if (this.#agent) {
|
|
555
|
-
return false
|
|
556
|
-
}
|
|
557
|
-
const { address } = await dns.lookup(parsed.hostname)
|
|
558
|
-
const addr = ipaddr.parse(address)
|
|
559
|
-
const range = addr.range()
|
|
560
|
-
return range !== 'unicast'
|
|
561
|
-
}
|
|
562
|
-
default:
|
|
563
|
-
return false
|
|
564
|
-
}
|
|
565
|
-
}
|
|
566
522
|
}
|
package/lib/actorstorage.js
CHANGED
|
@@ -3,6 +3,8 @@ import assert from 'node:assert'
|
|
|
3
3
|
|
|
4
4
|
const AS2_NS = 'https://www.w3.org/ns/activitystreams#'
|
|
5
5
|
|
|
6
|
+
const MS_PER_DAY = 24 * 60 * 60 * 1000
|
|
7
|
+
|
|
6
8
|
export class ActorStorage {
|
|
7
9
|
#connection = null
|
|
8
10
|
#formatter = null
|
|
@@ -330,6 +332,24 @@ export class ActorStorage {
|
|
|
330
332
|
)
|
|
331
333
|
}
|
|
332
334
|
|
|
335
|
+
async activeUsers (days) {
|
|
336
|
+
const [result] = await this.#connection.query(
|
|
337
|
+
`SELECT COUNT(DISTINCT username) as active_users
|
|
338
|
+
FROM actorcollectionpage
|
|
339
|
+
WHERE property = 'outbox'
|
|
340
|
+
AND createdat > ?;`,
|
|
341
|
+
{ replacements: [new Date(Date.now() - days * MS_PER_DAY)] }
|
|
342
|
+
)
|
|
343
|
+
if (result.length > 0) {
|
|
344
|
+
const row = result[0]
|
|
345
|
+
return (typeof row.active_users === 'string')
|
|
346
|
+
? parseInt(row.active_users, 10)
|
|
347
|
+
: row.active_users
|
|
348
|
+
} else {
|
|
349
|
+
return 0
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
333
353
|
async #getCollectionInfo (username, property) {
|
|
334
354
|
const [result] = await this.#connection.query(
|
|
335
355
|
`SELECT
|
package/lib/app.js
CHANGED
|
@@ -51,7 +51,7 @@ import { IntakeWorker } from './intakeworker.js'
|
|
|
51
51
|
import { RemoteObjectCache } from './remoteobjectcache.js'
|
|
52
52
|
import { HTTPMessageSignature } from './httpmessagesignature.js'
|
|
53
53
|
import { SignaturePolicyStorage } from './signaturepolicystorage.js'
|
|
54
|
-
import {
|
|
54
|
+
import { SafeFetcher } from './safefetcher.js'
|
|
55
55
|
import { EndpointCache } from './endpointcache.js'
|
|
56
56
|
|
|
57
57
|
const currentDir = dirname(fileURLToPath(import.meta.url))
|
|
@@ -98,10 +98,8 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
98
98
|
const throttler = new RequestThrottler(connection, logger)
|
|
99
99
|
const remoteObjectCache = new RemoteObjectCache(connection, logger)
|
|
100
100
|
const policyStore = new SignaturePolicyStorage(connection, logger)
|
|
101
|
-
const
|
|
102
|
-
|
|
103
|
-
: new SafeAgent()
|
|
104
|
-
const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger, throttler, remoteObjectCache, messageSigner, policyStore, agent)
|
|
101
|
+
const fetcher = new SafeFetcher({ allowPrivate: allowPrivateNetworkRequests })
|
|
102
|
+
const client = new ActivityPubClient(keyStorage, formatter, signer, digester, logger, throttler, remoteObjectCache, messageSigner, policyStore, fetcher)
|
|
105
103
|
const remoteKeyStorage = new RemoteKeyStorage(client, connection, logger)
|
|
106
104
|
const signature = new HTTPSignatureAuthenticator(remoteKeyStorage, signer, messageSigner, digester, logger)
|
|
107
105
|
const jobQueue = new JobQueue(connection, logger)
|
|
@@ -134,7 +132,9 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
134
132
|
)
|
|
135
133
|
|
|
136
134
|
// TODO: Make an endpoint for tagged objects
|
|
137
|
-
const transformer = new Transformer(
|
|
135
|
+
const transformer = new Transformer(
|
|
136
|
+
origin + '/tag/', client, fetcher, formatter
|
|
137
|
+
)
|
|
138
138
|
|
|
139
139
|
let redisClient
|
|
140
140
|
|
|
@@ -157,7 +157,8 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
157
157
|
formatter,
|
|
158
158
|
transformer,
|
|
159
159
|
logger,
|
|
160
|
-
bots
|
|
160
|
+
bots,
|
|
161
|
+
fetcher
|
|
161
162
|
)
|
|
162
163
|
))
|
|
163
164
|
)
|
|
@@ -179,7 +180,8 @@ export async function makeApp ({ databaseUrl, origin, bots, logLevel = 'silent',
|
|
|
179
180
|
formatter,
|
|
180
181
|
transformer,
|
|
181
182
|
logger,
|
|
182
|
-
bots
|
|
183
|
+
bots,
|
|
184
|
+
fetcher
|
|
183
185
|
))
|
|
184
186
|
|
|
185
187
|
memoryStatus('bots initialized')
|
package/lib/botcontext.js
CHANGED
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
|
|
3
3
|
import { nanoid } from 'nanoid'
|
|
4
|
-
import fetch from 'node-fetch'
|
|
5
4
|
|
|
6
5
|
import as2 from './activitystreams.js'
|
|
7
6
|
|
|
@@ -28,6 +27,7 @@ export class BotContext {
|
|
|
28
27
|
#transformer = null
|
|
29
28
|
#logger = null
|
|
30
29
|
#bots
|
|
30
|
+
#fetcher
|
|
31
31
|
|
|
32
32
|
get botId () {
|
|
33
33
|
return this.#botId
|
|
@@ -47,8 +47,20 @@ export class BotContext {
|
|
|
47
47
|
formatter,
|
|
48
48
|
transformer,
|
|
49
49
|
logger,
|
|
50
|
-
bots
|
|
50
|
+
bots,
|
|
51
|
+
fetcher
|
|
51
52
|
) {
|
|
53
|
+
assert.strictEqual(typeof botId, 'string')
|
|
54
|
+
assert.strictEqual(typeof botDataStorage, 'object')
|
|
55
|
+
assert.strictEqual(typeof objectStorage, 'object')
|
|
56
|
+
assert.strictEqual(typeof actorStorage, 'object')
|
|
57
|
+
assert.strictEqual(typeof client, 'object')
|
|
58
|
+
assert.strictEqual(typeof distributor, 'object')
|
|
59
|
+
assert.strictEqual(typeof formatter, 'object')
|
|
60
|
+
assert.strictEqual(typeof transformer, 'object')
|
|
61
|
+
assert.strictEqual(typeof logger, 'object')
|
|
62
|
+
assert.strictEqual(typeof bots, 'object')
|
|
63
|
+
assert.strictEqual(typeof fetcher, 'object')
|
|
52
64
|
this.#botId = botId
|
|
53
65
|
this.#botDataStorage = botDataStorage
|
|
54
66
|
this.#objectStorage = objectStorage
|
|
@@ -59,6 +71,7 @@ export class BotContext {
|
|
|
59
71
|
this.#transformer = transformer
|
|
60
72
|
this.#bots = bots
|
|
61
73
|
this.#logger = logger.child({ class: 'BotContext', botId })
|
|
74
|
+
this.#fetcher = fetcher
|
|
62
75
|
}
|
|
63
76
|
|
|
64
77
|
// copy constructor
|
|
@@ -73,7 +86,9 @@ export class BotContext {
|
|
|
73
86
|
this.#distributor,
|
|
74
87
|
this.#formatter,
|
|
75
88
|
this.#transformer,
|
|
76
|
-
this.#logger
|
|
89
|
+
this.#logger,
|
|
90
|
+
this.#bots,
|
|
91
|
+
this.#fetcher
|
|
77
92
|
)
|
|
78
93
|
}
|
|
79
94
|
|
|
@@ -377,10 +392,13 @@ export class BotContext {
|
|
|
377
392
|
}
|
|
378
393
|
|
|
379
394
|
async toActorId (webfinger) {
|
|
380
|
-
const [, domain] = webfinger.split('@')
|
|
395
|
+
const [username, domain] = webfinger.split('@')
|
|
396
|
+
if (domain.toLowerCase() === this.#formatter.hostname) {
|
|
397
|
+
return this.#formatter.format({ username })
|
|
398
|
+
}
|
|
381
399
|
const url = `https://${domain}/.well-known/webfinger` +
|
|
382
400
|
`?resource=acct:${webfinger}`
|
|
383
|
-
const res = await fetch(url)
|
|
401
|
+
const res = await this.#fetcher.fetch(url)
|
|
384
402
|
if (res.status !== 200) {
|
|
385
403
|
throw new Error(`Status ${res.status} fetching ${url}`)
|
|
386
404
|
}
|
package/lib/microsyntax.js
CHANGED
|
@@ -1,20 +1,37 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
|
-
import fetch from 'node-fetch'
|
|
3
2
|
|
|
4
3
|
const AS2 = 'https://www.w3.org/ns/activitystreams#'
|
|
5
4
|
|
|
6
5
|
export class Transformer {
|
|
7
6
|
#tagNamespace = null
|
|
8
7
|
#client = null
|
|
9
|
-
|
|
8
|
+
#fetcher = null
|
|
9
|
+
#formatter = null
|
|
10
|
+
|
|
11
|
+
constructor (tagNamespace, client, fetcher, formatter) {
|
|
12
|
+
assert.strictEqual(typeof tagNamespace, 'string')
|
|
13
|
+
assert.strictEqual(typeof client, 'object')
|
|
14
|
+
assert.strictEqual(typeof fetcher, 'object')
|
|
15
|
+
assert.strictEqual(typeof formatter, 'object')
|
|
16
|
+
|
|
10
17
|
this.#tagNamespace = tagNamespace
|
|
11
18
|
this.#client = client
|
|
19
|
+
this.#fetcher = fetcher
|
|
20
|
+
this.#formatter = formatter
|
|
21
|
+
|
|
12
22
|
assert.ok(this.#tagNamespace)
|
|
13
23
|
assert.ok(this.#client)
|
|
24
|
+
assert.ok(this.#fetcher)
|
|
25
|
+
assert.ok(this.#formatter)
|
|
14
26
|
}
|
|
15
27
|
|
|
16
28
|
async transform (text) {
|
|
17
29
|
let html = text
|
|
30
|
+
.replace(/&/g, '&')
|
|
31
|
+
.replace(/</g, '<')
|
|
32
|
+
.replace(/>/g, '>')
|
|
33
|
+
.replace(/"/g, '"')
|
|
34
|
+
.replace(/'/g, ''')
|
|
18
35
|
let tag = [];
|
|
19
36
|
({ html, tag } = this.#replaceUrls(html, tag));
|
|
20
37
|
({ html, tag } = this.#replaceHashtags(html, tag));
|
|
@@ -71,11 +88,14 @@ export class Transformer {
|
|
|
71
88
|
async #homePage (webfinger) {
|
|
72
89
|
if (!this.#client) return null
|
|
73
90
|
const [username, domain] = webfinger.split('@')
|
|
91
|
+
if (domain.toLowerCase() === this.#formatter.hostname) {
|
|
92
|
+
return this.#formatter.format({ username, type: 'profile' })
|
|
93
|
+
}
|
|
74
94
|
const url = `https://${domain}/.well-known/webfinger?` +
|
|
75
95
|
`resource=acct:${username}@${domain}`
|
|
76
96
|
let json = null
|
|
77
97
|
try {
|
|
78
|
-
const response = await fetch(url,
|
|
98
|
+
const response = await this.#fetcher.fetch(url,
|
|
79
99
|
{ headers: { Accept: 'application/jrd+json' } })
|
|
80
100
|
if (response.status !== 200) return null
|
|
81
101
|
json = await response.json()
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export const id = '012-actor-outbox-created-at-index'
|
|
2
|
+
|
|
3
|
+
export async function up (connection, queryOptions = {}) {
|
|
4
|
+
await connection.query(`
|
|
5
|
+
CREATE INDEX actorcollectionpage_outbox_createdat
|
|
6
|
+
ON actorcollectionpage (createdat)
|
|
7
|
+
WHERE property = 'outbox';
|
|
8
|
+
`, queryOptions)
|
|
9
|
+
}
|
package/lib/routes/nodeinfo.js
CHANGED
|
@@ -9,7 +9,7 @@ import { ProblemDetailsError } from '../errors.js'
|
|
|
9
9
|
const __filename = fileURLToPath(import.meta.url)
|
|
10
10
|
const __dirname = path.dirname(__filename)
|
|
11
11
|
|
|
12
|
-
const packageName = 'activitypub-bot'
|
|
12
|
+
const packageName = 'activitypub-dot-bot'
|
|
13
13
|
|
|
14
14
|
const { version: packageVersion } = JSON.parse(
|
|
15
15
|
fs.readFileSync(path.join(__dirname, '..', '..', 'package.json'), 'utf8')
|
|
@@ -17,7 +17,7 @@ const { version: packageVersion } = JSON.parse(
|
|
|
17
17
|
|
|
18
18
|
const router = Router()
|
|
19
19
|
|
|
20
|
-
const VERSIONS = ['2.0']
|
|
20
|
+
const VERSIONS = ['2.0', '2.1', '2.2']
|
|
21
21
|
|
|
22
22
|
router.get('/.well-known/nodeinfo', async (req, res, next) => {
|
|
23
23
|
const { origin } = req.app.locals
|
|
@@ -33,7 +33,7 @@ router.get('/.well-known/nodeinfo', async (req, res, next) => {
|
|
|
33
33
|
|
|
34
34
|
router.get('/nodeinfo/:nodeinfoVersion', async (req, res, next) => {
|
|
35
35
|
const { nodeinfoVersion } = req.params
|
|
36
|
-
const { keyStorage } = req.app.locals
|
|
36
|
+
const { keyStorage, actorStorage } = req.app.locals
|
|
37
37
|
if (!VERSIONS.includes(nodeinfoVersion)) {
|
|
38
38
|
throw new ProblemDetailsError(404, 'unsupported nodeinfo version')
|
|
39
39
|
}
|
|
@@ -51,7 +51,9 @@ router.get('/nodeinfo/:nodeinfoVersion', async (req, res, next) => {
|
|
|
51
51
|
openRegistrations: false,
|
|
52
52
|
usage: {
|
|
53
53
|
users: {
|
|
54
|
-
total: await keyStorage.count()
|
|
54
|
+
total: await keyStorage.count(),
|
|
55
|
+
activeMonth: await actorStorage.activeUsers(30),
|
|
56
|
+
activeHalfyear: await actorStorage.activeUsers(180)
|
|
55
57
|
}
|
|
56
58
|
},
|
|
57
59
|
metadata: {}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import fetch from 'node-fetch'
|
|
2
|
+
import dns from 'node:dns'
|
|
3
|
+
import dnsPromises from 'node:dns/promises'
|
|
4
|
+
import https from 'node:https'
|
|
5
|
+
|
|
6
|
+
import ipaddr from 'ipaddr.js'
|
|
7
|
+
|
|
8
|
+
function isPrivateIP (address) {
|
|
9
|
+
if (!ipaddr.isValid(address)) return false
|
|
10
|
+
const addr = ipaddr.parse(address)
|
|
11
|
+
const range = addr.range()
|
|
12
|
+
return range !== 'unicast'
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class PrivateNetworkError extends Error {
|
|
16
|
+
constructor (address) {
|
|
17
|
+
super(`Private network address ${address}`)
|
|
18
|
+
this.name = 'PrivateNetworkError'
|
|
19
|
+
this.address = address
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
class SafeAgent extends https.Agent {
|
|
24
|
+
constructor (options = {}) {
|
|
25
|
+
super({
|
|
26
|
+
keepAlive: true,
|
|
27
|
+
keepAliveMsecs: 1000,
|
|
28
|
+
maxSockets: 64,
|
|
29
|
+
maxFreeSockets: 256,
|
|
30
|
+
...options
|
|
31
|
+
})
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
createConnection (options, callback) {
|
|
35
|
+
dns.lookup(options.hostname, (err, address) => {
|
|
36
|
+
if (err) {
|
|
37
|
+
return callback(err)
|
|
38
|
+
}
|
|
39
|
+
if (isPrivateIP(address)) {
|
|
40
|
+
return callback(new PrivateNetworkError(address))
|
|
41
|
+
}
|
|
42
|
+
try {
|
|
43
|
+
const socket = super.createConnection({
|
|
44
|
+
...options,
|
|
45
|
+
host: address,
|
|
46
|
+
servername: options.servername || options.hostname
|
|
47
|
+
})
|
|
48
|
+
callback(null, socket)
|
|
49
|
+
} catch (e) {
|
|
50
|
+
callback(e)
|
|
51
|
+
}
|
|
52
|
+
})
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export class ProtocolError extends Error {
|
|
57
|
+
constructor (url) {
|
|
58
|
+
super(`URL ${url} uses a disallowed protocol`)
|
|
59
|
+
this.name = 'ProtocolError'
|
|
60
|
+
this.url = url
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export class SafeFetcher {
|
|
65
|
+
#allowPrivate
|
|
66
|
+
#agent
|
|
67
|
+
|
|
68
|
+
constructor (options = {}) {
|
|
69
|
+
const { allowPrivate } = options
|
|
70
|
+
this.#allowPrivate = !!allowPrivate
|
|
71
|
+
this.#agent = (this.#allowPrivate)
|
|
72
|
+
? null
|
|
73
|
+
: new SafeAgent()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
get allowPrivateNetworkRequests () {
|
|
77
|
+
return this.#allowPrivate
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
async fetch (url, options) {
|
|
81
|
+
if (!(await this.#checkProtocol(url))) {
|
|
82
|
+
throw new ProtocolError(url)
|
|
83
|
+
}
|
|
84
|
+
const fullOptions = {
|
|
85
|
+
...options,
|
|
86
|
+
agent: this.#agent ?? undefined,
|
|
87
|
+
signal: options?.signal ?? AbortSignal.timeout(10000),
|
|
88
|
+
size: 1024 * 1024,
|
|
89
|
+
follow: 10
|
|
90
|
+
}
|
|
91
|
+
return await fetch(url, fullOptions)
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
async #checkProtocol (url) {
|
|
95
|
+
const parsed = (new URL(url))
|
|
96
|
+
switch (parsed.protocol) {
|
|
97
|
+
case 'https:':
|
|
98
|
+
return true
|
|
99
|
+
case 'http:': {
|
|
100
|
+
if (this.#agent) {
|
|
101
|
+
return false
|
|
102
|
+
}
|
|
103
|
+
const { address } = await dnsPromises.lookup(parsed.hostname)
|
|
104
|
+
const addr = ipaddr.parse(address)
|
|
105
|
+
const range = addr.range()
|
|
106
|
+
return range !== 'unicast'
|
|
107
|
+
}
|
|
108
|
+
default:
|
|
109
|
+
return false
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
package/package.json
CHANGED
package/lib/safeagent.js
DELETED
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import https from 'node:https'
|
|
2
|
-
import dns from 'node:dns'
|
|
3
|
-
|
|
4
|
-
import ipaddr from 'ipaddr.js'
|
|
5
|
-
|
|
6
|
-
function isPrivateIP (address) {
|
|
7
|
-
if (!ipaddr.isValid(address)) return false
|
|
8
|
-
const addr = ipaddr.parse(address)
|
|
9
|
-
const range = addr.range()
|
|
10
|
-
return range !== 'unicast'
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
class PrivateNetworkError extends Error {
|
|
14
|
-
constructor (address) {
|
|
15
|
-
super(`Private network address ${address}`)
|
|
16
|
-
this.name = 'PrivateNetworkError'
|
|
17
|
-
this.address = address
|
|
18
|
-
}
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
export class SafeAgent extends https.Agent {
|
|
22
|
-
constructor (options = {}) {
|
|
23
|
-
super({
|
|
24
|
-
keepAlive: true,
|
|
25
|
-
keepAliveMsecs: 1000,
|
|
26
|
-
maxSockets: 64,
|
|
27
|
-
maxFreeSockets: 256,
|
|
28
|
-
...options
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
createConnection (options, callback) {
|
|
33
|
-
dns.lookup(options.hostname, (err, address) => {
|
|
34
|
-
if (err) {
|
|
35
|
-
return callback(err)
|
|
36
|
-
}
|
|
37
|
-
if (isPrivateIP(address)) {
|
|
38
|
-
return callback(new PrivateNetworkError(address))
|
|
39
|
-
}
|
|
40
|
-
try {
|
|
41
|
-
const socket = super.createConnection({
|
|
42
|
-
...options,
|
|
43
|
-
host: address,
|
|
44
|
-
servername: options.servername || options.hostname
|
|
45
|
-
})
|
|
46
|
-
callback(null, socket)
|
|
47
|
-
} catch (e) {
|
|
48
|
-
callback(e)
|
|
49
|
-
}
|
|
50
|
-
})
|
|
51
|
-
}
|
|
52
|
-
}
|