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
|
|
|
|
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');
|
|
|
|
|
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', () => {
|
2023-05-24 14:00:41 -04:00
|
|
|
before(() => {
|
2023-06-16 10:57:34 -04:00
|
|
|
meta.config.activitypubEnabled = 1;
|
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);
|
|
|
|
|
assert.strictEqual(response.statusCode, 403);
|
|
|
|
|
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()', () => {
|
|
|
|
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('.resolveLocalUid()', () => {
|
|
|
|
|
let uid;
|
|
|
|
|
let slug;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
slug = slugify(utils.generateUUID().slice(0, 8));
|
|
|
|
|
uid = await user.create({ username: slug });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should throw when an invalid input is passed in', async () => {
|
|
|
|
|
await assert.rejects(
|
|
|
|
|
activitypub.helpers.resolveLocalUid('ncl28h3qwhoiclwnevoinw3u'),
|
2024-01-08 15:03:46 -05:00
|
|
|
{ message: '[[error:activitypub.invalid-id]]' }
|
2023-12-11 14:35:04 -05:00
|
|
|
);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should return null when valid input is passed but does not resolve', async () => {
|
|
|
|
|
const uid = await activitypub.helpers.resolveLocalUid(`acct:foobar@${nconf.get('url_parsed').host}`);
|
|
|
|
|
assert.strictEqual(uid, null);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve to a local uid when given a webfinger-style string', async () => {
|
|
|
|
|
const found = await activitypub.helpers.resolveLocalUid(`acct:${slug}@${nconf.get('url_parsed').host}`);
|
|
|
|
|
assert.strictEqual(found, uid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve even without the "acct:" prefix', async () => {
|
|
|
|
|
const found = await activitypub.helpers.resolveLocalUid(`${slug}@${nconf.get('url_parsed').host}`);
|
|
|
|
|
assert.strictEqual(found, uid);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should resolve when passed a full URL', async () => {
|
|
|
|
|
const found = await activitypub.helpers.resolveLocalUid(`${nconf.get('url')}/user/${slug}`);
|
|
|
|
|
assert.strictEqual(found, uid);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
2023-05-23 16:13:16 -04:00
|
|
|
describe('ActivityPub screener middleware', () => {
|
|
|
|
|
let uid;
|
|
|
|
|
let slug;
|
|
|
|
|
|
|
|
|
|
beforeEach(async () => {
|
|
|
|
|
slug = slugify(utils.generateUUID().slice(0, 8));
|
|
|
|
|
uid = await user.create({ username: slug });
|
|
|
|
|
});
|
|
|
|
|
|
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
|
|
|
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/user/${slug}`, {
|
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 () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/user/${slug}`, {
|
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 () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/user/${slug}`, {
|
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
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
describe('Actor endpoint', () => {
|
|
|
|
|
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 () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/user/${slug}`, {
|
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
|
|
|
});
|
|
|
|
|
|
2023-12-21 14:38:16 -05:00
|
|
|
assert.strictEqual(body.id, body.url);
|
|
|
|
|
assert.strictEqual(body.type, 'Person');
|
2023-05-23 16:13:16 -04:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should contain a `publicKey` property with a public key', async () => {
|
2023-12-21 14:38:16 -05:00
|
|
|
const { response, body } = await request.get(`${nconf.get('url')}/user/${slug}`, {
|
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
|
|
|
|
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;
|
|
|
|
|
let username;
|
|
|
|
|
|
|
|
|
|
before(async () => {
|
|
|
|
|
username = utils.generateUUID().slice(0, 10);
|
|
|
|
|
uid = await user.create({ username });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
it('should create a key-pair for a user if the user does not have one already', async () => {
|
|
|
|
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
|
|
|
|
await activitypub.sign(uid, endpoint);
|
|
|
|
|
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 () => {
|
|
|
|
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
|
|
|
|
const { date, digest, signature } = await activitypub.sign(uid, endpoint);
|
|
|
|
|
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 () => {
|
|
|
|
|
const endpoint = `${nconf.get('url')}/user/${username}/inbox`;
|
|
|
|
|
const payload = { foo: 'bar' };
|
|
|
|
|
const { digest } = await activitypub.sign(uid, endpoint, payload);
|
|
|
|
|
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
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
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`;
|
|
|
|
|
const signature = await activitypub.sign(uid, endpoint);
|
|
|
|
|
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`;
|
|
|
|
|
const signature = await activitypub.sign(uid, endpoint, { foo: 'bar' });
|
|
|
|
|
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);
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
});
|
2023-05-22 23:38:11 -04:00
|
|
|
});
|