| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | const winston = require('winston'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const { createHash, createSign, createVerify } = require('crypto'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | const request = require('../request'); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | const db = require('../database'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const user = require('../user'); | 
					
						
							| 
									
										
										
										
											2024-01-22 16:18:49 -05:00
										 |  |  | const utils = require('../utils'); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const ttl = require('../cache/ttl'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-12 11:27:55 -05:00
										 |  |  | const requestCache = ttl({ ttl: 1000 * 60 * 5 }); // 5 minutes
 | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | const ActivityPub = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | ActivityPub._constants = Object.freeze({ | 
					
						
							|  |  |  | 	publicAddress: 'https://www.w3.org/ns/activitystreams#Public', | 
					
						
							|  |  |  | }); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | ActivityPub.helpers = require('./helpers'); | 
					
						
							| 
									
										
										
										
											2023-06-28 14:59:39 -04:00
										 |  |  | ActivityPub.inbox = require('./inbox'); | 
					
						
							| 
									
										
										
										
											2024-01-10 14:19:57 -05:00
										 |  |  | ActivityPub.mocks = require('./mocks'); | 
					
						
							| 
									
										
										
										
											2024-01-11 10:05:02 -05:00
										 |  |  | ActivityPub.notes = require('./notes'); | 
					
						
							| 
									
										
										
										
											2024-01-26 15:10:35 -05:00
										 |  |  | ActivityPub.actors = require('./actors'); | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-26 15:10:35 -05:00
										 |  |  | ActivityPub.resolveInboxes = async (ids) => { | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | 	const inboxes = new Set(); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	await Promise.all(ids.map(async (id) => { | 
					
						
							| 
									
										
										
										
											2024-01-26 15:10:35 -05:00
										 |  |  | 		const { inbox, sharedInbox } = await user.getUserFields(id, ['inbox', 'sharedInbox']); | 
					
						
							|  |  |  | 		if (sharedInbox || inbox) { | 
					
						
							|  |  |  | 			inboxes.add(sharedInbox || inbox); | 
					
						
							| 
									
										
										
										
											2024-01-24 11:44:10 -05:00
										 |  |  | 		} | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return Array.from(inboxes); | 
					
						
							|  |  |  | }; | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | ActivityPub.getPublicKey = async (uid) => { | 
					
						
							|  |  |  | 	let publicKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		({ publicKey } = await db.getObject(`uid:${uid}:keys`)); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		({ publicKey } = await ActivityPub.helpers.generateKeys(uid)); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return publicKey; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | ActivityPub.getPrivateKey = async (uid) => { | 
					
						
							|  |  |  | 	let privateKey; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	try { | 
					
						
							|  |  |  | 		({ privateKey } = await db.getObject(`uid:${uid}:keys`)); | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		({ privateKey } = await ActivityPub.helpers.generateKeys(uid)); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return privateKey; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.fetchPublicKey = async (uri) => { | 
					
						
							|  |  |  | 	// Used for retrieving the public key from the passed-in keyId uri
 | 
					
						
							| 
									
										
										
										
											2024-01-22 14:06:39 -05:00
										 |  |  | 	const { response, body } = await request.get(uri, { | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		headers: { | 
					
						
							|  |  |  | 			Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 14:06:39 -05:00
										 |  |  | 	if (!String(response.statusCode).startsWith('2') || !body.hasOwnProperty('publicKey')) { | 
					
						
							| 
									
										
										
										
											2024-01-18 15:21:46 -05:00
										 |  |  | 		throw new Error('[[error:activitypub.pubKey-not-found]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 	return body.publicKey; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.sign = async (uid, url, payload) => { | 
					
						
							| 
									
										
										
										
											2024-01-22 16:18:49 -05:00
										 |  |  | 	// Sanity checking
 | 
					
						
							|  |  |  | 	if (!utils.isNumber(uid) || parseInt(uid, 10) < 0) { | 
					
						
							|  |  |  | 		throw new Error('[[error:invalid-uid]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 	uid = parseInt(uid, 10); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	// Returns string for use in 'Signature' header
 | 
					
						
							|  |  |  | 	const { host, pathname } = new URL(url); | 
					
						
							|  |  |  | 	const date = new Date().toUTCString(); | 
					
						
							|  |  |  | 	const key = await ActivityPub.getPrivateKey(uid); | 
					
						
							|  |  |  | 	const userslug = await user.getUserField(uid, 'userslug'); | 
					
						
							| 
									
										
										
										
											2024-01-24 14:09:40 -05:00
										 |  |  | 	const keyId = `${nconf.get('url')}${uid > 0 ? `/user/${userslug}` : '/actor'}#key`; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	let digest = null; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	let headers = '(request-target) host date'; | 
					
						
							|  |  |  | 	let signed_string = `(request-target): ${payload ? 'post' : 'get'} ${pathname}\nhost: ${host}\ndate: ${date}`; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Calculate payload hash if payload present
 | 
					
						
							|  |  |  | 	if (payload) { | 
					
						
							|  |  |  | 		const payloadHash = createHash('sha256'); | 
					
						
							|  |  |  | 		payloadHash.update(JSON.stringify(payload)); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 		digest = `sha-256=${payloadHash.digest('base64')}`; | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		headers += ' digest'; | 
					
						
							|  |  |  | 		signed_string += `\ndigest: ${digest}`; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Sign string using private key
 | 
					
						
							|  |  |  | 	let signature = createSign('sha256'); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 	signature.update(signed_string); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	signature.end(); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 	signature = signature.sign(key, 'base64'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	// Construct signature header
 | 
					
						
							|  |  |  | 	return { | 
					
						
							|  |  |  | 		date, | 
					
						
							|  |  |  | 		digest, | 
					
						
							|  |  |  | 		signature: `keyId="${keyId}",headers="${headers}",signature="${signature}"`, | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | ActivityPub.verify = async (req) => { | 
					
						
							| 
									
										
										
										
											2024-01-19 11:43:21 -05:00
										 |  |  | 	if (!req.headers.hasOwnProperty('signature')) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 	// Break the signature apart
 | 
					
						
							|  |  |  | 	const { keyId, headers, signature } = req.headers.signature.split(',').reduce((memo, cur) => { | 
					
						
							|  |  |  | 		const split = cur.split('="'); | 
					
						
							|  |  |  | 		const key = split.shift(); | 
					
						
							|  |  |  | 		const value = split.join('="'); | 
					
						
							|  |  |  | 		memo[key] = value.slice(0, -1); | 
					
						
							|  |  |  | 		return memo; | 
					
						
							|  |  |  | 	}, {}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Re-construct signature string
 | 
					
						
							|  |  |  | 	const signed_string = headers.split(' ').reduce((memo, cur) => { | 
					
						
							|  |  |  | 		if (cur === '(request-target)') { | 
					
						
							| 
									
										
										
										
											2023-08-08 15:33:35 -04:00
										 |  |  | 			memo.push(`${cur}: ${String(req.method).toLowerCase()} ${req.baseUrl}${req.path}`); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		} else if (req.headers.hasOwnProperty(cur)) { | 
					
						
							|  |  |  | 			memo.push(`${cur}: ${req.headers[cur]}`); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return memo; | 
					
						
							|  |  |  | 	}, []).join('\n'); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Verify the signature string via public key
 | 
					
						
							|  |  |  | 	try { | 
					
						
							| 
									
										
										
										
											2024-01-18 15:21:46 -05:00
										 |  |  | 		// Retrieve public key from remote instance
 | 
					
						
							|  |  |  | 		const { publicKeyPem } = await ActivityPub.fetchPublicKey(keyId); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		const verify = createVerify('sha256'); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 		verify.update(signed_string); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		verify.end(); | 
					
						
							| 
									
										
										
										
											2023-12-22 12:58:46 -05:00
										 |  |  | 		const verified = verify.verify(publicKeyPem, signature, 'base64'); | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | 		return verified; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		return false; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | ActivityPub.get = async (uid, uri) => { | 
					
						
							| 
									
										
										
										
											2024-01-12 11:27:55 -05:00
										 |  |  | 	const cacheKey = [uid, uri].join(';'); | 
					
						
							|  |  |  | 	if (requestCache.has(cacheKey)) { | 
					
						
							|  |  |  | 		return requestCache.get(cacheKey); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-22 16:18:49 -05:00
										 |  |  | 	const headers = uid >= 0 ? await ActivityPub.sign(uid, uri) : {}; | 
					
						
							| 
									
										
										
										
											2024-01-13 22:27:02 -05:00
										 |  |  | 	winston.verbose(`[activitypub/get] ${uri}`); | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 	const { response, body } = await request.get(uri, { | 
					
						
							| 
									
										
										
										
											2024-01-05 11:39:17 -05:00
										 |  |  | 		headers: { | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 			...headers, | 
					
						
							| 
									
										
										
										
											2024-01-05 11:39:17 -05:00
										 |  |  | 			Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 	if (!String(response.statusCode).startsWith('2')) { | 
					
						
							|  |  |  | 		winston.error(`[activitypub/get] Received ${response.statusCode} when querying ${uri}`); | 
					
						
							|  |  |  | 		if (body.hasOwnProperty('error')) { | 
					
						
							|  |  |  | 			winston.error(`[activitypub/get] Error received: ${body.error}`); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2024-01-08 14:30:09 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 		throw new Error(`[[error:activitypub.get-failed]]`); | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-12 11:27:55 -05:00
										 |  |  | 	requestCache.set(cacheKey, body); | 
					
						
							| 
									
										
										
										
											2024-01-05 11:39:17 -05:00
										 |  |  | 	return body; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | ActivityPub.send = async (uid, targets, payload) => { | 
					
						
							|  |  |  | 	if (!Array.isArray(targets)) { | 
					
						
							|  |  |  | 		targets = [targets]; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	const userslug = await user.getUserField(uid, 'userslug'); | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | 	const inboxes = await ActivityPub.resolveInboxes(uid, targets); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	payload = { | 
					
						
							| 
									
										
										
										
											2023-12-13 13:15:03 -05:00
										 |  |  | 		'@context': 'https://www.w3.org/ns/activitystreams', | 
					
						
							| 
									
										
										
										
											2023-12-22 13:56:18 -05:00
										 |  |  | 		actor: `${nconf.get('url')}/user/${userslug}`, | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 		...payload, | 
					
						
							|  |  |  | 	}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	await Promise.all(inboxes.map(async (uri) => { | 
					
						
							| 
									
										
										
										
											2023-12-22 15:53:04 -05:00
										 |  |  | 		const headers = await ActivityPub.sign(uid, uri, payload); | 
					
						
							| 
									
										
										
										
											2023-12-21 14:46:03 -05:00
										 |  |  | 		const { response } = await request.post(uri, { | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 			headers: { | 
					
						
							| 
									
										
										
										
											2023-12-22 15:53:04 -05:00
										 |  |  | 				...headers, | 
					
						
							| 
									
										
										
										
											2023-06-23 15:25:00 -04:00
										 |  |  | 				'content-type': 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 			}, | 
					
						
							|  |  |  | 			body: payload, | 
					
						
							|  |  |  | 		}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-22 16:02:27 -05:00
										 |  |  | 		if (!String(response.statusCode).startsWith('2')) { | 
					
						
							| 
									
										
										
										
											2023-06-26 16:15:25 -04:00
										 |  |  | 			// todo: i18n this
 | 
					
						
							|  |  |  | 			throw new Error('activity-failed'); | 
					
						
							|  |  |  | 		} | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	})); | 
					
						
							|  |  |  | }; |