Changeset c55fb34 in Klonkt for src/services/ActivityPubService.js
- Timestamp:
- 07/01/2026 01:03:39 AM (2 months ago)
- Branches:
- main
- Children:
- cf314b4
- Parents:
- f0807cc
- File:
-
- 1 edited
-
src/services/ActivityPubService.js (modified) (4 diffs)
Legend:
- Unmodified
- Added
- Removed
-
src/services/ActivityPubService.js
rf0807cc rc55fb34 717 717 // Best-effort verification of an incoming signed request. Returns the sender's 718 718 // actor doc if the signature checks out, else null. (Not gating yet — MVP.) 719 // Max clock skew for the signed Date header (replay window). Generous default to tolerate 720 // federating servers with drifting clocks; an operator can widen it via env. 721 const SIG_MAX_SKEW_MS = (Number(process.env.AP_SIG_MAX_SKEW_MIN) || 60) * 60 * 1000; 719 722 export async function verifyRequest(req) { 720 723 const sigH = req.headers['signature']; … … 745 748 try { if (crypto.verify('sha256', Buffer.from(line), pem, _sig)) { ok = true; break; } } catch { /* try next host */ } 746 749 } 747 if (ok && hs.includes('digest') && req.rawBody) { 748 const exp = 'SHA-256=' + crypto.createHash('sha256').update(req.rawBody).digest('base64'); 749 if (req.headers['digest'] !== exp) ok = false; 750 // Replay defence: the Date header must be signed and recent. A captured signed request 751 // replayed later (or with a swapped body) is rejected. 752 if (ok) { 753 if (!hs.includes('date')) ok = false; 754 else { 755 const t = Date.parse(req.headers['date'] || ''); 756 if (isNaN(t) || Math.abs(Date.now() - t) > SIG_MAX_SKEW_MS) ok = false; 757 } 758 } 759 // Digest is MANDATORY when the request carries a body: without a signed digest the body 760 // isn't covered by the signature and could be swapped on a replay. 761 if (ok && req.rawBody && req.rawBody.length) { 762 if (!hs.includes('digest')) ok = false; 763 else { 764 const exp = 'SHA-256=' + crypto.createHash('sha256').update(req.rawBody).digest('base64'); 765 if (req.headers['digest'] !== exp) ok = false; 766 } 750 767 } 751 768 return ok ? actor : null; … … 903 920 if (bn && bn !== 404 && (bn.type === 'Note' || bn.type === 'Article') && bn.id) { 904 921 const origUri = actorUriOf(bn.attributedTo); 922 // Block completeness: even if you follow the booster, drop a boost whose ORIGINAL 923 // author is blocked — otherwise a block is bypassed via someone else's boost. 924 if (origUri && isBlockedAny(origUri)) { console.log('[AP] timeline boost dropped (blocked origin)', origUri, 'via', actorUri); return 202; } 905 925 const oai = actorInfo(await resolveActor(origUri), origUri); 906 926 const html = HtmlSanitizerService.sanitize(bn.content || ''); … … 1835 1855 try { 1836 1856 if (kind === 'domain') { 1837 const like = `%//${target}/%`; 1838 db.prepare('DELETE FROM ap_interactions WHERE actor_uri LIKE ?').run(like); 1839 db.prepare('DELETE FROM ap_timeline WHERE author_uri LIKE ?').run(like); 1840 db.prepare('DELETE FROM ap_followers WHERE actor_uri LIKE ?').run(like); 1857 // Exact host match (a URL LIKE over-/under-matches: it misses bare-domain or :port 1858 // actor URIs and can catch look-alikes). Filter by parsed host, same as isBlockedAny. 1859 const purge = (table, col) => { 1860 let rows = []; 1861 try { rows = db.prepare(`SELECT DISTINCT ${col} AS u FROM ${table} WHERE ${col} IS NOT NULL AND ${col} != ''`).all(); } catch { return; } 1862 const del = db.prepare(`DELETE FROM ${table} WHERE ${col} = ?`); 1863 for (const r of rows) { let h = ''; try { h = new URL(r.u).host; } catch { /* skip */ } if (h === target) { try { del.run(r.u); } catch { /* ignore */ } } } 1864 }; 1865 purge('ap_interactions', 'actor_uri'); 1866 purge('ap_timeline', 'author_uri'); 1867 purge('ap_followers', 'actor_uri'); 1841 1868 } else { 1842 1869 db.prepare('DELETE FROM ap_interactions WHERE actor_uri = ?').run(target);
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)