#!/usr/bin/env python3 """Tests for the FIFA 17 verified-patched-client capability negotiation. The additive empty-My-Packs switch on top of the P2 65534 sentinel: the sentinel is suppressed for ONE FIFA session only when the launcher has registered a verified resolver capability (v1) that binds to THAT process's UTAS session (keyed by the per-login-unique X-UT-SID; source IP + persona are auxiliary). Every failure / unknown / late / cross-process / cross-session case is fail-closed to the sentinel. The initial prototype keyed by source IP alone; this suite proves the hardened per-session binding, including two sessions that SHARE a source IP. Matrix (docs/plans/FIFA17_PATCHED_CLIENT_CAPABILITY.md): A no-capability, zero packs -> sentinel B verified v1, zero packs -> clean (no 65534) C real unopened pack + no capability -> genuine pack, no sentinel D real unopened pack + capability -> genuine pack, no sentinel E unsupported version / capability -> endpoint 400 AND mode sentinel F late capability after sentinel freeze -> stays sentinel G capability disappears after clean freeze -> stays clean (immutable) H two IPs (A verified, B none) -> A clean, B sentinel (no global leak) I new session after reset -> fresh unpatched -> sentinel J autopatch mismatch => never registers -> sentinel K SAME IP, two sessions (A patched, B not) -> A clean, B sentinel L SAME IP+persona relaunch (old ok, new not) -> new session sentinel M SAME IP, failed-patch second session -> first clean, second sentinel N late registration when sessions are frozen -> does not modify active sessions O session cleanup / TTL expiry -> capability gone, sentinel P duplicate registration for a session -> idempotent; no post-freeze change Q register-before-login (pending consumed) -> clean R topology freeze immutable per SID -> no flip either way; new SID fresh Standalone unit test in the project style: `python3 test_capability_negotiation.py`. """ 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) SENTINEL_ID = 65534 REAL_PACK_ID = 1 PERSONA = 111001 class _H: """Minimal request-handler stand-in: peer IP, optional X-UT-SID, optional body.""" def __init__(self, ip, body=None, sid=None): self.client_address = (ip, 54321) self.headers = {"X-UT-SID": sid} if sid is not None else {} self._body = json.dumps(body).encode("utf-8") if body is not None else b"" def _ids(catalog): return [p["id"] for p in catalog["purchase"]] 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) us = utas_server CLEAN, SENT = us.FIFA17_MODE_CLEAN, us.FIFA17_MODE_SENTINEL _orig_visible = us.visible_unopened_packs def set_zero_packs(): us.visible_unopened_packs = lambda: [] def set_real_pack(): us.visible_unopened_packs = lambda: [REAL_PACK_ID] def reset_state(): us._FIFA17_SESSIONS.clear() us._FIFA17_PENDING.clear() def auth(sid, ip, persona=PERSONA): """Simulate /ut/auth opening a per-login session with a chosen sid.""" us.fifa17_open_session(sid, ip, persona) def register(ip, version, persona=PERSONA, pid=4242): return us.fifa17_capability_route(_H(ip, { "capability": "empty_mypacks_resolver", "version": version, "personaId": persona, "fifaPid": pid, })) def store(sid, ip): status, cat = us.store_catalog(_H(ip, sid=sid)) assert status == 200, status return _ids(cat) def mode_of(sid): return us._FIFA17_SESSIONS[sid]["mode"] try: # ---- A. no capability, zero packs -> sentinel ---------------------- reset_state(); set_zero_packs() auth("sidA", "10.0.0.1") assert SENTINEL_ID in store("sidA", "10.0.0.1") assert mode_of("sidA") == SENT print("A no-capability zero-packs -> sentinel: OK") # ---- B. verified v1, zero packs -> clean --------------------------- reset_state(); set_zero_packs() auth("sidB", "10.0.0.2") assert register("10.0.0.2", 1)[0] == 200 ids = store("sidB", "10.0.0.2") assert SENTINEL_ID not in ids, ids assert mode_of("sidB") == CLEAN print("B verified-v1 zero-packs -> clean: OK") # ---- C. real pack + no capability -> genuine, no sentinel ---------- reset_state(); set_real_pack() auth("sidC", "10.0.0.3") ids = store("sidC", "10.0.0.3") assert REAL_PACK_ID in ids and SENTINEL_ID not in ids, ids print("C real-pack no-capability -> genuine, no sentinel: OK") # ---- D. real pack + capability -> genuine, no sentinel ------------- reset_state(); set_real_pack() auth("sidD", "10.0.0.4"); register("10.0.0.4", 1) ids = store("sidD", "10.0.0.4") assert REAL_PACK_ID in ids and SENTINEL_ID not in ids, ids print("D real-pack capability -> genuine, no sentinel: OK") # ---- E. unsupported version / capability -> 400 + sentinel --------- reset_state(); set_zero_packs() auth("sidE", "10.0.0.5") assert register("10.0.0.5", 2)[0] == 400 assert register("10.0.0.5", 99)[0] == 400 assert us.fifa17_capability_route( _H("10.0.0.5", {"capability": "bogus", "version": 1}))[0] == 400 assert SENTINEL_ID in store("sidE", "10.0.0.5") assert mode_of("sidE") == SENT print("E unsupported version/capability -> 400 + sentinel: OK") # ---- F. late capability after sentinel freeze -> sentinel ---------- reset_state(); set_zero_packs() auth("sidF", "10.0.0.6") assert SENTINEL_ID in store("sidF", "10.0.0.6") # freezes sentinel assert register("10.0.0.6", 1)[0] == 200 # session frozen -> ignored-late assert SENTINEL_ID in store("sidF", "10.0.0.6") assert mode_of("sidF") == SENT print("F late capability after sentinel freeze -> sentinel: OK") # ---- G. capability disappears after clean freeze -> clean ---------- reset_state(); set_zero_packs() auth("sidG", "10.0.0.7"); register("10.0.0.7", 1) assert SENTINEL_ID not in store("sidG", "10.0.0.7") # freezes clean us._FIFA17_SESSIONS["sidG"]["resolver"] = None # capability vanishes assert SENTINEL_ID not in store("sidG", "10.0.0.7") assert mode_of("sidG") == CLEAN print("G capability disappears after clean freeze -> clean: OK") # ---- H. two IPs (A verified, B none) -> no global leak ------------- reset_state(); set_zero_packs() auth("sidH1", "10.0.1.1"); register("10.0.1.1", 1) auth("sidH2", "10.0.1.2") assert SENTINEL_ID not in store("sidH1", "10.0.1.1") assert SENTINEL_ID in store("sidH2", "10.0.1.2") print("H two IPs (A clean, B sentinel) -> no global leak: OK") # ---- I. new session after reset -> fresh unpatched -> sentinel ----- reset_state(); set_zero_packs() auth("sidI1", "10.0.1.3"); register("10.0.1.3", 1) assert SENTINEL_ID not in store("sidI1", "10.0.1.3") # A clean us.fifa17_clear_pending("10.0.1.3") # relaunch boundary auth("sidI2", "10.0.1.3") # new SID, autopatch failed assert SENTINEL_ID in store("sidI2", "10.0.1.3") print("I new session after reset -> sentinel (no cross-process leak): OK") # ---- J. autopatch mismatch => never registers -> sentinel ---------- reset_state(); set_zero_packs() auth("sidJ", "10.0.1.4") assert SENTINEL_ID in store("sidJ", "10.0.1.4") print("J autopatch mismatch (never registers) -> sentinel: OK") # ---- K. SAME IP, two sessions: patched A clean, unpatched B sent --- reset_state(); set_zero_packs() IP = "10.0.2.1" auth("sidK_A", IP) assert register(IP, 1)[0] == 200 # A sole candidate -> bound auth("sidK_B", IP) # B joins, never registers assert SENTINEL_ID not in store("sidK_A", IP) assert SENTINEL_ID in store("sidK_B", IP) print("K same-IP two sessions -> A clean, B sentinel: OK") # ---- L. SAME IP+persona relaunch: old ok, new not -> new sentinel -- reset_state(); set_zero_packs() IP = "10.0.2.2" auth("sidL_old", IP, PERSONA); register(IP, 1, PERSONA) assert SENTINEL_ID not in store("sidL_old", IP) us.fifa17_clear_pending(IP) auth("sidL_new", IP, PERSONA) # same persona, unverified assert SENTINEL_ID in store("sidL_new", IP) print("L same-IP+persona relaunch -> new session sentinel: OK") # ---- M. SAME IP, failed-patch second session ----------------------- reset_state(); set_zero_packs() IP = "10.0.2.3" auth("sidM1", IP); register(IP, 1) assert SENTINEL_ID not in store("sidM1", IP) auth("sidM2", IP) # autopatch failed assert SENTINEL_ID in store("sidM2", IP) print("M same-IP failed-patch second session -> sentinel: OK") # ---- N. late reg when sessions frozen -> no active session change -- reset_state(); set_zero_packs() IP = "10.0.2.4" auth("sidN1", IP); register(IP, 1) assert SENTINEL_ID not in store("sidN1", IP) # N1 frozen clean auth("sidN2", IP) assert SENTINEL_ID in store("sidN2", IP) # N2 frozen sentinel assert register(IP, 1)[0] == 200 # late: both frozen -> ignored assert SENTINEL_ID not in store("sidN1", IP) # unchanged assert SENTINEL_ID in store("sidN2", IP) # unchanged print("N late registration does not modify active sessions: OK") # ---- O. session cleanup / TTL expiry -> capability gone ------------ reset_state(); set_zero_packs() IP = "10.0.2.5" auth("sidO", IP); register(IP, 1) assert SENTINEL_ID not in store("sidO", IP) # clean while live us._FIFA17_SESSIONS["sidO"]["last_seen"] = ( us._fifa17_now() - us.FIFA17_SESSION_TTL - 10.0) store("sidUNKNOWN", IP) # any op triggers reap assert "sidO" not in us._FIFA17_SESSIONS, "expired session not reaped" assert SENTINEL_ID in store("sidO", IP) # gone -> sentinel print("O session cleanup / TTL expiry -> sentinel: OK") # ---- P. duplicate registration -> idempotent, no post-freeze change reset_state(); set_zero_packs() IP = "10.0.2.6" auth("sidP", IP) assert register(IP, 1)[0] == 200 # bound assert register(IP, 1)[0] == 200 # duplicate -> ignored-late assert SENTINEL_ID not in store("sidP", IP) # still clean assert register(IP, 1)[0] == 200 # after freeze assert SENTINEL_ID not in store("sidP", IP) # unchanged assert mode_of("sidP") == CLEAN print("P duplicate registration -> idempotent: OK") # ---- Q. register-before-login: pending consumed at auth -> clean --- reset_state(); set_zero_packs() IP = "10.0.2.7" assert register(IP, 1)[0] == 200 # no session yet -> pending assert (IP, PERSONA) in us._FIFA17_PENDING auth("sidQ", IP, PERSONA) # consumes pending assert (IP, PERSONA) not in us._FIFA17_PENDING # single-use assert SENTINEL_ID not in store("sidQ", IP) assert mode_of("sidQ") == CLEAN print("Q register-before-login pending consumed -> clean: OK") # ---- R. topology freeze immutable per SID; new SID decides fresh ---- # F3 invariant: once a SID's store topology is decided it NEVER flips, # in either direction, and a different SID may decide differently. reset_state(); set_zero_packs() IP = "10.0.2.8" # frozen Sentinel never becomes Clean, even if a capability appears later auth("sidR_s", IP) assert SENTINEL_ID in store("sidR_s", IP) # freeze Sentinel register(IP, 1) us._FIFA17_SESSIONS["sidR_s"]["resolver"] = 1 # force-present capability assert SENTINEL_ID in store("sidR_s", IP) # STILL Sentinel assert mode_of("sidR_s") == SENT # frozen Clean never becomes Sentinel, even if the capability is wiped auth("sidR_c", IP); register(IP, 1) assert SENTINEL_ID not in store("sidR_c", IP) # freeze Clean us._FIFA17_SESSIONS["sidR_c"]["resolver"] = None # capability vanishes assert SENTINEL_ID not in store("sidR_c", IP) # STILL Clean assert mode_of("sidR_c") == CLEAN # a fresh SID (same IP) decides independently auth("sidR_new", IP) assert SENTINEL_ID in store("sidR_new", IP) print("R topology freeze immutable per SID; new SID fresh: OK") finally: us.visible_unopened_packs = _orig_visible print("capability negotiation matrix A-R: OK") return 0 if __name__ == "__main__": raise SystemExit(main())