Changes in / [a4fea5e:ed5e7ab] in Klonkt
- Files:
-
- 2 added
- 5 edited
-
deploy/MULTI-INSTANCE.md (modified) (1 diff)
-
scripts/klonkt-migrate-data.sh (modified) (1 diff)
-
scripts/klonkt-refresh-updater.sh (modified) (1 diff)
-
src/config/database.js (modified) (3 diffs)
-
src/services/ActivityPubService.js (modified) (1 diff)
-
test/ap-signature-keyid-binding.test.js (added)
-
test/feed-state-triggers.test.js (added)
Legend:
- Unmodified
- Added
- Removed
-
deploy/MULTI-INSTANCE.md
ra4fea5e red5e7ab 84 84 because the port comes from the same `.env`. 85 85 86 **Rolling back.** The old `klonkt.service` is disabled and masked, not deleted. 87 Masked because `disable` alone does not stop `systemctl restart klonkt` from 88 starting it again, and a resurrected unit no longer finds its `.env` (that moved 89 with the data): it would fall back to the defaults and write a fresh empty 90 database into the checkout. To go back, move the data into `/opt/klonkt/storage`, 91 restore the relative paths in `.env`, then `systemctl unmask klonkt` and 92 `systemctl enable --now klonkt`. 86 **The old unit is moved aside, not deleted.** It ends up next to its old place 87 as `klonkt.service.retired-<timestamp>`. Stopping and disabling is not enough: 88 `systemctl restart klonkt` starts a disabled unit anyway, which is exactly what 89 an updater generated before the split does. A resurrected `klonkt.service` no 90 longer finds its `.env` (that moved with the data), falls back to the built-in 91 defaults, and writes a fresh empty database into the checkout. Masking does not 92 work here either, because the unit file sits in `/etc/systemd/system` and 93 `systemctl mask` refuses while a real file is there. 94 95 **Rolling back.** Move the data into `/opt/klonkt/storage`, restore the relative 96 paths in `.env`, move the retired unit file back to 97 `/etc/systemd/system/klonkt.service`, then: 98 99 ```bash 100 sudo systemctl daemon-reload 101 sudo systemctl enable --now klonkt 102 ``` 93 103 94 104 ## Adding an instance -
scripts/klonkt-migrate-data.sh
ra4fea5e red5e7ab 128 128 run "systemctl daemon-reload" 129 129 130 step "Retiring $OLD_UNIT" 131 # Stopping and disabling is NOT enough: `systemctl restart klonkt` starts a 132 # disabled unit anyway, and that is exactly what an updater generated before 133 # the split does. A resurrected klonkt.service no longer finds its .env (that 134 # moved with the data), falls back to the built-in defaults, and writes a 135 # FRESH EMPTY database into the checkout. 136 # 137 # Masking does not help either: the unit file lives in /etc/systemd/system, 138 # the highest-priority directory, and `systemctl mask` refuses when a real 139 # file is already there ("File ... already exists"). Verified, not assumed. 140 # 141 # So the file is moved aside. systemd then no longer knows the unit at all and 142 # any restart fails loudly with "Unit klonkt.service not found". The file is 143 # kept next to its old place, timestamped, so a rollback is a move back. 144 if [ -f "/etc/systemd/system/$OLD_UNIT" ]; then 145 run "systemctl stop $OLD_UNIT 2>/dev/null || true" 146 run "systemctl disable $OLD_UNIT 2>/dev/null || true" 147 RETIRED="/etc/systemd/system/${OLD_UNIT}.retired-$(date +%Y%m%d%H%M%S)" 148 run "mv '/etc/systemd/system/$OLD_UNIT' '$RETIRED'" 149 run "systemctl daemon-reload" 150 say "stopped, disabled and moved aside → $RETIRED" 151 say "roll back by moving that file back and running: systemctl daemon-reload" 152 else 153 say "no $OLD_UNIT unit file to retire" 154 fi 155 130 156 step "Switching to klonkt@$SLUG" 131 if systemctl list-unit-files "$OLD_UNIT" >/dev/null 2>&1; then132 run "systemctl disable --now $OLD_UNIT"133 # Disable only removes the autostart link: `systemctl restart klonkt` would134 # still START it. That is not theoretical — an updater generated before the135 # split does exactly that, and the resurrected unit finds no .env (it moved136 # with the data), falls back to the built-in defaults and creates a FRESH137 # EMPTY database in the checkout. Masking makes any such call fail loudly.138 # Reversible: systemctl unmask klonkt.139 run "systemctl mask $OLD_UNIT"140 say "disabled and masked $OLD_UNIT (unmask to roll back)"141 fi142 157 run "systemctl enable --now 'klonkt@$SLUG'" 143 158 -
scripts/klonkt-refresh-updater.sh
ra4fea5e red5e7ab 77 77 echo "klonkt-update rewritten: branch ${BRANCH}, code ${KLONKT_DIR}, instances under ${DATA_ROOT}" 78 78 79 # On a split install the old single unit must not be startable. `disable` alone 80 # does not stop `systemctl restart klonkt` from starting it, and a resurrected 81 # klonkt.service has no .env (it moved with the data): it falls back to the 82 # defaults and writes a fresh empty database into the checkout. 79 # On a split install the old single unit must not be startable at all. 80 # `disable` is not enough (restart starts a disabled unit anyway) and `mask` 81 # refuses while the real file sits in /etc/systemd/system, the highest-priority 82 # directory. Moving the file aside is what actually works: systemd stops 83 # knowing the unit, so any restart fails loudly instead of quietly starting a 84 # second process that writes an empty database into the checkout. 83 85 SPLIT=0 84 86 for d in "${DATA_ROOT}"/*/; do [ -f "$d/.env" ] && SPLIT=1 && break; done 85 if [ "$SPLIT" = 1 ] && systemctl list-unit-files klonkt.service >/dev/null 2>&1; then 86 if ! systemctl is-enabled klonkt.service 2>/dev/null | grep -q masked; then 87 systemctl stop klonkt.service 2>/dev/null || true 88 systemctl disable klonkt.service 2>/dev/null || true 89 systemctl mask klonkt.service 90 echo "retired klonkt.service: stopped, disabled and masked (unmask to roll back)" 87 if [ "$SPLIT" = 1 ] && [ -f /etc/systemd/system/klonkt.service ]; then 88 systemctl stop klonkt.service 2>/dev/null || true 89 systemctl disable klonkt.service 2>/dev/null || true 90 RETIRED="/etc/systemd/system/klonkt.service.retired-$(date +%Y%m%d%H%M%S)" 91 if mv /etc/systemd/system/klonkt.service "$RETIRED"; then 92 systemctl daemon-reload 93 echo "retired klonkt.service → $RETIRED (move it back + daemon-reload to roll back)" 94 else 95 echo "WARNING: could not move /etc/systemd/system/klonkt.service aside." 96 echo " Until you do, any 'systemctl restart klonkt' starts a second" 97 echo " process that writes an empty database into ${KLONKT_DIR}." 91 98 fi 92 99 fi -
src/config/database.js
ra4fea5e red5e7ab 499 499 ); 500 500 CREATE INDEX IF NOT EXISTS idx_ap_timeline_slug ON ap_timeline(slug, published); 501 -- canonicalReactionUri herleidt een permalink naar het object-id door op (slug, url) 502 -- te zoeken. Zonder deze index viel dat terug op idx_ap_timeline_slug, dus een scan 503 -- van elke rij van die slug. Dat gebeurt PER REACTIE in getInteractions, en de 504 -- reactie-migratie erft het in haar re-key-join, die synchroon vóór listen draait: 505 -- de opstartkosten waren reacties maal tijdlijnrijen. 506 CREATE INDEX IF NOT EXISTS idx_ap_timeline_url ON ap_timeline(slug, url); 501 507 CREATE TABLE IF NOT EXISTS ap_blocks ( 502 508 id INTEGER PRIMARY KEY AUTOINCREMENT, … … 850 856 } 851 857 // slug + object_uri verschillen per bron; de rest is voor alle vier gelijk. 852 const zet = (naam, gebeurtenis, tabel, slug, uri, kind, extra = '' ) => `858 const zet = (naam, gebeurtenis, tabel, slug, uri, kind, extra = '', wanneer = '') => ` 853 859 DROP TRIGGER IF EXISTS ${naam}; 854 CREATE TRIGGER ${naam} AFTER ${gebeurtenis} ON ${tabel} BEGIN860 CREATE TRIGGER ${naam} AFTER ${gebeurtenis} ON ${tabel}${wanneer ? ` WHEN ${wanneer}` : ''} BEGIN 855 861 UPDATE ap_feed_rev SET n = n + 1; 856 862 INSERT INTO ap_feed_state (slug, object_uri, rev, kind) … … 874 880 // ap_interactions draagt geen slug: die hangt aan de POST. Vandaar de join, 875 881 // en vandaar dat deze drie niet in de gewone vorm passen. 876 zet('trg_feed_ia_ins', 'INSERT', 'ap_interactions', '', '', '', `${joinPosts('NEW.object_uri', 'new')} WHERE p.id = NEW.post_id`), 877 zet('trg_feed_ia_upd', 'UPDATE OF content, media_json, quote_json, embed_json', 'ap_interactions', '', '', '', `${joinPosts('NEW.object_uri', 'updated')} WHERE p.id = NEW.post_id`), 878 zet('trg_feed_ia_del', 'DELETE', 'ap_interactions', '', '', '', `${joinPosts('OLD.object_uri', 'deleted')} WHERE p.id = OLD.post_id`), 882 // 883 // De WHEN op kind='reply' is nodig omdat deze tabel ook likes en announces 884 // draagt, en die schrijven object_uri = '' (zie recordInteraction). Zonder de 885 // WHEN bumpte elke inkomende like de rev, werd elke wachter gewekt en kreeg 886 // die de hele collectie opnieuw terwijl er niets aan veranderd was: precies de 887 // kosten die de 304 moest wegnemen. Bovendien belandde er dan een rij op de 888 // lege string in ap_feed_state, die feedChangesSince vervolgens uitdeelt. 889 // De oude cursor filterde hier wel op kind; bij ap_timeline is dit ook gedaan 890 // (de UPDATE OF sluit liked/boosted uit) en één tabel verder vergeten. 891 zet('trg_feed_ia_ins', 'INSERT', 'ap_interactions', '', '', '', `${joinPosts('NEW.object_uri', 'new')} WHERE p.id = NEW.post_id`, "NEW.kind = 'reply'"), 892 zet('trg_feed_ia_upd', 'UPDATE OF content, media_json, quote_json, embed_json', 'ap_interactions', '', '', '', `${joinPosts('NEW.object_uri', 'updated')} WHERE p.id = NEW.post_id`, "NEW.kind = 'reply'"), 893 zet('trg_feed_ia_del', 'DELETE', 'ap_interactions', '', '', '', `${joinPosts('OLD.object_uri', 'deleted')} WHERE p.id = OLD.post_id`, "OLD.kind = 'reply'"), 879 894 ].join('\n')); 880 895 } catch (e) { -
src/services/ActivityPubService.js
ra4fea5e red5e7ab 1435 1435 const pem = actor && actor.publicKey && actor.publicKey.publicKeyPem; 1436 1436 if (!pem) return null; 1437 // Bind the key to the actor it speaks for. Without this we hand back whatever 1438 // `id` the fetched document claims, so anyone could host a document carrying a 1439 // VICTIM's id next to their OWN public key, sign with their own private half, 1440 // and be believed: the victim's server is never contacted. The caller decides on 1441 // `verified.id`, so the identity has to come from where the key was FETCHED, 1442 // never from what the document says about itself. 1443 // Adds conditions only, and there is no exemption list on purpose: an 1444 // "unless it's a known peer" escape hatch is exactly the door this closes. 1445 // Note this does not narrow what we accept in practice, since the line above 1446 // already requires the embedded publicKey object (an array or a bare URI 1447 // reference never worked here). 1448 const key = actor.publicKey; 1449 try { 1450 if (new URL(p.keyId).host !== new URL(actor.id).host) return null; // same origin as the key 1451 if (key.id && key.id !== p.keyId) return null; // this key, not a neighbour's 1452 if (key.owner && key.owner !== actor.id) return null; // and it belongs to this actor 1453 } catch { return null; } // unparseable id or keyId 1437 1454 const hs = (p.headers || '(request-target) host date').split(/\s+/); 1438 1455 // Behind a reverse proxy the raw Host header is the backend bind (e.g. localhost:3000, when
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)