| 
									
										
										
										
											2023-05-17 13:13:30 -04:00
										 |  |  | 'use strict'; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-19 17:29:22 -04:00
										 |  |  | const nconf = require('nconf'); | 
					
						
							|  |  |  | 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'); | 
					
						
							| 
									
										
										
										
											2023-06-02 14:22:43 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | ActivityPub.getActor = async (input) => { | 
					
						
							|  |  |  | 	// 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
										 |  |  | 	} | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-19 14:33:38 -05:00
										 |  |  | 	const { body: actor } = await request.get(uri, { | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 		headers: { | 
					
						
							|  |  |  | 			Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"', | 
					
						
							|  |  |  | 		}, | 
					
						
							|  |  |  | 	}); | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	actor.hostname = new URL(uri).hostname; | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-12-13 13:14:51 -05:00
										 |  |  | 	actorCache.set(uri, actor); | 
					
						
							| 
									
										
										
										
											2023-05-29 17:42:44 -04:00
										 |  |  | 	return actor; | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2024-01-04 16:25:46 -05:00
										 |  |  | ActivityPub.mockProfile = async (actors, callerUid = 0) => { | 
					
						
							|  |  |  | 	// Accepts an array containing actor objects (the output of getActor()), or uris
 | 
					
						
							|  |  |  | 	let single = false; | 
					
						
							|  |  |  | 	if (!Array.isArray(actors)) { | 
					
						
							|  |  |  | 		single = true; | 
					
						
							|  |  |  | 		actors = [actors]; | 
					
						
							| 
									
										
										
										
											2024-01-04 14:04:34 -05:00
										 |  |  | 	} | 
					
						
							| 
									
										
										
										
											2024-01-04 16:25:46 -05:00
										 |  |  | 
 | 
					
						
							|  |  |  | 	const profiles = await Promise.all(actors.map(async (actor) => { | 
					
						
							|  |  |  | 		// convert uri to actor object
 | 
					
						
							|  |  |  | 		if (typeof actor === 'string' && ActivityPub.helpers.isUri(actor)) { | 
					
						
							|  |  |  | 			actor = await ActivityPub.getActor(actor); | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const uid = actor.id; | 
					
						
							|  |  |  | 		const { preferredUsername, published, icon, image, name, summary, hostname } = actor; | 
					
						
							|  |  |  | 		const isFollowing = await db.isSortedSetMember(`followingRemote:${callerUid}`, uid); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		let picture; | 
					
						
							|  |  |  | 		if (icon) { | 
					
						
							|  |  |  | 			picture = typeof icon === 'string' ? icon : icon.url; | 
					
						
							|  |  |  | 		} | 
					
						
							|  |  |  | 		const iconBackgrounds = await user.getIconBackgrounds(); | 
					
						
							|  |  |  | 		let bgColor = Array.prototype.reduce.call(preferredUsername, (cur, next) => cur + next.charCodeAt(), 0); | 
					
						
							|  |  |  | 		bgColor = iconBackgrounds[bgColor % iconBackgrounds.length]; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		const payload = { | 
					
						
							|  |  |  | 			uid, | 
					
						
							|  |  |  | 			username: `${preferredUsername}@${hostname}`, | 
					
						
							|  |  |  | 			userslug: `${preferredUsername}@${hostname}`, | 
					
						
							|  |  |  | 			fullname: name, | 
					
						
							|  |  |  | 			joindate: new Date(published).getTime(), | 
					
						
							|  |  |  | 			picture, | 
					
						
							|  |  |  | 			'icon:text': (preferredUsername[0] || '').toUpperCase(), | 
					
						
							|  |  |  | 			'icon:bgColor': bgColor, | 
					
						
							|  |  |  | 			uploadedpicture: undefined, | 
					
						
							|  |  |  | 			'cover:url': !image || typeof image === 'string' ? image : image.url, | 
					
						
							|  |  |  | 			'cover:position': '50% 50%', | 
					
						
							|  |  |  | 			aboutme: summary, | 
					
						
							|  |  |  | 			aboutmeParsed: summary, | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 			isFollowing, | 
					
						
							|  |  |  | 		}; | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 		return payload; | 
					
						
							|  |  |  | 	})); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	return single ? profiles.pop() : profiles; | 
					
						
							| 
									
										
										
										
											2024-01-04 14:04:34 -05:00
										 |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											2023-06-23 14:59:47 -04:00
										 |  |  | ActivityPub.resolveInboxes = async ids => await Promise.all(ids.map(async (id) => { | 
					
						
							|  |  |  | 	const actor = await ActivityPub.getActor(id); | 
					
						
							|  |  |  | 	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; | 
					
						
							|  |  |  | 	} | 
					
						
							|  |  |  | }; | 
					
						
							|  |  |  | 
 | 
					
						
							| 
									
										
										
										
											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'); | 
					
						
							|  |  |  | 	const inboxes = await ActivityPub.resolveInboxes(targets); | 
					
						
							|  |  |  | 
 | 
					
						
							|  |  |  | 	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
										 |  |  | 	})); | 
					
						
							|  |  |  | }; |