| 
									
										
										
										
											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'); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const ttl = require('../cache/ttl'); | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours
 | 
					
						
							| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | const ActivityPub = module.exports; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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'); | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | ActivityPub.getActor = async (uid, input) => { | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	// Can be a webfinger id, uri, or object, handle as appropriate
 | 
					
						
							|  |  |  | 	let uri; | 
					
						
							| 
									
										
										
										
											2024-01-04 16:23:09 -05:00
										 |  |  | 	if (ActivityPub.helpers.isUri(input)) { | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 		uri = input; | 
					
						
							|  |  |  | 	} else if (input.indexOf('@') !== -1) { // Webfinger
 | 
					
						
							|  |  |  | 		({ actorUri: uri } = await ActivityPub.helpers.query(input)); | 
					
						
							|  |  |  | 	} else { | 
					
						
							|  |  |  | 		throw new Error('[[error:invalid-data]]'); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-14 13:47:28 -05:00
										 |  |  | 	if (!uri) { | 
					
						
							|  |  |  | 		throw new Error('[[error:invalid-uid]]'); | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	if (actorCache.has(uri)) { | 
					
						
							|  |  |  | 		return actorCache.get(uri); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-08 14:30:09 -05:00
										 |  |  | 	try { | 
					
						
							|  |  |  | 		const actor = await ActivityPub.get(uid, uri); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		// Follow counts
 | 
					
						
							|  |  |  | 		const [followers, following] = await Promise.all([ | 
					
						
							|  |  |  | 			actor.followers ? ActivityPub.get(uid, actor.followers) : { totalItems: 0 }, | 
					
						
							|  |  |  | 			actor.following ? ActivityPub.get(uid, actor.following) : { totalItems: 0 }, | 
					
						
							|  |  |  | 		]); | 
					
						
							|  |  |  | 		actor.followerCount = followers.totalItems; | 
					
						
							|  |  |  | 		actor.followingCount = following.totalItems; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-08 14:45:56 -05:00
										 |  |  | 		actor.hostname = new URL(uri).hostname; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-08 14:30:09 -05:00
										 |  |  | 		actorCache.set(uri, actor); | 
					
						
							|  |  |  | 		return actor; | 
					
						
							|  |  |  | 	} catch (e) { | 
					
						
							|  |  |  | 		winston.warn(`[activitypub/getActor] Unable to retrieve actor "${uri}", error: ${e.message}`); | 
					
						
							| 
									
										
										
										
											2024-01-05 15:31:09 -05:00
										 |  |  | 		return null; | 
					
						
							|  |  |  | 	} | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-05 22:45:33 -05:00
										 |  |  | ActivityPub.resolveInboxes = async (uid, ids) => await Promise.all(ids.map(async (id) => { | 
					
						
							|  |  |  | 	const actor = await ActivityPub.getActor(uid, id); | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | 	return actor.inbox; | 
					
						
							|  |  |  | })); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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
 | 
					
						
							| 
									
										
										
										
											2023-12-21 14:38:16 -05:00
										 |  |  | 	const { 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
										 |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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) => { | 
					
						
							|  |  |  | 	// 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'); | 
					
						
							|  |  |  | 	const keyId = `${nconf.get('url')}/user/${userslug}#key`; | 
					
						
							|  |  |  | 	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) => { | 
					
						
							|  |  |  | 	// 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; | 
					
						
							|  |  |  | 	}, {}); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// Retrieve public key from remote instance
 | 
					
						
							|  |  |  | 	const { publicKeyPem } = await ActivityPub.fetchPublicKey(keyId); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	// 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 { | 
					
						
							|  |  |  | 		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) => { | 
					
						
							|  |  |  | 	const headers = uid > 0 ? await ActivityPub.sign(uid, uri) : {}; | 
					
						
							|  |  |  | 	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-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
										 |  |  | 	})); | 
					
						
							|  |  |  | }; |