Changeset 29eb745 in Klonkt
- Timestamp:
- 08/08/2026 08:53:20 PM (4 weeks ago)
- Branches:
- main
- Children:
- dc0da72
- Parents:
- ea211b9
- git-author:
- Robin <roboburr@…> (08/08/2026 08:53:18 PM)
- git-committer:
- roboburr <roboburr@…> (08/08/2026 08:53:20 PM)
- Files:
-
- 1 added
- 5 edited
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
src/services/guardianship/handshake.js (modified) (2 diffs)
-
test/decisive-vote.test.js (modified) (1 diff)
-
test/ssrf-allowlist.test.js (added)
-
tools/ward-farm/farm.mjs (modified) (2 diffs)
-
tools/ward-farm/offer-many.mjs (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rea211b9 r29eb745 110 110 return true; // not an IP literal we recognise → refuse 111 111 } 112 /** 113 * Uitzonderingen op de SSRF-poort, voor een testkudde op de eigen machine 114 * (shaer-6wt: honderd wards met een guardian, Barts opdracht 8-8). 115 * 116 * WAAROM DIT MAG BESTAAN. De bescherming hierboven is er omdat een actor-URI van 117 * een VREEMDE komt: een aanvaller die "http://169.254.169.254/" doorgeeft laat 118 * ons zijn werk doen. Deze lijst gaat niet over vreemden -- hij staat in de 119 * omgeving van deze server, wordt door de beheerder gezet, en is leeg tenzij 120 * iemand hem expliciet vult. 121 * 122 * WAAROM HIJ ZO SMAL IS. Geen vlag die "loopback is oke" zegt, maar een lijst 123 * van precieze host:poort-paren. `[::1]:3060` opent niet 127.0.0.1, niet poort 124 * 3061, en niets in het interne netwerk. Een brede vlag zou de bescherming in 125 * een dev-omgeving uitzetten, en dev-omgevingen worden productie. 126 * 127 * AP_ALLOW_HOSTS="[::1]:3060,[::1]:3061" 128 */ 129 const AP_ALLOW_HOSTS = new Set( 130 String(process.env.AP_ALLOW_HOSTS || '').split(',').map((x) => x.trim().toLowerCase()).filter(Boolean), 131 ); 132 function isAllowedTestHost(u) { 133 if (!AP_ALLOW_HOSTS.size) return false; 134 return AP_ALLOW_HOSTS.has(u.host.toLowerCase()); 135 } 112 136 async function assertPublicHost(hostname) { 113 if (net.isIP(hostname)) { if (isBlockedIp(hostname)) throw new Error('ssrf-blocked-ip'); return; } 114 const addrs = await dns.promises.lookup(hostname, { all: true }); 137 // URL.hostname geeft een IPv6-literal MET blokhaken ("[::1]"), en net.isIP 138 // herkent die vorm niet. Zonder strippen viel elk IPv6-adres door naar de 139 // DNS-tak, waar het strandde op ENOTFOUND: geweigerd, maar per ongeluk en met 140 // de verkeerde reden. isBlockedIp strippde ze al -- die verwachtte dus input 141 // die hier nooit aankwam. 142 const naakt = String(hostname || '').replace(/^\[|\]$/g, ''); 143 if (net.isIP(naakt)) { if (isBlockedIp(naakt)) throw new Error('ssrf-blocked-ip'); return; } 144 const addrs = await dns.promises.lookup(naakt, { all: true }); 115 145 if (!addrs.length || addrs.some((a) => isBlockedIp(a.address))) throw new Error('ssrf-blocked-host'); 116 146 } … … 128 158 const u = new URL(target); // throws on malformed → caller's catch 129 159 if (u.protocol !== 'https:' && u.protocol !== 'http:') throw new Error('ssrf-bad-scheme'); 130 await assertPublicHost(u.hostname); 160 // Alleen op de precieze host:poort uit AP_ALLOW_HOSTS, en per hop opnieuw: 161 // een omleiding naar een ANDER intern adres blijft geweigerd. 162 if (!isAllowedTestHost(u)) await assertPublicHost(u.hostname); 131 163 const r = await fetch(target, { 132 164 ...opts, -
src/services/guardianship/handshake.js
rea211b9 r29eb745 44 44 if (type !== 'Undo') return null; 45 45 return parseRelationship(activity && activity.object); 46 } 47 48 /** 49 * De overgebleven guardians opnieuw vertellen of ZIJ nu de doorslag geven 50 * (shaer-8vt, Barts correctie 8-8). 51 * 52 * "Doorslaggevend" is geen eigenschap van een moment maar van een STAND: zodra 53 * er nog een stem nodig is, is iedereen die nog moet antwoorden het. Eenmalig 54 * berekenen bij het doorsturen bevriest een antwoord dat verandert. 55 * 56 * Nooit dragend: lukt de update niet, dan blijft de oude waarde staan. Die is 57 * dan te voorzichtig of te stil -- en juist daarom staat de FAALSTAND aan de 58 * kant van waarschuwen (isDecisive leest onbekend als "ja, jij beslist"). 59 */ 60 function herzieDoorslag(site, offerId, gsOffer, laatsteStem) { 61 try { 62 const p = gated.gatedProgress(site.slug, gsOffer.feature); 63 if (!gated.isDecisive(p.votes, p.need)) return; // nog niets veranderd 64 const me = deps.selfId(site.slug); 65 const gestemd = new Set([gsOffer.proposer, laatsteStem].filter(Boolean)); 66 for (const g of relations.listGuardians(site.slug).map((x) => x.other_uri)) { 67 if (gestemd.has(g)) continue; 68 deps.deliverTo(site, g, { 69 id: offerId, type: 'Offer', actor: me, to: [g], 70 object: { type: 'shaer:GatedSetting', 'shaer:ward': me, 'shaer:feature': gsOffer.feature, 'shaer:value': !!gsOffer.value }, 71 'shaer:proposer': gsOffer.proposer || undefined, 72 'shaer:decisive': true, 73 }).catch(() => { /* de bezorgwachtrij probeert opnieuw */ }); 74 } 75 } catch { /* nooit dragend */ } 46 76 } 47 77 … … 537 567 const r = gated.recordGatedVote(site.slug, gsOffer.feature, actor, value); 538 568 if (r.state === 'settled') answerGatedProposer(site, offerId, r); 569 // DOORSLAGGEVEND SCHUIFT MEE (Barts correctie, 8-8). Ik berekende dit een 570 // keer bij het doorsturen en bevroor het. Bij vijf guardians staat er dan 571 // "je beslist niets" -- en zodra er een ja bij komt IS elk van de anderen de 572 // doorslag. Dat is precies de stille kant: het scherm zwijgt op het moment 573 // dat het moet spreken. 574 // 575 // Dus na elke stem die het open laat: de overgeblevenen opnieuw vertellen 576 // waar ze staan. Alleen wie NOG NIET geantwoord heeft, en alleen als het 577 // antwoord verandert -- anders is dit een bericht per stem per guardian. 578 else herzieDoorslag(site, offerId, gsOffer, actor); 539 579 notify(site.slug, { kind: 'gated_setting', feature: gsOffer.feature, value, state: r.state }); 540 580 return true; -
test/decisive-vote.test.js
rea211b9 r29eb745 47 47 assert.equal(gated.isDecisive(null, null), true); 48 48 }); 49 50 test('doorslaggevend is een STAND, geen moment', () => { 51 // Barts correctie (8-8): het geldt voor ALLE opvolgende beslissers. Bij vijf 52 // guardians is bij het doorsturen niemand het (drempel 3, een stem), maar na 53 // de tweede ja is iedereen die nog moet antwoorden het wel. 54 const drempel = gated.thresholdFor(5); // 3 55 assert.equal(gated.isDecisive(1, drempel), false, 'bij het doorsturen nog niet'); 56 assert.equal(gated.isDecisive(2, drempel), true, 'na een ja erbij wel'); 57 }); 58 59 test('en bij drie guardians is het meteen zo', () => { 60 assert.equal(gated.isDecisive(1, gated.thresholdFor(3)), true); 61 }); -
tools/ward-farm/farm.mjs
rea211b9 r29eb745 33 33 const HERE = path.dirname(fileURLToPath(import.meta.url)); 34 34 const STATE = path.join(HERE, 'state.json'); 35 const BASE = process.env.BASE || 'https://wards.klonkt.com'; 35 // GEEN TLS, en dat hoort ook zo (Barts vraag, 8-8: "waarom via https? gewoon 36 // localhost ::1"). Een certificaat en een publieke hostname toevoegen aan een 37 // doos die ook andermans sites draait, om honderd neptestkinderen te kunnen 38 // bereiken, is de verkeerde prijs. Klonkt laat dit adres door via AP_ALLOW_HOSTS 39 // -- een precies host:poort-paar, geen "loopback mag"-vlag. 40 // 41 // EN HET TOETST IETS DAT WE TOCH MOESTEN KUNNEN: http met HTTP-signatures, zonder 42 // TLS. Dat is een echt federatiegeval (interne netwerken, onion), en tot nu toe 43 // was het nergens uitgeprobeerd. 44 const BASE = process.env.BASE || 'http://[::1]:3060'; 36 45 const PORT = Number(process.env.PORT || 3060); 37 46 const N = Number(process.env.WARDS || 100); … … 276 285 bewaarStaat(staat); 277 286 278 server.listen(PORT, ' 127.0.0.1', () => {287 server.listen(PORT, '::1', () => { 279 288 console.log(`[farm] ${N} wards op ${BASE} (luistert op 127.0.0.1:${PORT})`); 280 289 }); -
tools/ward-farm/offer-many.mjs
rea211b9 r29eb745 25 25 26 26 const slug = args.slug || 'dev'; 27 const farmBase = (args.base || 'http s://wards.klonkt.com').replace(/\/+$/, '');27 const farmBase = (args.base || 'http://[::1]:3060').replace(/\/+$/, ''); 28 28 const van = Number(args.from || 1); 29 29 const tot = Number(args.to || 100);
Note:
See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/chrome/site/your_project_logo.png)