| 1 | // Authorized fetch (shaer-afq): een instance in Mastodons secure mode geeft zijn
|
|---|
| 2 | // actor-document -- en dus zijn publieke sleutel -- alleen aan een ONDERTEKEND
|
|---|
| 3 | // verzoek. Zonder die handtekening kregen we 401, vonden we geen sleutel, en
|
|---|
| 4 | // wezen we elke correct ondertekende Follow van die instance af. Op boiert.eu
|
|---|
| 5 | // bleven daardoor vier accounts eindeloos hangen.
|
|---|
| 6 | //
|
|---|
| 7 | // De fetch is hier gestubd: het gaat om de vraag OF er ondertekend wordt en of
|
|---|
| 8 | // er wordt teruggevallen, niet om echte HTTP of crypto.
|
|---|
| 9 | //
|
|---|
| 10 | // Run: npm test
|
|---|
| 11 |
|
|---|
| 12 | import { test } from 'node:test';
|
|---|
| 13 | import assert from 'node:assert/strict';
|
|---|
| 14 |
|
|---|
| 15 | process.env.DATABASE_PATH = ':memory:';
|
|---|
| 16 | process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
|
|---|
| 17 |
|
|---|
| 18 | const dbMod = await import('../src/config/database.js');
|
|---|
| 19 | const db = dbMod.default;
|
|---|
| 20 | dbMod.initializeDatabase();
|
|---|
| 21 | const AP = await import('../src/services/ActivityPubService.js');
|
|---|
| 22 |
|
|---|
| 23 | db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
|
|---|
| 24 | .run('u1', 'u1', 'u1@test', 'x', 'god');
|
|---|
| 25 | db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
|
|---|
| 26 |
|
|---|
| 27 | // IP-literals: safeFetch slaat de DNS-lookup over, dus de test blijft offline.
|
|---|
| 28 | const OPEN_ACTOR = 'https://203.0.113.30/users/open'; // gewone instance
|
|---|
| 29 | const SECURE_ACTOR = 'https://203.0.113.40/users/gesloten'; // authorized fetch
|
|---|
| 30 |
|
|---|
| 31 | let verzoeken = [];
|
|---|
| 32 | const echteFetch = globalThis.fetch;
|
|---|
| 33 | globalThis.fetch = async (url, opts = {}) => {
|
|---|
| 34 | const u = String(url);
|
|---|
| 35 | const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
|
|---|
| 36 | verzoeken.push({ url: u, ondertekend });
|
|---|
| 37 | const doc = (id) => new Response(JSON.stringify({
|
|---|
| 38 | id, type: 'Person', preferredUsername: 'x', inbox: `${id}/inbox`,
|
|---|
| 39 | publicKey: { id: `${id}#main-key`, owner: id, publicKeyPem: '-----BEGIN PUBLIC KEY-----\nx\n-----END PUBLIC KEY-----' },
|
|---|
| 40 | }), { status: 200, headers: { 'content-type': 'application/activity+json' } });
|
|---|
| 41 |
|
|---|
| 42 | if (u === OPEN_ACTOR) return doc(OPEN_ACTOR);
|
|---|
| 43 | // De kern van secure mode: onbetekend is het 401, ondertekend krijg je hem wel.
|
|---|
| 44 | if (u === SECURE_ACTOR) return ondertekend ? doc(SECURE_ACTOR) : new Response('unauthorized', { status: 401 });
|
|---|
| 45 | return new Response('not found', { status: 404 });
|
|---|
| 46 | };
|
|---|
| 47 |
|
|---|
| 48 | test('een actor achter authorized fetch wordt nu wél opgehaald', async () => {
|
|---|
| 49 | verzoeken = [];
|
|---|
| 50 | const actor = await AP.fetchActor(SECURE_ACTOR, { asSlug: 'me' });
|
|---|
| 51 | assert.ok(actor, 'de actor hoort binnen te komen');
|
|---|
| 52 | assert.equal(actor.id, SECURE_ACTOR);
|
|---|
| 53 | assert.ok(verzoeken.some((v) => v.url === SECURE_ACTOR && v.ondertekend), 'het verzoek hoort ondertekend te zijn');
|
|---|
| 54 | });
|
|---|
| 55 |
|
|---|
| 56 | test('zonder ondertekenaar blijft dezelfde actor onbereikbaar', async () => {
|
|---|
| 57 | // Dit is precies het oude gedrag, en het bewijst dat de stub echt onderscheid
|
|---|
| 58 | // maakt in plaats van altijd mee te werken.
|
|---|
| 59 | const actor = await AP.fetchActor(SECURE_ACTOR);
|
|---|
| 60 | assert.equal(actor, null);
|
|---|
| 61 | });
|
|---|
| 62 |
|
|---|
| 63 | test('een gewone instance blijft werken, ondertekend of niet', async () => {
|
|---|
| 64 | assert.ok(await AP.fetchActor(OPEN_ACTOR), 'onbetekend');
|
|---|
| 65 | assert.ok(await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }), 'ondertekend');
|
|---|
| 66 | });
|
|---|
| 67 |
|
|---|
| 68 | test('mislukt ondertekend ophalen, dan volgt de onbetekende poging alsnog', async () => {
|
|---|
| 69 | // Niet elke 401 komt van secure mode, en een instance die geen handtekening
|
|---|
| 70 | // verwacht mag er niet door stukgaan. OPEN_ACTOR antwoordt op allebei, dus we
|
|---|
| 71 | // meten dat er ECHT twee pogingen zijn wanneer de eerste niets oplevert.
|
|---|
| 72 | verzoeken = [];
|
|---|
| 73 | const stubOrig = globalThis.fetch;
|
|---|
| 74 | let eerste = true;
|
|---|
| 75 | globalThis.fetch = async (url, opts = {}) => {
|
|---|
| 76 | const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
|
|---|
| 77 | if (String(url) === OPEN_ACTOR && ondertekend && eerste) { eerste = false; return new Response('nee', { status: 500 }); }
|
|---|
| 78 | return stubOrig(url, opts);
|
|---|
| 79 | };
|
|---|
| 80 | const actor = await AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' });
|
|---|
| 81 | globalThis.fetch = stubOrig;
|
|---|
| 82 | assert.ok(actor, 'de terugval hoort hem alsnog op te halen');
|
|---|
| 83 | });
|
|---|
| 84 |
|
|---|
| 85 | test('een onbekende actor blijft null, ondertekend of niet', async () => {
|
|---|
| 86 | assert.equal(await AP.fetchActor('https://203.0.113.99/users/weg', { asSlug: 'me' }), null);
|
|---|
| 87 | });
|
|---|
| 88 |
|
|---|
| 89 | test.after(() => { globalThis.fetch = echteFetch; });
|
|---|