Changeset bedd7a6 in Klonkt
- Timestamp:
- 08/06/2026 12:26:27 PM (5 weeks ago)
- Branches:
- main
- Children:
- 279ca0b
- Parents:
- f4f0a6d
- git-author:
- Robin <roboburr@…> (08/06/2026 12:26:18 PM)
- git-committer:
- roboburr <roboburr@…> (08/06/2026 12:26:27 PM)
- Files:
-
- 2 edited
-
src/services/ActivityPubService.js (modified) (2 diffs)
-
test/forwarded-activities.test.js (modified) (1 diff)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rf4f0a6d rbedd7a6 1726 1726 1727 1727 async function dereferenceForwarded(act, claimedActor, type, slugParam) { 1728 if (type !== 'Create' && type !== 'Update') return null; 1728 // Every exit states its reason. Five of the six used to return silently, so a 1729 // rejection count could not be told apart from a narrowing that closed too far 1730 // — and that is exactly the measurement shaer-drf is waiting for. Bounded by 1731 // the signer-mismatch rate (tens per hour), so this is not a noisy log. 1732 const skipped = (reason, detail) => { 1733 console.log(`[AP] inbox forwarded, skipped (${reason}):`, claimedActor, detail || ''); 1734 return null; 1735 }; 1736 if (type !== 'Create' && type !== 'Update') return skipped('not Create/Update', type); 1729 1737 const o = act && act.object; 1730 1738 const objId = typeof o === 'string' ? o : (o && o.id); 1731 if (!objId || typeof objId !== 'string' || !/^https:\/\//i.test(objId)) return null;1732 try { 1733 if (new URL(objId).host !== new URL(claimedActor).host) return null; // ankereis1734 } catch { return null; }1739 if (!objId || typeof objId !== 'string' || !/^https:\/\//i.test(objId)) return skipped('no https object id', objId || '(none)'); 1740 try { 1741 if (new URL(objId).host !== new URL(claimedActor).host) return skipped('host anchor', objId); // ankereis 1742 } catch { return skipped('unparsable id', objId); } 1735 1743 // Alleen dereferencen als het object beweert een antwoord te zijn op iets van 1736 1744 // ONS (shaer-drf). Zonder die eis zijn claimedActor en object.id allebei door … … 1742 1750 ? (typeof o.inReplyTo === 'string' ? o.inReplyTo : (o.inReplyTo && o.inReplyTo.id)) 1743 1751 : null; 1744 if (!knownNoteUri(parent)) { 1745 console.log('[AP] inbox forwarded, skipped (unknown inReplyTo):', claimedActor, parent || '(none)'); 1746 return null; 1747 } 1748 if (derefRecentlyFailed(objId)) return null; 1752 if (!knownNoteUri(parent)) return skipped('unknown inReplyTo', parent || '(none)'); 1753 if (derefRecentlyFailed(objId)) return skipped('recent failure', objId); 1749 1754 // Onbetekend eerst; tekenen alleen als terugval. Anders kan een ander ons een 1750 1755 // ONDERTEKEND verzoek naar een adres van zijn keuze laten sturen -- dezelfde 1751 1756 // reden als bij fetchActor sinds efe5633. 1752 1757 let fetched = await apGetJson(objId).catch(() => null); 1753 if ((!fetched || fetched.id !== objId) && slugParam) { 1754 fetched = await signedGetJson(slugParam, objId).catch(() => null); 1758 if (!fetched || fetched.id !== objId) { 1759 // The signer used to be slugParam, which is null on the shared inbox — and 1760 // that is where forwarded traffic lands, because we advertise a sharedInbox. 1761 // signedGetJson falls back to an unsigned GET for a null slug, so a source in 1762 // secure mode could never be dereferenced at all. Same fix verifyRequest got 1763 // in shaer-afq: any local actor is a valid signer. 1764 const asSlug = slugParam || anySigningSlug(); 1765 if (asSlug) fetched = await signedGetJson(asSlug, objId).catch(() => null); 1755 1766 } 1756 1767 const attributed = fetched && (typeof fetched.attributedTo === 'string' 1757 1768 ? fetched.attributedTo 1758 1769 : (fetched.attributedTo && fetched.attributedTo.id)); 1759 if (!fetched || fetched.id !== objId || attributed !== claimedActor) {1770 if (!fetched || fetched.id !== objId) { 1760 1771 noteDerefFailure(objId); 1761 return null; 1772 return skipped('fetch failed', objId); 1773 } 1774 if (attributed !== claimedActor) { 1775 // Not a transport hiccup: the source itself says someone else wrote this. 1776 noteDerefFailure(objId); 1777 return skipped('attributedTo mismatch', `${objId} claims ${attributed || '(none)'}`); 1762 1778 } 1763 1779 return fetched; -
test/forwarded-activities.test.js
rf4f0a6d rbedd7a6 199 199 }); 200 200 201 test('op de GEDEELDE inbox wordt de bron ook ondertekend opgehaald', async () => { 202 // Doorstuurverkeer landt op /ap/inbox, want we adverteren een sharedInbox -- 203 // en daar is slugParam null. signedGetJson valt bij een lege slug terug op een 204 // ONBETEKENDE GET, dus een bron in secure mode was langs deze weg helemaal niet 205 // te dereferencen. Elke lokale actor is een geldige ondertekenaar, net als in 206 // verifyRequest sinds shaer-afq. 207 const GESLOTEN = 'https://203.0.113.10/notes/secure-mode'; // eigen id: buiten de negatieve cache 208 const note = { 209 id: GESLOTEN, type: 'Note', attributedTo: AUTEUR, inReplyTo: ONZE_NOTE, 210 content: '<p>uit secure mode</p>', to: ['https://www.w3.org/ns/activitystreams#Public'], 211 }; 212 const stub = globalThis.fetch; 213 const pogingen = []; 214 globalThis.fetch = async (url, opts = {}) => { 215 if (String(url) !== GESLOTEN) return stub(url, opts); 216 const ondertekend = !!(opts.headers && (opts.headers.Signature || opts.headers.signature)); 217 pogingen.push(ondertekend); 218 return ondertekend 219 ? new Response(JSON.stringify(note), { status: 200, headers: { 'content-type': 'application/activity+json' } }) 220 : new Response('unauthorized', { status: 401 }); 221 }; 222 const status = await AP.handleInbox(req(doorgestuurd(note)), null, alsDoorstuurder); 223 globalThis.fetch = stub; 224 assert.equal(status, 202, 'de doorgestuurde Create hoort geaccepteerd te worden'); 225 assert.deepEqual(pogingen, [false, true], 'eerst onbetekend, daarna pas ondertekend'); 226 }); 227 201 228 test.after(() => { globalThis.fetch = echteFetch; });
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)