source: Klonkt/tools/ward-farm/offer-many.mjs@ 29eb745

main
Last change on this file since 29eb745 was 29eb745, checked in by roboburr <roboburr@…>, 4 weeks ago

Doorslaggevend schuift mee, en de daemons hoeven geen TLS

Drie dingen van Bart (8-8).

  1. DOORSLAGGEVEND IS EEN STAND, GEEN MOMENT -- een fout in wat ik een uur eerder

bouwde. Ik berekende shaer:decisive een keer bij het doorsturen en bevroor het.
Bij vijf guardians staat er dan "je beslist niets", en zodra er een ja bij komt IS
elk van de anderen de doorslag. Precies de stille kant: het scherm zwijgt op het
moment dat het moet spreken. Na elke stem die het besluit open laat krijgen de
overgeblevenen nu een herziene stand, en alleen zij.

  1. DE VOLGPOORT MAG UITGEROLD (Barts akkoord). Zit in deze reeks.
  1. GEEN TLS VOOR DE TESTKUDDE. Barts vraag: waarom via https, gewoon localhost

::1. Terecht -- een certificaat en een publieke hostname toevoegen aan een doos
die ook andermans sites draait, om honderd neptestkinderen te bereiken, is de
verkeerde prijs.

Daarvoor mag de SSRF-poort gericht open: AP_ALLOW_HOSTS, een lijst van precieze
host:poort-paren. Geen "loopback is oke"-vlag. [::1]:3060 opent niet 127.0.0.1,
niet 3061, en niets in het interne netwerk -- want een brede vlag zet de
bescherming uit in een dev-omgeving, en dev-omgevingen worden productie. Leeg
tenzij een beheerder hem vult, en dat is de stand van elke productie-instance.

En het toetst iets dat we toch moesten kunnen: http MET HTTP-signatures, zonder
TLS. Een echt federatiegeval, tot nu toe nergens uitgeprobeerd.

ONDERWEG EEN LATENTE FOUT GEVONDEN. URL.hostname geeft een IPv6-literal met
blokhaken, en net.isIP herkent die vorm niet -- elk IPv6-adres viel door naar de
DNS-tak en strandde daar op ENOTFOUND. Geweigerd, maar per ongeluk en met de
verkeerde reden; isBlockedIp strippte die haken al en verwachtte dus input die
nooit aankwam. Nu wordt een IPv6-literal echt gecontroleerd.

EN EEN SLAAGTEST, want die ontbrak. Al mijn SSRF-toetsen controleerden alleen
weigeringen: de lijst helemaal negeren gaf nul rode toetsen, en de farm zou
stilletjes nooit werken. Nu bewijst er een dat wat op de lijst staat er ook
doorheen komt.

Suite 755/755. Drie mutaties gecontroleerd.

  • Property mode set to 100644
File size: 3.5 KB
Line 
1/**
2 * Deze Klonkt biedt zich aan als guardian bij een reeks wards (FEP-633c 3.1).
3 *
4 * Bedoeld voor de caseload-test: honderd kinderen, een guardian. Draait tegen de
5 * database van de instance zelf, zoals de andere scripts hier -- geen OAuth, geen
6 * tweede weg. Wat het doet is precies wat de PWA-knop doet, honderd keer.
7 *
8 * DATABASE_PATH=... PUBLIC_BASE_URL=... node offer-many.mjs \
9 * --slug dev --base https://wards.klonkt.com --from 1 --to 100
10 *
11 * ECHTE FEDERATIE, geen kortsluiting: de wards wonen op een andere origin, dus
12 * elk aanbod gaat over de lijn en wordt daar ondertekend beantwoord. Dat is het
13 * hele punt -- een test die de co-locatieroute neemt meet iets anders dan wat er
14 * in productie gebeurt.
15 *
16 * VEILIG OM OPNIEUW TE DRAAIEN: een ward die al gecommit is wordt overgeslagen,
17 * en een aanbod dat al openstaat wordt niet nog eens gestuurd. Zonder dat maakt
18 * een tweede run honderd dubbele aanbiedingen bij mensen die al ja zeiden.
19 */
20
21const args = Object.fromEntries(
22 process.argv.slice(2).join(' ').split('--').filter(Boolean)
23 .map((s) => s.trim().split(/\s+/)).map(([k, ...v]) => [k, v.join(' ') || true]),
24);
25
26const slug = args.slug || 'dev';
27const farmBase = (args.base || 'http://[::1]:3060').replace(/\/+$/, '');
28const van = Number(args.from || 1);
29const tot = Number(args.to || 100);
30const pauze = Number(args.pauze || 150);
31
32const AP = await import('../../src/services/ActivityPubService.js');
33const Guardianship = await import('../../src/services/guardianship/index.js');
34const dbMod = await import('../../src/config/database.js');
35const db = dbMod.default;
36
37const site = db.prepare('SELECT * FROM sites WHERE slug = ?').get(slug);
38if (!site) { console.error(`geen site met slug "${slug}"`); process.exit(1); }
39
40const base = (process.env.PUBLIC_BASE_URL || '').replace(/\/+$/, '');
41const me = AP.actorId(base, site.slug);
42console.log(`[offer] ${me} biedt zich aan bij w${String(van).padStart(3, '0')}..w${String(tot).padStart(3, '0')} op ${farmBase}`);
43
44const alBekend = new Set(Guardianship.listWards(site.slug).map((w) => w.other_uri));
45let gestuurd = 0, overgeslagen = 0, mislukt = 0;
46
47for (let i = van; i <= tot; i++) {
48 const naam = `w${String(i).padStart(3, '0')}`;
49 const ward = `${farmBase}/u/${naam}`;
50 if (alBekend.has(ward)) { overgeslagen++; continue; }
51
52 // Precies de vorm die handleOutbox verwacht: een Offer van een
53 // Relationship{subject: ward, object: ik}. Door hem via de outbox te sturen
54 // loopt dit langs dezelfde code als de knop in de PWA -- er is geen tweede pad
55 // naar hetzelfde besluit, en dat is de les van vandaag.
56 const activity = {
57 type: 'Offer',
58 to: [ward],
59 object: {
60 type: 'Relationship',
61 subject: ward,
62 object: me,
63 relationship: 'https://shaer.klonkt.com/ns#guardian',
64 },
65 };
66
67 try {
68 const uit = await AP.ingestOutboxActivity(site, { id: site.owner_id, username: slug }, activity);
69 if (uit && (uit.status === 201 || uit.status === 200)) gestuurd++;
70 else { mislukt++; console.warn(` ${naam}: ${uit && uit.status} ${uit && uit.error}`); }
71 } catch (e) {
72 mislukt++;
73 console.warn(` ${naam}: ${e.message}`);
74 }
75 // Een adempauze. Honderd handshakes tegelijk zegt meer over de bezorgwachtrij
76 // dan over het paneel, en dit script is er om het paneel te kunnen bekijken.
77 if (pauze) await new Promise((r) => setTimeout(r, pauze));
78}
79
80console.log(`[offer] verstuurd ${gestuurd}, overgeslagen ${overgeslagen}, mislukt ${mislukt}`);
81console.log('[offer] de commit valt pas als elke ward heeft geaccepteerd; kijk over een minuut met listWards.');
82process.exit(0);
Note: See TracBrowser for help on using the repository browser.