#!/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())