/// In-process LSX server (port 3216 / EA App Local Services Exchange). /// /// Runs in a background thread inside FIFA's process so Wine's wineserver /// routes FIFA's connect() directly here without needing any external process. /// /// Protocol: server speaks first (sends XML greeting with challenge key), /// then both sides do an AES-128-ECB challenge/response handshake, then /// all subsequent messages are AES-128-ECB encrypted. use windows_sys::Win32::Networking::WinSock::{ WSAStartup, WSACleanup, socket, bind, listen, accept, recv, send, closesocket, setsockopt, WSADATA, SOCKADDR, SOCKET, SOCKET_ERROR, INVALID_SOCKET, AF_INET, SOCK_STREAM, IPPROTO_TCP, SOMAXCONN, SO_REUSEADDR, SOL_SOCKET, }; const PORT: u16 = 3216; const GREETING_KEY: &str = "cacf897a20b6d612ad0c05e011df52bb"; fn server_loop() { unsafe { let mut wsa = core::mem::zeroed::(); if WSAStartup(0x0202, &mut wsa) != 0 { crate::write_log("ea_stub: WSAStartup failed\n"); return; } let srv = socket(AF_INET as i32, SOCK_STREAM, IPPROTO_TCP as i32); if srv == INVALID_SOCKET { crate::write_log("ea_stub: socket() failed\n"); WSACleanup(); return; } let yes: i32 = 1; setsockopt(srv, SOL_SOCKET as i32, SO_REUSEADDR, &yes as *const i32 as *const u8, 4); // sockaddr_in: sin_family(u16-LE) + sin_port(u16-BE) + sin_addr(u32) + padding let mut addr = [0u8; 16]; let family = AF_INET as u16; addr[0] = (family & 0xFF) as u8; addr[1] = (family >> 8) as u8; addr[2] = (PORT >> 8) as u8; addr[3] = (PORT & 0xFF) as u8; if bind(srv, addr.as_ptr() as *const SOCKADDR, addr.len() as i32) == SOCKET_ERROR { crate::write_log("ea_stub: bind() failed — port 3216 in use\n"); closesocket(srv); WSACleanup(); return; } listen(srv, SOMAXCONN as i32); crate::write_log("ea_stub: listening on port 3216\n"); loop { crate::write_log("ea_stub: calling accept...\n"); let client = accept(srv, core::ptr::null_mut(), core::ptr::null_mut()); if client == INVALID_SOCKET { use windows_sys::Win32::Networking::WinSock::WSAGetLastError; let e = WSAGetLastError(); crate::write_log(&format!("ea_stub: accept FAILED wsa_err={e}\n")); break; } crate::write_log("ea_stub: connection accepted\n"); handle_lsx(client); } closesocket(srv); WSACleanup(); } } unsafe fn lsx_send(sock: SOCKET, msg: &str) -> bool { // LSX messages are null-terminated let mut buf = msg.as_bytes().to_vec(); buf.push(0); let n = send(sock, buf.as_ptr(), buf.len() as i32, 0); if n == SOCKET_ERROR { use windows_sys::Win32::Networking::WinSock::WSAGetLastError; let e = WSAGetLastError(); crate::write_log(&format!("ea_stub: send FAILED wsa_err={e}\n")); false } else { crate::write_log(&format!("ea_stub: sent {n} bytes\n")); true } } unsafe fn lsx_recv(sock: SOCKET) -> Option { let mut buf = vec![0u8; 8192]; let n = recv(sock, buf.as_mut_ptr(), buf.len() as i32, 0); if n <= 0 { use windows_sys::Win32::Networking::WinSock::WSAGetLastError; let e = WSAGetLastError(); crate::write_log(&format!("ea_stub: recv returned {n} wsa_err={e}\n")); return None; } let text = String::from_utf8_lossy(&buf[..n as usize]) .trim_matches('\0') .to_string(); crate::write_log(&format!("ea_stub: recv {n} bytes: {}\n", &text[..text.len().min(300)])); Some(text) } unsafe fn handle_lsx(sock: SOCKET) { // ── 1. Send greeting (server speaks first) ──────────────────────────── let greeting = format!( "\r\n \r\n \r\n \r\n" ); crate::write_log("ea_stub: sending LSX greeting\n"); if !lsx_send(sock, &greeting) { closesocket(sock); return; } // ── 2. Receive FIFA's ChallengeResponse ─────────────────────────────── let challenge_xml = match lsx_recv(sock) { Some(s) => s, None => { closesocket(sock); return; } }; // Parse: split on '"' — EAappEmulater style // let parts: Vec<&str> = challenge_xml.split('"').collect(); let id = parts.get(3).copied().unwrap_or("1"); let key = parts.get(7).copied().unwrap_or(""); crate::write_log(&format!("ea_stub: challenge id={id} key={key}\n")); let our_response = crate::lsx::make_challenge_response(key); let seed = compute_seed(&our_response); crate::write_log(&format!("ea_stub: our_response={our_response} seed={seed}\n")); // ── 3. Send ChallengeAccepted ───────────────────────────────────────── let accepted = format!( "\r\n \r\n \r\n \r\n" ); crate::write_log("ea_stub: sending ChallengeAccepted\n"); if !lsx_send(sock, &accepted) { closesocket(sock); return; } // ── 4. Session loop ─────────────────────────────────────────────────── loop { let encrypted = match lsx_recv(sock) { Some(s) => s, None => break, }; if encrypted.trim().is_empty() { continue; } let request = crate::lsx::lsx_decrypt(&encrypted, seed); crate::write_log(&format!("ea_stub: request: {}\n", &request[..request.len().min(300)])); if request.trim().is_empty() { crate::write_log("ea_stub: empty decrypted request — skipping\n"); continue; } let response_xml = crate::lsx::dispatch(request.trim()); crate::write_log(&format!("ea_stub: response: {}\n", &response_xml[..response_xml.len().min(300)])); let encrypted_resp = crate::lsx::lsx_encrypt(&response_xml, seed); if !lsx_send(sock, &encrypted_resp) { break; } } closesocket(sock); crate::write_log("ea_stub: client disconnected\n"); } fn compute_seed(hex: &str) -> u16 { let b0 = u8::from_str_radix(&hex[..2.min(hex.len())], 16).unwrap_or(0); let b1 = u8::from_str_radix(&hex[2..4.min(hex.len())], 16).unwrap_or(0); ((b0 as u16) << 8) | (b1 as u16) } pub fn start() { std::thread::spawn(server_loop); }