social-butterfly
Version:
Incorporate federated social network protocols easily. Used with Hello, world federated blog.
193 lines (155 loc) • 5.62 kB
JavaScript
;
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;
}
}