edab23f04a
Emulates FIFA 17's full online + Ultimate Team stack against an offline,
clean-room backend (no EA servers). Proven end-to-end 2026-08-01:
Origin login -> Blaze login -> device-trust -> the FUT hub.
Package:
- tools/openfut-fut.sh one-command orchestrator (start/stop/status/restart)
- tools/root_arm.sh idempotent host arm (sysctls, DNAT, /etc/hosts easw)
- tools/{lsx_responder_v2,blaze_responder_v3b,roster_server,utas_server,autopatch}.py
the 5 servers (Origin LSX :4216, Blaze :42127/42130/42131, roster :8081,
FUT/UTAS :8099) + heat2.py (Fire2/Heat2 TDF codec)
- FUT-RUNBOOK.md runbook + gate-ladder troubleshooting
- docs/, tools/login_dump/*.md the reverse-engineering write-ups
All findings are clean-room, from binaries we own; nothing from any leak.
The wire protocol maps 1:1 to FIFA 23.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01PN5bmpDVQR1aXgefyWAt7o
80 lines
2.9 KiB
Python
80 lines
2.9 KiB
Python
#!/usr/bin/env python3
|
|
"""FUT roster-update HTTPS server for FIFA 17 (OpenFUT).
|
|
|
|
The FUT loading flow (checkFUTRostersFlow / state CheckFUTRosterUpdateXML) downloads
|
|
a roster-update XML from ROSTERUPDATE_URL (which blaze_responder_v3b.py now serves as
|
|
https://127.0.0.1:8081/fifa17/fut/rosterupdate.xml). On download SUCCESS the flow
|
|
raises `advance` -> CheckFUTSquadBinFile -> (no squad) -> EnterFUT -> CardsDLL loads.
|
|
On FAIL it raises `back` -> abort. So this must return something FIFA ACCEPTS.
|
|
|
|
We don't have (or need) EA's real roster: the base player DB is baked into CardsDLL;
|
|
the roster-update is an optional delta. Start by serving a minimal "no update" body,
|
|
LOG every request (path/headers) so we learn exactly what FIFA fetches, and iterate.
|
|
|
|
HTTPS because EA's value is https and the DirtySDK download mgr may reject http; FIFA's
|
|
ProtoSSL cert-verify is patched (autopatch), so our self-signed cert is accepted.
|
|
"""
|
|
import http.server, ssl, os, sys, datetime
|
|
|
|
HERE = os.path.dirname(os.path.abspath(__file__))
|
|
CERT = os.path.join(HERE, "redir_cert.pem")
|
|
KEY = os.path.join(HERE, "redir_key.pem")
|
|
LOG = "/tmp/roster_server.log"
|
|
ADDR = ("127.0.0.1", 8081)
|
|
|
|
# Minimal "no update available" roster body. Unknown-format -> iterate from the log.
|
|
ROSTER_XML = b'<?xml version="1.0" encoding="utf-8"?>\n<rosterupdate version="0"/>\n'
|
|
|
|
|
|
def log(m):
|
|
line = "[%s] %s" % (datetime.datetime.now().strftime("%H:%M:%S"), m)
|
|
print(line, flush=True)
|
|
with open(LOG, "a") as f:
|
|
f.write(line + "\n")
|
|
|
|
|
|
class H(http.server.BaseHTTPRequestHandler):
|
|
def _handle(self, method):
|
|
log("%s %s from %s" % (method, self.path, self.client_address))
|
|
for k, v in self.headers.items():
|
|
log(" %s: %s" % (k, v))
|
|
body = ROSTER_XML
|
|
self.send_response(200)
|
|
self.send_header("Content-Type", "application/xml")
|
|
self.send_header("Content-Length", str(len(body)))
|
|
self.send_header("Connection", "close")
|
|
self.end_headers()
|
|
if method == "GET":
|
|
self.wfile.write(body)
|
|
log(" -> 200 %dB (%r)" % (len(body), body[:60]))
|
|
|
|
def do_GET(self): self._handle("GET")
|
|
def do_HEAD(self): self._handle("HEAD")
|
|
def do_POST(self):
|
|
n = int(self.headers.get("Content-Length", 0) or 0)
|
|
if n:
|
|
log(" POST body: %r" % self.rfile.read(n)[:200])
|
|
self._handle("POST")
|
|
|
|
def log_message(self, *a): # silence default stderr logging
|
|
pass
|
|
|
|
|
|
def main():
|
|
open(LOG, "a").close()
|
|
ctx = ssl.SSLContext(ssl.PROTOCOL_TLS_SERVER)
|
|
ctx.load_cert_chain(CERT, KEY)
|
|
ctx.minimum_version = ssl.TLSVersion.TLSv1
|
|
try:
|
|
ctx.set_ciphers("ALL:@SECLEVEL=0")
|
|
except Exception:
|
|
pass
|
|
httpd = http.server.HTTPServer(ADDR, H)
|
|
httpd.socket = ctx.wrap_socket(httpd.socket, server_side=True)
|
|
log("=== roster_server https://%s:%d (FUT roster-update) ===" % ADDR)
|
|
httpd.serve_forever()
|
|
|
|
|
|
if __name__ == "__main__":
|
|
main()
|