Files
felhom.eu/documentation/audits/visitors-2026-10-01/tools/r773_live.py
T

66 lines
4.0 KiB
Python

#!/usr/bin/env python3
"""r773_live.py — R-773 on 9202 through the product's own endpoints: install Karakeep (the household passes the setup
gate and makes the first account — the fixture), see sign-up closed, take the app's backup with the night chain's debug
action (the sanctioned by-day trigger), REMOVE keeping backups, press restore, and ask as a STRANGER (no session, no gate
cookie, through traefik) whether sign-up is closed again. Env: SC (0600 scratch with .ctlpw), EV. Secrets never printed."""
import json, os, subprocess, sys, time
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
import box_walk as w
import upgrade_fixtures_box as fixtures
APP, SUB = "karakeep", "bookmarks"
HOST = f"{SUB}.{w.DOMAIN}"
os.makedirs(f"{w.EV}", exist_ok=True)
log = open(f"{w.EV}/r773-live.txt", "a", buffering=1)
def say(*a):
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
def stranger():
def c(*a):
r = subprocess.run(["curl", "-sk", "--max-time", "10", "-o", "/dev/null", "-w", "%{http_code}", "-H", f"Host: {HOST}"] + list(a),
capture_output=True, text=True)
return r.stdout.strip()
body = json.dumps({"0": {"json": {"name": "stranger", "email": "stranger@example.com", "password": "Stranger-pass-123",
"confirmPassword": "Stranger-pass-123"}}})
return {"GET /signup": c(f"{w.BASE}/signup"),
"POST users.create": c("-X", "POST", "-H", "Content-Type: application/json", "--data", body,
f"{w.BASE}/api/trpc/users.create?batch=1")}
def record():
out = w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml 2>/dev/null; echo ---; "
f"ls /opt/docker/stacks/traefik/dynamic/ | grep -E 'signup-block-{APP}|setup-gate-{APP}' || echo 'no block/gate file'")
return " | ".join(l.strip() for l in out.strip().splitlines() if l.strip())
w.login()
say(f"##### R-773 live on 9202 — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()} ({time.strftime('%FT%TZ', time.gmtime())})")
ok = w.deploy(APP, SUB)
say("deploy ->", ok)
tok = fixtures.FIXTURES[APP].seed(w, SUB, say) # the household: through the gate, the first account
say("seed (first account) ->", "ok" if tok else "FAILED")
for _ in range(60): # the gate opens by its probe → the block goes up
if "signup-block" in record(): break
time.sleep(5)
say("BEFORE remove — lock record + files:", record())
say("BEFORE remove — a stranger:", stranger())
code, d = w.ctl("POST", "/api/debug/backup/night-chain")
say("night chain (debug action) ->", code, str(d)[:160])
for _ in range(120):
if w.snapshots(APP): break
time.sleep(10)
say("restore points offered:", [(s.get("id") or s.get("snapshot_id"), s.get("time") or s.get("created")) for s in w.snapshots(APP)][:3])
for _ in range(90): # let the chain finish before the remove
out = w.guest("docker logs --since 30m felhom-controller 2>&1 | grep -c 'night-chain\\] update leg: done\\|night-chain.*chain done\\|night-chain\\] manual run.*done'").strip()
if out not in ("", "0"): break
time.sleep(10)
w.ctl("POST", f"/api/stacks/{APP}/stop"); time.sleep(10)
code, d = w.ctl("POST", f"/api/stacks/{APP}/remove", {"remove_hdd_data": False, "remove_backups": False})
say(f"remove KEEPING backups -> {code} {str(d)[:200]}")
time.sleep(8)
say("AFTER remove — lock record + files:", record())
r = w.restore(APP)
say("restore:", {k: r.get(k) for k in ("ok", "snapshot_id", "http", "seconds", "state_after", "hold_after", "why")})
w.wait_app(SUB, "/", tries=40)
say("AFTER remove + restore — lock record + files:", record())
say("AFTER remove + restore — a stranger:", stranger())
say("the household's data back:", fixtures.FIXTURES[APP].verify(w, SUB, tok, say) if tok else "no seed")
say("controller log:", w.guest("docker logs --since 40m felhom-controller 2>&1 | grep -E 'karakeep: (restored after a removal|the household|sign-up)|signup' | tail -6"))