fifa17-python: commit working FUT backend deployment (client/server split)
Freeze the running offline FUT backend into version control as fifa17-recon/docker/fifa17-python/ - declarative and rebuildable from a fresh checkout: * OPENFUT_BIND / OPENFUT_ADVERTISE client/server split in the responders (lsx, blaze, roster, utas, pow) + entrypoint.sh; OPENFUT_ADVERTISE is required for remote mode (compose and entrypoint fail without it) * docker-compose.yml reproducing the frozen baseline container exactly (env, ports incl. the 8085->8080 POW-content remap, /state bind, restart) * .env.example / .env for site config - the LAN IP is never hardcoded in source * tools/ + data/ staged from openfut-fut-backend:python-baseline-2026-08-10, verified byte-identical to the running container at freeze time * client_arm.sh (the 105 client-side arming counterpart) * Dockerfile bakes /app/SHA256SUMS.txt so any image is self-identifying * docs/BASELINE-python-2026-08-10.md: frozen image/container/hash record, restore instructions and rebuild-equivalence procedure Secrets (redir key/cert, .env) and runtime state (docker/state) stay gitignored. The live container is untouched pending the .105 launcher audit.
This commit is contained in:
@@ -0,0 +1,134 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Regression tests for FIFA 17's account-scoped phishing/security gate."""
|
||||
import importlib
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import tempfile
|
||||
|
||||
TOOLS = os.path.dirname(os.path.abspath(__file__))
|
||||
if TOOLS not in sys.path:
|
||||
sys.path.insert(0, TOOLS)
|
||||
|
||||
DEVICE_ID = "1" * 32
|
||||
TRANSFORMED_ANSWER = "a" * 32 # sanitized replay value, not a real answer
|
||||
|
||||
|
||||
class Request:
|
||||
def __init__(self, method, path, sid=None):
|
||||
self.command = method
|
||||
self.path = path
|
||||
self.headers = {"X-UT-SID": sid} if sid is not None else {}
|
||||
self._body = b""
|
||||
|
||||
|
||||
def request(utas_server, method, suffix, sid=None):
|
||||
sid = utas_server.SID if sid is None else sid
|
||||
h = Request(method, "/ut/game/fifa17/phishing/" + suffix, sid)
|
||||
return utas_server.security_question_route(h)
|
||||
|
||||
|
||||
def profile(state, persona_id):
|
||||
path = os.path.join(state, "accounts", str(persona_id), "fifa17_profile.json")
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
|
||||
|
||||
def main():
|
||||
with tempfile.TemporaryDirectory() as state:
|
||||
os.environ["FUT_ACCOUNT_PATH"] = os.path.join(state, "active_account.json")
|
||||
os.environ["FUT_PROFILE_ROOT"] = os.path.join(state, "accounts")
|
||||
os.environ.pop("FUT_PROFILE", None)
|
||||
|
||||
import fut_account
|
||||
import fut_store
|
||||
import fut_accounts
|
||||
import utas_server
|
||||
importlib.reload(fut_account)
|
||||
importlib.reload(fut_store)
|
||||
importlib.reload(fut_accounts)
|
||||
importlib.reload(utas_server)
|
||||
|
||||
# New/missing state: launcher account selection initializes one account only.
|
||||
fut_accounts.activate({"personaId": 771001, "personaName": "SEC_A"})
|
||||
p = profile(state, 771001)
|
||||
assert p["securityQuestion"] == {"version": 1, "verified": True}
|
||||
|
||||
# Actual trusted-device response fields parsed by CardsDLL 0x18012a170.
|
||||
code, body = request(
|
||||
utas_server, "GET", "trusteddevice?deviceId=" + DEVICE_ID)
|
||||
assert code == 200
|
||||
assert body == {
|
||||
"changed": False,
|
||||
"exists": True,
|
||||
"locked": False,
|
||||
"trusted": True,
|
||||
}
|
||||
|
||||
# Existing initialized state survives a fresh Store instance/process view.
|
||||
reopened = fut_store.Store(fut_store.profile_path_for(771001))
|
||||
assert reopened.profile()["securityQuestion"] == {
|
||||
"version": 1, "verified": True}
|
||||
|
||||
# FIFA's observed repeat-session request: POST, empty body, opaque 32-hex
|
||||
# deviceId and transformed answer in the query string. The answer is accepted
|
||||
# for OpenFUT compatibility but never persisted.
|
||||
code, body = request(
|
||||
utas_server,
|
||||
"POST",
|
||||
"validate?deviceId=%s&answer=%s" % (DEVICE_ID, TRANSFORMED_ANSWER),
|
||||
)
|
||||
assert (code, body) == (200, {})
|
||||
saved = profile(state, 771001)
|
||||
assert TRANSFORMED_ANSWER not in json.dumps(saved)
|
||||
|
||||
# Question lookup uses the three fields parsed by CardsDLL 0x180129850.
|
||||
code, body = request(
|
||||
utas_server, "GET", "question?deviceId=" + DEVICE_ID)
|
||||
assert code == 200
|
||||
assert set(body) == {"question", "attempts", "recoverAttempts"}
|
||||
assert all(isinstance(body[k], int) for k in body)
|
||||
|
||||
# Malformed values/methods and missing sessions fail explicitly.
|
||||
code, _ = request(utas_server, "POST", "validate?deviceId=bad&answer=bad")
|
||||
assert code == 400
|
||||
code, _ = request(
|
||||
utas_server, "DELETE", "trusteddevice?deviceId=" + DEVICE_ID)
|
||||
assert code == 405
|
||||
h = Request(
|
||||
"GET", "/ut/game/fifa17/phishing/trusteddevice?deviceId=" + DEVICE_ID)
|
||||
code, _ = utas_server.security_question_route(h)
|
||||
assert code == 400
|
||||
|
||||
# Ordinary request logging must redact answer query values.
|
||||
raw_path = "/ut/game/fifa17/phishing/validate?deviceId=%s&answer=%s" % (
|
||||
DEVICE_ID, TRANSFORMED_ANSWER)
|
||||
safe_path = utas_server.safe_request_path(raw_path)
|
||||
assert TRANSFORMED_ANSWER not in safe_path
|
||||
assert "answer=%5BREDACTED%5D" in safe_path
|
||||
|
||||
# Multiple profiles receive independent persisted state; selecting B must not
|
||||
# alter A's initialized record.
|
||||
fut_accounts.activate({"personaId": 771002, "personaName": "SEC_B"})
|
||||
assert profile(state, 771002)["securityQuestion"] == {
|
||||
"version": 1, "verified": True}
|
||||
assert profile(state, 771001)["securityQuestion"] == {
|
||||
"version": 1, "verified": True}
|
||||
|
||||
# Legacy profile with the field removed is repaired once and persisted.
|
||||
b_path = os.path.join(state, "accounts", "771002", "fifa17_profile.json")
|
||||
b = profile(state, 771002)
|
||||
b.pop("securityQuestion")
|
||||
with open(b_path, "w") as f:
|
||||
json.dump(b, f)
|
||||
fut_store.STORE._p = None
|
||||
code, body = request(
|
||||
utas_server, "GET", "trusteddevice?deviceId=" + DEVICE_ID)
|
||||
assert code == 200 and body["exists"] and body["trusted"]
|
||||
assert profile(state, 771002)["securityQuestion"]["verified"] is True
|
||||
|
||||
print("security-question compatibility: PASS")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user