Files
felhom.eu/documentation/audits/restore-holes-2026-10-24/tools/t1.py
T

100 lines
4.1 KiB
Python

#!/usr/bin/env python3
"""Drive one box's controller the way the dashboard does (restore-holes night, 2026-10-24).
Usage (the password comes ONLY from the environment, through `sec run`, and is never printed):
sec run TESTER1_PASSWORD -- python3 t1.py tester-1 login
python3 t1.py tester-1 api GET /api/stacks
python3 t1.py tester-1 form POST /backups/restore stack_name=metube snapshot_id=unit
python3 t1.py tester-1 guest '<bash script>' # inside the customer guest
python3 t1.py tester-1 host '<bash script>' # on the box's Proxmox host
Session cookie + CSRF token live in the session scratchpad (0600), never in the repo.
"""
import os, re, subprocess, sys, secrets as _s
SC = os.environ.get("T1_SCRATCH", "/tmp/claude-1000/-mnt-5-hdd-felhom-eu-git/63b6c84e-0c09-4657-a160-855d2e4fe326/scratchpad/t1")
os.makedirs(SC, mode=0o700, exist_ok=True)
BOXES = {
"tester-1": {"ssh": ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "root@192.168.0.154"], "guest": "9201",
"domain": "enkicsifelhom.hu", "pwenv": "TESTER1_PASSWORD"},
"9202": {"ssh": ["ssh", "-o", "BatchMode=yes", "-o", "ConnectTimeout=20", "demo-hp"], "guest": "9202",
"domain": "enkisfelhom.hu", "pwenv": "REHEARSAL_2026_10_20_9202_PW"},
}
box = BOXES[sys.argv[1]]
cmd = sys.argv[2]
HOST = f"Host: felhom.{box['domain']}"
pre = f"{SC}/{sys.argv[1]}"
def run(args, inp=None, timeout=900):
return subprocess.run(args, input=inp, capture_output=True, text=True, errors="replace", timeout=timeout)
def host(script, timeout=900):
return run(box["ssh"] + ["bash -s"], inp=script, timeout=timeout)
def guest(script, timeout=900):
t = f"/tmp/rh-{os.getpid()}-{_s.token_hex(4)}.sh"
g = box["guest"]
return run(box["ssh"] + [f"cat > {t}; pct push {g} {t} {t}; pct exec {g} -- bash {t}; pct exec {g} -- rm -f {t}; rm -f {t}"],
inp=script, timeout=timeout)
def base():
r = guest("ip -4 -o addr show eth0")
m = re.search(r"inet (\d+\.\d+\.\d+\.\d+)/", r.stdout)
if not m:
sys.exit("no guest address: " + (r.stderr or r.stdout)[-200:])
return "https://" + m.group(1)
def login():
pw = os.environ.get(box["pwenv"])
if not pw:
sys.exit(f"{box['pwenv']} is not in the environment — run through `sec run {box['pwenv']} --`")
b = base()
hdr = f"{pre}.hdr"
run(["curl", "-sk", "-D", hdr, "-o", "/dev/null", "-H", HOST, "-X", "POST", "--data-urlencode", f"password={pw}", f"{b}/login"])
h = open(hdr).read()
os.remove(hdr)
m = re.search(r"felhom_session=[A-Za-z0-9._-]+", h, re.I)
if not m:
sys.exit("login failed: no session cookie (" + h.splitlines()[0] + ")")
r = run(["curl", "-sk", "-L", "-H", HOST, "-H", f"Cookie: {m.group(0)}", f"{b}/"])
c = re.search(r'<meta name="csrf-token" content="([^"]+)"', r.stdout or "")
if not c:
sys.exit("login failed: no csrf token")
for name, val in (("sess", m.group(0)), ("csrf", c.group(1)), ("base", b)):
fd = os.open(f"{pre}.{name}", os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
os.write(fd, val.encode())
os.close(fd)
print("login ok", b)
def call(method, path, form=None, json_body=None):
sess, csrf, b = (open(f"{pre}.{n}").read().strip() for n in ("sess", "csrf", "base"))
a = ["curl", "-sk", "-H", HOST, "-H", f"Cookie: {sess}", "-w", "\n%{http_code}"]
if method != "GET":
a += ["-X", method, "-H", f"X-CSRF-Token: {csrf}"]
if form is not None:
a += ["--data-urlencode", f"_csrf={csrf}"]
for kv in form:
a += ["--data-urlencode", kv]
if json_body is not None:
a += ["-H", "Content-Type: application/json", "--data", json_body]
r = run(a + [b + path])
print(r.stdout)
if cmd == "login":
login()
elif cmd == "api":
call(sys.argv[3], sys.argv[4], json_body=sys.argv[5] if len(sys.argv) > 5 else None)
elif cmd == "form":
call(sys.argv[3], sys.argv[4], form=sys.argv[5:])
elif cmd in ("guest", "host"):
r = (guest if cmd == "guest" else host)(sys.argv[3])
sys.stdout.write(r.stdout)
sys.stderr.write(r.stderr[-2000:])
sys.exit(r.returncode)