@evanp/activitypub-bot 0.38.4 → 0.39.1
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/lib/activitypubclient.js +113 -12
- package/lib/app.js +43 -19
- package/lib/errors.js +158 -0
- package/lib/httpmessagesignature.js +1 -1
- package/lib/httpsignature.js +11 -11
- package/lib/httpsignatureauthenticator.js +15 -15
- package/lib/migrations/008-signature-policy.js +16 -0
- package/lib/ratelimiter.js +1 -1
- package/lib/remoteobjectcache.js +1 -1
- package/lib/routes/collection.js +7 -7
- package/lib/routes/health.js +2 -2
- package/lib/routes/inbox.js +21 -14
- package/lib/routes/object.js +13 -13
- package/lib/routes/profile.js +2 -2
- package/lib/routes/proxy.js +5 -5
- package/lib/routes/sharedinbox.js +13 -9
- package/lib/routes/user.js +9 -9
- package/lib/routes/webfinger.js +9 -9
- package/lib/signaturepolicystorage.js +61 -0
- package/package.json +1 -2
- package/web/profile.html +12 -8
package/lib/ratelimiter.js
CHANGED
|
@@ -11,7 +11,7 @@ export class RateLimiter {
|
|
|
11
11
|
assert.strictEqual(typeof connection, 'object')
|
|
12
12
|
assert.strictEqual(typeof logger, 'object')
|
|
13
13
|
this.#connection = connection
|
|
14
|
-
this.#logger = logger
|
|
14
|
+
this.#logger = logger.child({ class: this.constructor.name })
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
async limit (host, maxWaitTime = 30000) {
|
package/lib/remoteobjectcache.js
CHANGED
package/lib/routes/collection.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
|
|
3
3
|
import express from 'express'
|
|
4
|
-
import createHttpError from 'http-errors'
|
|
5
4
|
|
|
6
5
|
import as2 from '../activitystreams.js'
|
|
7
6
|
import BotMaker from '../botmaker.js'
|
|
7
|
+
import { ProblemDetailsError, PrincipalNotAuthorizedError } from '../errors.js'
|
|
8
8
|
|
|
9
9
|
const collections = ['outbox', 'liked', 'followers', 'following']
|
|
10
10
|
const router = express.Router()
|
|
@@ -40,7 +40,7 @@ function collectionHandler (collection) {
|
|
|
40
40
|
const { actorStorage, bots } = req.app.locals
|
|
41
41
|
const bot = await BotMaker.makeBot(bots, username)
|
|
42
42
|
if (!bot) {
|
|
43
|
-
return next(
|
|
43
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
44
44
|
}
|
|
45
45
|
const coll = await actorStorage.getCollection(username, collection)
|
|
46
46
|
res.status(200)
|
|
@@ -57,20 +57,20 @@ function collectionPageHandler (collection) {
|
|
|
57
57
|
try {
|
|
58
58
|
pageNo = parseInt(n)
|
|
59
59
|
} catch (err) {
|
|
60
|
-
return next(
|
|
60
|
+
return next(new ProblemDetailsError(400, `Invalid page ${n}`))
|
|
61
61
|
}
|
|
62
62
|
const { actorStorage, bots, authorizer, objectStorage, formatter, client } = req.app.locals
|
|
63
63
|
const bot = await BotMaker.makeBot(bots, username)
|
|
64
64
|
|
|
65
65
|
if (!bot) {
|
|
66
|
-
return next(
|
|
66
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
67
67
|
}
|
|
68
68
|
|
|
69
69
|
if (collection === 'inbox') {
|
|
70
|
-
return next(
|
|
70
|
+
return next(new PrincipalNotAuthorizedError(`No access to ${collection} collection`, { resource: req.url }))
|
|
71
71
|
}
|
|
72
72
|
if (!await actorStorage.hasPage(username, collection, parseInt(n))) {
|
|
73
|
-
return next(
|
|
73
|
+
return next(new ProblemDetailsError(404, `No such page ${n} for collection ${collection} for user ${username}`))
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
let exported = null
|
|
@@ -113,7 +113,7 @@ function collectionPageHandler (collection) {
|
|
|
113
113
|
{ err: error, username, collection, n },
|
|
114
114
|
'error loading collection page'
|
|
115
115
|
)
|
|
116
|
-
return next(
|
|
116
|
+
return next(new ProblemDetailsError(500, 'Error loading collection page'))
|
|
117
117
|
}
|
|
118
118
|
res.status(200)
|
|
119
119
|
res.type(as2.mediaType)
|
package/lib/routes/health.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
|
-
import
|
|
2
|
+
import { ProblemDetailsError } from '../errors.js'
|
|
3
3
|
|
|
4
4
|
const router = express.Router()
|
|
5
5
|
|
|
@@ -17,7 +17,7 @@ router.get('/readyz', async (req, res, next) => {
|
|
|
17
17
|
res.type('text/plain')
|
|
18
18
|
res.end('OK')
|
|
19
19
|
} catch (err) {
|
|
20
|
-
return next(
|
|
20
|
+
return next(new ProblemDetailsError(503, 'Service Unavailable'))
|
|
21
21
|
}
|
|
22
22
|
})
|
|
23
23
|
|
package/lib/routes/inbox.js
CHANGED
|
@@ -1,10 +1,17 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
2
|
|
|
3
3
|
import express from 'express'
|
|
4
|
-
import createHttpError from 'http-errors'
|
|
5
4
|
|
|
6
5
|
import as2 from '../activitystreams.js'
|
|
7
6
|
import BotMaker from '../botmaker.js'
|
|
7
|
+
import {
|
|
8
|
+
ProblemDetailsError,
|
|
9
|
+
UnsupportedTypeError,
|
|
10
|
+
DuplicateDeliveryError,
|
|
11
|
+
PrincipalActorMismatchError,
|
|
12
|
+
ActorNotAuthorizedError,
|
|
13
|
+
PrincipalNotAuthorizedError
|
|
14
|
+
} from '../errors.js'
|
|
8
15
|
|
|
9
16
|
const router = express.Router()
|
|
10
17
|
|
|
@@ -16,15 +23,15 @@ router.post('/user/:username/inbox', async (req, res, next) => {
|
|
|
16
23
|
|
|
17
24
|
const bot = await BotMaker.makeBot(bots, username)
|
|
18
25
|
if (!bot) {
|
|
19
|
-
return next(
|
|
26
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
20
27
|
}
|
|
21
28
|
|
|
22
29
|
if (!subject) {
|
|
23
|
-
return next(
|
|
30
|
+
return next(new ProblemDetailsError(401, 'Unauthorized'))
|
|
24
31
|
}
|
|
25
32
|
|
|
26
33
|
if (!req.body) {
|
|
27
|
-
return next(
|
|
34
|
+
return next(new ProblemDetailsError(400, 'No request body provided'))
|
|
28
35
|
}
|
|
29
36
|
|
|
30
37
|
let activity
|
|
@@ -34,37 +41,37 @@ router.post('/user/:username/inbox', async (req, res, next) => {
|
|
|
34
41
|
} catch (err) {
|
|
35
42
|
logger.warn({ reqId: req.id, err }, 'Failed to import activity')
|
|
36
43
|
logger.debug({ reqId: req.id, body: req.body }, 'Request body')
|
|
37
|
-
return next(
|
|
44
|
+
return next(new ProblemDetailsError(400, 'Invalid request body'))
|
|
38
45
|
}
|
|
39
46
|
|
|
40
47
|
if (!activity.isActivity()) {
|
|
41
|
-
return next(
|
|
48
|
+
return next(new UnsupportedTypeError('Request body is not an activity', { objectType: activity.type }))
|
|
42
49
|
}
|
|
43
50
|
|
|
44
51
|
const actor = deliverer.getActor(activity)
|
|
45
52
|
|
|
46
53
|
if (!actor) {
|
|
47
|
-
return next(
|
|
54
|
+
return next(new ProblemDetailsError(400, 'No actor found in activity'))
|
|
48
55
|
}
|
|
49
56
|
|
|
50
57
|
if (!actor.id) {
|
|
51
|
-
return next(
|
|
58
|
+
return next(new ProblemDetailsError(400, 'No actor id found in activity'))
|
|
52
59
|
}
|
|
53
60
|
|
|
54
61
|
if (actor.id !== subject && !(await bot.actorOK(subject, activity))) {
|
|
55
|
-
return next(
|
|
62
|
+
return next(new PrincipalActorMismatchError(`${subject} is not the actor ${actor.id}`, { principal: subject, actor: actor.id }))
|
|
56
63
|
}
|
|
57
64
|
|
|
58
65
|
if (await actorStorage.isInCollection(username, 'blocked', actor)) {
|
|
59
|
-
return next(
|
|
66
|
+
return next(new ActorNotAuthorizedError('Blocked actor', { actor: actor.id, resource: req.url }))
|
|
60
67
|
}
|
|
61
68
|
|
|
62
69
|
if (!activity.id) {
|
|
63
|
-
return next(
|
|
70
|
+
return next(new ProblemDetailsError(400, 'No activity id found in activity'))
|
|
64
71
|
}
|
|
65
72
|
|
|
66
73
|
if (await actorStorage.isInCollection(bot.username, 'inbox', activity)) {
|
|
67
|
-
return next(
|
|
74
|
+
return next(new DuplicateDeliveryError('Activity already delivered', { id: activity.id }))
|
|
68
75
|
}
|
|
69
76
|
|
|
70
77
|
logger.info(
|
|
@@ -84,11 +91,11 @@ router.post('/user/:username/inbox', async (req, res, next) => {
|
|
|
84
91
|
})
|
|
85
92
|
|
|
86
93
|
router.get('/user/:username/inbox', async (req, res, next) => {
|
|
87
|
-
return next(
|
|
94
|
+
return next(new PrincipalNotAuthorizedError('No access to inbox collection', { resource: req.url }))
|
|
88
95
|
})
|
|
89
96
|
|
|
90
97
|
router.get('/user/:username/inbox/:n', async (req, res, next) => {
|
|
91
|
-
return next(
|
|
98
|
+
return next(new PrincipalNotAuthorizedError('No access to inbox collection', { resource: req.url }))
|
|
92
99
|
})
|
|
93
100
|
|
|
94
101
|
export default router
|
package/lib/routes/object.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
|
-
import createHttpError from 'http-errors'
|
|
3
2
|
|
|
4
3
|
import as2 from '../activitystreams.js'
|
|
4
|
+
import { ProblemDetailsError, PrincipalNotAuthorizedError } from '../errors.js'
|
|
5
5
|
|
|
6
6
|
const router = express.Router()
|
|
7
7
|
|
|
@@ -10,17 +10,17 @@ export default router
|
|
|
10
10
|
router.get('/user/:username/:type/:nanoid', async (req, res, next) => {
|
|
11
11
|
const { username, type, nanoid } = req.params
|
|
12
12
|
if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
|
|
13
|
-
return next(
|
|
13
|
+
return next(new ProblemDetailsError(400, `Invalid nanoid ${nanoid}`))
|
|
14
14
|
}
|
|
15
15
|
const { objectStorage, formatter, authorizer } = req.app.locals
|
|
16
16
|
const id = formatter.format({ username, type, nanoid })
|
|
17
17
|
const object = await objectStorage.read(id)
|
|
18
18
|
if (!object) {
|
|
19
|
-
return next(
|
|
19
|
+
return next(new ProblemDetailsError(404, `Object ${id} not found`))
|
|
20
20
|
}
|
|
21
21
|
const remote = (req.auth?.subject) ? await as2.import({ id: req.auth.subject }) : null
|
|
22
22
|
if (!await authorizer.canRead(remote, object)) {
|
|
23
|
-
return next(
|
|
23
|
+
return next(new PrincipalNotAuthorizedError('Forbidden to read object', { id }))
|
|
24
24
|
}
|
|
25
25
|
res.status(200)
|
|
26
26
|
res.type(as2.mediaType)
|
|
@@ -30,20 +30,20 @@ router.get('/user/:username/:type/:nanoid', async (req, res, next) => {
|
|
|
30
30
|
router.get('/user/:username/:type/:nanoid/:collection', async (req, res, next) => {
|
|
31
31
|
const { objectStorage, formatter, authorizer } = req.app.locals
|
|
32
32
|
if (!['replies', 'likes', 'shares', 'thread'].includes(req.params.collection)) {
|
|
33
|
-
return next(
|
|
33
|
+
return next(new ProblemDetailsError(404, 'Not Found'))
|
|
34
34
|
}
|
|
35
35
|
const { username, type, nanoid } = req.params
|
|
36
36
|
if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
|
|
37
|
-
return next(
|
|
37
|
+
return next(new ProblemDetailsError(400, `Invalid nanoid ${nanoid}`))
|
|
38
38
|
}
|
|
39
39
|
const id = formatter.format({ username, type, nanoid })
|
|
40
40
|
const object = await objectStorage.read(id)
|
|
41
41
|
if (!object) {
|
|
42
|
-
return next(
|
|
42
|
+
return next(new ProblemDetailsError(404, 'Not Found'))
|
|
43
43
|
}
|
|
44
44
|
const remote = (req.auth?.subject) ? await as2.import({ id: req.auth.subject }) : null
|
|
45
45
|
if (!await authorizer.canRead(remote, object)) {
|
|
46
|
-
return next(
|
|
46
|
+
return next(new ProblemDetailsError(403, 'Forbidden'))
|
|
47
47
|
}
|
|
48
48
|
const collection = await objectStorage.getCollection(id, req.params.collection)
|
|
49
49
|
res.status(200)
|
|
@@ -57,23 +57,23 @@ router.get('/user/:username/:type/:nanoid/:collection/:n', async (req, res, next
|
|
|
57
57
|
try {
|
|
58
58
|
pageNo = parseInt(n)
|
|
59
59
|
} catch (err) {
|
|
60
|
-
return next(
|
|
60
|
+
return next(new ProblemDetailsError(400, `Invalid page ${n}`))
|
|
61
61
|
}
|
|
62
62
|
if (!nanoid.match(/^[A-Za-z0-9_\\-]{21}$/)) {
|
|
63
|
-
return next(
|
|
63
|
+
return next(new ProblemDetailsError(400, `Invalid nanoid ${nanoid}`))
|
|
64
64
|
}
|
|
65
65
|
const { objectStorage, formatter, authorizer } = req.app.locals
|
|
66
66
|
if (!['replies', 'likes', 'shares', 'thread'].includes(req.params.collection)) {
|
|
67
|
-
return next(
|
|
67
|
+
return next(new ProblemDetailsError(404, 'Not Found'))
|
|
68
68
|
}
|
|
69
69
|
const id = formatter.format({ username, type, nanoid })
|
|
70
70
|
const object = await objectStorage.read(id)
|
|
71
71
|
if (!object) {
|
|
72
|
-
return next(
|
|
72
|
+
return next(new ProblemDetailsError(404, 'Not Found'))
|
|
73
73
|
}
|
|
74
74
|
const remote = (req.auth?.subject) ? await as2.import({ id: req.auth.subject }) : null
|
|
75
75
|
if (!await authorizer.canRead(remote, object)) {
|
|
76
|
-
return next(
|
|
76
|
+
return next(new ProblemDetailsError(403, 'Forbidden'))
|
|
77
77
|
}
|
|
78
78
|
const collectionPage = await objectStorage.getCollectionPage(
|
|
79
79
|
id,
|
package/lib/routes/profile.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
|
-
import
|
|
2
|
+
import { ProblemDetailsError } from '../errors.js'
|
|
3
3
|
import BotMaker from '../botmaker.js'
|
|
4
4
|
|
|
5
5
|
const router = express.Router()
|
|
@@ -9,7 +9,7 @@ router.get('/profile/:username', async (req, res, next) => {
|
|
|
9
9
|
const { profileFileName, bots } = req.app.locals
|
|
10
10
|
const bot = await BotMaker.makeBot(bots, username)
|
|
11
11
|
if (!bot) {
|
|
12
|
-
return next(
|
|
12
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
13
13
|
}
|
|
14
14
|
res.type('html')
|
|
15
15
|
res.sendFile(profileFileName)
|
package/lib/routes/proxy.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import express from 'express'
|
|
2
2
|
|
|
3
3
|
import as2 from '../activitystreams.js'
|
|
4
|
-
import
|
|
4
|
+
import { ProblemDetailsError } from '../errors.js'
|
|
5
5
|
|
|
6
6
|
const router = express.Router()
|
|
7
7
|
|
|
@@ -17,7 +17,7 @@ router.post('/shared/proxy', async (req, res, next) => {
|
|
|
17
17
|
const { id } = req.body
|
|
18
18
|
|
|
19
19
|
if (!id) {
|
|
20
|
-
return next(
|
|
20
|
+
return next(new ProblemDetailsError(400, 'Missing id parameter'))
|
|
21
21
|
}
|
|
22
22
|
|
|
23
23
|
let url
|
|
@@ -25,11 +25,11 @@ router.post('/shared/proxy', async (req, res, next) => {
|
|
|
25
25
|
try {
|
|
26
26
|
url = new URL(id)
|
|
27
27
|
} catch (error) {
|
|
28
|
-
return next(
|
|
28
|
+
return next(new ProblemDetailsError(400, 'id must be an URL'))
|
|
29
29
|
}
|
|
30
30
|
|
|
31
31
|
if (url.protocol !== 'https:') {
|
|
32
|
-
return next(
|
|
32
|
+
return next(new ProblemDetailsError(400, 'id must be an https: URL'))
|
|
33
33
|
}
|
|
34
34
|
|
|
35
35
|
let obj
|
|
@@ -38,7 +38,7 @@ router.post('/shared/proxy', async (req, res, next) => {
|
|
|
38
38
|
obj = await client.get(id)
|
|
39
39
|
} catch (err) {
|
|
40
40
|
logger.warn({ reqId: req.id, err, id }, 'Error fetching object in proxy')
|
|
41
|
-
return next(
|
|
41
|
+
return next(new ProblemDetailsError(400, `Error fetching object ${id}`))
|
|
42
42
|
}
|
|
43
43
|
|
|
44
44
|
res.status(200)
|
|
@@ -1,9 +1,13 @@
|
|
|
1
1
|
import http from 'node:http'
|
|
2
2
|
|
|
3
3
|
import express from 'express'
|
|
4
|
-
import createHttpError from 'http-errors'
|
|
5
4
|
|
|
6
5
|
import as2 from '../activitystreams.js'
|
|
6
|
+
import {
|
|
7
|
+
ProblemDetailsError,
|
|
8
|
+
UnsupportedTypeError,
|
|
9
|
+
PrincipalActorMismatchError
|
|
10
|
+
} from '../errors.js'
|
|
7
11
|
|
|
8
12
|
const router = express.Router()
|
|
9
13
|
|
|
@@ -28,11 +32,11 @@ router.post('/shared/inbox', async (req, res, next) => {
|
|
|
28
32
|
const { subject } = req.auth
|
|
29
33
|
|
|
30
34
|
if (!subject) {
|
|
31
|
-
return next(
|
|
35
|
+
return next(new ProblemDetailsError(401, 'Unauthorized'))
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
if (!req.body) {
|
|
35
|
-
return next(
|
|
39
|
+
return next(new ProblemDetailsError(400, 'No request body provided'))
|
|
36
40
|
}
|
|
37
41
|
|
|
38
42
|
let activity
|
|
@@ -42,29 +46,29 @@ router.post('/shared/inbox', async (req, res, next) => {
|
|
|
42
46
|
} catch (err) {
|
|
43
47
|
logger.warn({ reqId: req.id, err }, 'Failed to import activity')
|
|
44
48
|
logger.debug({ reqId: req.id, body: req.body }, 'Request body')
|
|
45
|
-
return next(
|
|
49
|
+
return next(new ProblemDetailsError(400, 'Invalid request body'))
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
if (!activity.isActivity()) {
|
|
49
|
-
return next(
|
|
53
|
+
return next(new UnsupportedTypeError('Request body is not an activity', { objectType: activity.type }))
|
|
50
54
|
}
|
|
51
55
|
|
|
52
56
|
const actor = deliverer.getActor(activity)
|
|
53
57
|
|
|
54
58
|
if (!actor) {
|
|
55
|
-
return next(
|
|
59
|
+
return next(new ProblemDetailsError(400, 'No actor found in activity'))
|
|
56
60
|
}
|
|
57
61
|
|
|
58
62
|
if (!actor.id) {
|
|
59
|
-
return next(
|
|
63
|
+
return next(new ProblemDetailsError(400, 'No actor id found in activity'))
|
|
60
64
|
}
|
|
61
65
|
|
|
62
66
|
if (actor.id !== subject && !(await actorOK(subject, activity, bots))) {
|
|
63
|
-
return next(
|
|
67
|
+
return next(new PrincipalActorMismatchError(`${subject} is not the actor ${actor.id}`, { principal: subject, actor: actor.id }))
|
|
64
68
|
}
|
|
65
69
|
|
|
66
70
|
if (!activity.id) {
|
|
67
|
-
return next(
|
|
71
|
+
return next(new ProblemDetailsError(400, 'No activity id found in activity'))
|
|
68
72
|
}
|
|
69
73
|
|
|
70
74
|
logger.info(
|
package/lib/routes/user.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { fileURLToPath } from 'node:url'
|
|
2
2
|
import express from 'express'
|
|
3
3
|
import as2 from '../activitystreams.js'
|
|
4
|
-
import
|
|
4
|
+
import { ProblemDetailsError } from '../errors.js'
|
|
5
5
|
import BotMaker from '../botmaker.js'
|
|
6
6
|
|
|
7
7
|
const router = express.Router()
|
|
@@ -28,7 +28,7 @@ router.get('/user/:username', async (req, res, next) => {
|
|
|
28
28
|
const { actorStorage, keyStorage, formatter, bots, origin } = req.app.locals
|
|
29
29
|
const bot = await BotMaker.makeBot(bots, username)
|
|
30
30
|
if (!bot) {
|
|
31
|
-
return next(
|
|
31
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
32
32
|
}
|
|
33
33
|
const publicKeyPem = await keyStorage.getPublicKey(username)
|
|
34
34
|
const acct = formatter.acct(username)
|
|
@@ -73,7 +73,7 @@ router.get('/user/:username/publickey', async (req, res, next) => {
|
|
|
73
73
|
const { formatter, keyStorage, bots } = req.app.locals
|
|
74
74
|
const bot = await BotMaker.makeBot(bots, username)
|
|
75
75
|
if (!bot) {
|
|
76
|
-
return next(
|
|
76
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
77
77
|
}
|
|
78
78
|
const publicKeyPem = await keyStorage.getPublicKey(username)
|
|
79
79
|
const publicKey = await as2.import({
|
|
@@ -100,14 +100,14 @@ router.get('/user/:username/icon', async (req, res, next) => {
|
|
|
100
100
|
const { bots } = req.app.locals
|
|
101
101
|
const bot = await BotMaker.makeBot(bots, username)
|
|
102
102
|
if (!bot) {
|
|
103
|
-
return next(
|
|
103
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
104
104
|
}
|
|
105
105
|
if (!bot.icon) {
|
|
106
|
-
return next(
|
|
106
|
+
return next(new ProblemDetailsError(404, `No icon for ${username} found`))
|
|
107
107
|
}
|
|
108
108
|
|
|
109
109
|
if (typeof bot.icon !== 'object' || !(bot.icon instanceof URL)) {
|
|
110
|
-
return next(
|
|
110
|
+
return next(new ProblemDetailsError(500, 'Incorrect image format from bot'))
|
|
111
111
|
}
|
|
112
112
|
|
|
113
113
|
if (bot.icon.protocol === 'file:') {
|
|
@@ -122,14 +122,14 @@ router.get('/user/:username/image', async (req, res, next) => {
|
|
|
122
122
|
const { bots } = req.app.locals
|
|
123
123
|
const bot = await BotMaker.makeBot(bots, username)
|
|
124
124
|
if (!bot) {
|
|
125
|
-
return next(
|
|
125
|
+
return next(new ProblemDetailsError(404, `User ${username} not found`))
|
|
126
126
|
}
|
|
127
127
|
if (!bot.image) {
|
|
128
|
-
return next(
|
|
128
|
+
return next(new ProblemDetailsError(404, `No image for ${username} found`))
|
|
129
129
|
}
|
|
130
130
|
|
|
131
131
|
if (typeof bot.image !== 'object' || !(bot.image instanceof URL)) {
|
|
132
|
-
return next(
|
|
132
|
+
return next(new ProblemDetailsError(500, 'Incorrect image format from bot'))
|
|
133
133
|
}
|
|
134
134
|
|
|
135
135
|
if (bot.image.protocol === 'file:') {
|
package/lib/routes/webfinger.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import assert from 'node:assert'
|
|
2
2
|
|
|
3
3
|
import { Router } from 'express'
|
|
4
|
-
import
|
|
4
|
+
import { ProblemDetailsError } from '../errors.js'
|
|
5
5
|
|
|
6
6
|
import BotMaker from '../botmaker.js'
|
|
7
7
|
|
|
@@ -11,7 +11,7 @@ async function botWebfinger (username, req, res, next) {
|
|
|
11
11
|
const { formatter, bots } = req.app.locals
|
|
12
12
|
const bot = await BotMaker.makeBot(bots, username)
|
|
13
13
|
if (!bot) {
|
|
14
|
-
return next(
|
|
14
|
+
return next(new ProblemDetailsError(404, `No such bot '${username}'`))
|
|
15
15
|
}
|
|
16
16
|
res.status(200)
|
|
17
17
|
res.type('application/jrd+json')
|
|
@@ -37,7 +37,7 @@ async function profileWebfinger (username, profileUrl, req, res, next) {
|
|
|
37
37
|
const { formatter, bots } = req.app.locals
|
|
38
38
|
const bot = await BotMaker.makeBot(bots, username)
|
|
39
39
|
if (!bot) {
|
|
40
|
-
return next(
|
|
40
|
+
return next(new ProblemDetailsError(404, `No such bot '${username}'`))
|
|
41
41
|
}
|
|
42
42
|
res.status(200)
|
|
43
43
|
res.type('application/jrd+json')
|
|
@@ -57,7 +57,7 @@ async function httpsWebfinger (resource, req, res, next) {
|
|
|
57
57
|
const { formatter } = req.app.locals
|
|
58
58
|
assert.ok(formatter)
|
|
59
59
|
if (!formatter.isLocal(resource)) {
|
|
60
|
-
return next(
|
|
60
|
+
return next(new ProblemDetailsError(400, 'Only local URLs'))
|
|
61
61
|
}
|
|
62
62
|
const parts = formatter.unformat(resource)
|
|
63
63
|
if (parts.username && !parts.type && !parts.collection) {
|
|
@@ -65,18 +65,18 @@ async function httpsWebfinger (resource, req, res, next) {
|
|
|
65
65
|
} else if (parts.username && parts.type === 'profile') {
|
|
66
66
|
return await profileWebfinger(parts.username, resource, req, res, next)
|
|
67
67
|
} else {
|
|
68
|
-
return next(
|
|
68
|
+
return next(new ProblemDetailsError(400, `No webfinger lookup for url ${resource}`))
|
|
69
69
|
}
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
async function acctWebfinger (resource, req, res, next) {
|
|
73
73
|
const [username, domain] = resource.substring(5).split('@')
|
|
74
74
|
if (!username || !domain) {
|
|
75
|
-
return next(
|
|
75
|
+
return next(new ProblemDetailsError(400, `Invalid resource parameter ${resource}`))
|
|
76
76
|
}
|
|
77
77
|
const { host } = new URL(req.app.locals.origin)
|
|
78
78
|
if (domain !== host) {
|
|
79
|
-
return next(
|
|
79
|
+
return next(new ProblemDetailsError(400, `Invalid domain ${domain} in resource parameter`))
|
|
80
80
|
}
|
|
81
81
|
return await botWebfinger(username, req, res, next)
|
|
82
82
|
}
|
|
@@ -84,7 +84,7 @@ async function acctWebfinger (resource, req, res, next) {
|
|
|
84
84
|
router.get('/.well-known/webfinger', async (req, res, next) => {
|
|
85
85
|
const { resource } = req.query
|
|
86
86
|
if (!resource) {
|
|
87
|
-
return next(
|
|
87
|
+
return next(new ProblemDetailsError(400, 'resource parameter is required'))
|
|
88
88
|
}
|
|
89
89
|
const colon = resource.indexOf(':')
|
|
90
90
|
const protocol = resource.slice(0, colon)
|
|
@@ -96,7 +96,7 @@ router.get('/.well-known/webfinger', async (req, res, next) => {
|
|
|
96
96
|
return await httpsWebfinger(resource, req, res, next)
|
|
97
97
|
default:
|
|
98
98
|
return next(
|
|
99
|
-
|
|
99
|
+
new ProblemDetailsError(400, `Unsupported resource protocol '${protocol}'`)
|
|
100
100
|
)
|
|
101
101
|
}
|
|
102
102
|
})
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import assert from 'node:assert'
|
|
2
|
+
|
|
3
|
+
export class SignaturePolicyStorage {
|
|
4
|
+
static RFC9421 = 'rfc9421'
|
|
5
|
+
static DRAFT_CAVAGE_12 = 'draft-cavage-12'
|
|
6
|
+
static #policies = [
|
|
7
|
+
SignaturePolicyStorage.RFC9421,
|
|
8
|
+
SignaturePolicyStorage.DRAFT_CAVAGE_12
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
static #EXPIRY_OFFSET = 30 * 24 * 60 * 60 * 1000
|
|
12
|
+
|
|
13
|
+
#connection
|
|
14
|
+
#logger
|
|
15
|
+
constructor (connection, logger) {
|
|
16
|
+
assert.ok(connection)
|
|
17
|
+
assert.strictEqual(typeof connection, 'object')
|
|
18
|
+
assert.ok(logger)
|
|
19
|
+
assert.strictEqual(typeof logger, 'object')
|
|
20
|
+
this.#connection = connection
|
|
21
|
+
this.#logger = logger.child({ class: this.constructor.name })
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async get (origin) {
|
|
25
|
+
assert.ok(origin)
|
|
26
|
+
assert.strictEqual(typeof origin, 'string')
|
|
27
|
+
const [rows] = await this.#connection.query(
|
|
28
|
+
'SELECT policy, expiry FROM signature_policy WHERE origin = ?',
|
|
29
|
+
{ replacements: [origin] }
|
|
30
|
+
)
|
|
31
|
+
if (rows.length === 0) {
|
|
32
|
+
return null
|
|
33
|
+
}
|
|
34
|
+
const { policy, expiry } = rows[0]
|
|
35
|
+
return ((new Date(expiry)) > (new Date()))
|
|
36
|
+
? policy
|
|
37
|
+
: null
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async set (origin, policy) {
|
|
41
|
+
assert.ok(origin)
|
|
42
|
+
assert.strictEqual(typeof origin, 'string')
|
|
43
|
+
assert.ok(policy)
|
|
44
|
+
assert.strictEqual(typeof policy, 'string')
|
|
45
|
+
assert.ok(SignaturePolicyStorage.#policies.includes(policy))
|
|
46
|
+
|
|
47
|
+
const expiry = new Date(
|
|
48
|
+
Date.now() + SignaturePolicyStorage.#EXPIRY_OFFSET
|
|
49
|
+
)
|
|
50
|
+
|
|
51
|
+
await this.#connection.query(
|
|
52
|
+
`INSERT INTO signature_policy (origin, policy, expiry)
|
|
53
|
+
VALUES (?, ?, ?)
|
|
54
|
+
ON CONFLICT (origin) DO UPDATE
|
|
55
|
+
SET policy = EXCLUDED.policy,
|
|
56
|
+
expiry = EXCLUDED.expiry,
|
|
57
|
+
updated_at = CURRENT_TIMESTAMP`,
|
|
58
|
+
{ replacements: [origin, policy, expiry] }
|
|
59
|
+
)
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@evanp/activitypub-bot",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.39.1",
|
|
4
4
|
"description": "server-side ActivityPub bot framework",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|
|
@@ -32,7 +32,6 @@
|
|
|
32
32
|
"@isaacs/ttlcache": "^2.1.4",
|
|
33
33
|
"activitystrea.ms": "^3.3.0",
|
|
34
34
|
"express": "^5.2.1",
|
|
35
|
-
"http-errors": "^2.0.0",
|
|
36
35
|
"humanhash": "^1.0.4",
|
|
37
36
|
"lru-cache": "^11.1.0",
|
|
38
37
|
"nanoid": "^5.1.5",
|
package/web/profile.html
CHANGED
|
@@ -45,14 +45,18 @@
|
|
|
45
45
|
const u = new URL(url)
|
|
46
46
|
if (u.origin == page.origin) {
|
|
47
47
|
return await fetch(u, options)
|
|
48
|
-
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
return await fetch(u, options)
|
|
52
|
+
} catch (err) {
|
|
49
53
|
return await fetch('/shared/proxy', {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
54
|
+
method: 'POST',
|
|
55
|
+
headers: {
|
|
56
|
+
'Content-Type': 'application/x-www-form-urlencoded'
|
|
57
|
+
},
|
|
58
|
+
body: new URLSearchParams({ id: url })
|
|
59
|
+
})
|
|
56
60
|
}
|
|
57
61
|
}
|
|
58
62
|
|
|
@@ -69,4 +73,4 @@
|
|
|
69
73
|
</script>
|
|
70
74
|
</body>
|
|
71
75
|
|
|
72
|
-
</html>
|
|
76
|
+
</html>
|