202366611e
The sold A/B stalled twice on preconditions no headless check exercised: the staging identity had no squad (the hub refuses to open, showing a squad update error), and any route without a Rust owner falls through to a deliberately dead Python upstream and answers 502. Each cost a full operator cycle. Sweeps the routes the client is observed to request and separates three failure classes that need different fixes: 502/PYTHON_FALLBACK (no Rust owner), missing_integrity (200 but the underlying state is absent -- exactly 'no extension stored' before the squad was seeded), and 200-but-unusable (a squad with zero occupied slots). A 200 is not proof the client is satisfied, so squad responses are judged on occupied slots. Also reads the host's own classification for the requests just made, since the host is the authority on ownership and integrity rather than the response body. Every path is verified against what the client actually sends. A first pass flagged five 'fatal' routes that were my own guesses -- /accountinfo (client uses /user/accountinfo), bare /squad (uses /squad/active), and /watchlist (camelCase watchList). Crying wolf about the stack is worse than not checking, so the list now carries only observed paths and that trap is written down in the comment. Current result: 14 ok, 0 integrity warnings, 0 fatal.
138 lines
5.2 KiB
Python
Executable File
138 lines
5.2 KiB
Python
Executable File
#!/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())
|