mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-07-06 09:48:12 +02:00
feat: application actor + public key for uid 0 signs, moved homepage route to after core routes are generated
This commit is contained in:
@@ -7,6 +7,7 @@ const { createHash, createSign, createVerify } = require('crypto');
|
|||||||
const request = require('../request');
|
const request = require('../request');
|
||||||
const db = require('../database');
|
const db = require('../database');
|
||||||
const user = require('../user');
|
const user = require('../user');
|
||||||
|
const utils = require('../utils');
|
||||||
const ttl = require('../cache/ttl');
|
const ttl = require('../cache/ttl');
|
||||||
|
|
||||||
const requestCache = ttl({ ttl: 1000 * 60 * 5 }); // 5 minutes
|
const requestCache = ttl({ ttl: 1000 * 60 * 5 }); // 5 minutes
|
||||||
@@ -103,12 +104,18 @@ ActivityPub.fetchPublicKey = async (uri) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
ActivityPub.sign = async (uid, url, payload) => {
|
ActivityPub.sign = async (uid, url, payload) => {
|
||||||
|
// Sanity checking
|
||||||
|
if (!utils.isNumber(uid) || parseInt(uid, 10) < 0) {
|
||||||
|
throw new Error('[[error:invalid-uid]]');
|
||||||
|
}
|
||||||
|
uid = parseInt(uid, 10);
|
||||||
|
|
||||||
// Returns string for use in 'Signature' header
|
// Returns string for use in 'Signature' header
|
||||||
const { host, pathname } = new URL(url);
|
const { host, pathname } = new URL(url);
|
||||||
const date = new Date().toUTCString();
|
const date = new Date().toUTCString();
|
||||||
const key = await ActivityPub.getPrivateKey(uid);
|
const key = await ActivityPub.getPrivateKey(uid);
|
||||||
const userslug = await user.getUserField(uid, 'userslug');
|
const userslug = await user.getUserField(uid, 'userslug');
|
||||||
const keyId = `${nconf.get('url')}/user/${userslug}#key`;
|
const keyId = `${nconf.get('url')}${uid > 0 ? `/user/${userslug}` : ''}#key`;
|
||||||
let digest = null;
|
let digest = null;
|
||||||
|
|
||||||
let headers = '(request-target) host date';
|
let headers = '(request-target) host date';
|
||||||
@@ -183,7 +190,7 @@ ActivityPub.get = async (uid, uri) => {
|
|||||||
return requestCache.get(cacheKey);
|
return requestCache.get(cacheKey);
|
||||||
}
|
}
|
||||||
|
|
||||||
const headers = uid > 0 ? await ActivityPub.sign(uid, uri) : {};
|
const headers = uid >= 0 ? await ActivityPub.sign(uid, uri) : {};
|
||||||
winston.verbose(`[activitypub/get] ${uri}`);
|
winston.verbose(`[activitypub/get] ${uri}`);
|
||||||
const { response, body } = await request.get(uri, {
|
const { response, body } = await request.get(uri, {
|
||||||
headers: {
|
headers: {
|
||||||
|
|||||||
68
src/controllers/activitypub/actors.js
Normal file
68
src/controllers/activitypub/actors.js
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
'use strict';
|
||||||
|
|
||||||
|
const nconf = require('nconf');
|
||||||
|
|
||||||
|
const user = require('../../user');
|
||||||
|
const meta = require('../../meta');
|
||||||
|
const activitypub = require('../../activitypub');
|
||||||
|
|
||||||
|
const Actors = module.exports;
|
||||||
|
|
||||||
|
Actors.application = async function (req, res) {
|
||||||
|
const publicKey = await activitypub.getPublicKey(0);
|
||||||
|
const name = meta.config.title || 'NodeBB';
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
],
|
||||||
|
id: `${nconf.get('url')}`,
|
||||||
|
url: `${nconf.get('url')}`,
|
||||||
|
inbox: `${nconf.get('url')}/inbox`,
|
||||||
|
outbox: `${nconf.get('url')}/outbox`,
|
||||||
|
|
||||||
|
type: 'Application',
|
||||||
|
name,
|
||||||
|
|
||||||
|
publicKey: {
|
||||||
|
id: `${nconf.get('url')}#key`,
|
||||||
|
owner: nconf.get('url'),
|
||||||
|
publicKeyPem: publicKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
Actors.user = async function (req, res) {
|
||||||
|
// todo: view:users priv gate
|
||||||
|
const { userslug } = req.params;
|
||||||
|
const { uid } = res.locals;
|
||||||
|
const { username, displayname: name, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid);
|
||||||
|
const publicKey = await activitypub.getPublicKey(uid);
|
||||||
|
|
||||||
|
res.status(200).json({
|
||||||
|
'@context': [
|
||||||
|
'https://www.w3.org/ns/activitystreams',
|
||||||
|
'https://w3id.org/security/v1',
|
||||||
|
],
|
||||||
|
id: `${nconf.get('url')}/user/${userslug}`,
|
||||||
|
url: `${nconf.get('url')}/user/${userslug}`,
|
||||||
|
followers: `${nconf.get('url')}/user/${userslug}/followers`,
|
||||||
|
following: `${nconf.get('url')}/user/${userslug}/following`,
|
||||||
|
inbox: `${nconf.get('url')}/user/${userslug}/inbox`,
|
||||||
|
outbox: `${nconf.get('url')}/user/${userslug}/outbox`,
|
||||||
|
|
||||||
|
type: 'Person',
|
||||||
|
name,
|
||||||
|
preferredUsername: username,
|
||||||
|
summary: aboutme,
|
||||||
|
icon: picture ? `${nconf.get('url')}${picture}` : null,
|
||||||
|
image: cover ? `${nconf.get('url')}${cover}` : null,
|
||||||
|
|
||||||
|
publicKey: {
|
||||||
|
id: `${nconf.get('url')}/user/${userslug}#key`,
|
||||||
|
owner: `${nconf.get('url')}/user/${userslug}`,
|
||||||
|
publicKeyPem: publicKey,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
@@ -9,43 +9,10 @@ const helpers = require('../helpers');
|
|||||||
|
|
||||||
const Controller = module.exports;
|
const Controller = module.exports;
|
||||||
|
|
||||||
|
Controller.actors = require('./actors');
|
||||||
Controller.profiles = require('./profiles');
|
Controller.profiles = require('./profiles');
|
||||||
Controller.topics = require('./topics');
|
Controller.topics = require('./topics');
|
||||||
|
|
||||||
Controller.getActor = async (req, res) => {
|
|
||||||
// todo: view:users priv gate
|
|
||||||
const { userslug } = req.params;
|
|
||||||
const { uid } = res.locals;
|
|
||||||
const { username, displayname: name, aboutme, picture, 'cover:url': cover } = await user.getUserData(uid);
|
|
||||||
const publicKey = await activitypub.getPublicKey(uid);
|
|
||||||
|
|
||||||
res.status(200).json({
|
|
||||||
'@context': [
|
|
||||||
'https://www.w3.org/ns/activitystreams',
|
|
||||||
'https://w3id.org/security/v1',
|
|
||||||
],
|
|
||||||
id: `${nconf.get('url')}/user/${userslug}`,
|
|
||||||
url: `${nconf.get('url')}/user/${userslug}`,
|
|
||||||
followers: `${nconf.get('url')}/user/${userslug}/followers`,
|
|
||||||
following: `${nconf.get('url')}/user/${userslug}/following`,
|
|
||||||
inbox: `${nconf.get('url')}/user/${userslug}/inbox`,
|
|
||||||
outbox: `${nconf.get('url')}/user/${userslug}/outbox`,
|
|
||||||
|
|
||||||
type: 'Person',
|
|
||||||
name,
|
|
||||||
preferredUsername: username,
|
|
||||||
summary: aboutme,
|
|
||||||
icon: picture ? `${nconf.get('url')}${picture}` : null,
|
|
||||||
image: cover ? `${nconf.get('url')}${cover}` : null,
|
|
||||||
|
|
||||||
publicKey: {
|
|
||||||
id: `${nconf.get('url')}/user/${userslug}#key`,
|
|
||||||
owner: `${nconf.get('url')}/user/${userslug}`,
|
|
||||||
publicKeyPem: publicKey,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
Controller.getFollowing = async (req, res) => {
|
Controller.getFollowing = async (req, res) => {
|
||||||
const { followingCount: totalItems } = await user.getUserFields(res.locals.uid, ['followingCount']);
|
const { followingCount: totalItems } = await user.getUserFields(res.locals.uid, ['followingCount']);
|
||||||
|
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ module.exports = function (app, middleware, controllers) {
|
|||||||
|
|
||||||
const middlewares = [middleware.activitypub.enabled, middleware.activitypub.assertS2S, middleware.exposeUid];
|
const middlewares = [middleware.activitypub.enabled, middleware.activitypub.assertS2S, middleware.exposeUid];
|
||||||
|
|
||||||
app.get('/user/:userslug', middlewares, controllers.activitypub.getActor);
|
app.get('/', middlewares, controllers.activitypub.actors.application);
|
||||||
|
app.get('/user/:userslug', middlewares, controllers.activitypub.actors.user);
|
||||||
|
|
||||||
app.get('/user/:userslug/inbox', middlewares, controllers.activitypub.getInbox);
|
app.get('/user/:userslug/inbox', middlewares, controllers.activitypub.getInbox);
|
||||||
app.post('/user/:userslug/inbox', [...middlewares, middleware.activitypub.validate], controllers.activitypub.postInbox);
|
app.post('/user/:userslug/inbox', [...middlewares, middleware.activitypub.validate], controllers.activitypub.postInbox);
|
||||||
|
|||||||
@@ -137,17 +137,17 @@ module.exports = async function (app, middleware) {
|
|||||||
|
|
||||||
app.use(middleware.stripLeadingSlashes);
|
app.use(middleware.stripLeadingSlashes);
|
||||||
|
|
||||||
|
await plugins.reloadRoutes({ router: router });
|
||||||
|
await authRoutes.reloadRoutes({ router: router });
|
||||||
|
await writeRoutes.reload({ router: router });
|
||||||
|
addCoreRoutes(app, router, middleware, mounts);
|
||||||
|
|
||||||
// handle custom homepage routes
|
// handle custom homepage routes
|
||||||
router.use('/', controllers.home.rewrite);
|
router.use('/', controllers.home.rewrite);
|
||||||
|
|
||||||
// homepage handled by `action:homepage.get:[route]`
|
// homepage handled by `action:homepage.get:[route]`
|
||||||
setupPageRoute(router, '/', [], controllers.home.pluginHook);
|
setupPageRoute(router, '/', [], controllers.home.pluginHook);
|
||||||
|
|
||||||
await plugins.reloadRoutes({ router: router });
|
|
||||||
await authRoutes.reloadRoutes({ router: router });
|
|
||||||
await writeRoutes.reload({ router: router });
|
|
||||||
addCoreRoutes(app, router, middleware, mounts);
|
|
||||||
|
|
||||||
winston.info('[router] Routes added');
|
winston.info('[router] Routes added');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -174,7 +174,7 @@ describe('ActivityPub integration', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('Actor endpoint', () => {
|
describe('User Actor endpoint', () => {
|
||||||
let uid;
|
let uid;
|
||||||
let slug;
|
let slug;
|
||||||
|
|
||||||
@@ -216,6 +216,43 @@ describe('ActivityPub integration', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('Instance Actor endpoint', () => {
|
||||||
|
let response;
|
||||||
|
let body;
|
||||||
|
|
||||||
|
before(async () => {
|
||||||
|
({ response, body } = await request.get(nconf.get('url'), {
|
||||||
|
headers: {
|
||||||
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should respond properly', async () => {
|
||||||
|
assert(response);
|
||||||
|
assert.strictEqual(response.statusCode, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a valid ActivityPub Actor JSON-LD payload', async () => {
|
||||||
|
console.log(body);
|
||||||
|
assert(body.hasOwnProperty('@context'));
|
||||||
|
assert(body['@context'].includes('https://www.w3.org/ns/activitystreams'));
|
||||||
|
|
||||||
|
['id', 'url', 'inbox', 'outbox'].forEach((prop) => {
|
||||||
|
assert(body.hasOwnProperty(prop));
|
||||||
|
assert(body[prop]);
|
||||||
|
});
|
||||||
|
|
||||||
|
assert.strictEqual(body.id, body.url);
|
||||||
|
assert.strictEqual(body.type, 'Application');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should contain a `publicKey` property with a public key', async () => {
|
||||||
|
assert(body.hasOwnProperty('publicKey'));
|
||||||
|
assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('http signature signing and verification', () => {
|
describe('http signature signing and verification', () => {
|
||||||
describe('.sign()', () => {
|
describe('.sign()', () => {
|
||||||
let uid;
|
let uid;
|
||||||
@@ -256,6 +293,33 @@ describe('ActivityPub integration', () => {
|
|||||||
assert(digest);
|
assert(digest);
|
||||||
assert.strictEqual(digest, `sha-256=${checksum}`);
|
assert.strictEqual(digest, `sha-256=${checksum}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should create a key for NodeBB itself if a uid of 0 is passed in', async () => {
|
||||||
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
||||||
|
await activitypub.sign(0, endpoint);
|
||||||
|
const { publicKey, privateKey } = await db.getObject(`uid:0:keys`);
|
||||||
|
|
||||||
|
assert(publicKey);
|
||||||
|
assert(privateKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return headers with an appropriate key id uri', async () => {
|
||||||
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
||||||
|
const { signature } = await activitypub.sign(uid, endpoint);
|
||||||
|
const [keyId] = signature.split(',');
|
||||||
|
|
||||||
|
assert(signature);
|
||||||
|
assert.strictEqual(keyId, `keyId="${nconf.get('url')}/user/${username}#key"`);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the instance key id when uid is 0', async () => {
|
||||||
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
||||||
|
const { signature } = await activitypub.sign(0, endpoint);
|
||||||
|
const [keyId] = signature.split(',');
|
||||||
|
|
||||||
|
assert(signature);
|
||||||
|
assert.strictEqual(keyId, `keyId="${nconf.get('url')}#key"`);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('.verify()', () => {
|
describe('.verify()', () => {
|
||||||
|
|||||||
Reference in New Issue
Block a user