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
68 lines
3.8 KiB
Python
68 lines
3.8 KiB
Python
"""fg.py — the family gate's live client on 9202 (product endpoints only). LAN = curl to 9202's traefik with a Host header;
|
|
tunnel = a curl container at cloudflared's fixed address 172.16.253.2 sending what Cloudflare sends. Never prints secrets."""
|
|
import json, os, re, subprocess, sys, urllib.parse
|
|
sys.path.insert(0, "/mnt/5_hdd/felhom.eu/git/app-catalog-felhom.eu/scripts")
|
|
import box_walk as w
|
|
DOM = w.DOMAIN; CTL = f"felhom.{DOM}"
|
|
GSH = "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/05c3d295-b388-481a-8c36-44a2a80b7d7d/scratchpad/g.sh"
|
|
|
|
def lan(host, path, *a, method=None, data=None, cookie=None, hdrs=(), timeout=15):
|
|
args = ["curl", "-sk", "--max-time", str(timeout), "-D", "-", "-H", f"Host: {host}"]
|
|
for h in hdrs: args += ["-H", h]
|
|
if cookie: args += ["-H", f"Cookie: {cookie}"]
|
|
if method: args += ["-X", method]
|
|
if data is not None: args += ["--data", data]
|
|
r = subprocess.run(args + list(a) + [f"{w.BASE}{path}"], capture_output=True, text=True, errors="replace")
|
|
head, _, body = r.stdout.replace("\r\n", "\n").partition("\n\n")
|
|
while re.match(r"HTTP/\S+ 100", head) and body:
|
|
head, _, body = body.partition("\n\n")
|
|
m = re.match(r"HTTP/\S+ (\d+)", head)
|
|
return (m.group(1) if m else "000"), head, body
|
|
|
|
def tun(host, path, visitor, forged=None, *a, timeout=15):
|
|
xff = f"{forged}, {visitor}" if forged else visitor
|
|
extra = " ".join("'" + x.replace("'", "'\\''") + "'" for x in a)
|
|
cmd = (f"docker run --rm --network felhom-tunnel --ip 172.16.253.2 curlimages/curl:8.11.1 -sk --max-time {timeout} -D - "
|
|
f"'https://traefik{path}' -H 'Host: {host}' -H 'X-Forwarded-For: {xff}' -H 'CF-Connecting-IP: {visitor}' {extra}")
|
|
r = subprocess.run([GSH, "9202", cmd], capture_output=True, text=True, errors="replace")
|
|
head, _, body = r.stdout.replace("\r\n", "\n").partition("\n\n")
|
|
m = re.match(r"HTTP/\S+ (\d+)", head)
|
|
return (m.group(1) if m else "000"), head, body
|
|
|
|
def loc(head):
|
|
m = re.search(r"(?im)^location: *(\S+)", head); return m.group(1) if m else ""
|
|
|
|
def setc(head, name):
|
|
m = re.search(rf"(?im)^set-cookie: *({name}=[^;\n]*)", head); return m.group(1) if m else None
|
|
|
|
def is_gate(code, head, body):
|
|
if code == "302" and f"https://{CTL}/__family/start" in loc(head): return True
|
|
return code == "401" and "sign in with your family login" in body
|
|
|
|
def dash_session():
|
|
return open(f"{w.SC}/sess{os.getpid()}.txt").read().strip() # "felhom_session=…"
|
|
|
|
def family_login(name, pw, visitor_tunnel=None):
|
|
data = None
|
|
c, h, b = lan(CTL, "/__family/login")
|
|
ft = re.search(r'name="_ft" value="([^"]+)"', b)
|
|
form = urllib.parse.urlencode({"_ft": ft.group(1) if ft else "", "name": name, "password": pw, "rd": ""})
|
|
if visitor_tunnel:
|
|
c, h, b = tun(CTL, "/__family/login", visitor_tunnel, None, "-X", "POST", "-H", "Content-Type: application/x-www-form-urlencoded", "--data", form)
|
|
else:
|
|
c, h, b = lan(CTL, "/__family/login", method="POST", data=form, hdrs=("Content-Type: application/x-www-form-urlencoded",))
|
|
return c, setc(h, "felhom_family"), h, b
|
|
|
|
def app_cookie(sub, session_cookie):
|
|
"""Walk a browser holding session_cookie (family or dashboard) through start → callback → felhom_famgate."""
|
|
host = f"{sub}.{DOM}"
|
|
c, h, b = lan(host, "/", hdrs=("Accept: text/html",))
|
|
l = loc(h)
|
|
if "/__family/start" not in l: return None, f"app did not send to /__family/start ({c} {l[:60]})"
|
|
u = urllib.parse.urlsplit(l)
|
|
c, h, b = lan(u.hostname, f"{u.path}?{u.query}", cookie=session_cookie, hdrs=("Accept: text/html",))
|
|
l = loc(h); u = urllib.parse.urlsplit(l)
|
|
if "/__felhom_gate/fcb" not in u.path: return None, f"start did not hand a callback ({c} {l[:60]})"
|
|
c, h, b = lan(u.hostname, f"{u.path}?{u.query}", hdrs=("Accept: text/html",))
|
|
return setc(h, "felhom_famgate"), h
|