feat(launcher): one-click client arming + modular preflight/services
Add a GUI "Arm client" button that reproduces client_arm.sh in a single pkexec batch: kernel.yama.ptrace_scope=0, DNAT of EA's hardcoded redirector IP to the OpenFUT server (+ MASQUERADE reply path), and /etc/hosts rewrites for every dead EA hostname (removing foreign shadow lines first, so glibc's first-match resolution can't land on a stale loopback entry). All steps are idempotent (delete-then-add) and injection-safe: config values are charset- validated and rejected on a surprising character, never shell-escaped. arm() returns the concrete change list, which the button logs line-by-line and echoes as an inline pass/fail status on the pre-launch tab (no tab jump, no reuse of the local-services toast). This necessarily lands the surrounding launcher modularization the arm feature is built on, extracted from the former monolithic app.rs/process.rs: - preflight: advisory pre-launch checks (ptrace, redirector DNAT, hostnames, backend reachability) that colour rows but never block Launch - local_services: launcher-owned LSX/autopatch child processes - game_launch, account_sync, health, netcheck helpers - openfut-common: dependency-free shared server-destination/port mapping, used by both the launcher and (separately) openfut_hook.dll openfut-hook RE changes are intentionally left uncommitted (separate concern). fmt + clippy -D warnings clean; 46 tests pass.
This commit is contained in:
@@ -0,0 +1,77 @@
|
||||
//! "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<bool> {
|
||||
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\""))
|
||||
}
|
||||
Reference in New Issue
Block a user