//! "Test Connection" support: verify the configured OpenFUT server is actually //! reachable before the user launches FIFA. //! //! This resolves the configured host through the SAME shared path the hook uses //! ([`openfut_common::ServerConfig::resolve`]) and then does a bounded TCP //! connect to the OpenFUT destination port(s). It never falls back to loopback: //! if the server isn't configured/resolvable, it reports that plainly. use std::io::{Read, Write}; use std::net::{SocketAddr, TcpStream}; use std::time::Duration; use openfut_common::ServerConfig; const CONNECT_TIMEOUT: Duration = Duration::from_secs(3); /// Outcome of a connection test, suitable for showing in the UI. pub struct TestOutcome { pub ok: bool, pub message: String, } /// Resolve `cfg` and attempt to reach the OpenFUT server. Checks the HTTPS /// destination port (the one EA :443 traffic is redirected to) since that is the /// service the client relies on first. On success, also reports whether the core /// `/health` endpoint answered (best-effort; a plain-text probe, TLS not spoken). pub fn test_connection(cfg: &ServerConfig) -> TestOutcome { let resolved = match cfg.resolve() { Ok(r) => r, Err(e) => { return TestOutcome { ok: false, message: format!("Cannot resolve OpenFUT server: {e}"), }; } }; let addr = SocketAddr::from((resolved.redirect_ip, resolved.ports.https)); match TcpStream::connect_timeout(&addr, CONNECT_TIMEOUT) { Ok(mut stream) => { // Best-effort HTTP probe of /health. The bridge front door speaks // TLS, so a plaintext request may not get a clean 200 — a successful // TCP connect already proves reachability, so we don't fail on this. let health = probe_health(&mut stream); let detail = match health { Some(true) => " (core /health responded OK)".to_string(), _ => String::new(), }; TestOutcome { ok: true, message: format!( "Reachable: {}:{} is accepting connections{detail}.", resolved.redirect_ip, resolved.ports.https ), } } Err(e) => TestOutcome { ok: false, message: format!( "Could not reach {}:{} — {e}. Check the server is running and the \ address/port are correct.", resolved.redirect_ip, resolved.ports.https ), }, } } fn probe_health(stream: &mut TcpStream) -> Option { let _ = stream.set_read_timeout(Some(CONNECT_TIMEOUT)); let _ = stream.set_write_timeout(Some(CONNECT_TIMEOUT)); let req = "GET /health HTTP/1.0\r\nConnection: close\r\n\r\n"; stream.write_all(req.as_bytes()).ok()?; let mut buf = [0u8; 512]; let n = stream.read(&mut buf).ok()?; let text = String::from_utf8_lossy(&buf[..n]); Some(text.contains("200") || text.contains("\"status\"")) }