Files
OpenFUT/openfut-adapter-fifa17/src/blaze/session.rs
T
funman300 cf961603fe openfut-adapter-fifa17: FIFA 17 Blaze adapter, oracle-tested
The second migration step: the layer above the codec, deciding WHAT to say
rather than how to encode it. Sits on openfut-protocol-blaze and supplies
what that crate deliberately refuses to know.

  blaze/ids.rs           component/command/notification tables
  blaze/config.rs        injectable identity + endpoints, nothing hardcoded
  blaze/session.rs       per-connection state
  blaze/client_config.rs the fetchClientConfig tables
  blaze/responses.rs     16 Blaze::* response bodies
  blaze/dispatch.rs      (component, command) -> Vec<Frame>

Parity is tested, not asserted. fixtures/generate.py drives the real
blaze_responder_v3b.dispatch() and records 49 request->response(s)
transactions, replayed in order against a shared session per connection so
ordering-dependent behaviour is exercised: preAuth captures the locale later
ALOC fields echo, login sets the auth code getAuthToken returns. Comparison
is byte-for-byte including frame count and order.

98 tests green across both crates; clippy clean.

MUTATION TESTED, and it found a real defect in this commit's own design.
Swapping two post-login notifications and flipping one enum inside
AccountInfo both turned the suite red as intended. Hardcoding an address in
utas_base() did NOT -- the config templating substituted raw hosts directly,
making those helpers dead code that merely looked load-bearing. The table now
templates on URL-level tokens ({utas_base}, {nucleus_base},
{pow_content_url}) so they are the single place a URL shape is defined, and
the mutation is caught.

The client config table (227-243 rows per CFID) is generated from the oracle
rather than transcribed: it is reverse-engineered data, not logic, and 400
hand-copied string literals would add a typo class no reviewer can catch. The
generator substitutes real addresses back in and diffs against the oracle for
every section before writing, so the templating is verified rather than
assumed.

Reproduces one known defect deliberately: nucleusConnect is built from BIND,
not advertise, so the live split deployment tells a client on another machine
to reach Nucleus at http://0.0.0.0:42131. Confirmed against the running
container. Reproduced because it is what the only proven-working config does;
fixing it needs live validation and is a separate change. It also implies the
Nucleus stub is not reached in the current remote flow.

Blaze carries no FUT domain state -- no coins, packs, clubs or squads on this
wire -- so Session stays a session key, locale, service name, auth code and a
flag. That boundary will need defending when UTAS is migrated.

Not wired into anything. The crate answers frames; it opens no socket and
owns no runtime. The Python backend remains the live service and the oracle,
and is unmodified (contract suite still green).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-11 01:18:29 +00:00

111 lines
4.0 KiB
Rust

//! 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<String>, 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_"));
}
}