UNPKG

social-butterfly

Version:

Incorporate federated social network protocols easily. Used with Hello, world federated blog.

193 lines (155 loc) 5.62 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.send = send; exports.default = void 0; var _activitystreams = require("./activitystreams"); var _url_factory = require("./util/url_factory"); var _crypto = _interopRequireDefault(require("crypto")); var _express = _interopRequireDefault(require("express")); var _nodeFetch = _interopRequireDefault(require("node-fetch")); var _nodeForge = _interopRequireDefault(require("node-forge")); var _magicSignatures = _interopRequireDefault(require("magic-signatures")); function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } var _default = options => { const activityPubRouter = _express.default.Router(); activityPubRouter.get('/actor', Actor(options)); activityPubRouter.post('/inbox', Inbox(options)); activityPubRouter.get('/message', Message(options)); return activityPubRouter; }; exports.default = _default; const Actor = options => async (req, res, next) => { const resource = req.query.resource; const user = await options.getLocalUser(resource, req); if (!user) { return res.sendStatus(404); } const actorUrl = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/actor', searchParams: { resource } }); const inboxUrl = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/inbox', searchParams: { resource } }); const json = { '@context': ['https://www.w3.org/ns/activitystreams', 'https://w3id.org/security/v1'], id: actorUrl, type: 'Person', preferredUsername: user.username, inbox: inboxUrl, url: user.url, publicKey: { id: `${actorUrl}#main-key`, owner: actorUrl, publicKeyPem: _nodeForge.default.pki.publicKeyToPem(_magicSignatures.default.magicToRSA(user.magic_key)) } }; res.json(json); }; const Inbox = options => async (req, res, next) => { if (!req.query.resource) { return res.sendStatus(400); } const user = await options.getLocalUser(req.query.resource, req); if (!user) { return res.sendStatus(404); } const userRemote = await (0, _activitystreams.findUserRemote)(options, req.body, res, user); if (!userRemote) { console.log('activitypub fail: ', req.body); return res.sendStatus(401); } const success = verifyMessage(req, userRemote); if (!success) { return res.sendStatus(401); } await (0, _activitystreams.handle)(req.body.type, options, req, res, req.body, user, userRemote); (0, _activitystreams.accept)(req, user, userRemote); res.sendStatus(204); }; const Message = options => async (req, res, next) => { const content = await options.getLocalContent(req.query.resource, req); const user = await options.getLocalUser(req.query.resource, req); if (!content || !user) { return res.sendStatus(404); } const json = (await (0, _activitystreams.createArticle)(req, content, user)).object; json['@context'] = 'https://www.w3.org/ns/activitystreams'; res.json(json); }; async function send(req, userRemote, contentOwner, message) { const { currentDate, signatureHeader } = signMessage(req, contentOwner, userRemote); const inboxUrl = new URL(userRemote.activitypub_inbox_url); try { await (0, _nodeFetch.default)(userRemote.activitypub_inbox_url, { method: 'POST', body: JSON.stringify(message), headers: { Host: inboxUrl.hostname, Date: currentDate.toUTCString(), Signature: signatureHeader, 'Content-Type': 'application/ld+json' } }); } catch (ex) {// Not a big deal if this fails. // TODO(mime): add logging later. } } function signMessage(req, contentOwner, userRemote) { const currentDate = new Date(); const inboxUrl = new URL(userRemote.activitypub_inbox_url); const signer = _crypto.default.createSign('sha256').update(`(request-target): post ${inboxUrl.pathname}${inboxUrl.search}\n`).update(`host: ${inboxUrl.hostname}\n`).update(`date: ${currentDate.toUTCString()}`).end(); const signature = signer.sign(contentOwner.private_key).toString('base64'); const actorUrl = (0, _url_factory.buildUrl)({ req, pathname: '/api/social/activitypub/actor', searchParams: { resource: contentOwner.url } }); const signatureHeader = `keyId="${actorUrl}",headers="(request-target) host date",signature="${signature}"`; return { currentDate, signatureHeader }; } function verifyMessage(req, userRemote) { try { const signatureMap = {}; req.headers['signature'].split(',').forEach(keyValue => { const keyValuePair = keyValue.split('='); signatureMap[keyValuePair[0]] = keyValuePair.slice(1).join('=').replace(/^"/, '').replace(/"$/, ''); }); if ((new Date() - new Date(req.headers['Date'])) / 1000 < 60 * 5 /* 5 minutes */ ) { // Date is not within a reasonable time frame. return false; } const data = signatureMap['headers'].split(' ').map(header => { return header === '(request-target)' ? `(request-target): post ${req.originalUrl}` : `${header}: ${req.headers[header]}`; }).join('\n'); const verify = _crypto.default.createVerify('sha256'); verify.write(data); verify.end(); let publicKey = userRemote.magic_key; if (publicKey.startsWith('RSA.')) { publicKey = _nodeForge.default.pki.publicKeyToPem(_magicSignatures.default.magicToRSA(userRemote.magic_key)); } return verify.verify(publicKey, signatureMap['signature'], 'base64'); } catch (ex) { return false; } }