Files
OpenFUT/fifa17-recon/docker/fifa17-python/tools/blaze_responder_v2.py
T
root 70a64e3709 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.
2026-08-10 23:54:04 +00:00

579 lines
23 KiB
Python

#!/usr/bin/env python3
# FROZEN: not started by openfut-fut.sh (see its SERVERS array); identity is NOT
# sourced from fut_account.py here. The live pair is lsx_responder_v2.py +
# blaze_responder_v3b.py -- edit those. Kept for reference/bisecting only.
"""FIFA17 Blaze redirector + SESSION SERVER (v2).
Two listeners:
* TLS on 42127 -- the redirector. Answers POST /redirector/getServerInstance
with a <serverinstanceinfo> pointing the client at 127.0.0.1:BLAZE_PORT
(secure=0). UNCHANGED from blaze_responder.py -- it already works.
* Plain TCP on 42130 -- the Blaze session server. Properly frames Fire2,
decodes the Heat2 TDF body, logs everything, and ANSWERS:
Util(0x0009)/preAuth(0x0007) -> PreAuthResponse (the current gate)
Util(0x0009)/ping(0x0002) -> PingResponse {STIM, TIME}
msgType PING(4) -> PING_REPLY(5), empty body
Everything else is logged in full and (optionally) answered with an empty
REPLY so the client is never left hanging. See the TODO block near
dispatch() for the next RPCs on the path.
CLEAN ROOM. Schema comes from (a) FIFA17.exe's own in-process TDF reflection
metadata that we walked in live memory, (b) our own captured preAuth REQUEST,
and (c) independent third-party clean-room BlazeSDK-15.x reimplementations used
only to cross-check structure. No EA/FIFA leaked source was consulted.
Run: python3 blaze_responder_v2.py (binds 42127 + 42130)
Log: /tmp/blaze_responder.log
"""
from __future__ import annotations
import binascii
import os
import socket
import ssl
import struct
import sys
import threading
import time
from collections import OrderedDict
sys.path.insert(0, os.path.dirname(os.path.abspath(__file__)))
import heat2 # noqa: E402
from heat2 import INT, STRING, STRUCT, LIST, MAP, encode_tdf, decode_tdf # noqa: E402
# ------------------------------------------------------------------ config
HOST = "127.0.0.1"
REDIR_PORT = 42127
BLAZE_PORT = 42130
BLAZE_IP_STR = "127.0.0.1"
BLAZE_IP_U32 = (127 << 24) | 1 # 2130706433
LOG = "/tmp/blaze_responder.log"
HERE = os.path.dirname(os.path.abspath(__file__))
CERT = os.path.join(HERE, "redir_cert.pem")
KEY = os.path.join(HERE, "redir_key.pem")
# If True, any RPC we do not implement still gets an empty REPLY frame so the
# client's request does not time out. Flip to False to see which RPC the
# client is actually blocking on.
REPLY_EMPTY_TO_UNKNOWN = True
# Dump every frame we receive to /tmp/blaze_rx_<comp>_<cmd>_<n>.bin
DUMP_FRAMES = True
_log_lock = threading.Lock()
def log(m: str) -> None:
line = "[%s] %s" % (time.strftime("%H:%M:%S"), m)
with _log_lock:
print(line, flush=True)
with open(LOG, "a") as fh:
fh.write(line + "\n")
def hexdump(b: bytes, limit: int = 512) -> str:
out = []
for i in range(0, min(len(b), limit), 16):
chunk = b[i:i + 16]
txt = "".join(chr(c) if 32 <= c < 127 else "." for c in chunk)
out.append(" %04x: %-47s %s"
% (i, binascii.hexlify(chunk, " ").decode(), txt))
if len(b) > limit:
out.append(" ... (%d more bytes)" % (len(b) - limit))
return "\n".join(out)
# ------------------------------------------------------------------ Fire2
#
# CORRECTED 16-byte big-endian header (heat2.build_fire2_frame /
# heat2.parse_fire2_frame encode the OLD, WRONG layout -- do not use them):
#
# [0:4] u32 payload length
# [4:6] u16 metadata length
# [6:8] u16 component
# [8:10] u16 command
# [10:13] u24 msgNum <- 3 bytes; what we once read as "msgType"
# [13] u8 (msgType << 5) | (userIndex & 0x1F)
# [14] u8 options
# [15] u8 reserved
# wire = header(16) || metadata || payload
#
# There is NO error field in the Fire2 header (that is Fire v1's 12-byte frame).
FIRE2_HDR = 16
MESSAGE, REPLY, NOTIFICATION, ERROR_REPLY, PING, PING_REPLY = range(6)
MSGTYPE_NAME = {0: "MESSAGE", 1: "REPLY", 2: "NOTIFICATION",
3: "ERROR_REPLY", 4: "PING", 5: "PING_REPLY"}
COMP_AUTH = 0x0001
COMP_GAMEMANAGER = 0x0004
COMP_REDIRECTOR = 0x0005
COMP_STATS = 0x0007
COMP_UTIL = 0x0009
COMP_MESSAGING = 0x000F
COMP_ASSOCLISTS = 0x0019
COMP_GAMEREPORTING = 0x001C
COMP_USERSESSIONS = 0x7802
CMD_FETCHCLIENTCONFIG = 0x0001
CMD_PING = 0x0002
CMD_PREAUTH = 0x0007
CMD_POSTAUTH = 0x0008
CMD_SETCLIENTSTATE = 0x001C
# Util command table recovered from the binary's getCommandName switch.
UTIL_CMDS = {
0x01: "fetchClientConfig", 0x02: "ping", 0x03: "setClientData",
0x04: "localizeStrings", 0x05: "getTelemetryServer", 0x06: "getTickerServer",
0x07: "preAuth", 0x08: "postAuth", 0x0A: "userSettingsLoad",
0x0B: "userSettingsSave", 0x0C: "userSettingsLoadAll",
0x0E: "userSettingsDelete", 0x0F: "userSettingsLoadAllForUser",
0x14: "filterForProfanity", 0x15: "fetchQosConfig",
0x16: "setClientMetrics", 0x17: "setConnectionState",
0x19: "getUserOptions", 0x1A: "setUserOptions", 0x1B: "suspendUserPing",
0x1C: "setClientState",
}
COMP_NAMES = {
COMP_AUTH: "Authentication", COMP_GAMEMANAGER: "GameManager",
COMP_REDIRECTOR: "Redirector", COMP_STATS: "Stats", COMP_UTIL: "Util",
COMP_MESSAGING: "Messaging", COMP_ASSOCLISTS: "AssociationLists",
COMP_GAMEREPORTING: "GameReporting", COMP_USERSESSIONS: "UserSessions",
}
def rpc_name(component: int, command: int) -> str:
comp = COMP_NAMES.get(component, "Component:0x%04x" % component)
if component == COMP_UTIL:
cmd = UTIL_CMDS.get(command, "cmd:0x%04x" % command)
else:
cmd = "cmd:0x%04x" % command
return "%s::%s" % (comp, cmd)
def fire2(component: int, command: int, msg_num: int, msg_type: int,
payload: bytes = b"", metadata: bytes = b"",
user_index: int = 0, options: int = 0) -> bytes:
h = bytearray(16)
struct.pack_into(">I", h, 0, len(payload))
struct.pack_into(">H", h, 4, len(metadata))
struct.pack_into(">H", h, 6, component & 0xFFFF)
struct.pack_into(">H", h, 8, command & 0xFFFF)
h[10] = (msg_num >> 16) & 0xFF
h[11] = (msg_num >> 8) & 0xFF
h[12] = msg_num & 0xFF
h[13] = ((msg_type & 0x07) << 5) | (user_index & 0x1F)
h[14] = options & 0xFF
h[15] = 0
return bytes(h) + metadata + payload
def parse_fire2_header(buf: bytes) -> dict:
return dict(
payload_len=struct.unpack_from(">I", buf, 0)[0],
metadata_len=struct.unpack_from(">H", buf, 4)[0],
component=struct.unpack_from(">H", buf, 6)[0],
command=struct.unpack_from(">H", buf, 8)[0],
msg_num=(buf[10] << 16) | (buf[11] << 8) | buf[12],
msg_type=(buf[13] >> 5) & 0x07,
user_index=buf[13] & 0x1F,
options=buf[14],
reserved=buf[15],
)
def reply_to(hdr: dict, payload: bytes = b"", msg_type: int = REPLY) -> bytes:
"""A Blaze reply echoes component/command/msgNum/userIndex verbatim and
only overwrites the msgType bits."""
return fire2(hdr["component"], hdr["command"], hdr["msg_num"], msg_type,
payload, user_index=hdr["user_index"])
# ------------------------------------------------------- PreAuthResponse
#
# Reconciled schema: reflection descriptor VA 0x144875600 (14 members) INTERSECT
# the independent clean-room emulators. Members are emitted in ascending
# packed-tag order; heat2.encode_tdf enforces that automatically.
# EA numeric title id. NOT reverse engineered -- onPreAuthResponse only
# memcpy's ASRC/ESRC/RSRC, so any value is accepted here; Authentication
# (component 1) may care later.
TITLE_ID = "309111"
# Nucleus client id. Plausible convention, not RE'd.
CLIENT_ID = "FIFA17-PC-SERVER-BLAZE"
# Persona namespace. Client caps this field at 32 bytes.
PERSONA_NAMESPACE = "cem_ea_id"
PLATFORM = "pc"
SERVER_VERSION = "Blaze 15.1.1.3.0 (OpenFUT)\n" # EA's real value ends in \n
# Component ids recovered from each component's own notification dispatcher in
# FIFA17.exe. This is the client's view of "which components exist server
# side"; later components look themselves up in this list.
COMPONENT_IDS = [
COMP_AUTH, # 1 Authentication
COMP_GAMEMANAGER, # 4 GameManager
COMP_REDIRECTOR, # 5 Redirector
COMP_STATS, # 7 Stats
COMP_UTIL, # 9 Util
COMP_MESSAGING, # 15 Messaging
COMP_ASSOCLISTS, # 25 AssociationLists
COMP_GAMEREPORTING, # 28 GameReporting
COMP_USERSESSIONS, # 30722 UserSessions
]
# The request carried FCCR{CFID="BlazeSDK"}, i.e. an embedded fetchClientConfig
# for the "BlazeSDK" section -- so CONF.CONF is that section. These five keys
# are the ones ConnectionManager::onPreAuthResponse actually reads (verified by
# disassembly); every one has a fallback, so nothing here is strictly required.
# Time values are MICROSECONDS: the client divides by 1000 to get ms.
BLAZESDK_CONFIG = [
("connIdleTimeout", "90000000"), # 90 s
("defaultRequestTimeout", "30000000"), # 30 s
("enableQosBandwidthTest", "false"), # exact string "false" clears bit 1
("enableQosFirewallTest", "false"), # exact string "false" clears bit 0
("pingPeriod", "20000000"), # 20 s (default would be 15000 ms)
]
# TODO(auth gate): the BlazeSDK section also carries the Nucleus endpoints
# nucleusConnect / nucleusConnectTrusted / nucleusPortal / nucleusProxy.
# Pointing those at a local HTTPS shim is our lever for offline login.
# Not emitted yet -- unread at preAuth, and wrong values may send the client
# at a real EA host during Authentication::login.
def qos_config() -> "OrderedDict":
"""Blaze::QosConfigInfo -- 4 members per reflection (there is NO SVID in
FIFA17's descriptor, unlike Mirror's Edge Catalyst)."""
return OrderedDict([
("BWPS", (STRUCT, OrderedDict([ # Blaze::QosPingSiteInfo
("PSA", (STRING, "127.0.0.1")), # address
("PSP", (INT, 17502)), # port
]))),
("LNP", (INT, 10)), # numLatencyProbes
("LTPS", (MAP, (STRING, STRUCT, []))), # pingSiteInfoByAliasMap: EMPTY
("TIME", (INT, 5000000)), # timeout, microseconds
])
def preauth_response_fields(service_name: str = "fifa-2017-pc") -> "OrderedDict":
return OrderedDict([
("ASRC", (STRING, TITLE_ID)), # authenticationSource
("CIDS", (LIST, (INT, COMPONENT_IDS))), # componentIds
("CLID", (STRING, CLIENT_ID)), # clientId
("CONF", (STRUCT, OrderedDict([ # Util::FetchConfigResponse
("CONF", (MAP, (STRING, STRING, list(BLAZESDK_CONFIG)))),
]))),
("ESRC", (STRING, TITLE_ID)), # entitlementSource
("INST", (STRING, service_name)), # serviceName -- echo CDAT.SVCN
("MAID", (INT, 0)), # machineId
("MINR", (INT, 0)), # underageSupported = false
("NASP", (STRING, PERSONA_NAMESPACE)), # personaNamespace
("PILD", (STRING, "")), # legalDocGameIdentifier
("PLAT", (STRING, PLATFORM)), # platform
("QOSS", (STRUCT, qos_config())), # qosSettings
("RSRC", (STRING, TITLE_ID)), # registrationSource
("SVER", (STRING, SERVER_VERSION)), # serverVersion
])
def ping_response_fields() -> "OrderedDict":
"""Blaze 15.1.1.1.0+ reads STIM, 15.1.1.0.x reads TIME. FIFA17 reports
BSDK 15.1.1.3.0, so STIM is the live one -- but unknown tags are ignored,
so emit both and stay version-proof. (Tag order STIM < TIME is handled by
heat2's ascending-tag sort.)"""
now = int(time.time())
return OrderedDict([("STIM", (INT, now)), ("TIME", (INT, now))])
def extract_service_name(fields) -> str:
"""PreAuthRequest.CDAT.SVCN -- echo it back as INST."""
try:
cdat = fields.get("CDAT")
if cdat and cdat[0] == STRUCT:
svcn = cdat[1].get("SVCN")
if svcn and svcn[0] == STRING and svcn[1]:
return svcn[1]
except Exception:
pass
return "fifa-2017-pc"
# ------------------------------------------------------------------ dispatch
def dispatch(hdr: dict, fields, raw_payload: bytes):
"""-> bytes to send back, or None to stay silent."""
comp, cmd, mtype = hdr["component"], hdr["command"], hdr["msg_type"]
# Transport-level PING frame (msgType 4) -- answer with PING_REPLY (5).
if mtype == PING:
log(" -> transport PING, answering PING_REPLY (empty)")
return reply_to(hdr, b"", msg_type=PING_REPLY)
if mtype not in (MESSAGE, PING):
log(" -> msgType %s is not a request; not answering"
% MSGTYPE_NAME.get(mtype, mtype))
return None
if comp == COMP_UTIL and cmd == CMD_PREAUTH:
svcn = extract_service_name(fields) if fields is not None else "fifa-2017-pc"
resp = preauth_response_fields(service_name=svcn)
payload = encode_tdf(resp)
log(" -> PreAuthResponse (INST=%r, %d payload bytes):\n%s"
% (svcn, len(payload), heat2.dump(resp)))
return reply_to(hdr, payload)
if comp == COMP_UTIL and cmd == CMD_PING:
resp = ping_response_fields()
log(" -> PingResponse %s" % dict((k, v[1]) for k, v in resp.items()))
return reply_to(hdr, encode_tdf(resp))
# ---------------------------------------------------------------- TODO
# Expected next RPCs on the FIFA17 login path (in order):
#
# 1. Util::fetchClientConfig (9/1) with FCCR/CFID="IdentityParams"
# -> FetchConfigResponse{CONF: map<str,str>} carrying `display` and
# `redirect_uri`; this drives the Nucleus web login overlay.
# 2. Authentication::login (1/0x0A) with AUTH=<nucleus auth code>
# -> plus server NOTIFICATION 0x7802/8 UserAuthenticated.
# 3. Util::postAuth (9/8)
# -> PostAuthResponse{TELE, TICK, UROP}; plus notifications
# 0x7802/5 and 0x7802/1|2 (UserExtendedData).
# 4. Util::setClientState (9/0x1C), Authentication::getAuthToken (1/0x24),
# AssociationLists::getLists (25/6), UserSessions::updateNetworkInfo
# (0x7802/0x14).
#
# Notifications are msgType=2 with msgNum=0 and are pushed unsolicited.
# Error replies are msgType=3 but the ERROR-CODE placement is UNRESOLVED
# (three clean-room sources disagree: header[14:16] vs metadata ERRC vs
# payload CNTX/ERRC) -- do not emit one until it is verified.
# ----------------------------------------------------------------------
if REPLY_EMPTY_TO_UNKNOWN:
log(" -> UNIMPLEMENTED %s; sending EMPTY REPLY so the client does not "
"hang (all fields fall back to client-side defaults)"
% rpc_name(comp, cmd))
return reply_to(hdr, b"")
log(" -> UNIMPLEMENTED %s; staying silent" % rpc_name(comp, cmd))
return None
# ------------------------------------------------------------- blaze server
def recv_exactly(sock: socket.socket, n: int, buf: bytearray) -> bool:
"""Fill `buf` to at least n bytes. False on clean EOF / short close."""
while len(buf) < n:
try:
chunk = sock.recv(65536)
except socket.timeout:
return False
if not chunk:
return False
buf += chunk
return True
_frame_counter = [0]
def blaze_handle(raw: socket.socket, addr) -> None:
log("*** BLAZE CONNECT from %s ***" % (addr,))
buf = bytearray()
raw.settimeout(300)
try:
while True:
if not recv_exactly(raw, FIRE2_HDR, buf):
break
hdr = parse_fire2_header(bytes(buf[:FIRE2_HDR]))
total = FIRE2_HDR + hdr["metadata_len"] + hdr["payload_len"]
if hdr["payload_len"] > 4 * 1024 * 1024:
log("BLAZE %s: absurd payload_len %d, dropping connection\n%s"
% (addr, hdr["payload_len"], hexdump(bytes(buf[:64]))))
break
if not recv_exactly(raw, total, buf):
log("BLAZE %s: EOF mid-frame (want %d, have %d)"
% (addr, total, len(buf)))
break
frame = bytes(buf[:total])
del buf[:total]
metadata = frame[FIRE2_HDR:FIRE2_HDR + hdr["metadata_len"]]
payload = frame[FIRE2_HDR + hdr["metadata_len"]:]
_frame_counter[0] += 1
n = _frame_counter[0]
log("RX #%d %s msgType=%s msgNum=%d userIdx=%d opts=0x%02x "
"meta=%dB payload=%dB"
% (n, rpc_name(hdr["component"], hdr["command"]),
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:
try:
fn = "/tmp/blaze_rx_%04x_%04x_%d.bin" % (
hdr["component"], hdr["command"], n)
with open(fn, "wb") as fh:
fh.write(frame)
log("RX #%d saved -> %s" % (n, fn))
except Exception as e:
log("RX #%d save failed: %s" % (n, e))
fields = None
if payload:
try:
fields = decode_tdf(payload)
log("RX #%d TDF:\n%s" % (n, heat2.dump(fields)))
except Exception as e:
log("RX #%d TDF DECODE FAILED: %s" % (n, e))
else:
log("RX #%d TDF: (empty payload)" % n)
try:
out = dispatch(hdr, fields, payload)
except Exception as e:
log("RX #%d DISPATCH ERROR: %r" % (n, e))
out = None
if out:
raw.sendall(out)
ohdr = parse_fire2_header(out)
log("TX #%d %s msgType=%s msgNum=%d %dB total (%d payload)"
% (n, rpc_name(ohdr["component"], ohdr["command"]),
MSGTYPE_NAME.get(ohdr["msg_type"], ohdr["msg_type"]),
ohdr["msg_num"], len(out), ohdr["payload_len"]))
log("TX #%d HEX:\n%s" % (n, hexdump(out, limit=1024)))
except ConnectionResetError:
log("BLAZE %s: connection reset by client" % (addr,))
except Exception as e:
log("BLAZE %s ERR: %r" % (addr, e))
finally:
try:
raw.close()
except Exception:
pass
log("BLAZE %s: closed" % (addr,))
# --------------------------------------------------------- redirector (TLS)
def build_redirect_response() -> bytes:
# Confirmed schema (clean-room, MEC Catalyst): ServerInstanceInfo.address is
# a ServerAddress union -> Heat2 XML union = <address member="N"><valu>...
# member="0" = ipAddress variant {hostname, ip(uint32 decimal), port(uint16)}
body = (
'<?xml version="1.0" encoding="UTF-8"?>\n'
'<serverinstanceinfo>\n'
'\t<address member="0">\n'
'\t\t<valu>\n'
f'\t\t\t<hostname>{BLAZE_IP_STR}</hostname>\n'
f'\t\t\t<ip>{BLAZE_IP_U32}</ip>\n'
f'\t\t\t<port>{BLAZE_PORT}</port>\n'
'\t\t</valu>\n'
'\t</address>\n'
'\t<secure>0</secure>\n'
'\t<trialservicename></trialservicename>\n'
'\t<defaultdnsaddress>0</defaultdnsaddress>\n'
'</serverinstanceinfo>\n'
)
b = body.encode()
hdr = ("HTTP/1.1 200 OK\r\nContent-Type: application/xml\r\n"
f"Content-Length: {len(b)}\r\nConnection: close\r\n\r\n").encode()
return hdr + b
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
ctx.load_cert_chain(CERT, KEY)
ctx.minimum_version = ssl.TLSVersion.TLSv1
ctx.set_ciphers("ALL:@SECLEVEL=0")
def redir_handle(raw: socket.socket, addr) -> None:
try:
tls = ctx.wrap_socket(raw, server_side=True)
except ssl.SSLError as e:
log("REDIR REJECTED %s: %s" % (addr, e))
raw.close()
return
log("REDIR TLS-OK %s cipher=%s" % (addr, tls.cipher()[0]))
try:
tls.settimeout(8)
req = b""
while b"\r\n\r\n" not in req:
c = tls.recv(4096)
if not c:
break
req += c
if b"content-length:" in req.lower():
head, _, rest = req.partition(b"\r\n\r\n")
cl = int([l.split(b":")[1] for l in head.split(b"\r\n")
if l.lower().startswith(b"content-length")][0])
while len(rest) < cl:
c = tls.recv(4096)
if not c:
break
rest += c
req = head + b"\r\n\r\n" + rest
line0 = req.split(b"\r\n", 1)[0].decode(errors="replace")
log("REDIR REQ %s: %s" % (addr, line0))
resp = build_redirect_response()
tls.sendall(resp)
log("REDIR SENT %s %dB serverinstanceinfo -> %s:%d"
% (addr, len(resp), BLAZE_IP_STR, BLAZE_PORT))
time.sleep(0.3)
tls.close()
except Exception as e:
log("REDIR ERR %s: %s" % (addr, e))
# ------------------------------------------------------------------ serve
def serve(port: int, handler, name: str) -> None:
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind((HOST, port))
s.listen(16)
log("%s listening on %s:%d" % (name, HOST, port))
while True:
c, a = s.accept()
threading.Thread(target=handler, args=(c, a), daemon=True).start()
def _selftest() -> None:
"""Sanity: build the preAuth reply and round-trip it through the decoder."""
fields = preauth_response_fields()
payload = encode_tdf(fields)
frame = fire2(COMP_UTIL, CMD_PREAUTH, 0, REPLY, payload)
h = parse_fire2_header(frame)
assert h["component"] == COMP_UTIL and h["command"] == CMD_PREAUTH
assert h["msg_type"] == REPLY and h["payload_len"] == len(payload)
assert frame[13] == 0x20, frame[13]
back = decode_tdf(frame[16:])
assert list(back.keys()) == ["ASRC", "CIDS", "CLID", "CONF", "ESRC", "INST",
"MAID", "MINR", "NASP", "PILD", "PLAT", "QOSS",
"RSRC", "SVER"], list(back.keys())
assert encode_tdf(back) == payload
print("selftest OK: preAuth reply = %d bytes (%d payload)"
% (len(frame), len(payload)))
print("header:", frame[:16].hex(" "))
print(heat2.dump(fields))
if __name__ == "__main__":
if "--selftest" in sys.argv:
_selftest()
raise SystemExit(0)
log("=== RESPONDER v2 START (redir %d / blaze %d) ===" % (REDIR_PORT, BLAZE_PORT))
threading.Thread(target=serve, args=(BLAZE_PORT, blaze_handle, "BLAZE"),
daemon=True).start()
serve(REDIR_PORT, redir_handle, "REDIR")