//! Per-connection Blaze session state. //! //! Note how little there is: a session key, the client's locale, the echoed //! service name, an auth code and a logged-in flag. That is the whole of it. //! //! This is the point of the adapter boundary. Blaze is an auth/session/config //! protocol — coins, packs, clubs, squads and the rest of the FUT domain never //! appear on this wire, so there is nothing here tempting the adapter into //! becoming a second backend. When UTAS is migrated that discipline will need //! actively defending; here it comes for free. /// State carried across RPCs on one Blaze connection. #[derive(Debug, Clone, PartialEq, Eq)] pub struct Session { /// Minted once per connection. Must appear byte-identically in /// `LoginResponse.SESS.KEY`, the `UserAuthenticated` push, and /// `PostAuthResponse.TELE.SESS`. pub session_key: String, /// Whatever `LoginRequest.AUTH` carried; echoed back by `getAuthToken`. pub auth_code: String, /// Packed four-char locale, seeded from config and overwritten by the /// client's own preAuth `LANG`/`LOC`. pub account_locale: i64, /// Echoed back as `PreAuthResponse.INST`. pub service_name: String, pub logged_in: bool, pub login_time: i64, } /// The oracle's default service name when preAuth carries no `CDAT.SVCN`. pub const DEFAULT_SERVICE_NAME: &str = "fifa-2017-pc"; impl Session { /// Start a session with an explicit key. /// /// The key is injected rather than generated internally so it can be made /// deterministic for differential tests — it appears verbatim in three /// different responses, so a self-generated one would make every login /// fixture unreproducible. pub fn new(session_key: impl Into, account_locale: i64) -> Session { Session { session_key: session_key.into(), auth_code: String::new(), account_locale, service_name: DEFAULT_SERVICE_NAME.into(), logged_in: false, login_time: 0, } } /// The token `getAuthToken` returns. /// /// Before login there is no auth code, so the oracle synthesises one from /// the session key. Reproduced exactly, including the 16-character slice. pub fn auth_token(&self) -> String { if !self.auth_code.is_empty() { return self.auth_code.clone(); } // Byte slicing is safe here in practice (session keys are ASCII), but // char_indices keeps it correct for any injected key. let cut = self .session_key .char_indices() .nth(16) .map(|(i, _)| i) .unwrap_or(self.session_key.len()); format!("OPENFUT-{}", &self.session_key[..cut]) } } /// Mint a Blaze-shaped session key: 16 hex, an underscore, then 44 alphanumerics. /// /// The client never validates the format — one public emulator ships the /// literal `"0"` — so this only has to be stable within a connection. Callers /// supply the randomness so this crate needs no RNG dependency and stays /// deterministic under test. pub fn format_session_key(high_bits: u64, tail: &str) -> String { format!("{high_bits:016x}_{tail}") } #[cfg(test)] mod tests { use super::*; #[test] fn synthesises_a_token_before_login() { let s = Session::new("0123456789abcdef_TAIL", 0); assert_eq!(s.auth_token(), "OPENFUT-0123456789abcdef"); } #[test] fn echoes_the_login_auth_code_afterwards() { let mut s = Session::new("0123456789abcdef_TAIL", 0); s.auth_code = "REAL-CODE".into(); assert_eq!(s.auth_token(), "REAL-CODE"); } #[test] fn short_session_keys_do_not_panic() { assert_eq!(Session::new("abc", 0).auth_token(), "OPENFUT-abc"); assert_eq!(Session::new("", 0).auth_token(), "OPENFUT-"); } #[test] fn session_key_has_the_blaze_shape() { let k = format_session_key(0x0123456789abcdef, &"x".repeat(44)); assert_eq!(k.len(), 16 + 1 + 44); assert!(k.starts_with("0123456789abcdef_")); } }