//! OpenFUT clean-room LSX responder for FIFA 17 -- Rust port of //! `fifa17-recon/tools/lsx_responder_v2.py` (v2, EVENT-PUSHING). //! //! v1 (`lsx_responder.py`) was REQUEST-DRIVEN ONLY. It answered every verb the //! client asked for and never sent an unsolicited frame. That is exactly why the //! client never issued GetAuthCode and never sent Blaze Authentication::login. //! //! THE ORIGIN SDK HAS TWO INDEPENDENT FLAGS, FED BY TWO DIFFERENT MECHANISMS: //! //! 1. "internet is reachable" -> OriginMgr online byte [0x1448a3ac0], //! fed by the REQUEST verb `GetInternetConnectedState -> connected="1"` //! (v1 already beat this; live-confirmed == 1). //! 2. "a user is LOGGED IN" -> `OriginMgr.m_isLoggedIn` [OriginMgr+0x13], //! fed ONLY by a server-PUSHED ``. //! There is NO request verb that can set it. //! //! v1 fed (1) and never fed (2), so `m_isLoggedIn` was 0 for the whole session, //! FIFA never enqueued an auth-code request into FirstPartyAuthTokenRetriever //! (both request slots live-read as 0x0), DoTick @0x146f199c0 exited immediately, //! OriginRequestAuthCodeSync @0x1470db3c0 was never called, LoginRequest.AUTH //! could never be filled -> no Blaze login -> "Unable to retrieve account //! information." //! //! The binary evidence for the push (dispatcher @0x146f1e060 case 2, the `` //! matcher @0x147102880 and its silent sender strcmp, the service-name tables at //! 0x144341420 / sdk+0x3b0, the `strcmp(v,"false")` truthiness of `IsLoggedIn`) //! lives in the Python's module docstring; the specific facts that constrain code //! are repeated at the site that depends on them. Nothing here is derived from //! the 2021 EA/FIFA leak: every constant came from our own static+dynamic //! analysis of binaries we own plus traffic we captured ourselves. //! //! Transport: TCP `127.0.0.1:4216`, every message NUL-terminated (send strlen+1). pub mod crypto; pub mod events; pub mod identity; pub mod protocol; /// Loopback port the Steampunks Origin emulator stub binds; we must own it /// BEFORE FIFA 17 starts so the stub's own `bind()` fails. pub const LSX_PORT: u16 = 4216; /// Emu's own advertised challenge (any 32 hex chars work; the client echoes it /// back). pub const CHALLENGE_KEY: &str = "2b8ee7faea76e8a34f5f5d20e5328e32"; pub const BUILD: &str = "release"; pub const VERSION: &str = "10,4,13,6637"; /// Success-signal files the run's watch steps poll. Written only from a REAL /// GetAuthCode on a live connection. pub const AUTHCODE_FILE: &str = "/tmp/openfut_authcode.txt"; pub const CLIENTID_FILE: &str = "/tmp/openfut_lsx_clientid.txt"; /// How the modules read environment knobs. Indirected through a closure so the /// tests can pin an environment without mutating the process (env vars are /// process-global and `cargo test` runs threads in parallel). pub type Env<'a> = &'a dyn Fn(&str) -> Option; /// The real environment. Present-but-empty is Some(""), which matters: the /// Python's `os.environ.get(...)` returns "" for `FOO=`, and "" is not None, so /// an empty setting WINS over the built-in default. pub fn os_env(name: &str) -> Option { std::env::var_os(name).map(|v| v.to_string_lossy().into_owned()) } /// The Python's recurring `os.environ.get(NAME, "0") != "0"` rule: ANY value /// other than the literal "0" enables the knob (including the empty string). pub fn env_flag(env: Env<'_>, name: &str, default_on: bool) -> bool { match env(name) { Some(v) => v != "0", None => default_on, } } /// An env knob was set to something the Python's own `int()`/`float()` would /// reject. The Python dies at import in that case; we refuse to start for the /// same reason -- a silently different persona, or a silently absent heartbeat, /// is worse than a loud failure. #[derive(Debug, Clone, PartialEq, Eq)] pub struct ConfigError { pub var: &'static str, pub value: String, pub expected: &'static str, } impl std::fmt::Display for ConfigError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!( f, "{} must be {} (got {:?})", self.var, self.expected, self.value ) } } impl std::error::Error for ConfigError {} /// Every diagnostic line carries the `[lsx] ` prefix and is flushed per line: /// the launcher pipes our stdout+stderr into its log buffer and parses some of /// these lines, so the formats are a contract. #[macro_export] macro_rules! log { ($($arg:tt)*) => {{ use std::io::Write; let mut out = std::io::stdout().lock(); let _ = writeln!(out, "[lsx] {}", format_args!($($arg)*)); let _ = out.flush(); }}; }