mirror of
				https://github.com/NodeBB/NodeBB.git
				synced 2025-10-31 19:15:58 +01:00 
			
		
		
		
	
		
			
				
	
	
		
			144 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			144 lines
		
	
	
		
			5.1 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
| 'use strict';
 | |
| 
 | |
| const db = require('../database');
 | |
| 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());
 | |
| 		return activitypub._constants.acceptableTypes.includes(value || parts[0]);
 | |
| 	})) || (contentType && activitypub._constants.acceptableTypes.includes(contentType));
 | |
| 
 | |
| 	if (!pass) {
 | |
| 		return next('route');
 | |
| 	}
 | |
| 
 | |
| 	next();
 | |
| };
 | |
| 
 | |
| middleware.verify = async function (req, res, next) {
 | |
| 	// Verifies the HTTP Signature if present (required for POST)
 | |
| 	const passthrough = [/\/actor/, /\/uid\/\d+/];
 | |
| 	if (req.method === 'GET' && passthrough.some(regex => regex.test(req.path))) {
 | |
| 		return next();
 | |
| 	}
 | |
| 
 | |
| 	const verified = await activitypub.verify(req);
 | |
| 	if (!verified && req.method === 'POST') {
 | |
| 		activitypub.helpers.log('[middleware/activitypub] HTTP signature verification failed.');
 | |
| 		return res.sendStatus(400);
 | |
| 	}
 | |
| 
 | |
| 	// Set calling user
 | |
| 	if (req.headers.signature) {
 | |
| 		const keyId = req.headers.signature.split(',').filter(line => line.startsWith('keyId="'));
 | |
| 		if (keyId.length) {
 | |
| 			req.uid = keyId.shift().slice(7, -1).replace(/#.*$/, '');
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	activitypub.helpers.log('[middleware/activitypub] HTTP signature verification passed.');
 | |
| 	next();
 | |
| };
 | |
| 
 | |
| middleware.assertPayload = async function (req, res, next) {
 | |
| 	// Checks the validity of the incoming payload against the sender and rejects on failure
 | |
| 	activitypub.helpers.log('[middleware/activitypub] Validating incoming payload...');
 | |
| 
 | |
| 	// Sanity-check payload schema
 | |
| 	const required = ['id', 'type', 'actor', 'object'];
 | |
| 	if (!required.every(prop => req.body.hasOwnProperty(prop))) {
 | |
| 		activitypub.helpers.log('[middleware/activitypub] Request body missing required properties.');
 | |
| 		return res.sendStatus(400);
 | |
| 	}
 | |
| 	activitypub.helpers.log('[middleware/activitypub] Request body check passed.');
 | |
| 
 | |
| 	// History check
 | |
| 	const seen = await db.isSortedSetMember('activities:datetime', req.body.id);
 | |
| 	if (seen) {
 | |
| 		activitypub.helpers.log(`[middleware/activitypub] Activity already seen, ignoring (${req.body.id}).`);
 | |
| 		return res.sendStatus(200);
 | |
| 	}
 | |
| 
 | |
| 	let { actor, object } = req.body;
 | |
| 
 | |
| 	// Actor normalization
 | |
| 	if (typeof actor === 'object' && actor.hasOwnProperty('id')) {
 | |
| 		actor = actor.id;
 | |
| 		req.body.actor = actor;
 | |
| 	}
 | |
| 	if (Array.isArray(actor)) {
 | |
| 		actor = actor.map(a => (typeof a === 'string' ? a : a.id));
 | |
| 		req.body.actor = actor;
 | |
| 	}
 | |
| 
 | |
| 	// Domain check
 | |
| 	const { hostname } = new URL(actor);
 | |
| 	const allowed = await activitypub.instances.isAllowed(hostname);
 | |
| 	if (!allowed) {
 | |
| 		activitypub.helpers.log(`[middleware/activitypub] Blocked incoming activity from ${hostname}.`);
 | |
| 		return res.sendStatus(403);
 | |
| 	}
 | |
| 	await db.sortedSetAdd('instances:lastSeen', Date.now(), hostname);
 | |
| 
 | |
| 	// Origin checking
 | |
| 	if (typeof object !== 'string' && object.hasOwnProperty('id')) {
 | |
| 		const actorHostnames = Array.isArray(actor) ? actor.map(a => new URL(a).hostname) : [new URL(actor).hostname];
 | |
| 		const objectHostname = new URL(object.id).hostname;
 | |
| 		// require that all actors have the same hostname as the object for now
 | |
| 		if (!actorHostnames.every(actorHostname => actorHostname === objectHostname)) {
 | |
| 			activitypub.helpers.log('[middleware/activitypub] Origin check failed, stripping object down to id.');
 | |
| 			req.body.object = [object.id];
 | |
| 		}
 | |
| 		activitypub.helpers.log('[middleware/activitypub] Origin check passed.');
 | |
| 	}
 | |
| 
 | |
| 	// 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;
 | |
| 	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');
 | |
| 	if (`"${compare}"` !== keyId) {
 | |
| 		activitypub.helpers.log('[middleware/activitypub] Key ownership cross-check failed.');
 | |
| 		return res.sendStatus(403);
 | |
| 	}
 | |
| 	activitypub.helpers.log('[middleware/activitypub] Key ownership cross-check passed.');
 | |
| 
 | |
| 	next();
 | |
| };
 | |
| 
 | |
| middleware.resolveObjects = async function (req, res, next) {
 | |
| 	const { type, object } = req.body;
 | |
| 	if (type !== 'Delete' && (typeof object === 'string' || (Array.isArray(object) && object.every(o => typeof o === 'string')))) {
 | |
| 		activitypub.helpers.log('[middleware/activitypub] Resolving object(s)...');
 | |
| 		try {
 | |
| 			req.body.object = await activitypub.helpers.resolveObjects(object);
 | |
| 			activitypub.helpers.log('[middleware/activitypub] Object(s) successfully resolved.');
 | |
| 		} catch (e) {
 | |
| 			activitypub.helpers.log('[middleware/activitypub] Failed to resolve object(s).');
 | |
| 			return res.sendStatus(424);
 | |
| 		}
 | |
| 	}
 | |
| 	next();
 | |
| };
 | |
| 
 | |
| middleware.configureResponse = async function (req, res, next) {
 | |
| 	res.header('Content-Type', 'application/activity+json');
 | |
| 	next();
 | |
| };
 |