0138a39f3e
Implement the LSX (EA App bootstrap, TCP :3216→:3217) server in the bridge
and clear the loading-screen crash gate. FIFA 23 now completes the full LSX
handshake + session against OpenFUT and advances to the online/Blaze phase
("<persona> is connecting to the EA Servers…") — verified live, not a cached
session (the displayed persona "fun" comes from our GetProfile).
Root cause of the invariant loading-screen crash: get_config() omitted the
PROGRESSIVE_INSTALLATION and PROGRESSIVE_INSTALLATION_EVENT services (Name="PI").
FIFA builds its service registry from GetConfig and resolves that handle during
online-init; the missing service left a null handle it dereferenced at
FIFA23.exe+0x133dfc5 (mov rax,[r13+0x18], r13=NULL) ~2s after LSX — identical
across every prior run. Found via FIFA's own CrashReport_*.json (stable RIP) +
diffing anadius64.dll's service-registration table against ours.
Also in this change:
- process_frame(): handle FIFA's pipelined (batched) LSX messages per read,
eliminating the ~15s handshake hang.
- Real anadius values (RE'd from anadius64.dll + anadius.cfg): PersonaId/UserId/
persona, locale list, GetGameInfo fixed `GameInfo="<value>"` attribute,
GetAllGameInfo 14-attr schema, GetSetting `Setting=` attribute.
- IsProgressiveInstallationAvailable served from sender="PI".
- docs/connection-gate-findings.md: full RE trail; relocate stale openfut-hook
copy under docs/ (canonical hook lives in openfut-launcher).
Next gate: Blaze/online (M2), ridden over ProtoSSL in-process.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
82 lines
3.3 KiB
Rust
82 lines
3.3 KiB
Rust
use std::{path::Path, sync::Arc};
|
|
use tokio_rustls::TlsAcceptor;
|
|
|
|
/// Load a previously persisted cert/key pair, or generate and save a new one.
|
|
///
|
|
/// Reusing the same cert across restarts means clients only need to trust it once.
|
|
pub fn load_or_generate_cert(cert_path: &Path, key_path: &Path) -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
|
|
if cert_path.exists() && key_path.exists() {
|
|
let cert_pem = std::fs::read(cert_path)?;
|
|
let key_pem = std::fs::read(key_path)?;
|
|
tracing::info!("Loaded existing TLS cert from {:?}", cert_path);
|
|
return Ok((cert_pem, key_pem));
|
|
}
|
|
|
|
let (cert_pem, key_pem) = generate_self_signed_cert()?;
|
|
if let Some(parent) = cert_path.parent() {
|
|
std::fs::create_dir_all(parent)?;
|
|
}
|
|
std::fs::write(cert_path, &cert_pem)?;
|
|
std::fs::write(key_path, &key_pem)?;
|
|
tracing::info!("Generated new TLS cert, saved to {:?}", cert_path);
|
|
Ok((cert_pem, key_pem))
|
|
}
|
|
|
|
/// Generate a self-signed certificate covering EA FUT hostnames.
|
|
/// CN is set to `fut.ea.com` so ProtoSSL's CN hostname check passes.
|
|
/// SANs include wildcard entries for all `*.ea.com` and `*.easports.com` subdomains
|
|
/// so connections to any EA subdomain are covered.
|
|
pub fn generate_self_signed_cert() -> anyhow::Result<(Vec<u8>, Vec<u8>)> {
|
|
use rcgen::{Certificate, CertificateParams, DistinguishedName, DnType, SanType};
|
|
use std::net::{IpAddr, Ipv4Addr};
|
|
|
|
let mut params = CertificateParams::new(vec![
|
|
"fut.ea.com".to_string(),
|
|
"utas.mob.v4.fut.ea.com".to_string(),
|
|
"accounts.ea.com".to_string(),
|
|
"pin.data.ea.com".to_string(),
|
|
"gateway.ea.com".to_string(),
|
|
"localhost".to_string(),
|
|
]);
|
|
|
|
// Wildcard SANs cover every EA subdomain the game might contact
|
|
params.subject_alt_names.push(SanType::DnsName("*.ea.com".to_string()));
|
|
params.subject_alt_names.push(SanType::DnsName("*.easports.com".to_string()));
|
|
params.subject_alt_names.push(SanType::DnsName("*.ugc.footapi.com".to_string()));
|
|
params.subject_alt_names.push(SanType::IpAddress(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1))));
|
|
|
|
// CN must match the primary hostname so ProtoSSL's CN check passes
|
|
let mut dn = DistinguishedName::new();
|
|
dn.push(DnType::CommonName, "fut.ea.com");
|
|
params.distinguished_name = dn;
|
|
|
|
let cert = Certificate::from_params(params)?;
|
|
let cert_pem = cert.serialize_pem()?.into_bytes();
|
|
let key_pem = cert.serialize_private_key_pem().into_bytes();
|
|
Ok((cert_pem, key_pem))
|
|
}
|
|
|
|
/// Build a TlsAcceptor from PEM-encoded certificate and private key bytes.
|
|
pub fn make_tls_acceptor(cert_pem: &[u8], key_pem: &[u8]) -> anyhow::Result<TlsAcceptor> {
|
|
use rustls::{Certificate, PrivateKey, ServerConfig};
|
|
|
|
let certs: Vec<Certificate> = rustls_pemfile::certs(&mut std::io::Cursor::new(cert_pem))?
|
|
.into_iter()
|
|
.map(Certificate)
|
|
.collect();
|
|
|
|
let mut keys = rustls_pemfile::pkcs8_private_keys(&mut std::io::Cursor::new(key_pem))?;
|
|
if keys.is_empty() {
|
|
anyhow::bail!("no PKCS8 private keys found in key PEM");
|
|
}
|
|
|
|
let config = Arc::new(
|
|
ServerConfig::builder()
|
|
.with_safe_defaults()
|
|
.with_no_client_auth()
|
|
.with_single_cert(certs, PrivateKey(keys.remove(0)))?,
|
|
);
|
|
|
|
Ok(TlsAcceptor::from(config))
|
|
}
|