diff --git a/scripts/sold-staging-hub-sweep.py b/scripts/sold-staging-hub-sweep.py new file mode 100755 index 0000000..599bf7a --- /dev/null +++ b/scripts/sold-staging-hub-sweep.py @@ -0,0 +1,137 @@ +#!/usr/bin/env python3 +"""Probe every route the FIFA 17 FUT hub touches on the staging stack, and fail loudly +on anything that would break it. + +The sold-row A/B kept stalling on preconditions the headless checks never exercised: +the identity had no squad (hub refuses to open), and any route not owned by Rust falls +through to a deliberately dead Python upstream and answers 502. Each of those cost a +full operator cycle -- launch, observe, report, diagnose -- so this front-loads the +whole surface instead of discovering the next gap one restart at a time. + +Flags three distinct failure classes, because they need different fixes: + * 502 / PYTHON_FALLBACK -- route not implemented in Rust; the staging stack has no + Python upstream, so it is fatal here even though production would proxy it. + * missing_integrity -- Rust answered 200 but the underlying state is absent + (this is exactly what "no extension stored" was before the squad was seeded). + * empty-but-required -- 200 with a body the hub cannot work with, e.g. a squad + with zero occupied slots. A 200 is not proof the client is satisfied. + + python3 scripts/sold-staging-hub-sweep.py +""" +import http.client +import json +import os +import sys + +HOST, PORT = "127.0.0.1", 8299 +FORBIDDEN = {8099, 8199, 18080, 8443, 42127, 42130, 42131, 4216} +HEADERS = {"X-OpenFUT-Game": "fifa17"} +LOG = "/home/alex/openfut-sold-staging/logs/utas-host.log" + +# Only paths the CLIENT is actually observed to request (host log route names), with +# casing exactly as it sends them. An invented path that 502s is a bug in this list, +# not in the stack: a first pass here flagged five "fatal" routes that turned out to +# be guesses -- `/accountinfo` (the client uses `/user/accountinfo`), bare `/squad` +# (it uses `/squad/active`), and `/watchlist` (it is camelCase `watchList`). Crying +# wolf about the stack is worse than not checking, so every entry below is verified. +ROUTES = [ + "/ut/game/fifa17/user/accountinfo", + "/ut/game/fifa17/squad/0", + "/ut/game/fifa17/squad/active", + "/ut/game/fifa17/club?count=200", + "/ut/game/fifa17/club/stats/staff", + "/ut/game/fifa17/club/stats/club", + "/ut/game/fifa17/clientdata/store", + "/ut/game/fifa17/purchased/items", + "/ut/game/fifa17/tradePile", + "/ut/game/fifa17/tradePile/counts", + "/ut/game/fifa17/watchList", + "/ut/game/fifa17/trade/status", + "/ut/game/fifa17/userMassInfo", + "/ut/game/fifa17/settings", +] + + +def get(path): + assert PORT not in FORBIDDEN, "refusing to touch a production port" + c = http.client.HTTPConnection(HOST, PORT, timeout=20) + c.request("GET", path, headers=HEADERS) + r = c.getresponse() + raw = r.read() + c.close() + try: + return r.status, json.loads(raw), raw + except Exception: + return r.status, None, raw + + +def squad_occupied(body): + if not isinstance(body, dict): + return None + players = body.get("players") + if not isinstance(players, list): + return None + return sum(1 for p in players if (p.get("itemData") or {}).get("id")) + + +def main(): + start = 0 + if os.path.exists(LOG): + start = len(open(LOG, errors="replace").read().splitlines()) + + fatal, warn, ok = [], [], [] + print(f"{'route':<44} {'code':>4} finding") + print("-" * 90) + for path in ROUTES: + status, body, raw = get(path) + note = "" + if status == 502: + note = "FATAL: no Rust owner -> dead Python upstream" + fatal.append((path, note)) + elif status >= 400: + note = f"FATAL: {raw[:60]!r}" + fatal.append((path, note)) + else: + occ = squad_occupied(body) + if occ is not None and "squad" in path: + note = f"squad occupied slots = {occ}" + if occ == 0: + note += " FATAL: hub cannot open on an empty squad" + fatal.append((path, note)) + else: + ok.append(path) + else: + ok.append(path) + if isinstance(body, dict): + n = body.get("total", body.get("totalResults")) + note = f"ok{'' if n is None else f', total={n}'}" + else: + note = "ok" + print(f"{path:<44} {status:>4} {note}") + + # The host's own classification is the authority on ownership and integrity, so + # read what it logged for the requests just made rather than inferring from bodies. + if os.path.exists(LOG): + new = open(LOG, errors="replace").read().splitlines()[start:] + flagged = [l for l in new + if "PYTHON_FALLBACK" in l or "missing_integrity" in l + or "ERROR" in l] + if flagged: + print("\nhost-side findings for those requests:") + for l in flagged: + print(f" {l[:160]}") + if "missing_integrity" in l: + warn.append(l) + + print(f"\n=== {len(ok)} ok, {len(warn)} integrity warning(s), {len(fatal)} fatal ===") + for path, note in fatal: + print(f" FATAL {path}: {note}") + if fatal: + print("\nThe hub will not work until the fatal rows are resolved.") + return 1 + print("Every hub route the client needs is served. Safe to restart the client.") + return 0 + + +if __name__ == "__main__": + sys.exit(main())