source: Klonkt/src/services/Scheduler.js@ 16e99e5

main
Last change on this file since 16e99e5 was 3dd99d3, checked in by Robin Genis <roboburr@…>, 3 months ago

harden(fediverse): SSRF guard, remote-URL XSS scheme-guard, scoped Delete, gate-by-default + queue fixes

From a 3-agent hardening review of this session's fediverse code:

  • SSRF: all outbound fetches (deliver/fetchActor/webfingerResolve) now go through safeFetch — http(s)-only, rejects hosts resolving to private/loopback/link-local ranges on the initial host AND every redirect hop (redirect:manual), + actor-doc size cap. Blocks inbox-driven SSRF to cloud-metadata/internal services.
  • Stored XSS: remote actor url/icon, timeline media + author urls, and remote-note images/object_uri are now run through an http(s) scheme-guard before storage, so a malicious actor can't smuggle javascript:/data: into owner-only-rendered href/src.
  • Cross-actor Delete: inbound Delete is now scoped to the signing actor (can't wipe another actor's replies/timeline rows).
  • Gate-by-default: Add/Remove/Update added to the signature-enforced activity list.
  • Delivery queue: re-entrancy guard (30 rows x 8s can exceed the 60s tick -> no double-delivery) + backoff off-by-one fix (1-min first retry no longer skipped).
  • Scheduler: delete-before-insert on FTS so a re-flipped post has no duplicate row.
  • /meldingen: don't mark-seen for a viewer (GET-side mutation the global guard misses).
  • Activity ids get a random suffix to avoid same-millisecond collisions.

Co-Authored-By: Claude <noreply@…>

  • Property mode set to 100644
File size: 2.6 KB
Line 
1/**
2 * Scheduler — release planning (premium #3).
3 *
4 * Scheduled posts have status 'scheduled' + publish_at (future). A lightweight
5 * timer flips them to 'published' once publish_at is reached. This means public
6 * queries (status='published') need NO changes — a scheduled post simply isn't
7 * 'published' yet and therefore invisible until that moment.
8 */
9
10import db from '../config/database.js';
11import HtmlSanitizerService from './HtmlSanitizerService.js';
12import ActivityPubService from './ActivityPubService.js';
13
14export function flipScheduledPosts() {
15 try {
16 const due = db.prepare(`
17 SELECT p.id, p.site_id, p.slug, p.title, p.content, p.cover_image_url, p.fan_only,
18 p.published_at, p.publish_at, p.created_at, u.username
19 FROM posts p JOIN users u ON u.id = p.author_id
20 WHERE p.status = 'scheduled' AND p.publish_at IS NOT NULL AND datetime(p.publish_at) <= datetime('now')
21 `).all();
22 if (!due.length) return 0;
23 const upd = db.prepare(
24 "UPDATE posts SET status = 'published', published_at = COALESCE(published_at, publish_at, CURRENT_TIMESTAMP) WHERE id = ?"
25 );
26 const ftsDel = db.prepare('DELETE FROM posts_fts WHERE post_id = ?');
27 const fts = db.prepare('INSERT INTO posts_fts(content, title, author, post_id) VALUES (?, ?, ?, ?)');
28 const siteStmt = db.prepare('SELECT * FROM sites WHERE id = ?');
29 for (const p of due) {
30 upd.run(p.id);
31 // Delete-before-insert so a re-scheduled (previously published) post doesn't
32 // get a duplicate FTS row → duplicate search hits.
33 try { ftsDel.run(p.id); fts.run(HtmlSanitizerService.toPlainText(p.content || ''), p.title || '', p.username || '', p.id); } catch { /* FTS failure is non-fatal */ }
34 // ActivityPub: federate the now-published post to followers.
35 if (!p.fan_only) {
36 try {
37 const site = siteStmt.get(p.site_id);
38 if (site) {
39 ActivityPubService.deliverCreate(site, {
40 id: p.id, slug: p.slug, title: p.title || p.slug,
41 content: p.content, cover_image_url: p.cover_image_url || null,
42 published_at: p.published_at || p.publish_at, created_at: p.created_at,
43 }).catch(() => { /* best-effort */ });
44 }
45 } catch { /* non-fatal */ }
46 }
47 }
48 return due.length;
49 } catch { return 0; }
50}
51
52let _timer = null;
53export function startScheduler() {
54 flipScheduledPosts(); // run immediately on boot
55 if (_timer) return;
56 _timer = setInterval(flipScheduledPosts, 60 * 1000); // every minute
57 if (_timer.unref) _timer.unref();
58}
Note: See TracBrowser for help on using the repository browser.