e6d1ebd152
gates / gates (push) Successful in 28s
Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
137 lines
12 KiB
Python
137 lines
12 KiB
Python
#!/usr/bin/env python3
|
||
"""fg_items.py — the VERDICT's exit items 1–5 again, now THROUGH THE PRODUCT on 9202 (controller 0.287.0, no hand-made
|
||
traefik files), plus the build's own rules (reset/remove end access, controller down = closed) and the cost."""
|
||
import hashlib, json, os, re, statistics, subprocess, sys, time
|
||
from fg import *
|
||
SC = w.SC
|
||
log = open(f"{w.EV}/items.txt", "a", buffering=1)
|
||
def say(*a):
|
||
w.say(*a); log.write(" ".join(map(str, a)) + "\n")
|
||
w.login()
|
||
fam = json.load(open(f"{SC}/fam.json")); gm = json.load(open(f"{SC}/gm.json"))
|
||
GMH, MTH = f"library.{DOM}", f"video.{DOM}"
|
||
say(f"##### exit items through the product — {time.strftime('%FT%TZ', time.gmtime())}")
|
||
WS = ("Connection: Upgrade", "Upgrade: websocket", "Sec-WebSocket-Version: 13", "Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ==")
|
||
cases = [(GMH, "GET", "/", ("Accept: text/html",), None), (GMH, "GET", "/", (), None), (GMH, "GET", "/api/v1/books", (), None),
|
||
(GMH, "GET", "/api/v1/healthcheck", (), None), (GMH, "POST", "/api/v1/setup", ("Content-Type: application/json",), '{"username":"x","password":"Stranger-1234"}'),
|
||
(GMH, "POST", "/api/v1/auth/login", ("Content-Type: application/json",), '{"username":"admin","password":"guess"}'),
|
||
(GMH, "GET", "/ws/websocket", WS, None), (GMH, "GET", "/api/v1/opdsx", (), None), (GMH, "GET", "/api/v1/opds-evil", (), None),
|
||
(GMH, "GET", "/api/v1/opds/../../api/v1/books", ("X-Path: as-is",), None), (GMH, "GET", "/no-such-page", ("Accept: text/html",), None),
|
||
(MTH, "GET", "/", ("Accept: text/html",), None), (MTH, "GET", "/", (), None), (MTH, "GET", "/history", (), None),
|
||
(MTH, "POST", "/add", ("Content-Type: application/json",), '{"url":"https://example.com/v","quality":"best"}'),
|
||
(MTH, "GET", "/socket.io/?EIO=4&transport=polling", (), None), (MTH, "GET", "/socket.io/?EIO=4&transport=websocket", WS, None),
|
||
(MTH, "GET", "/download/x.mp4", (), None)]
|
||
say("## item 1 — a STRANGER (no cookie), LAN and simulated tunnel")
|
||
apps = 0
|
||
for host, m, p, h, d in cases:
|
||
extra = ["--path-as-is"] if "as-is" in " ".join(h) else []
|
||
c, hh, b = lan(host, p, *extra, method=m, data=d, hdrs=h)
|
||
tc, th, tb = tun(host, p, "198.51.100.66", None, *extra, "-X", m, *sum([["-H", x] for x in h], []), *(["--data", d] if d else []))
|
||
g1, g2 = is_gate(c, hh, b), is_gate(tc, th, tb)
|
||
apps += (not g1) + (not g2)
|
||
say(f" {host.split('.')[0]:8s} {m:4s} {p:40s} LAN {c} {'GATE' if g1 else 'APP!'} | tunnel {tc} {'GATE' if g2 else 'APP!'}")
|
||
say(f"item 1: {apps} app answers of {2*len(cases)} -> {'PASS' if apps == 0 else 'FAIL'}")
|
||
|
||
say("## item 2 — family members, their OWN logins, through the product's sign-in page")
|
||
c, sess, h, b = family_login("anna", fam["anna"])
|
||
ma = re.search(r"(?i)max-age=(\d+)", h); path = re.search(r"(?i)path=([^;\n]+)", h)
|
||
say(f" anna signs in on {CTL}/__family/login: {c}; felhom_family {'set' if sess else 'NONE'}; Max-Age {int(ma.group(1))//86400 if ma else '?'} d; Path={path.group(1).strip() if path else '?'}; dashboard cookie set: {'felhom_session=' in h}")
|
||
ga = app_cookie("library", sess)[0]; mb_s = family_login("bela", fam["bela"])[1]; mt = app_cookie("video", mb_s)[0]
|
||
for host, ck, p, hh in ((GMH, ga, "/", ("Accept: text/html",)), (GMH, ga, "/api/v1/healthcheck", ()), (MTH, mt, "/", ()),
|
||
(MTH, mt, "/socket.io/?EIO=4&transport=polling", ()), (MTH, mt, "/history", ())):
|
||
c, h2, b2 = lan(host, p, cookie=ck, hdrs=hh)
|
||
say(f" member GET {host.split('.')[0]}{p} -> {c} {'GATE' if is_gate(c, h2, b2) else 'APP'}")
|
||
c, h2, b2 = lan(MTH, "/socket.io/?EIO=4&transport=websocket", "--http1.1", cookie=mt, hdrs=WS, timeout=4)
|
||
say(f" bela MeTube websocket upgrade -> {c} {'GATE' if is_gate(c, h2, b2) else 'APP'} {[l for l in h2.splitlines() if l.lower().startswith('upgrade')]}")
|
||
c, h2, b2 = lan(GMH, "/", cookie=mt, hdrs=("Accept: text/html",))
|
||
say(f" bela's MeTube cookie at Grimmory -> {c} {'GATE' if is_gate(c, h2, b2) else 'APP!'}")
|
||
w.guest("docker restart felhom-controller >/dev/null; sleep 25")
|
||
c, h2, b2 = lan(GMH, "/", cookie=ga, hdrs=("Accept: text/html",))
|
||
say(f" after the CONTROLLER restarted, anna's Grimmory cookie -> {c} {'GATE' if is_gate(c, h2, b2) else 'APP (survived)'}")
|
||
w.login()
|
||
c, h2, b2 = lan(CTL, "/__family/logout", method="POST", cookie=sess)
|
||
c2, h3, b3 = lan(GMH, "/", cookie=ga, hdrs=("Accept: text/html",))
|
||
say(f" anna logs out ({c}) -> her Grimmory cookie {c2} {'GATE (refused)' if is_gate(c2, h3, b3) else 'APP! (still in)'}")
|
||
|
||
say("## item 3 — a stranger's guesses at the family sign-in lock only the stranger (simulated tunnel)")
|
||
seq = [family_login("anna", f"guess-{i}", visitor_tunnel="198.51.100.66")[0] for i in range(1, 8)]
|
||
say(f" stranger 198.51.100.66, 7 wrong for anna: {seq}")
|
||
say(f" stranger with anna's RIGHT password while locked: {family_login('anna', fam['anna'], visitor_tunnel='198.51.100.66')[0]}")
|
||
c, s2, h, b = family_login("anna", fam["anna"], visitor_tunnel="203.0.113.10")
|
||
say(f" anna herself from 203.0.113.10 (tunnel), at once: {c}, session {'set' if s2 else 'NONE'}")
|
||
c, s3, h, b = family_login("bela", fam["bela"])
|
||
say(f" bela from the LAN, at once: {c}, session {'set' if s3 else 'NONE'}")
|
||
say(" controller log:", w.guest("docker logs --since 2m felhom-controller 2>&1 | grep -E 'family sign-in' | tail -6"))
|
||
|
||
say("## item 4 — Grimmory's e-reader exceptions keep Grimmory's OWN login (no family cookie)")
|
||
ha = app_cookie("library", w.dash_cookie() if hasattr(w, "dash_cookie") else dash_session())[0]
|
||
c, h, b = lan(GMH, "/api/v1/auth/login", method="POST", cookie=ha, hdrs=("Content-Type: application/json",), data=json.dumps({"username": "admin", "password": gm["admin_pw"]}))
|
||
tok = json.loads(b).get("accessToken"); A = ("Content-Type: application/json", f"Authorization: Bearer {tok}")
|
||
opw, kpw = __import__("secrets").token_hex(8), __import__("secrets").token_hex(8)
|
||
lan(GMH, "/api/v1/settings", method="PUT", cookie=ha, hdrs=A, data=json.dumps([{"name": "OPDS_SERVER_ENABLED", "value": True}]))
|
||
lan(GMH, "/api/v2/opds-users", method="POST", cookie=ha, hdrs=A, data=json.dumps({"username": "olvaso", "password": opw}))
|
||
c, h, b = lan(GMH, "/api/v1/kobo-settings/token", method="PUT", cookie=ha, hdrs=A); ktok = json.loads(b).get("token", "")
|
||
lan(GMH, "/api/v1/koreader-users/me", method="PUT", cookie=ha, hdrs=A, data=json.dumps({"username": "korolvaso", "password": kpw}))
|
||
lan(GMH, "/api/v1/koreader-users/me/sync?enabled=true", method="PATCH", cookie=ha, hdrs=A)
|
||
say(" (setup as the household through the gate: OPDS on + an OPDS user, a Kobo token, a KOReader user — values not printed)")
|
||
md5 = hashlib.md5(kpw.encode()).hexdigest()
|
||
def row(label, path, *a, via="LAN", hd=(), timeout=40):
|
||
c, h, b = lan(GMH, path, *a, hdrs=hd, timeout=timeout) if via == "LAN" else tun(GMH, path, "198.51.100.66", None, *a, *sum([["-H", x] for x in hd], []), timeout=timeout)
|
||
say(f" {label:52s} {via:6s} -> {c} {'GATE' if is_gate(c, h, b) else 'app'} {b[:60].strip()!r}")
|
||
row("OPDS, the OPDS user's own login", "/api/v1/opds", "-u", f"olvaso:{opw}")
|
||
row("OPDS, the OPDS user's own login", "/api/v1/opds", "-u", f"olvaso:{opw}", via="tunnel")
|
||
row("OPDS, a stranger with a wrong password", "/api/v1/opds", "-u", "olvaso:wrong", via="tunnel")
|
||
row("OPDS, a stranger with nothing", "/api/v1/opds", via="tunnel")
|
||
row("Kobo initialization, the device token", f"/api/kobo/{ktok}/v1/initialization")
|
||
row("Kobo, a made-up token", "/api/kobo/0123456789abcdef/v1/library/sync", via="tunnel")
|
||
row("KOReader sign-in, its own user + md5 key", "/api/koreader/users/auth", hd=("x-auth-user: korolvaso", f"x-auth-key: {md5}", "Accept: application/vnd.koreader.v1+json"))
|
||
row("KOReader, a stranger's wrong key", "/api/koreader/users/auth", hd=("x-auth-user: korolvaso", "x-auth-key: 00000000000000000000000000000000", "Accept: application/vnd.koreader.v1+json"), via="tunnel")
|
||
row("Komga API, a stranger with nothing", "/komga/api/v1/libraries", via="tunnel")
|
||
row("look-alike /api/v1/opdsx (F1)", "/api/v1/opdsx", via="tunnel")
|
||
row("look-alike /api/koreaderx", "/api/koreaderx/users/auth", via="tunnel")
|
||
row("../ out of an exception", "/api/v1/opds/../../api/v1/books", "--path-as-is", via="tunnel")
|
||
say("## R-775 re-measured — a stranger cannot reach Grimmory's web sign-in at all, so its 15-minute lock cannot be aimed from outside")
|
||
seq = [tun(GMH, "/api/v1/auth/login", "198.51.100.66", None, "-X", "POST", "-H", "Content-Type: application/json", "--data", '{"username":"admin","password":"guess"}')[0] for _ in range(6)]
|
||
c, h, b = lan(GMH, "/api/v1/auth/login", method="POST", cookie=ha, hdrs=("Content-Type: application/json",), data=json.dumps({"username": "admin", "password": gm["admin_pw"]}))
|
||
say(f" stranger x6 at /api/v1/auth/login (tunnel): {seq} -> the household then signs in: {c}")
|
||
|
||
say("## item 5 — the family login and the box dashboard")
|
||
c, s4, h, b = family_login("anna", fam["anna"])
|
||
c1, h1, b1 = lan(CTL, "/launcher", cookie=f"{s4}; {ga}")
|
||
c2, h2, b2 = lan(CTL, "/api/stacks", cookie=s4)
|
||
c3, h3, b3 = lan(CTL, "/login", method="POST", data=f"password={fam['anna']}", hdrs=("Content-Type: application/x-www-form-urlencoded",))
|
||
say(f" family cookies at /launcher -> {c1} {loc(h1)} | at /api/stacks -> {c2} | anna's family password at the dashboard login -> {c3}, session {'SET!' if 'felhom_session=' in h3 else 'none'}")
|
||
|
||
say("## reset and removal end access at the next request (every gated app)")
|
||
c, s5, h, b = family_login("bela", fam["bela"]); g5 = app_cookie("library", s5)[0]; m5 = app_cookie("video", s5)[0]
|
||
sess = dash_session(); csrf = open(f"{SC}/csrf{os.getpid()}.txt").read().strip()
|
||
def card(action, name):
|
||
r = subprocess.run(["curl", "-sk", "-H", f"Host: {CTL}", "-H", f"Cookie: {sess}", "-H", f"X-CSRF-Token: {csrf}", "-X", "POST",
|
||
"--data", f"name={name}", f"{w.BASE}/family/members/{action}"], capture_output=True, text=True)
|
||
return json.loads(r.stdout)
|
||
d = card("reset", "bela"); fam["bela"] = d["data"]["password"]
|
||
say(f" card: new password for bela -> ok={d['ok']}; bela's Grimmory cookie -> {lan(GMH, '/', cookie=g5, hdrs=('Accept: text/html',))[0]}, MeTube cookie -> {lan(MTH, '/', cookie=m5, hdrs=('Accept: text/html',))[0]} (302 = sent to sign in)")
|
||
c, s6, h, b = family_login("bela", fam["bela"]); g6 = app_cookie("video", s6)[0]
|
||
d = card("remove", "bela")
|
||
say(f" card: remove bela -> ok={d['ok']} members={d['data']['members']}; bela's MeTube cookie -> {lan(MTH, '/', cookie=g6)[0]}; bela signs in again -> {family_login('bela', fam['bela'])[0]}")
|
||
json.dump(fam, open(f"{SC}/fam.json", "w"))
|
||
|
||
say("## cost — the same request through the gated name, and straight at the container (60 pairs)")
|
||
def t(cmd):
|
||
r = subprocess.run(cmd, capture_output=True, text=True).stdout.split(); return r[0], float(r[1]) * 1000
|
||
g, d = [], []
|
||
for _ in range(60):
|
||
c1, t1 = t(["curl", "-sk", "-o", "/dev/null", "-w", "%{http_code} %{time_total}", "-H", f"Host: {MTH}", "-H", f"Cookie: {mt}", f"{w.BASE}/version"])
|
||
g.append(t1)
|
||
ip = w.guest("docker inspect metube --format '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}'").split()[0]
|
||
dd = w.guest(f"for i in $(seq 1 60); do curl -s -o /dev/null -w '%{{time_total}}\\n' http://{ip}:8081/version; done").split()
|
||
say(f" gated (LAN→traefik→forwardAuth→app) median {statistics.median(g):.1f} ms, p90 {sorted(g)[54]:.1f} ms; the app straight (guest→container) median {statistics.median([float(x)*1000 for x in dd]):.1f} ms — the honest number is the gate's own: see the controller's forwardAuth timing below")
|
||
say("## the controller DOWN — a gated app answers an error, never the app; an exception still reaches the app's own login")
|
||
w.guest("docker stop felhom-controller >/dev/null")
|
||
c1, h1, b1 = lan(MTH, "/", cookie=mt); c2, h2, b2 = lan(GMH, "/api/v1/books", cookie=ga); c3, h3, b3 = lan(GMH, "/api/v1/opds", "-u", "olvaso:wrong")
|
||
say(f" controller stopped: MeTube with a member cookie -> {c1} {b1[:40]!r}; Grimmory API -> {c2}; Grimmory OPDS exception (wrong password) -> {c3} (the app's own refusal)")
|
||
w.guest("docker start felhom-controller >/dev/null; sleep 25")
|
||
w.login()
|
||
c1, h1, b1 = lan(MTH, "/", cookie=mt)
|
||
say(f" controller back: MeTube with bela's earlier cookie (bela was removed) -> {c1} {'GATE' if is_gate(c1,h1,b1) else 'APP'}")
|