feat: actor cache, method to resolve inboxes, stub code for sending requests. Now base64 encoding digest as expected by Mastodon

This commit is contained in:
Julian Lam
2023-06-23 14:59:47 -04:00
parent 2e89900886
commit cdc4275fec
5 changed files with 87 additions and 27 deletions

View File

@@ -1,7 +1,7 @@
'use strict'; 'use strict';
const request = require('request-promise-native'); const request = require('request-promise-native');
const { generateKeyPairSync, sign } = require('crypto'); const { generateKeyPairSync } = require('crypto');
const winston = require('winston'); const winston = require('winston');
const db = require('../database'); const db = require('../database');

View File

@@ -7,12 +7,18 @@ const { createHash, createSign, createVerify } = require('crypto');
const db = require('../database'); const db = require('../database');
const user = require('../user'); const user = require('../user');
const ttl = require('../cache/ttl');
const actorCache = ttl({ ttl: 1000 * 60 * 60 * 24 }); // 24 hours
const ActivityPub = module.exports; const ActivityPub = module.exports;
ActivityPub.helpers = require('./helpers'); ActivityPub.helpers = require('./helpers');
ActivityPub.getActor = async (id) => { ActivityPub.getActor = async (id) => {
if (actorCache.has(id)) {
return actorCache.get(id);
}
const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id); const { hostname, actorUri: uri } = await ActivityPub.helpers.query(id);
if (!uri) { if (!uri) {
return false; return false;
@@ -28,9 +34,15 @@ ActivityPub.getActor = async (id) => {
actor.hostname = hostname; actor.hostname = hostname;
actorCache.set(id, actor);
return actor; return actor;
}; };
ActivityPub.resolveInboxes = async ids => await Promise.all(ids.map(async (id) => {
const actor = await ActivityPub.getActor(id);
return actor.inbox;
}));
ActivityPub.getPublicKey = async (uid) => { ActivityPub.getPublicKey = async (uid) => {
let publicKey; let publicKey;
@@ -84,7 +96,7 @@ ActivityPub.sign = async (uid, url, payload) => {
if (payload) { if (payload) {
const payloadHash = createHash('sha256'); const payloadHash = createHash('sha256');
payloadHash.update(JSON.stringify(payload)); payloadHash.update(JSON.stringify(payload));
digest = payloadHash.digest('hex'); digest = `sha-256=${payloadHash.digest('base64')}`;
headers += ' digest'; headers += ' digest';
signed_string += `\ndigest: ${digest}`; signed_string += `\ndigest: ${digest}`;
} }
@@ -146,26 +158,43 @@ ActivityPub.verify = async (req) => {
} }
}; };
/** ActivityPub.send = async (uid, targets, payload) => {
* This is just some code to test signing and verification. This should really be in the test suite. if (!Array.isArray(targets)) {
*/ targets = [targets];
// setTimeout(async () => { }
// const payload = {
// foo: 'bar',
// };
// const signature = await ActivityPub.sign(1, 'http://127.0.0.1:4567/user/julian/inbox', payload);
// const res = await request({ const userslug = await user.getUserField(uid, 'userslug');
// uri: 'http://127.0.0.1:4567/user/julian/inbox', const inboxes = await ActivityPub.resolveInboxes(targets);
// method: 'post',
// headers: {
// Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
// ...signature,
// },
// json: true,
// body: payload,
// simple: false,
// });
// console.log(res); payload = {
// }, 1000); ...{
'@context': 'https://www.w3.org/ns/activitystreams',
actor: {
type: 'Person',
name: `${userslug}@${nconf.get('url_parsed').host}`,
},
},
...payload,
};
await Promise.all(inboxes.map(async (uri) => {
const { date, digest, signature } = await ActivityPub.sign(uid, uri, payload);
const response = await request(uri, {
method: payload ? 'post' : 'get',
headers: {
date,
digest,
signature,
'content-type': 'application/ld+json; profile="http://www.w3.org/ns/activitystreams',
accept: 'application/ld+json; profile="http://www.w3.org/ns/activitystreams',
},
json: true,
body: payload,
simple: false,
resolveWithFullResponse: true,
});
console.log(response.statusCode, response.body);
}));
};

View File

@@ -2,7 +2,7 @@
const nconf = require('nconf'); const nconf = require('nconf');
const user = require('../user'); const user = require('../../user');
const activitypub = require('../../activitypub'); const activitypub = require('../../activitypub');
const Controller = module.exports; const Controller = module.exports;
@@ -103,3 +103,24 @@ Controller.postInbox = async (req, res) => {
res.sendStatus(201); res.sendStatus(201);
}; };
/**
* Main ActivityPub verbs
*/
Controller.follow = async (req, res) => {
await activitypub.send(req.uid, req.params.uid, {
type: 'Follow',
object: {
type: 'Person',
name: req.params.uid,
},
});
res.sendStatus(201);
};
Controller.unfollow = async (req, res) => {
console.log('got here');
res.sendStatus(201);
};

View File

@@ -9,6 +9,8 @@ const user = require('../../user');
const helpers = require('../helpers'); const helpers = require('../helpers');
const activitypubController = require('../activitypub');
const Users = module.exports; const Users = module.exports;
Users.redirectBySlug = async (req, res) => { Users.redirectBySlug = async (req, res) => {
@@ -92,11 +94,19 @@ Users.changePassword = async (req, res) => {
}; };
Users.follow = async (req, res) => { Users.follow = async (req, res) => {
if (req.params.uid.indexOf('@') !== -1) {
return await activitypubController.follow(req, res);
}
await api.users.follow(req, req.params); await api.users.follow(req, req.params);
helpers.formatApiResponse(200, res); helpers.formatApiResponse(200, res);
}; };
Users.unfollow = async (req, res) => { Users.unfollow = async (req, res) => {
if (req.params.uid.indexOf('@') !== -1) {
return await activitypubController.unfollow(req, res);
}
await api.users.unfollow(req, req.params); await api.users.unfollow(req, req.params);
helpers.formatApiResponse(200, res); helpers.formatApiResponse(200, res);
}; };

View File

@@ -251,14 +251,14 @@ describe('ActivityPub integration', () => {
const { digest } = await activitypub.sign(uid, endpoint, payload); const { digest } = await activitypub.sign(uid, endpoint, payload);
const hash = createHash('sha256'); const hash = createHash('sha256');
hash.update(JSON.stringify(payload)); hash.update(JSON.stringify(payload));
const checksum = hash.digest('hex'); const checksum = hash.digest('base64');
assert(digest); assert(digest);
assert.strictEqual(digest, checksum); assert.strictEqual(digest, `sha-256=${checksum}`);
}); });
}); });
describe.only('.verify()', () => { describe('.verify()', () => {
let uid; let uid;
let username; let username;
const mockReqBase = { const mockReqBase = {