fifa17-recon: take running-backend versions of 8 runtime files (direction fix)
The earlier reconcile committed the local working-tree versions of these files, which are OLDER than the deployed backend. The running container (C) is byte-identical to docker/fifa17-python/tools (B) and is a strict superset: it adds profile_path_for/select_account/ensure_security_question (fut_store), safe_header_for_log/safe_request_path/security_question_route (utas_server), account_sync_route/_match_call/match_ready_body, plus POW balance fields and match lifecycle support, with zero unique local functions lost. Reconciled tree is now a strict superset of B with every shared file byte-identical; verified via md5 map (0 missing, 0 differing).
This commit is contained in:
@@ -128,14 +128,52 @@ CLIENT_ID = ACCOUNT.CLIENT_ID
|
||||
PLATFORM = ACCOUNT.PLATFORM
|
||||
SERVER_VERSION = "Blaze 15.1.1.3.0 (OpenFUT)\n"
|
||||
|
||||
# ================================================================== config
|
||||
|
||||
HOST = "127.0.0.1"
|
||||
def refresh_account_identity():
|
||||
"""Refresh launcher-selected identity before constructing a Blaze session.
|
||||
|
||||
The account sync endpoint runs in the separate UTAS process and atomically
|
||||
replaces the shared active-account file. Blaze snapshots these aliases for
|
||||
its response builders, so refresh them once at each new TCP session.
|
||||
"""
|
||||
global PERSONA_ID, PERSONA_NAME, USER_ID, EXT_ID, EMAIL, ACCOUNT_LOCALE_FALLBACK
|
||||
ACCOUNT.load(force=True)
|
||||
PERSONA_ID = ACCOUNT.persona_id
|
||||
PERSONA_NAME = ACCOUNT.persona_name
|
||||
USER_ID = ACCOUNT.user_id
|
||||
EXT_ID = ACCOUNT.ext_id
|
||||
EMAIL = ACCOUNT.email
|
||||
ACCOUNT_LOCALE_FALLBACK = ACCOUNT.account_locale_int
|
||||
|
||||
# ================================================================== config
|
||||
#
|
||||
# Client/server split support (OpenFUT dev-container): two env vars, both
|
||||
# defaulting to loopback so the original all-on-localhost flow is byte-identical.
|
||||
# OPENFUT_BIND — the address the listeners bind (0.0.0.0 in a container).
|
||||
# OPENFUT_ADVERTISE — the address this server hands back to the client for the
|
||||
# NEXT hop (Blaze host, roster/UTAS/telemetry/QoS URLs). On
|
||||
# 105-local this is 127.0.0.1; on the 120 server it is the
|
||||
# server's LAN IP so the game dials 120 directly after the
|
||||
# first (hook/DNAT-redirected) contact.
|
||||
import os as _os_cfg
|
||||
_ADVERTISE = _os_cfg.environ.get("OPENFUT_ADVERTISE", "127.0.0.1")
|
||||
_BIND = _os_cfg.environ.get("OPENFUT_BIND", "127.0.0.1")
|
||||
|
||||
def _ip_str_to_u32(ip):
|
||||
"""Dotted-quad -> big-endian u32 (matches the original (127<<24)|1 layout).
|
||||
Falls back to loopback if the advertise value isn't a bare IPv4 literal."""
|
||||
try:
|
||||
a, b, c, d = (int(x) for x in ip.split("."))
|
||||
return (a << 24) | (b << 16) | (c << 8) | d
|
||||
except Exception:
|
||||
return (127 << 24) | 1
|
||||
|
||||
HOST = _BIND
|
||||
REDIR_PORT = 42127
|
||||
BLAZE_PORT = 42130
|
||||
NUCLEUS_PORT = 42131
|
||||
BLAZE_IP_STR = "127.0.0.1"
|
||||
BLAZE_IP_U32 = (127 << 24) | 1
|
||||
BLAZE_IP_STR = _ADVERTISE
|
||||
BLAZE_IP_U32 = _ip_str_to_u32(_ADVERTISE)
|
||||
LOG = "/tmp/blaze_responder.log"
|
||||
RXDIR = "/tmp/blaze_rx"
|
||||
HERE = os.path.dirname(os.path.abspath(__file__))
|
||||
@@ -157,7 +195,9 @@ REPLY_EMPTY_TO_UNKNOWN = True
|
||||
# (grid-blaze order) or after (pamplona order). Both are reported to work.
|
||||
NOTIFY_BEFORE_LOGIN_REPLY = False
|
||||
|
||||
DUMP_FRAMES = True
|
||||
# Raw Fire2 frames and decoded TDF can contain auth/session material. Keep the
|
||||
# reverse-engineering capture path, but require an explicit opt-in for it.
|
||||
DUMP_FRAMES = os.environ.get("OPENFUT_BLAZE_DUMP_FRAMES") == "1"
|
||||
|
||||
_log_lock = threading.Lock()
|
||||
|
||||
@@ -525,7 +565,7 @@ OSDK_TICKER = []
|
||||
# branch does NOT wrap the value ("https://%s" is only the ini path) -> ABSOLUTE url.
|
||||
# Serve HTTPS (EA's production value is https; the DirtySDK download mgr may reject
|
||||
# http). Our ProtoSSL cert-verify is patched (autopatch), so a self-signed cert is OK.
|
||||
ROSTER_HOST = "127.0.0.1:8081"
|
||||
ROSTER_HOST = "%s:8081" % _ADVERTISE
|
||||
POW_CONTENT_HOST = os.environ.get("POW_CONTENT_HOST", "127.0.0.1:8080")
|
||||
OSDK_ROSTER = [
|
||||
("ROSTERUPDATE_URL", "https://%s/fifa17/fut/rosterupdate.xml" % ROSTER_HOST),
|
||||
@@ -603,7 +643,7 @@ CLIENT_CONFIGS = {
|
||||
# /etc/hosts easw.easports.com->127.0.0.1 redirect. MUST be exactly "http://127.0.0.1:8099/"
|
||||
# (scheme + trailing slash mandatory on the auth path). Do NOT serve FUT_TARGET_PORT
|
||||
# (bug @0x1801808e8 reads FUT_MAX_HOPS instead) nor FUT/MODULE_BASEURL_* (dead code).
|
||||
UTAS_BASE = "http://127.0.0.1:8099/"
|
||||
UTAS_BASE = "http://%s:8099/" % _ADVERTISE
|
||||
FUT_RS4_MODULES = [
|
||||
"AUCTIONHOUSE", "CLUB_USER", "CLUB_INFO", "CLUB", "DREAM", "SQUAD",
|
||||
"DELETE_SQUAD", "LBOPTIONS", "LBDEFAULT", "PAFPRACTICE", "UT", "USER",
|
||||
@@ -739,10 +779,12 @@ FUT_RS4_CONFIG = (
|
||||
|
||||
|
||||
def client_config_for(cfid: str) -> list:
|
||||
"""-> sorted [(key, value)]. Unknown CFID -> [] (an EMPTY MAP, which we
|
||||
still wrap in a present CONF field -- never an empty frame).
|
||||
FUT_RS4_* base-URL keys ride on EVERY CFID (merged '_all' store; which section
|
||||
CardsDLL reads is unproven, so serve them everywhere)."""
|
||||
"""Return sorted config rows for one section.
|
||||
|
||||
Unknown CFIDs still receive the shared FUT/content/POW rows because those
|
||||
consumers read the merged ``_all`` store and the contributing section is
|
||||
unproven. The response always carries a present CONF field.
|
||||
"""
|
||||
# OSDK_POW rides on EVERY CFID for the same reason FUT_RS4_* does: powdll's
|
||||
# FUN_18005a460 reads FIFA_POW_URL out of the merged '_all' store, and which
|
||||
# section it happens to read is unproven. Empty list when FUT_POW is unset, so
|
||||
@@ -774,7 +816,7 @@ def qos_config() -> "OrderedDict":
|
||||
has NO SVID, unlike Mirror's Edge Catalyst)."""
|
||||
return OrderedDict([
|
||||
("BWPS", (STRUCT, OrderedDict([ # Blaze::QosPingSiteInfo
|
||||
("PSA", (STRING, "127.0.0.1")),
|
||||
("PSA", (STRING, _ADVERTISE)),
|
||||
("PSP", (INT, 17502)),
|
||||
]))),
|
||||
("LNP", (INT, 10)),
|
||||
@@ -1100,7 +1142,7 @@ def post_auth_response_fields(sess: Session) -> "OrderedDict":
|
||||
client to have a well-formed config and then fail to connect quietly rather
|
||||
than resolve a real EA hostname."""
|
||||
tele = OrderedDict([ # GetTelemetryServerResponse (15)
|
||||
("ADRS", (STRING, "127.0.0.1")),
|
||||
("ADRS", (STRING, _ADVERTISE)),
|
||||
("ANON", (INT, 0)),
|
||||
("DISA", (STRING, "")),
|
||||
("EDCT", (INT, 0)),
|
||||
@@ -1117,7 +1159,7 @@ def post_auth_response_fields(sess: Session) -> "OrderedDict":
|
||||
("SVNM", (STRING, "telemetry-openfut")),
|
||||
])
|
||||
tick = OrderedDict([ # GetTickerServerResponse (3)
|
||||
("ADRS", (STRING, "127.0.0.1")),
|
||||
("ADRS", (STRING, _ADVERTISE)),
|
||||
("PORT", (INT, 8999)),
|
||||
("SKEY", (STRING, "")),
|
||||
])
|
||||
@@ -1261,8 +1303,10 @@ def dispatch(hdr: dict, fields, raw_payload: bytes, sess: Session) -> list:
|
||||
log(" -- client locale 0x%08x captured for ALOC" % loc)
|
||||
resp = preauth_response_fields(service_name=sess.service_name)
|
||||
payload = encode_tdf(resp)
|
||||
log(" -> PreAuthResponse (INST=%r, %d payload bytes):\n%s"
|
||||
% (sess.service_name, len(payload), heat2.dump(resp)))
|
||||
log(" -> PreAuthResponse (INST=%r, %d payload bytes)"
|
||||
% (sess.service_name, len(payload)))
|
||||
if DUMP_FRAMES:
|
||||
log(" -> PreAuthResponse TDF:\n%s" % heat2.dump(resp))
|
||||
return [reply_to(hdr, payload)]
|
||||
|
||||
if cmd == CMD_PING:
|
||||
@@ -1276,8 +1320,9 @@ def dispatch(hdr: dict, fields, raw_payload: bytes, sess: Session) -> list:
|
||||
n = len(resp["CONF"][1][2])
|
||||
log(" -> FetchConfigResponse CFID=%r -> %d key(s)%s"
|
||||
% (cfid, n, "" if n else " (EMPTY MAP, unknown CFID)"))
|
||||
for k, v in resp["CONF"][1][2]:
|
||||
log(" %-32s = %s" % (k, v))
|
||||
if DUMP_FRAMES:
|
||||
for k, v in resp["CONF"][1][2]:
|
||||
log(" %-32s = %s" % (k, v))
|
||||
return [reply_to(hdr, encode_tdf(resp))]
|
||||
|
||||
if cmd == CMD_POSTAUTH:
|
||||
@@ -1311,12 +1356,13 @@ def dispatch(hdr: dict, fields, raw_payload: bytes, sess: Session) -> list:
|
||||
sess.auth_code = get_str(fields or {}, "AUTH", "")
|
||||
sess.logged_in = True
|
||||
sess.login_time = int(time.time())
|
||||
log(" == Authentication::login AUTH=%r (accepted WITHOUT Nucleus "
|
||||
"validation -- forged offline session)" % sess.auth_code)
|
||||
log(" == Authentication::login AUTH=[REDACTED] "
|
||||
"(accepted as an offline OpenFUT session)")
|
||||
resp = login_response_fields(sess)
|
||||
payload = encode_tdf(resp)
|
||||
log(" -> LoginResponse (%d bytes):\n%s"
|
||||
% (len(payload), heat2.dump(resp)))
|
||||
log(" -> LoginResponse (%d bytes)" % len(payload))
|
||||
if DUMP_FRAMES:
|
||||
log(" -> LoginResponse TDF:\n%s" % heat2.dump(resp))
|
||||
notifs = build_login_notifications(sess, sess.login_time)
|
||||
out = []
|
||||
if NOTIFY_BEFORE_LOGIN_REPLY:
|
||||
@@ -1467,9 +1513,10 @@ _frame_counter = [0]
|
||||
|
||||
|
||||
def blaze_handle(raw: socket.socket, addr) -> None:
|
||||
refresh_account_identity()
|
||||
log("*** BLAZE CONNECT from %s ***" % (addr,))
|
||||
sess = Session()
|
||||
log(" session key minted: %s" % sess.session_key)
|
||||
log(" session key minted: [REDACTED]")
|
||||
buf = bytearray()
|
||||
raw.settimeout(300)
|
||||
try:
|
||||
@@ -1500,10 +1547,10 @@ def blaze_handle(raw: socket.socket, addr) -> None:
|
||||
MSGTYPE_NAME.get(hdr["msg_type"], hdr["msg_type"]),
|
||||
hdr["msg_num"], hdr["user_index"], hdr["options"],
|
||||
hdr["metadata_len"], hdr["payload_len"]))
|
||||
log("RX #%d HEX:\n%s" % (n, hexdump(frame)))
|
||||
if metadata:
|
||||
log("RX #%d METADATA:\n%s" % (n, hexdump(metadata)))
|
||||
if DUMP_FRAMES:
|
||||
log("RX #%d HEX:\n%s" % (n, hexdump(frame)))
|
||||
if metadata:
|
||||
log("RX #%d METADATA:\n%s" % (n, hexdump(metadata)))
|
||||
try:
|
||||
os.makedirs(RXDIR, exist_ok=True)
|
||||
fn = os.path.join(RXDIR, "rx_%04d_%04x_%04x.bin"
|
||||
@@ -1518,7 +1565,8 @@ def blaze_handle(raw: socket.socket, addr) -> None:
|
||||
if payload:
|
||||
try:
|
||||
fields = decode_tdf(payload)
|
||||
log("RX #%d TDF:\n%s" % (n, heat2.dump(fields)))
|
||||
if DUMP_FRAMES:
|
||||
log("RX #%d TDF:\n%s" % (n, heat2.dump(fields)))
|
||||
except Exception as e:
|
||||
log("RX #%d TDF DECODE FAILED: %s" % (n, e))
|
||||
else:
|
||||
@@ -1539,7 +1587,8 @@ def blaze_handle(raw: socket.socket, addr) -> None:
|
||||
ohdr["msg_type"]),
|
||||
MSGTYPE_NAME.get(ohdr["msg_type"], ohdr["msg_type"]),
|
||||
ohdr["msg_num"], len(out), ohdr["payload_len"]))
|
||||
log("TX #%d.%d HEX:\n%s" % (n, k, hexdump(out, limit=1024)))
|
||||
if DUMP_FRAMES:
|
||||
log("TX #%d.%d HEX:\n%s" % (n, k, hexdump(out, limit=1024)))
|
||||
except ConnectionResetError:
|
||||
log("BLAZE %s: connection reset by client" % (addr,))
|
||||
except Exception as e:
|
||||
@@ -1637,6 +1686,10 @@ def redir_handle(raw: socket.socket, addr) -> None:
|
||||
# client can never reach accounts.ea.com. Note the exact spacing in the JSON:
|
||||
# the client searches for the literal '"access_token" : "'.
|
||||
|
||||
def nucleus_sent_log(addr, size):
|
||||
return "NUCLEUS SENT %s %dB access_token=[REDACTED]" % (addr, size)
|
||||
|
||||
|
||||
def nucleus_handle(raw: socket.socket, addr) -> None:
|
||||
try:
|
||||
raw.settimeout(10)
|
||||
@@ -1649,9 +1702,9 @@ def nucleus_handle(raw: socket.socket, addr) -> None:
|
||||
head, _, rest = req.partition(b"\r\n\r\n")
|
||||
line0 = head.split(b"\r\n", 1)[0].decode(errors="replace") if head else ""
|
||||
log("NUCLEUS REQ %s: %s" % (addr, line0))
|
||||
if head:
|
||||
if head and DUMP_FRAMES:
|
||||
log("NUCLEUS HEADERS:\n%s" % head.decode(errors="replace"))
|
||||
if rest:
|
||||
if rest and DUMP_FRAMES:
|
||||
log("NUCLEUS BODY: %r" % rest[:512])
|
||||
|
||||
token = "OPENFUT_" + "".join(
|
||||
@@ -1665,7 +1718,7 @@ def nucleus_handle(raw: socket.socket, addr) -> None:
|
||||
b"Cache-Control: no-store\r\nContent-Length: "
|
||||
+ str(len(body)).encode() + b"\r\nConnection: close\r\n\r\n" + body)
|
||||
raw.sendall(out)
|
||||
log("NUCLEUS SENT %s %dB access_token=%s" % (addr, len(out), token))
|
||||
log(nucleus_sent_log(addr, len(out)))
|
||||
except Exception as e:
|
||||
log("NUCLEUS ERR %s: %s" % (addr, e))
|
||||
finally:
|
||||
@@ -1717,6 +1770,10 @@ def _selftest() -> None:
|
||||
sess.account_locale = 0x656E5553
|
||||
now = 1469000000
|
||||
|
||||
nucleus_summary = nucleus_sent_log(("127.0.0.1", 1234), 380)
|
||||
assert "[REDACTED]" in nucleus_summary
|
||||
assert "OPENFUT_selftest_secret" not in nucleus_summary
|
||||
|
||||
# ---- 1. preAuth still round-trips (regression guard vs v2)
|
||||
pre = preauth_response_fields()
|
||||
p = _check_roundtrip("PreAuthResponse", pre)
|
||||
@@ -1740,9 +1797,11 @@ def _selftest() -> None:
|
||||
assert items == client_config_for(cfid), cfid
|
||||
print("[ok] fetchClientConfig %-26s %2d keys, %4d payload bytes"
|
||||
% (cfid, len(items), len(pb)))
|
||||
assert client_config_for("TOTALLY_UNKNOWN") == [], "unknown CFID must be []"
|
||||
shared = sorted(FUT_RS4_CONFIG + FUT_CONTENT_CONFIG + OSDK_POW)
|
||||
assert client_config_for("TOTALLY_UNKNOWN") == shared, \
|
||||
"unknown CFID must carry only the shared merged-store rows"
|
||||
assert len(fetch_config_response_fields("TOTALLY_UNKNOWN")) == 1, \
|
||||
"unknown CFID must still carry a CONF field (empty map, not empty frame)"
|
||||
"unknown CFID must still carry a CONF field"
|
||||
|
||||
# ---- 3. LoginResponse
|
||||
lr = login_response_fields(sess)
|
||||
|
||||
Reference in New Issue
Block a user