2024-01-18 11:50:14 -05:00
|
|
|
'use strict';
|
|
|
|
|
|
2024-02-21 13:43:56 -05:00
|
|
|
const db = require('../database');
|
2024-01-18 11:50:14 -05:00
|
|
|
const meta = require('../meta');
|
|
|
|
|
const activitypub = require('../activitypub');
|
|
|
|
|
|
|
|
|
|
const middleware = module.exports;
|
|
|
|
|
|
|
|
|
|
middleware.enabled = async (req, res, next) => next(!meta.config.activitypubEnabled ? 'route' : undefined);
|
|
|
|
|
|
|
|
|
|
middleware.assertS2S = async function (req, res, next) {
|
|
|
|
|
// For whatever reason, express accepts does not recognize "profile" as a valid differentiator
|
|
|
|
|
// Therefore, manual header parsing is used here.
|
|
|
|
|
const { accept, 'content-type': contentType } = req.headers;
|
|
|
|
|
if (!(accept || contentType)) {
|
|
|
|
|
return next('route');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const pass = (accept && accept.split(',').some((value) => {
|
|
|
|
|
const parts = value.split(';').map(v => v.trim());
|
2024-04-08 20:06:26 +02:00
|
|
|
return activitypub._constants.acceptableTypes.includes(value || parts[0]);
|
|
|
|
|
})) || (contentType && activitypub._constants.acceptableTypes.includes(contentType));
|
2024-01-18 11:50:14 -05:00
|
|
|
|
|
|
|
|
if (!pass) {
|
|
|
|
|
return next('route');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
next();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
middleware.validate = async function (req, res, next) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Validating incoming payload...');
|
2024-01-18 11:50:14 -05:00
|
|
|
|
|
|
|
|
// Sanity-check payload schema
|
2024-05-14 12:06:59 -04:00
|
|
|
const required = ['id', 'type', 'actor', 'object'];
|
2024-01-18 11:50:14 -05:00
|
|
|
if (!required.every(prop => req.body.hasOwnProperty(prop))) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Request body missing required properties.');
|
2024-01-18 11:50:14 -05:00
|
|
|
return res.sendStatus(400);
|
|
|
|
|
}
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Request body check passed.');
|
2024-01-18 11:50:14 -05:00
|
|
|
|
2024-05-14 12:06:59 -04:00
|
|
|
// History check
|
2024-06-14 11:49:25 -04:00
|
|
|
/*
|
|
|
|
|
const seen = await db.isSortedSetMember('activities:datetime', req.body.id);
|
|
|
|
|
if (seen) {
|
|
|
|
|
// winston.verbose(`[middleware/activitypub] Activity already seen, ignoring (${req.body.id}).`);
|
|
|
|
|
return res.sendStatus(200);
|
|
|
|
|
}
|
|
|
|
|
*/
|
2024-05-14 12:06:59 -04:00
|
|
|
|
|
|
|
|
// Checks the validity of the incoming payload against the sender and rejects on failure
|
|
|
|
|
const verified = await activitypub.verify(req);
|
|
|
|
|
if (!verified) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] HTTP signature verification failed.');
|
2024-05-14 12:06:59 -04:00
|
|
|
return res.sendStatus(400);
|
|
|
|
|
}
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] HTTP signature verification passed.');
|
2024-05-14 12:06:59 -04:00
|
|
|
|
2024-04-12 11:08:31 -04:00
|
|
|
let { actor, object } = req.body;
|
2024-02-21 13:43:56 -05:00
|
|
|
|
2024-04-12 16:42:54 +02:00
|
|
|
// Actor normalization
|
|
|
|
|
if (typeof actor === 'object' && actor.hasOwnProperty('id')) {
|
2024-04-12 11:08:31 -04:00
|
|
|
actor = actor.id;
|
|
|
|
|
req.body.actor = actor;
|
2024-04-12 16:42:54 +02:00
|
|
|
}
|
|
|
|
|
if (Array.isArray(actor)) {
|
2024-04-12 11:08:31 -04:00
|
|
|
actor = actor.map(a => (typeof a === 'string' ? a : a.id));
|
|
|
|
|
req.body.actor = actor;
|
2024-04-12 16:42:54 +02:00
|
|
|
}
|
|
|
|
|
|
2024-06-17 15:08:22 -04:00
|
|
|
// Domain check
|
|
|
|
|
const { hostname } = new URL(actor);
|
|
|
|
|
await db.sortedSetAdd('instances:lastSeen', Date.now(), hostname);
|
|
|
|
|
|
2024-02-21 13:43:56 -05:00
|
|
|
// Origin checking
|
2024-02-26 11:39:32 -05:00
|
|
|
if (typeof object !== 'string' && object.hasOwnProperty('id')) {
|
2024-04-12 16:42:54 +02:00
|
|
|
const actorHostnames = Array.isArray(actor) ? actor.map(a => new URL(a).hostname) : [new URL(actor).hostname];
|
2024-02-21 14:05:54 -05:00
|
|
|
const objectHostname = new URL(object.id).hostname;
|
2024-04-12 16:42:54 +02:00
|
|
|
// require that all actors have the same hostname as the object for now
|
|
|
|
|
if (!actorHostnames.every(actorHostname => actorHostname === objectHostname)) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Origin check failed, stripping object down to id.');
|
2024-04-09 23:58:00 +02:00
|
|
|
req.body.object = [object.id];
|
2024-02-21 14:05:54 -05:00
|
|
|
}
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Origin check passed.');
|
2024-02-21 13:43:56 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Cross-check key ownership against received actor
|
|
|
|
|
await activitypub.actors.assert(actor);
|
|
|
|
|
const compare = await db.getObjectField(`userRemote:${actor}:keys`, 'id');
|
|
|
|
|
const { signature } = req.headers;
|
2024-04-28 23:25:46 -04:00
|
|
|
const keyId = new Map(signature.split(',').filter(Boolean).map((v) => {
|
|
|
|
|
const index = v.indexOf('=');
|
|
|
|
|
return [v.substring(0, index), v.slice(index + 1)];
|
|
|
|
|
})).get('keyId');
|
2024-02-21 13:43:56 -05:00
|
|
|
if (`"${compare}"` !== keyId) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Key ownership cross-check failed.');
|
2024-02-21 13:43:56 -05:00
|
|
|
return res.sendStatus(403);
|
|
|
|
|
}
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Key ownership cross-check passed.');
|
2024-02-21 13:43:56 -05:00
|
|
|
|
2024-01-18 11:50:14 -05:00
|
|
|
next();
|
|
|
|
|
};
|
2024-02-05 14:11:32 -05:00
|
|
|
|
2024-04-09 23:58:00 +02:00
|
|
|
middleware.resolveObjects = async function (req, res, next) {
|
2024-05-09 15:48:58 -04:00
|
|
|
const { type, object } = req.body;
|
|
|
|
|
if (type !== 'Delete' && (typeof object === 'string' || (Array.isArray(object) && object.every(o => typeof o === 'string')))) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Resolving object(s)...');
|
2024-04-09 23:58:00 +02:00
|
|
|
try {
|
|
|
|
|
req.body.object = await activitypub.helpers.resolveObjects(object);
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Object(s) successfully resolved.');
|
2024-04-09 23:58:00 +02:00
|
|
|
} catch (e) {
|
2024-06-07 12:13:28 -04:00
|
|
|
// winston.verbose('[middleware/activitypub] Failed to resolve object(s).');
|
2024-04-10 18:15:50 +02:00
|
|
|
return res.sendStatus(424);
|
2024-04-09 23:58:00 +02:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
next();
|
|
|
|
|
};
|
|
|
|
|
|
2024-02-05 14:11:32 -05:00
|
|
|
middleware.configureResponse = async function (req, res, next) {
|
|
|
|
|
res.header('Content-Type', 'application/activity+json');
|
|
|
|
|
next();
|
|
|
|
|
};
|