Files
OpenFUT/fifa17-recon/tools/blaze_responder_v2.py
T
funman300 5d5198f5d1 fifa17-recon: match rewards, POW online layer, account backend, quick sell
Second session. FUT core loop, the EASFC/POW online layer, a central account
backend, and a lot of corrections. Everything risky is behind an env flag with
the default set to whatever was live-proven.

WORKING END TO END (live-verified this session):
  * match loop -- POST/PUT/POST/DELETE ut/%s/match, rewards via FutDestroyMatch
    (0x180121b60). Play a match, get coins, W/D/L updates.
  * packs -- buy, cards land in the club, session survives (FUT_PACK_AUTOCLUB=1)
  * quick sell -- POST ut/delete/%s/item was UNMAPPED and paid NOTHING; six cards
    were destroyed for 0 coins. Now credits discardValue.
  * POW/EASFC online -- the "EA FC servers unreachable" banner is powdll's layer,
    a THIRD http api on :8094 nobody had served. Redirect needs no root: powdll
    FUN_18005a460 reads FIFA_POW_URL from the same client-config store as
    ROSTERUPDATE_URL. FUT_POW=1.
  * account backend -- fut_account.py replaces 7 hardcoded copies of the persona
    across 5 files; club/persona/online-profile editable via CLI.

CORRECTIONS TO ENDPOINT_MAP (all re-extracted from the deserializers):
  * FutStoreGetPackTypes: id/packType/isPremium/quantity/saleType/purchaseLimit/
    purchaseCount are NOT skipped no-ops -- all are parsed. extPrice inner objects
    take externalPriceId(0x11a), not amount/currency.
  * FutMoveCard 0x180128600 has NO skip handler (FUN_180135ff0 appears zero times,
    unique among FUT deserializers) and parses only itemData -> dreamSquads.
  * class -> deserializer resolution: the name literal is preceded by a 4-BYTE
    HEADER and the factory LEA points at the header, so look up name_addr - 4.
    Six attempts failed on this; now ghidra_env.class_deser(). Unlocked 11 SBC/
    Draft schemas.
  * live-only endpoints the request table never lists: ut/%s/squad/list,
    ut/%s/user/club, ut/%s/club/stats/*, ut/%s/clientdata/<key>. The template
    table is a floor, not a ceiling -- the log is the only ground truth.
  * 163 RS4 call names exist; we served 17. All now served.

FIXED: club/stats/* was answering with the entire 28-item club inventory on every
poll (it fell through to the generic /club route).

UNSOLVED: the pack reveal's "Send to Club" (PUT ut/%s/item) kills the FUT session
whatever we answer -- {} included -- while its sibling quick-sell endpoint accepts
a bare {}. Seven hypotheses eliminated by live test, documented in
REBUILD_RESEARCH.md S14c so none get re-walked. FUT_PACK_AUTOCLUB routes around it.
Also unfixed: store tiles render "unknown" (displayGroup is parsed RECURSIVELY by
the same element parser; sending it FROZE the store, so FUT_STORE_GROUPS=1 is
default off).

Tests: test_fut_contract.py 380 (live, read-only) + test_match_rewards.py 51 (pure).

Note: fut_store.py carries some pre-existing uncommitted changes from before this
session (pack catalogue ids, pending-pile behaviour) that could not be separated
from this session's additions in the same file.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01VUT92pz6RWKih9dSr8ZpxW
2026-08-04 09:42:59 -07: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")