Part A proof (R-1045 narrowed): Plex setup gate on demo-hp 9201 — expired code, stranger gets only the gate (public + LAN), household passes, gate file removed with the app
gates / gates (push) Successful in 6m18s
gates / gates (push) Successful in 6m18s
Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
1 parent
3302a33cf5
commit
e0a5fe9630
6 files changed
+182
-1
No files matched your search
@@ -0,0 +1,2 @@
|
||||
08:27:26 plex deployed=False state=not_deployed setup_gate=null
|
||||
08:27:26 template on the box: setup_gate=True probe=None
|
||||
@@ -0,0 +1,22 @@
|
||||
08:27:27 POST /api/stacks/plex/deploy {"DOMAIN": "enkisfelhom.hu", "SUBDOMAIN": "plex", "PLEX_CLAIM": "claim-expired0000000000000", "HDD_PATH": "/mnt/felhom-drives/hdd_1"} -> 202 {"ok": true, "message": "The install has started – the card shows how it is going"}
|
||||
08:27:57 after 30s: deployed=True state=running setup_gate={"state": "closed", "since": "2026-10-15T06:27:27Z", "hosts": ["plex.enkisfelhom.hu"]}
|
||||
-rw-r--r-- 1 root root 602 Oct 15 06:27 setup-gate-plex.yml
|
||||
# Setup gate for plex — managed by felhom-controller (`09` §3 decision 46).
|
||||
# The app is closed to everyone but the household until its first setup is done; then this file is removed.
|
||||
http:
|
||||
middlewares:
|
||||
felhom-setup-gate-plex:
|
||||
forwardAuth:
|
||||
address: "http://felhom-controller:8080/__felhom_gate/auth"
|
||||
routers:
|
||||
felhom-setup-gate-plex-plex:
|
||||
rule: "Host(`plex.enkisfelhom.hu`)"
|
||||
priority: 100027
|
||||
entryPoints:
|
||||
- websecure
|
||||
tls:
|
||||
certResolver: letsencrypt
|
||||
middlewares:
|
||||
- felhom-setup-gate-plex@file
|
||||
service: "plex@docker"
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
08:28:26 == (1) a stranger over the PUBLIC path (no cookie)
|
||||
08:28:26 GET https://plex.enkisfelhom.hu/ (Accept text/html) -> 302 loc=https://felhom.enkisfelhom.hu/__gate/start?rd=https%3A%2F%2Fplex.enkisfelhom.hu%2F body='<a href="https://felhom.enkisfelhom.hu/__gate/start?rd=https%3A%2F%2Fplex.enkisfelhom.hu%2F">Found</a>. '
|
||||
08:28:26 GET https://plex.enkisfelhom.hu/web/index.html (Accept text/html) -> 302 loc=https://felhom.enkisfelhom.hu/__gate/start?rd=https%3A%2F%2Fplex.enkisfelhom.hu%2Fweb%2Findex.html body='<a href="https://felhom.enkisfelhom.hu/__gate/start?rd=https%3A%2F%2Fplex.enkisfelhom.hu%2Fweb%2Findex.html">Found</a>. '
|
||||
08:28:27 GET https://plex.enkisfelhom.hu/identity (Accept application/json) -> 401 loc=- body='{"error":"this app is waiting for its first setup"}'
|
||||
08:28:27 GET https://plex.enkisfelhom.hu/library/sections (Accept application/json) -> 401 loc=- body='{"error":"this app is waiting for its first setup"}'
|
||||
08:28:27 GET https://plex.enkisfelhom.hu/:/prefs (Accept */*) -> 401 loc=- body='{"error":"this app is waiting for its first setup"}'
|
||||
08:28:27 == (2) a stranger on the LAN path (traefik :443 on the guest)
|
||||
08:28:31 GET / (Accept text/html) -> HTTP/2 302
|
||||
location: https://felhom.enkisfelhom.hu/__gate/start?rd=https%3A%2F%2Fplex.enkisfelhom.hu%2F
|
||||
<a href="https://felhom.enkisfelhom.hu/__gate/start?rd=https%3A%2F%2Fplex.enkisfelhom.hu%2F">Found</a>.
|
||||
08:28:34 GET /library/sections (Accept application/json) -> HTTP/2 401
|
||||
{"error":"this app is waiting for its first setup"}
|
||||
08:28:34 == (3) the household: dashboard session -> the gate handshake
|
||||
08:28:44 step1 → felhom.enkisfelhom.hu/__gate/start; step2 → plex.enkisfelhom.hu/__felhom_gate/cb; step3 set-cookie felhom_gate: yes
|
||||
08:28:47 with the gate cookie: GET /identity -> HTTP/2 200
|
||||
{"MediaContainer":{"size":0,"apiVersion":"0.1.0","claimed":false,"machineIdentifier":"9103c96d2169eea7012ca35f1bd5379c0c
|
||||
08:28:47 == (4) the server itself (docker network, no token)
|
||||
08:28:50 identity: {"MediaContainer":{"size":0,"apiVersion":"0.1.0","claimed":false,"machineIdentifier":"9103c96d2169eea7012ca35f1bd5379c0ca0c571","version":"1.41.4.9463-630c9f557"}}
|
||||
sections-no-token: HTTP 200
|
||||
prefs-no-token: HTTP 200
|
||||
@@ -0,0 +1,24 @@
|
||||
08:28:57 POST /api/stacks/plex/remove remove_hdd_data=false -> 409 {"ok": false, "error": "stack \"plex\" is still running — stop it first before removing"}
|
||||
containers: 1
|
||||
gate file: 1
|
||||
volumes: 2
|
||||
ls: cannot access '/opt/felhom/stacks/plex': No such file or directory
|
||||
|
||||
08:31:17 POST /api/stacks/plex/stop -> 200 {"ok": true, "message": "Stack plex stop completed"}
|
||||
08:31:47 POST /api/stacks/plex/remove remove_hdd_data=false -> 200 {"ok": true, "data": {"removed": "plex", "volumes_removed": ["plex_plex_config", "plex_plex_transcode"], "hdd_paths_removed": [], "hdd_paths_preserved": [], "us
|
||||
containers: 0
|
||||
gate file: 1
|
||||
volumes:
|
||||
documents
|
||||
downloads
|
||||
import
|
||||
media
|
||||
roms
|
||||
|
||||
08:32:03 gate file present: 0
|
||||
2026/10/15 06:28:27 setup_gate.go:248: [DEBUG] [web] setup gate plex: GET /library/sections without a pass — 401 (visitor 37.191.56.193)
|
||||
2026/10/15 06:28:27 setup_gate.go:248: [DEBUG] [web] setup gate plex: GET /:/prefs without a pass — 401 (visitor 37.191.56.193)
|
||||
2026/10/15 06:28:33 setup_gate.go:248: [DEBUG] [web] setup gate plex: GET /library/sections without a pass — 401 (visitor 172.16.253.1)
|
||||
2026/10/15 06:28:43 setup_gate.go:232: [INFO] [web] setup gate plex: the household passed (a dashboard session vouched for this browser) — visitor 172.16.253.1
|
||||
2026/10/15 06:31:56 setup_gate.go:468: [INFO] [stacks] plex: removed a setup-gate file for an app whose gate is not closed
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
#!/usr/bin/env python3
|
||||
"""plexgate.py state|install|probe|remove — Part A (R-1045): the live catalog's Plex setup gate on a box.
|
||||
Env: TARGET (box_walk row, e.g. 9201), SC (0600 dir with .ctlpw). Uses ONLY the endpoints the dashboard uses.
|
||||
|
||||
install POST /api/stacks/plex/deploy with an EXPIRED claim code (a made-up `claim-…`, not a secret: plex.tv refuses it),
|
||||
HDD_PATH = the box's drive. Refuses if Plex is installed (live-probes rule).
|
||||
probe four looks, each quoted: (1) a stranger over the PUBLIC path (no cookie, Cloudflare) on /, /identity,
|
||||
/library/sections, /web; (2) a stranger on the LAN path (traefik :443 on the guest, Host header);
|
||||
(3) the household: dashboard session → the gate handshake → /identity through traefik;
|
||||
(4) the server itself on the docker network: claimed? and does it answer /library/sections with no token?
|
||||
remove POST /api/stacks/plex/remove with remove_hdd_data=false, then read back: no container.
|
||||
"""
|
||||
import json, os, re, subprocess, sys, time, urllib.request, urllib.error, ssl
|
||||
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
||||
import box_walk as w
|
||||
|
||||
APP = "plex"
|
||||
HOST = f"plex.{w.DOMAIN}"
|
||||
UA = "Mozilla/5.0 (X11; Linux x86_64) felhom-plexgate-check"
|
||||
|
||||
|
||||
def public(path, accept="text/html"):
|
||||
"""A stranger over the internet path (DooPlex → Cloudflare → tunnel). No cookie, no redirect following."""
|
||||
class NR(urllib.request.HTTPRedirectHandler):
|
||||
def redirect_request(self, *a, **k): return None
|
||||
op = urllib.request.build_opener(NR)
|
||||
try:
|
||||
r = op.open(urllib.request.Request(f"https://{HOST}{path}", headers={"User-Agent": UA, "Accept": accept}), timeout=20)
|
||||
code, loc, body = r.status, r.headers.get("Location"), r.read(300)
|
||||
except urllib.error.HTTPError as e:
|
||||
code, loc, body = e.code, e.headers.get("Location"), e.read(300)
|
||||
except Exception as e:
|
||||
return f"no answer ({type(e).__name__})"
|
||||
b = body.decode("utf-8", "replace").replace("\n", " ")[:120]
|
||||
return f"{code} loc={loc or '-'} body={b!r}"
|
||||
|
||||
|
||||
def lan(path, cookie=None, accept="text/html"):
|
||||
"""The LAN path: traefik on the guest's own address, Host header, from inside the guest."""
|
||||
c = f"-H 'Cookie: {cookie}'" if cookie else ""
|
||||
out = w.guest(f"curl -sk -o /tmp/pg.$$ -D - -H 'Host: {HOST}' -H 'Accept: {accept}' {c} https://127.0.0.1{path} | grep -iE '^HTTP|^location|^set-cookie' | sed 's/=[A-Za-z0-9._-]\\{{20,\\}}/=<redacted>/g'; head -c 120 /tmp/pg.$$ | tr '\\n' ' '; rm -f /tmp/pg.$$")
|
||||
return out.strip().replace("\r", "")
|
||||
|
||||
|
||||
def server():
|
||||
return w.guest("IP=$(docker inspect plex -f '{{range .NetworkSettings.Networks}}{{.IPAddress}} {{end}}' | awk '{print $1}'); "
|
||||
"echo identity: $(curl -s -H 'Accept: application/json' http://$IP:32400/identity); "
|
||||
"echo sections-no-token: HTTP $(curl -s -o /dev/null -w '%{http_code}' http://$IP:32400/library/sections); "
|
||||
"echo prefs-no-token: HTTP $(curl -s -o /dev/null -w '%{http_code}' http://$IP:32400/:/prefs)").strip()
|
||||
|
||||
|
||||
def gate_cookie():
|
||||
"""The household's handshake (memory box-walk-through-setup-gate): app → 302 to the dashboard's /__gate/start →
|
||||
with the dashboard session → 302 to the app's /__felhom_gate/cb → Set-Cookie felhom_gate."""
|
||||
sess = open(f"{w.SC}/sess{os.getpid()}.txt").read().strip()
|
||||
r1 = w.guest(f"curl -sk -o /dev/null -D - -H 'Host: {HOST}' -H 'Accept: text/html' https://127.0.0.1/ | grep -i '^location' | tr -d '\\r'")
|
||||
loc = r1.split(":", 1)[1].strip() if ":" in r1 else ""
|
||||
m = re.match(r"https?://([^/]+)(/.*)", loc)
|
||||
if not m:
|
||||
return None, f"step 1 gave no redirect: {r1.strip()!r}"
|
||||
r2 = w.guest(f"curl -sk -o /dev/null -D - -H 'Host: {m.group(1)}' -H 'Cookie: {sess}' 'https://127.0.0.1{m.group(2)}' | grep -i '^location' | tr -d '\\r'")
|
||||
loc2 = r2.split(":", 1)[1].strip() if ":" in r2 else ""
|
||||
m2 = re.match(r"https?://([^/]+)(/.*)", loc2)
|
||||
if not m2:
|
||||
return None, f"step 2 gave no redirect: {r2.strip()[:120]!r}"
|
||||
r3 = w.guest(f"curl -sk -o /dev/null -D - -H 'Host: {m2.group(1)}' 'https://127.0.0.1{m2.group(2)}' | grep -i '^set-cookie' | tr -d '\\r'")
|
||||
c = re.search(r"(felhom_gate=[^;]+)", r3)
|
||||
return (c.group(1) if c else None), f"step1 → {m.group(1)}{m.group(2).split('?')[0]}; step2 → {m2.group(1)}{m2.group(2).split('?')[0]}; step3 set-cookie felhom_gate: {'yes' if c else 'NO'}"
|
||||
|
||||
|
||||
step = sys.argv[1]
|
||||
w.login()
|
||||
st = w.stack(APP)
|
||||
if step == "state":
|
||||
w.say(f"plex deployed={st.get('deployed')} state={st.get('state')} setup_gate={json.dumps((st.get('app_config') or {}).get('setup_gate'))}")
|
||||
meta = st.get("meta") or {}
|
||||
w.say(f"template on the box: setup_gate={meta.get('setup_gate')} probe={meta.get('setup_done_probe')}")
|
||||
elif step == "install":
|
||||
if st.get("deployed"):
|
||||
sys.exit(w.say("plex is installed — not touched") or 1)
|
||||
vals = {"DOMAIN": w.DOMAIN, "SUBDOMAIN": "plex", "PLEX_CLAIM": "claim-expired0000000000000", "HDD_PATH": os.environ.get("HDD", "/mnt/felhom-drives/hdd_1")}
|
||||
c, d = w.ctl("POST", f"/api/stacks/{APP}/deploy", {"values": vals})
|
||||
w.say(f"POST /api/stacks/plex/deploy {json.dumps(vals)} -> {c} {json.dumps(d, ensure_ascii=False)[:160]}")
|
||||
t0 = time.time()
|
||||
for _ in range(120):
|
||||
time.sleep(5)
|
||||
st = w.stack(APP)
|
||||
if st.get("deployed") and (st.get("app_config") or {}).get("pinned_images") and st.get("state") in ("running", "unhealthy", "degraded"):
|
||||
break
|
||||
w.say(f"after {time.time()-t0:.0f}s: deployed={st.get('deployed')} state={st.get('state')} setup_gate={json.dumps((st.get('app_config') or {}).get('setup_gate'))}")
|
||||
print(w.guest("ls -l /opt/docker/stacks/traefik/dynamic/ | grep -i gate; sed -n 1,30p /opt/docker/stacks/traefik/dynamic/setup-gate-plex.yml 2>/dev/null"))
|
||||
elif step == "probe":
|
||||
w.say("== (1) a stranger over the PUBLIC path (no cookie)")
|
||||
for p, a in (("/", "text/html"), ("/web/index.html", "text/html"), ("/identity", "application/json"), ("/library/sections", "application/json"), ("/:/prefs", "*/*")):
|
||||
w.say(f" GET https://{HOST}{p} (Accept {a}) -> {public(p, a)}")
|
||||
w.say("== (2) a stranger on the LAN path (traefik :443 on the guest)")
|
||||
for p, a in (("/", "text/html"), ("/library/sections", "application/json")):
|
||||
w.say(f" GET {p} (Accept {a}) -> {lan(p, accept=a)}")
|
||||
w.say("== (3) the household: dashboard session -> the gate handshake")
|
||||
gc, how = gate_cookie()
|
||||
w.say(f" {how}")
|
||||
if gc:
|
||||
w.say(f" with the gate cookie: GET /identity -> {lan('/identity', cookie=gc, accept='application/json')}")
|
||||
w.say("== (4) the server itself (docker network, no token)")
|
||||
w.say(" " + server().replace("\n", "\n "))
|
||||
elif step == "remove":
|
||||
c, d = w.ctl("POST", f"/api/stacks/{APP}/remove", {"remove_hdd_data": False, "remove_backups": False})
|
||||
w.say(f"POST /api/stacks/plex/remove remove_hdd_data=false -> {c} {json.dumps(d, ensure_ascii=False)[:160]}")
|
||||
for _ in range(40):
|
||||
time.sleep(3)
|
||||
if not w.stack(APP).get("deployed"):
|
||||
break
|
||||
print(w.guest("echo containers: $(docker ps -aq -f name=^plex$ | wc -l); echo gate file: $(ls /opt/docker/stacks/traefik/dynamic/setup-gate-plex.yml 2>/dev/null | wc -l); echo volumes: $(docker volume ls -q | grep -c plex); ls -d /opt/felhom/stacks/plex 2>&1 | tail -1"))
|
||||
Reference in new issue
Block a user