Changeset 952baf3 in Klonkt for test/authorized-fetch.test.js


Ignore:
Timestamp:
08/07/2026 05:15:52 PM (5 weeks ago)
Author:
Robin <roboburr@…>
Branches:
main
Children:
ba76bf5
Parents:
f85b2c3 (diff), 0d5bd2c (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent.
Message:

Merge GitHub-main (1.7.0) met de VPS-lijn

De twee mains waren een dag gedivergeerd en bevatten elk echt werk. GitHub had 66
commits die nooit langs prutfolio.git zijn gekomen, omdat een parallelle sessie
rechtstreeks naar GitHub pushte vanaf een kloon in /tmp op de VPS. De VPS had twee
commits die GitHub niet had. Geen van beide bevatte de ander, en stable had geen van
de twee.

Bewust een merge en geen rebase: dan blijft beide historie intact en wordt er niets
herschreven waar iemand anders al op voortbouwt.

Drie bestanden raakten beide kanten. Alle drie zijn nagekeken, want dat een merge
automatisch slaagt zegt niets over of hij inhoudelijk klopt:

src/services/ActivityPubService.js

  • de sleutelbinding staat nu boven de nieuwe asSlug-aanroep van 1.7.0, dus de controle komt nog steeds voor de handtekeningcontrole

scripts/klonkt-refresh-updater.sh

  • alleen de opzij-aanpak overleefde; systemctl mask staat nergens meer als code

deploy/MULTI-INSTANCE.md

  • spreekt zichzelf niet tegen: beschrijft opzij zetten, met de reden waarom mask weigert

remarks: het gat dat in de review naar boven kwam staat hiermee ook op de 1.7.0-lijn.
De andere bevindingen uit die review staan nog open en zijn niet in deze merge
opgelost; die horen als beads. Ook nog te doen: dezelfde sleutelbinding op stable
als 1.6.1, want daar is het gat nog open bij self-hosters.

-robo
Co-Authored-By: Claude Opus 4.8 <noreply@…>

File:
1 edited

Legend:

Unmodified
Added
Removed
  • test/authorized-fetch.test.js

    rf85b2c3 r952baf3  
    1 // FEP-633c §5.3 note: a committed guardian is recognised for authorized fetch.
     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
    212import { test } from 'node:test';
    313import assert from 'node:assert/strict';
    414
    515process.env.DATABASE_PATH = ':memory:';
    6 process.env.PUBLIC_BASE_URL = 'https://test.example';
     16process.env.PUBLIC_BASE_URL = 'https://klonkt.test';
    717
    818const dbMod = await import('../src/config/database.js');
    919const db = dbMod.default;
    1020dbMod.initializeDatabase();
    11 const AP = (await import('../src/services/ActivityPubService.js')).default;
     21const AP = await import('../src/services/ActivityPubService.js');
    1222
    13 db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)').run('u1', 'u1', 'u1@t', 'x', 'god');
    14 db.prepare('INSERT INTO sites (id, slug, title, owner_id, is_primary) VALUES (?,?,?,?,1)').run('s1', 'kid', 'kid', 'u1');
    15 // Commit a guardian relation: kid (ward) is guarded by mom.
    16 const MOM = 'https://mom.example/ap/users/mom';
    17 db.prepare("INSERT INTO ap_guardianships (slug, role, other_uri, status, created_at) VALUES ('kid','ward',?, 'accepted', CURRENT_TIMESTAMP)").run(MOM);
     23db.prepare('INSERT INTO users (id, username, email, password_hash, role) VALUES (?,?,?,?,?)')
     24  .run('u1', 'u1', 'u1@test', 'x', 'god');
     25db.prepare('INSERT INTO sites (id, slug, title, owner_id) VALUES (?,?,?,?)').run('s1', 'me', 'Me', 'u1');
    1826
    19 test('a committed guardian is recognised; a stranger is not', () => {
    20   assert.equal(AP.isWardGuardian('kid', MOM), true);
    21   assert.equal(AP.isWardGuardian('kid', 'https://x.example/ap/users/stranger'), false);
    22   assert.equal(AP.isWardGuardian('nosuch', MOM), false);
     27// IP-literals: safeFetch slaat de DNS-lookup over, dus de test blijft offline.
     28const OPEN_ACTOR = 'https://203.0.113.30/users/open';       // gewone instance
     29const SECURE_ACTOR = 'https://203.0.113.40/users/gesloten'; // authorized fetch
     30
     31let verzoeken = [];
     32const echteFetch = globalThis.fetch;
     33globalThis.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
     48test('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');
    2354});
     55
     56test('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
     63test('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
     68test('een OPEN instance wordt NIET ondertekend opgehaald', () => {
     69  // De veiligheidskant van shaer-afq: verifyRequest haalt de keyId-URL op
     70  // voordat er iets geverifieerd is, en die URL komt uit een header die iedereen
     71  // mag sturen. Tekenden we standaard, dan kan een vreemde ons een ondertekend
     72  // verzoek naar een adres van zijn keuze laten sturen, met onze identiteit
     73  // eronder. Onbetekend eerst dus, en alleen tekenen als het anders niet lukt.
     74  verzoeken = [];
     75  return AP.fetchActor(OPEN_ACTOR, { asSlug: 'me' }).then((actor) => {
     76    assert.ok(actor);
     77    assert.equal(verzoeken.length, 1, 'één poging, geen tweede');
     78    assert.equal(verzoeken[0].ondertekend, false, 'en die was onbetekend');
     79  });
     80});
     81
     82test('een document zonder sleutel telt als mislukt en leidt tot een ondertekende poging', async () => {
     83  // Sommige instances serveren onbetekend wel iets, maar zonder publicKey. Voor
     84  // een verificatie hebben we daar niets aan.
     85  const KAAL = 'https://203.0.113.50/users/kaal';
     86  const stubOrig = globalThis.fetch;
     87  globalThis.fetch = async (url, opts = {}) => {
     88    const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature));
     89    if (String(url) === KAAL) {
     90      verzoeken.push({ url: String(url), ondertekend });
     91      const body = ondertekend
     92        ? { id: KAAL, type: 'Person', publicKey: { id: `${KAAL}#k`, owner: KAAL, publicKeyPem: 'x' } }
     93        : { id: KAAL, type: 'Person' };   // kaal: geen sleutel
     94      return new Response(JSON.stringify(body), { status: 200, headers: { 'content-type': 'application/activity+json' } });
     95    }
     96    return stubOrig(url, opts);
     97  };
     98  verzoeken = [];
     99  const actor = await AP.fetchActor(KAAL, { asSlug: 'me' });
     100  globalThis.fetch = stubOrig;
     101  assert.ok(actor.publicKey && actor.publicKey.publicKeyPem, 'de ondertekende poging levert de sleutel');
     102  assert.deepEqual(verzoeken.map((v) => v.ondertekend), [false, true], 'eerst onbetekend, daarna pas ondertekend');
     103});
     104
     105test('een onbekende actor blijft null, ondertekend of niet', async () => {
     106  assert.equal(await AP.fetchActor('https://203.0.113.99/users/weg', { asSlug: 'me' }), null);
     107});
     108
     109test.after(() => { globalThis.fetch = echteFetch; });
Note: See TracChangeset for help on using the changeset viewer.