mirror of
https://github.com/NodeBB/NodeBB.git
synced 2026-01-05 07:10:30 +01: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 db = require('../database');
|
||||
const user = require('../user');
|
||||
const utils = require('../utils');
|
||||
const ttl = require('../cache/ttl');
|
||||
|
||||
const requestCache = ttl({ ttl: 1000 * 60 * 5 }); // 5 minutes
|
||||
@@ -103,12 +104,18 @@ ActivityPub.fetchPublicKey = async (uri) => {
|
||||
};
|
||||
|
||||
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
|
||||
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`;
|
||||
const keyId = `${nconf.get('url')}${uid > 0 ? `/user/${userslug}` : ''}#key`;
|
||||
let digest = null;
|
||||
|
||||
let headers = '(request-target) host date';
|
||||
@@ -183,7 +190,7 @@ ActivityPub.get = async (uid, uri) => {
|
||||
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}`);
|
||||
const { response, body } = await request.get(uri, {
|
||||
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;
|
||||
|
||||
Controller.actors = require('./actors');
|
||||
Controller.profiles = require('./profiles');
|
||||
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) => {
|
||||
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];
|
||||
|
||||
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.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);
|
||||
|
||||
await plugins.reloadRoutes({ router: router });
|
||||
await authRoutes.reloadRoutes({ router: router });
|
||||
await writeRoutes.reload({ router: router });
|
||||
addCoreRoutes(app, router, middleware, mounts);
|
||||
|
||||
// handle custom homepage routes
|
||||
router.use('/', controllers.home.rewrite);
|
||||
|
||||
// homepage handled by `action:homepage.get:[route]`
|
||||
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');
|
||||
};
|
||||
|
||||
|
||||
@@ -174,7 +174,7 @@ describe('ActivityPub integration', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Actor endpoint', () => {
|
||||
describe('User Actor endpoint', () => {
|
||||
let uid;
|
||||
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('.sign()', () => {
|
||||
let uid;
|
||||
@@ -256,6 +293,33 @@ describe('ActivityPub integration', () => {
|
||||
assert(digest);
|
||||
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()', () => {
|
||||
|
||||
Reference in New Issue
Block a user