#!/usr/bin/env python3 """sparky_box.py — SparkyFitness's checklist rows on scratch 9202 (live catalog, controller 0.286.1), through the product's own endpoints. Strangers through traefik with no session and no gate cookie; the tunnel SIMULATED by a curl container at 172.16.253.2 (Part A's method). Secrets never printed. Env: SC, EV.""" import json, os, re, 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 = "sparkyfitness", "sparky" HOST = f"{SUB}.{w.DOMAIN}"; FX = fixtures.FIXTURES[APP] log = open(f"{w.EV}/sparky-box.txt", "a", buffering=1) def say(*a): w.say(*a); log.write(" ".join(map(str, a)) + "\n") def lan(path, *a): r = subprocess.run(["curl", "-sk", "--max-time", "10", "-o", "/dev/null", "-w", "%{http_code}", "-H", f"Host: {HOST}"] + list(a) + [f"{w.BASE}{path}"], capture_output=True, text=True) return r.stdout.strip() def tun(visitor, forged, path, data): cmd = (f"docker run --rm --network felhom-tunnel --ip 172.16.253.2 curlimages/curl:8.11.1 -sk -o /dev/null -w '%{{http_code}}' " f"'https://traefik{path}' -H 'Host: {HOST}' -H 'X-Forwarded-For: {forged}, {visitor}' -H 'CF-Connecting-IP: {visitor}' " f"-H 'Origin: https://{HOST}' -H 'Content-Type: application/json' --data '{data}'") return w.guest(cmd).strip()[-3:] def state(): return w.stack(APP).get("state") w.login() say(f"##### SparkyFitness on 9202 — controller {w.guest('docker inspect felhom-controller --format {{.Config.Image}}').strip()} | catalog " + w.guest("cd /var/lib/docker/volumes/felhom-controller-data/_data/data/catalog-cache && git log --oneline -1").strip()[:9] + f" | {time.strftime('%FT%TZ', time.gmtime())}") # 3.5 / 4.3 — a stranger polls from the press; the install timed w.guest(f"""cat > /root/sp-poll.sh <<'PEOF' out=/root/sp-poll.txt; : > $out; t0=$(date +%s) while [ $(( $(date +%s) - t0 )) -lt 240 ]; do r=$(curl -sk --max-time 4 -o /root/sp-poll.body -w '%{{http_code}}' -H 'Host: {HOST}' -H 'Content-Type: application/json' -X POST --data '{{"email":"s@x.hu","password":"Stranger-1234","name":"s"}}' https://127.0.0.1/api/auth/sign-up/email) echo "$(date +%H:%M:%S) $r $(head -c 60 /root/sp-poll.body | tr '\\n' ' ')" >> $out; sleep 1 done PEOF (nohup sh /root/sp-poll.sh >/dev/null 2>&1 &)""") t0 = time.time(); ok = w.deploy(APP, SUB); say(f"deploy -> {ok} ({round(time.time()-t0)} s to `deployed`)") healthy = None for _ in range(150): hs = w.guest("for c in sparkyfitness sparkyfitness-server sparkyfitness-db; do docker inspect -f '{{if .State.Health}}{{.State.Health.Status}}{{else}}nohc{{end}}' $c; done").split() if hs and all(h in ("healthy", "nohc") for h in hs): healthy = time.time() - t0; break time.sleep(2) say(f"4.3 every container healthy {round(healthy) if healthy else 'NEVER'} s after the press; restarts:", w.guest("for c in sparkyfitness sparkyfitness-server sparkyfitness-db; do echo -n \"$c=$(docker inspect -f '{{.RestartCount}}' $c) \"; done").strip(), "| state:", state()) say("3.3 before the setup, a stranger (LAN) on / , /api/auth/sign-up/email:", lan("/", "-H", "Accept: text/html"), lan("/api/auth/sign-up/email", "-X", "POST", "-H", "Content-Type: application/json", "--data", "{}")) tok = FX.seed(w, SUB, say) # 3.1: the household through the gate, the first account say("3.1 the household's first account through the gate ->", "ok" if tok else "FAILED " + str(getattr(FX, "tried", ""))) time.sleep(3); w.guest("pkill -f sp-poll.sh || true") poll = w.guest("cat /root/sp-poll.txt").strip().splitlines(); codes = {} for l in poll: codes[l.split(" ")[1]] = codes.get(l.split(" ")[1], 0) + 1 say(f"3.5 a stranger's {len(poll)} sign-up tries (1/s) from the press until the household's account: codes {codes}; any 200: {any(' 200 ' in l for l in poll)}") # the household's 'Done, I set it up' (no probe in the template) csrf = open(f"{w.SC}/csrf{os.getpid()}.txt").read().strip(); sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip() r = subprocess.run(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code}", "-H", w.HOSTHDR, "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}", "-X", "POST", f"{w.BASE}/apps/{APP}/setup-gate/open"], capture_output=True, text=True) say("the household presses 'Done, I set it up' ->", r.stdout) for _ in range(40): if "native_lock: applied" in w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml"): break time.sleep(5) say("3.4 lock record:", " | ".join(x.strip() for x in w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml").splitlines()), "| app env SPARKY_FITNESS_DISABLE_SIGNUP =", w.guest("docker exec sparkyfitness-server printenv SPARKY_FITNESS_DISABLE_SIGNUP").strip()) w.wait_app(SUB, "/", want=("200",), tries=40) body = json.dumps({"email": "stranger@example.com", "password": "Stranger-pass-1234", "name": "S"}) for p in ("/api/auth/sign-up/email", "/API/Auth/Sign-Up/email", "//api//auth/sign-up/email"): say(f"3.4 a stranger signs up at {p} (LAN) ->", lan(p, "-X", "POST", "-H", "Content-Type: application/json", "-H", f"Origin: https://{HOST}", "--data", body)) say("3.4 the same straight at the server (behind the block — the app's own switch):", w.guest("docker run --rm --network traefik-public curlimages/curl:8.11.1 -s -o /dev/null -w '%{http_code}' http://sparkyfitness-server:3010/api/auth/sign-up/email " f"-H 'Content-Type: application/json' -H 'Origin: https://{HOST}' --data '{body}' || true").strip()[-3:]) say("3.9 browser-shaped sign-in (Origin, JSON) right / wrong:", FX._signin(w, SUB, tok["email"], tok["pw"])[0], FX._signin(w, SUB, tok["email"], "wrong-x")[0]) # 3.6 — lock-out through the simulated tunnel say("3.6 waiting 20 s (better-auth's own window)"); time.sleep(20) seq = [] for i in range(1, 9): seq.append(tun("198.51.100.66", f"10.6.0.{i}", "/api/auth/sign-in/email", json.dumps({"email": tok["email"], "password": f"wrong-{i}"}))) say(f"3.6 stranger 198.51.100.66, 8 wrong sign-ins for the household's e-mail, a new forged leftmost each: {seq}") t1 = time.time(); hh = [] for _ in range(20): c = tun("203.0.113.10", "203.0.113.10", "/api/auth/sign-in/email", json.dumps({"email": tok["email"], "password": tok["pw"]})) hh.append((round(time.time() - t1), c)) if c == "200": break time.sleep(3) say(f"3.6 the household from 203.0.113.10, right password, from the stranger's last try on (s, code): {hh}") # 1.5 / 1.8 / 0.4 say("1.5 processes:", w.guest("for c in sparkyfitness-server sparkyfitness; do echo -n \"$c: \"; docker exec $c sh -c 'cat /proc/1/cmdline | tr \"\\000\" \" \"'; echo; done").strip()) say("1.8 an unknown page ->", lan("/felhom-no-such-page-xyz/", "-H", "Accept: text/html"), "| an unknown API ->", lan("/api/felhom-no-such")) say("0.4 outbound hosts named in the server's first-start log:", sorted(set(re.findall(r"https?://([a-z0-9.-]+\.[a-z]{2,})", w.guest("docker logs sparkyfitness-server 2>&1 | head -400"))))[:12]) # 4.4 — negative control w.guest("docker stop sparkyfitness-server >/dev/null"); seen = [] for i in range(9): time.sleep(5); s = state() if not seen or seen[-1][1] != s: seen.append((5 * (i + 1), s)) say(f"4.4 sparkyfitness-server STOPPED — states (s, state): {seen}; the front door / -> {lan('/')} ; /api/health -> {lan('/api/health')}") w.guest("docker start sparkyfitness-server >/dev/null") for i in range(40): time.sleep(5) if state() == "running": say(f" running again {5*(i+1)} s after docker start"); break # 2.7 size say("2.7 volumes after the seed:", w.guest("for v in $(docker volume ls -q | grep sparkyfitness); do echo -n \"$v=$(du -sh /var/lib/docker/volumes/$v/_data | cut -f1) \"; done").strip()) # 2.5 backup → remove keeping backups → restore → read back code, d = w.ctl("POST", "/api/debug/backup/night-chain"); say("2.5 the night chain (debug action) ->", code) for _ in range(60): if "finished in" in w.guest("docker logs --since 3m felhom-controller 2>&1 | grep 'night-chain\\] finished' | tail -1"): break time.sleep(5) say(" restore points:", [(s.get("id") or s.get("snapshot_id"), s.get("time") or s.get("created")) for s in w.snapshots(APP)][:2]) 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"2.6 remove KEEPING backups -> {code} {str(d)[:220]}") time.sleep(6); say(" left after:", w.guest(f"docker ps -a --format '{{{{.Names}}}}' | grep -E '^{APP}'; docker volume ls -q | grep -i {APP}").strip() or "nothing") r = w.restore(APP); say("2.5 restore:", {k: r.get(k) for k in ("ok", "snapshot_id", "seconds", "state_after", "hold_after")}) w.wait_app(SUB, "/", want=("200",), tries=60) say("2.5 the seed reads back after remove + restore:", FX.verify(w, SUB, tok, say)) say(" R-773 on a restored sparkyfitness: record", " | ".join(x.strip() for x in w.guest(f"grep -A8 '^setup_gate:' /opt/docker/stacks/{APP}/app.yaml").splitlines()[:7]), "| a stranger signs up ->", lan("/api/auth/sign-up/email", "-X", "POST", "-H", "Content-Type: application/json", "-H", f"Origin: https://{HOST}", "--data", body)) # 2.6 remove with data w.ctl("POST", f"/api/stacks/{APP}/stop"); time.sleep(10) code, d = w.ctl("POST", f"/api/stacks/{APP}/remove", {"remove_hdd_data": True, "remove_backups": True}) say(f"2.6 remove WITH data and backups -> {code} {str(d)[:220]}") time.sleep(6); say(" left after:", w.guest(f"ls -d /opt/docker/stacks/{APP} 2>/dev/null; docker ps -a --format '{{{{.Names}}}}' | grep -E '^{APP}'; docker volume ls -q | grep -i {APP}").strip() or "nothing") w.guest("rm -f /root/sp-poll.sh /root/sp-poll.txt /root/sp-poll.body")