2023-05-22 23:38:11 -04:00
|
|
|
'use strict';
|
|
|
|
|
|
2023-05-23 16:13:16 -04:00
|
|
|
const assert = require('assert');
|
2023-06-21 15:45:29 -04:00
|
|
|
const { createHash } = require('crypto');
|
2023-05-22 23:38:11 -04:00
|
|
|
const nconf = require('nconf');
|
|
|
|
|
|
|
|
|
|
const db = require('./mocks/databasemock');
|
2023-05-23 16:13:16 -04:00
|
|
|
const slugify = require('../src/slugify');
|
|
|
|
|
const utils = require('../src/utils');
|
2023-12-21 14:38:16 -05:00
|
|
|
const request = require('../src/request');
|
2023-05-23 16:13:16 -04:00
|
|
|
|
2024-02-26 14:22:35 -05:00
|
|
|
const install = require('../src/install');
|
2023-05-24 14:00:41 -04:00
|
|
|
const meta = require('../src/meta');
|
2023-05-23 16:13:16 -04:00
|
|
|
const user = require('../src/user');
|
2024-01-26 21:39:20 -05:00
|
|
|
const categories = require('../src/categories');
|
|
|
|
|
const topics = require('../src/topics');
|
|
|
|
|
const posts = require('../src/posts');
|
2023-05-23 16:13:16 -04:00
|
|
|
const privileges = require('../src/privileges');
|
2023-06-21 15:45:29 -04:00
|
|
|
const activitypub = require('../src/activitypub');
|
2023-05-22 23:38:11 -04:00
|
|
|
|
|
|
|
|
describe('ActivityPub integration', () => {
|
2024-02-26 14:22:35 -05:00
|
|
|
before(async () => {
|
2023-06-16 10:57:34 -04:00
|
|
|
meta.config.activitypubEnabled = 1;
|
2024-02-26 14:22:35 -05:00
|
|
|
await install.giveWorldPrivileges();
|
2023-05-24 14:00:41 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
after(() => {
|
2023-06-16 10:57:34 -04:00
|
|
|
delete meta.config.activitypubEnabled;
|
2023-05-24 14:00:41 -04:00
|
|
|
});
|
|
|
|
|
|
2023-05-22 23:38:11 -04:00
|
|
|
describe('WebFinger endpoint', () => {
|
2023-05-23 16:13:16 -04:00
|
|
|
let uid;
|
|
|
|
|
let slug;
|
2023-06-26 15:09:47 -04:00
|
|
|
const { host } = nconf.get('url_parsed');
|
2023-05-23 16:13:16 -04:00
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
slug = slugify(utils.generateUUID().slice(0, 8));
|
|
|
|
|
uid = await user.create({ username: slug });
|
|
|
|
|
});
|
|
|
|
|
|
2023-05-22 23:38:11 -04:00
|
|
|
it('should return a 404 Not Found if no user exists by that username', async () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar@${host}`);
|
2023-05-23 16:13:16 -04:00
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 404);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return a 400 Bad Request if the request is malformed', async () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:foobar`);
|
2023-05-23 16:13:16 -04:00
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 400);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return 403 Forbidden if the calling user is not allowed to view the user list/profiles', async () => {
|
|
|
|
|
await privileges.global.rescind(['groups:view:users'], 'guests');
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${host}`);
|
2023-05-23 16:13:16 -04:00
|
|
|
|
|
|
|
|
assert(response);
|
2024-02-05 14:12:23 -05:00
|
|
|
assert.strictEqual(response.statusCode, 400);
|
2023-05-23 16:13:16 -04:00
|
|
|
await privileges.global.give(['groups:view:users'], 'guests');
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return a valid WebFinger response otherwise', async () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${slug}@${host}`);
|
2023-05-23 16:13:16 -04:00
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
|
|
|
|
|
|
|
|
|
['subject', 'aliases', 'links'].forEach((prop) => {
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.hasOwnProperty(prop));
|
|
|
|
|
assert(body[prop]);
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
2023-12-21 14:38:16 -05:00
|
|
|
assert.strictEqual(body.subject, `acct:${slug}@${host}`);
|
2023-05-23 16:13:16 -04:00
|
|
|
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(Array.isArray(body.aliases));
|
|
|
|
|
assert([`${nconf.get('url')}/uid/${uid}`, `${nconf.get('url')}/user/${slug}`].every(url => body.aliases.includes(url)));
|
2023-05-23 16:13:16 -04:00
|
|
|
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(Array.isArray(body.links));
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-12-13 13:38:52 -05:00
|
|
|
describe('Helpers', () => {
|
2023-12-11 14:35:04 -05:00
|
|
|
describe('.query()', () => {
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('.generateKeys()', () => {
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
2024-02-21 13:43:56 -05:00
|
|
|
describe('.resolveId()', () => {
|
2024-02-21 10:58:20 -05:00
|
|
|
let url;
|
|
|
|
|
let resolved;
|
|
|
|
|
|
|
|
|
|
before(() => {
|
|
|
|
|
url = 'https://example.org/topic/foobar';
|
|
|
|
|
resolved = 'https://example.org/tid/1234';
|
|
|
|
|
activitypub._cache.set(`0;${url}`, {
|
|
|
|
|
id: resolved,
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the resolved id when queried', async () => {
|
|
|
|
|
const id = await activitypub.resolveId(0, url);
|
|
|
|
|
assert.strictEqual(id, resolved);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null when the query fails', async () => {
|
|
|
|
|
const id = await activitypub.resolveId(0, 'https://example.org/sdlknsdfnsd');
|
|
|
|
|
assert.strictEqual(id, null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null when the resolved host does not match the queried host', async () => {
|
|
|
|
|
const url = 'https://example.com/topic/foobar'; // .com attempting to overwrite .org data
|
|
|
|
|
const resolved = 'https://example.org/tid/1234'; // .org
|
|
|
|
|
activitypub._cache.set(`0;${url}`, {
|
|
|
|
|
id: resolved,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const id = await activitypub.resolveId(0, url);
|
|
|
|
|
assert.strictEqual(id, null);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-02-05 16:57:17 -05:00
|
|
|
describe('.resolveLocalId()', () => {
|
2023-12-11 14:35:04 -05:00
|
|
|
let uid;
|
|
|
|
|
let slug;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
slug = slugify(utils.generateUUID().slice(0, 8));
|
|
|
|
|
uid = await user.create({ username: slug });
|
|
|
|
|
});
|
|
|
|
|
|
2024-02-21 10:26:26 -05:00
|
|
|
it('should return null when an invalid input is passed in', async () => {
|
|
|
|
|
const { type, id } = await activitypub.helpers.resolveLocalId('ncl28h3qwhoiclwnevoinw3u');
|
|
|
|
|
assert.strictEqual(type, null);
|
|
|
|
|
assert.strictEqual(id, null);
|
2023-12-11 14:35:04 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null when valid input is passed but does not resolve', async () => {
|
2024-02-21 10:26:26 -05:00
|
|
|
const { type, id } = await activitypub.helpers.resolveLocalId(`acct:foobar@${nconf.get('url_parsed').host}`);
|
|
|
|
|
assert.strictEqual(type, 'user');
|
2024-02-05 16:57:17 -05:00
|
|
|
assert.strictEqual(id, null);
|
2023-12-11 14:35:04 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve to a local uid when given a webfinger-style string', async () => {
|
2024-02-05 16:57:17 -05:00
|
|
|
const { id } = await activitypub.helpers.resolveLocalId(`acct:${slug}@${nconf.get('url_parsed').host}`);
|
|
|
|
|
assert.strictEqual(id, uid);
|
2023-12-11 14:35:04 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve even without the "acct:" prefix', async () => {
|
2024-02-05 16:57:17 -05:00
|
|
|
const { id } = await activitypub.helpers.resolveLocalId(`${slug}@${nconf.get('url_parsed').host}`);
|
|
|
|
|
assert.strictEqual(id, uid);
|
2023-12-11 14:35:04 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve when passed a full URL', async () => {
|
2024-02-05 16:57:17 -05:00
|
|
|
const { id } = await activitypub.helpers.resolveLocalId(`${nconf.get('url')}/user/${slug}`);
|
|
|
|
|
assert.strictEqual(id, uid);
|
2023-12-11 14:35:04 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-05-23 16:13:16 -04:00
|
|
|
describe('ActivityPub screener middleware', () => {
|
|
|
|
|
let uid;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
uid = await user.create({ username: slugify(utils.generateUUID().slice(0, 8)) });
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
2023-05-24 14:00:41 -04:00
|
|
|
it('should return regular user profile html if federation is disabled', async () => {
|
2023-06-16 10:57:34 -04:00
|
|
|
delete meta.config.activitypubEnabled;
|
2023-05-24 14:00:41 -04:00
|
|
|
|
2024-02-05 14:12:23 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, {
|
2023-05-24 14:00:41 -04:00
|
|
|
headers: {
|
|
|
|
|
Accept: 'text/html',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.startsWith('<!DOCTYPE html>'));
|
2023-05-24 14:00:41 -04:00
|
|
|
|
2023-06-16 10:57:34 -04:00
|
|
|
meta.config.activitypubEnabled = 1;
|
2023-05-24 14:00:41 -04:00
|
|
|
});
|
|
|
|
|
|
2023-05-23 16:13:16 -04:00
|
|
|
it('should return regular user profile html if Accept header is not ActivityPub-related', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, {
|
2023-05-23 16:13:16 -04:00
|
|
|
headers: {
|
|
|
|
|
Accept: 'text/html',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.startsWith('<!DOCTYPE html>'));
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the ActivityPub Actor JSON-LD payload if the correct Accept header is provided', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, {
|
2023-05-22 23:38:11 -04:00
|
|
|
headers: {
|
2023-05-23 16:13:16 -04:00
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
2023-05-22 23:38:11 -04:00
|
|
|
},
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.hasOwnProperty('@context'));
|
|
|
|
|
assert(body['@context'].includes('https://www.w3.org/ns/activitystreams'));
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2024-01-22 16:18:49 -05:00
|
|
|
describe('User Actor endpoint', () => {
|
2023-05-23 16:13:16 -04:00
|
|
|
let uid;
|
|
|
|
|
let slug;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
slug = slugify(utils.generateUUID().slice(0, 8));
|
|
|
|
|
uid = await user.create({ username: slug });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return a valid ActivityPub Actor JSON-LD payload', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, {
|
2023-05-23 16:13:16 -04:00
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert(response);
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.hasOwnProperty('@context'));
|
|
|
|
|
assert(body['@context'].includes('https://www.w3.org/ns/activitystreams'));
|
2023-05-23 16:13:16 -04:00
|
|
|
|
|
|
|
|
['id', 'url', 'followers', 'following', 'inbox', 'outbox'].forEach((prop) => {
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.hasOwnProperty(prop));
|
|
|
|
|
assert(body[prop]);
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
2024-02-05 14:12:23 -05:00
|
|
|
assert.strictEqual(body.id, `${nconf.get('url')}/uid/${uid}`);
|
2023-12-21 14:38:16 -05:00
|
|
|
assert.strictEqual(body.type, 'Person');
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should contain a `publicKey` property with a public key', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/uid/${uid}`, {
|
2023-05-23 16:13:16 -04:00
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
2023-05-22 23:38:11 -04:00
|
|
|
},
|
|
|
|
|
});
|
2023-05-23 16:13:16 -04:00
|
|
|
|
2023-12-21 14:38:16 -05:00
|
|
|
assert(body.hasOwnProperty('publicKey'));
|
|
|
|
|
assert(['id', 'owner', 'publicKeyPem'].every(prop => body.publicKey.hasOwnProperty(prop)));
|
2023-05-22 23:38:11 -04:00
|
|
|
});
|
|
|
|
|
});
|
2023-06-21 15:45:29 -04:00
|
|
|
|
2024-01-22 16:18:49 -05:00
|
|
|
describe('Instance Actor endpoint', () => {
|
|
|
|
|
let response;
|
|
|
|
|
let body;
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
2024-01-24 14:09:40 -05:00
|
|
|
({ response, body } = await request.get(`${nconf.get('url')}/actor`, {
|
2024-01-22 16:18:49 -05:00
|
|
|
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 () => {
|
|
|
|
|
assert(body.hasOwnProperty('@context'));
|
|
|
|
|
assert(body['@context'].includes('https://www.w3.org/ns/activitystreams'));
|
|
|
|
|
|
2024-01-23 11:22:18 -05:00
|
|
|
['id', 'url', 'inbox', 'outbox', 'name', 'preferredUsername'].forEach((prop) => {
|
2024-01-22 16:18:49 -05:00
|
|
|
assert(body.hasOwnProperty(prop));
|
|
|
|
|
assert(body[prop]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.strictEqual(body.id, body.url);
|
|
|
|
|
assert.strictEqual(body.type, 'Application');
|
2024-01-23 11:22:18 -05:00
|
|
|
assert.strictEqual(body.name, meta.config.site_title || 'NodeBB');
|
|
|
|
|
assert.strictEqual(body.preferredUsername, nconf.get('url_parsed').hostname);
|
2024-01-22 16:18:49 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
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)));
|
|
|
|
|
});
|
2024-01-23 11:22:18 -05:00
|
|
|
|
|
|
|
|
it('should also have a valid WebFinger response tied to `preferredUsername`', async () => {
|
|
|
|
|
const { response, body: body2 } = await request.get(`${nconf.get('url')}/.well-known/webfinger?resource=acct:${body.preferredUsername}@${nconf.get('url_parsed').host}`);
|
|
|
|
|
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
|
|
|
|
assert(body2 && body2.aliases && body2.links);
|
|
|
|
|
assert(body2.aliases.includes(nconf.get('url')));
|
2024-01-25 11:01:22 -05:00
|
|
|
assert(body2.links.some(item => item.rel === 'self' && item.type === 'application/activity+json' && item.href === `${nconf.get('url')}/actor`));
|
2024-01-23 11:22:18 -05:00
|
|
|
});
|
2024-01-22 16:18:49 -05:00
|
|
|
});
|
|
|
|
|
|
2023-06-26 15:09:47 -04:00
|
|
|
describe('http signature signing and verification', () => {
|
2023-06-21 15:45:29 -04:00
|
|
|
describe('.sign()', () => {
|
|
|
|
|
let uid;
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
uid = await user.create({ username: utils.generateUUID().slice(0, 10) });
|
2023-06-21 15:45:29 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a key-pair for a user if the user does not have one already', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
|
|
|
|
await activitypub.sign(keyData, endpoint);
|
2023-06-21 15:45:29 -04:00
|
|
|
const { publicKey, privateKey } = await db.getObject(`uid:${uid}:keys`);
|
|
|
|
|
|
|
|
|
|
assert(publicKey);
|
|
|
|
|
assert(privateKey);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return an object with date, a null digest, and signature, if no payload is passed in', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
|
|
|
|
const { date, digest, signature } = await activitypub.sign(keyData, endpoint);
|
2023-06-21 15:45:29 -04:00
|
|
|
const dateObj = new Date(date);
|
|
|
|
|
|
|
|
|
|
assert(signature);
|
|
|
|
|
assert(dateObj);
|
|
|
|
|
assert.strictEqual(digest, null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should also return a digest hash if payload is passed in', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
2023-06-21 15:45:29 -04:00
|
|
|
const payload = { foo: 'bar' };
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
|
|
|
|
const { digest } = await activitypub.sign(keyData, endpoint, payload);
|
2023-06-21 15:45:29 -04:00
|
|
|
const hash = createHash('sha256');
|
|
|
|
|
hash.update(JSON.stringify(payload));
|
2023-06-23 14:59:47 -04:00
|
|
|
const checksum = hash.digest('base64');
|
2023-06-21 15:45:29 -04:00
|
|
|
|
|
|
|
|
assert(digest);
|
2023-06-23 14:59:47 -04:00
|
|
|
assert.strictEqual(digest, `sha-256=${checksum}`);
|
2023-06-21 15:45:29 -04:00
|
|
|
});
|
2024-01-22 16:18:49 -05:00
|
|
|
|
|
|
|
|
it('should create a key for NodeBB itself if a uid of 0 is passed in', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', 0);
|
|
|
|
|
await activitypub.sign(keyData, endpoint);
|
2024-01-22 16:18:49 -05:00
|
|
|
const { publicKey, privateKey } = await db.getObject(`uid:0:keys`);
|
|
|
|
|
|
|
|
|
|
assert(publicKey);
|
|
|
|
|
assert(privateKey);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return headers with an appropriate key id uri', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
|
|
|
|
const { signature } = await activitypub.sign(keyData, endpoint);
|
2024-01-22 16:18:49 -05:00
|
|
|
const [keyId] = signature.split(',');
|
|
|
|
|
|
|
|
|
|
assert(signature);
|
2024-02-05 14:12:23 -05:00
|
|
|
assert.strictEqual(keyId, `keyId="${nconf.get('url')}/uid/${uid}#key"`);
|
2024-01-22 16:18:49 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the instance key id when uid is 0', async () => {
|
2024-02-05 14:12:23 -05:00
|
|
|
const endpoint = `${nconf.get('url')}/uid/${uid}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', 0);
|
|
|
|
|
const { signature } = await activitypub.sign(keyData, endpoint);
|
2024-01-22 16:18:49 -05:00
|
|
|
const [keyId] = signature.split(',');
|
|
|
|
|
|
|
|
|
|
assert(signature);
|
2024-01-24 14:09:40 -05:00
|
|
|
assert.strictEqual(keyId, `keyId="${nconf.get('url')}/actor#key"`);
|
2024-01-22 16:18:49 -05:00
|
|
|
});
|
2023-06-21 15:45:29 -04:00
|
|
|
});
|
|
|
|
|
|
2023-06-23 14:59:47 -04:00
|
|
|
describe('.verify()', () => {
|
2023-06-21 15:45:29 -04:00
|
|
|
let uid;
|
|
|
|
|
let username;
|
2023-08-08 15:33:35 -04:00
|
|
|
const baseUrl = nconf.get('relative_path');
|
2023-06-21 15:45:29 -04:00
|
|
|
const mockReqBase = {
|
|
|
|
|
method: 'GET',
|
|
|
|
|
// path: ...
|
2023-08-08 15:33:35 -04:00
|
|
|
baseUrl,
|
2023-06-21 15:45:29 -04:00
|
|
|
headers: {
|
|
|
|
|
// host: ...
|
|
|
|
|
// date: ...
|
|
|
|
|
// signature: ...
|
|
|
|
|
// digest: ...
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
|
|
|
|
username = utils.generateUUID().slice(0, 10);
|
|
|
|
|
uid = await user.create({ username });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return true when the proper signature and relevant headers are passed in', async () => {
|
|
|
|
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
|
|
|
|
const path = `/user/${username}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
|
|
|
|
const signature = await activitypub.sign(keyData, endpoint);
|
2023-06-21 15:45:29 -04:00
|
|
|
const { host } = nconf.get('url_parsed');
|
|
|
|
|
const req = {
|
|
|
|
|
...mockReqBase,
|
|
|
|
|
...{
|
|
|
|
|
path,
|
|
|
|
|
headers: { ...signature, host },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const verified = await activitypub.verify(req);
|
|
|
|
|
assert.strictEqual(verified, true);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return true when a digest is also passed in', async () => {
|
|
|
|
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
|
|
|
|
const path = `/user/${username}/inbox`;
|
2024-02-05 16:57:17 -05:00
|
|
|
const keyData = await activitypub.getPrivateKey('uid', uid);
|
|
|
|
|
const signature = await activitypub.sign(keyData, endpoint, { foo: 'bar' });
|
2023-06-21 15:45:29 -04:00
|
|
|
const { host } = nconf.get('url_parsed');
|
|
|
|
|
const req = {
|
|
|
|
|
...mockReqBase,
|
|
|
|
|
...{
|
|
|
|
|
method: 'POST',
|
|
|
|
|
path,
|
|
|
|
|
headers: { ...signature, host },
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const verified = await activitypub.verify(req);
|
|
|
|
|
assert.strictEqual(verified, true);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-01-26 21:39:20 -05:00
|
|
|
|
2024-02-05 14:12:23 -05:00
|
|
|
describe('Receipt of ActivityPub events to inboxes (federating IN)', () => {
|
2024-01-26 21:39:20 -05:00
|
|
|
describe('Create', () => {
|
|
|
|
|
describe('Note', () => {
|
2024-02-21 10:26:26 -05:00
|
|
|
const slug = utils.generateUUID();
|
|
|
|
|
const id = `https://example.org/status/${slug}`;
|
|
|
|
|
const remoteNote = {
|
|
|
|
|
'@context': 'https://www.w3.org/ns/activitystreams',
|
|
|
|
|
id,
|
|
|
|
|
url: id,
|
|
|
|
|
type: 'Note',
|
|
|
|
|
to: ['https://www.w3.org/ns/activitystreams#Public'],
|
|
|
|
|
cc: ['https://example.org/user/foobar/followers'],
|
|
|
|
|
inReplyTo: null,
|
|
|
|
|
attributedTo: 'https://example.org/user/foobar',
|
|
|
|
|
name: 'Foo Bar',
|
|
|
|
|
content: '<b>Baz quux</b>',
|
|
|
|
|
published: new Date().toISOString(),
|
|
|
|
|
source: {
|
|
|
|
|
content: '**Baz quux**',
|
|
|
|
|
mediaType: 'text/markdown',
|
|
|
|
|
},
|
|
|
|
|
};
|
|
|
|
|
|
2024-01-26 21:39:20 -05:00
|
|
|
let topic;
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
2024-02-21 10:26:26 -05:00
|
|
|
const controllers = require('../src/controllers');
|
|
|
|
|
|
|
|
|
|
activitypub._cache.set(`0;${id}`, remoteNote);
|
|
|
|
|
await controllers.activitypub.postInbox({
|
|
|
|
|
body: {
|
|
|
|
|
type: 'Create',
|
|
|
|
|
actor: 'https://example.org/user/foobar',
|
|
|
|
|
object: remoteNote,
|
|
|
|
|
},
|
|
|
|
|
}, { sendStatus: () => {} });
|
2024-01-26 21:39:20 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a new topic if Note is at root-level or its parent has not been seen before', async () => {
|
2024-02-21 10:26:26 -05:00
|
|
|
const saved = await db.getObject(`post:${id}`);
|
2024-01-26 21:39:20 -05:00
|
|
|
|
|
|
|
|
assert(saved);
|
|
|
|
|
assert(saved.tid);
|
2024-02-21 10:26:26 -05:00
|
|
|
|
|
|
|
|
topic = await topics.getTopicData(saved.tid);
|
|
|
|
|
assert(topic);
|
|
|
|
|
assert.strictEqual(saved.uid, 'https://example.org/user/foobar');
|
|
|
|
|
assert.strictEqual(saved.content, '<b>Baz quux</b>');
|
2024-01-26 21:39:20 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should properly save the topic title in the topic hash', async () => {
|
2024-02-21 10:26:26 -05:00
|
|
|
assert.strictEqual(topic.title, 'Foo Bar');
|
2024-01-26 21:39:20 -05:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should properly save the mainPid in the topic hash', async () => {
|
2024-02-21 10:26:26 -05:00
|
|
|
assert.strictEqual(topic.mainPid, id);
|
2024-01-26 21:39:20 -05:00
|
|
|
});
|
2024-01-26 22:35:02 -05:00
|
|
|
|
|
|
|
|
// todo: test topic replies, too
|
2024-01-26 21:39:20 -05:00
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-02-05 14:12:23 -05:00
|
|
|
|
|
|
|
|
describe('Serving of local assets to remote clients', () => {
|
|
|
|
|
let category;
|
|
|
|
|
let uid;
|
|
|
|
|
let postData;
|
|
|
|
|
let topicData;
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
|
|
|
|
category = await categories.create({ name: utils.generateUUID().slice(0, 8) });
|
|
|
|
|
const slug = slugify(utils.generateUUID().slice(0, 8));
|
|
|
|
|
uid = await user.create({ username: slug });
|
|
|
|
|
|
|
|
|
|
({ postData, topicData } = await topics.post({
|
|
|
|
|
uid,
|
|
|
|
|
cid: category.cid,
|
|
|
|
|
title: 'Lipsum title',
|
|
|
|
|
content: 'Lorem ipsum dolor sit amet',
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Note', () => {
|
|
|
|
|
let body;
|
|
|
|
|
let response;
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
|
|
|
|
({ body, response } = await request.get(`${nconf.get('url')}/post/${postData.pid}`, {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
|
|
|
},
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return a 404 on a non-existant post', async () => {
|
|
|
|
|
const { response } = await request.get(`${nconf.get('url')}/post/${parseInt(postData.pid, 10) + 1}`, {
|
|
|
|
|
headers: {
|
|
|
|
|
Accept: 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"',
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
assert.strictEqual(response.statusCode, 404);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return a 200 response on an existing post', () => {
|
|
|
|
|
assert.strictEqual(response.statusCode, 200);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return the expected Content-Type header', () => {
|
|
|
|
|
assert.strictEqual(response.headers['content-type'], 'application/activity+json; charset=utf-8');
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-05-22 23:38:11 -04:00
|
|
|
});
|