Files
felhom.eu/documentation/audits/family-gate-2026-10-02/tools/fg.py
T

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