d619c992c1
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.
181 lines
6.5 KiB
Rust
181 lines
6.5 KiB
Rust
use std::{
|
|
path::{Path, PathBuf},
|
|
process::Command,
|
|
};
|
|
|
|
// ── Cert installation ─────────────────────────────────────────────────────────
|
|
|
|
/// Find the bridge cert in the given captures dir.
|
|
pub fn find_bridge_cert(captures_dir: &str) -> Option<PathBuf> {
|
|
let p = Path::new(captures_dir).join("bridge_cert.pem");
|
|
p.exists().then_some(p)
|
|
}
|
|
|
|
/// Install the bridge cert into the Wine/Proton prefix cert store so the game
|
|
/// trusts TLS connections to the bridge. Falls back to the system CA store.
|
|
pub fn install_cert(cert_src: &Path) -> anyhow::Result<()> {
|
|
// Try certutil inside the Proton prefix first (Wine cert store)
|
|
let wine_result = try_wine_certutil(cert_src);
|
|
if wine_result.is_ok() {
|
|
return wine_result;
|
|
}
|
|
|
|
// Fallback: Linux system CA store via pkexec/sudo
|
|
let dest = Path::new("/usr/local/share/ca-certificates/openfut-bridge.crt");
|
|
let script = format!(
|
|
"cp '{}' '{}' && update-ca-certificates",
|
|
cert_src.display(),
|
|
dest.display()
|
|
);
|
|
run_elevated(&script)
|
|
}
|
|
|
|
fn try_wine_certutil(cert_src: &Path) -> anyhow::Result<()> {
|
|
// certutil is available inside a Wine prefix via winetricks or natively
|
|
let status = Command::new("wine")
|
|
.args([
|
|
"certutil",
|
|
"-addstore",
|
|
"-user",
|
|
"Root",
|
|
&cert_src.to_string_lossy(),
|
|
])
|
|
.status();
|
|
|
|
match status {
|
|
Ok(s) if s.success() => Ok(()),
|
|
Ok(s) => anyhow::bail!("wine certutil exited {s}"),
|
|
Err(e) => anyhow::bail!("wine certutil not available: {e}"),
|
|
}
|
|
}
|
|
|
|
pub(crate) fn run_elevated(script: &str) -> anyhow::Result<()> {
|
|
let status = Command::new("pkexec").args(["sh", "-c", script]).status();
|
|
|
|
match status {
|
|
Ok(s) if s.success() => Ok(()),
|
|
_ => {
|
|
let s = Command::new("sudo").args(["sh", "-c", script]).status()?;
|
|
if s.success() {
|
|
Ok(())
|
|
} else {
|
|
anyhow::bail!("elevated command failed")
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// ── DLL hook deployment ───────────────────────────────────────────────────────
|
|
|
|
/// Deploy openfut_hook.dll into the FIFA 23 game directory and write
|
|
/// openfut.cfg with the structured server configuration the hook reads.
|
|
/// `cfg_contents` must be the full `openfut.cfg` body (see
|
|
/// `LauncherConfig::hook_cfg_contents`) — this function does not invent any
|
|
/// address itself, so a missing server can never silently become loopback.
|
|
/// Uses `version.dll` as the hijack name — FIFA 23 loads it but defers to
|
|
/// the system copy, so Proton picks up our local one first.
|
|
pub fn deploy_hook_dll(dll_src: &Path, game_dir: &Path, cfg_contents: &str) -> anyhow::Result<()> {
|
|
if !dll_src.exists() {
|
|
anyhow::bail!(
|
|
"Hook DLL not found at {}. Build it first with:\n\
|
|
cargo build --release --target x86_64-pc-windows-gnu\n\
|
|
(inside openfut-hook/)",
|
|
dll_src.display()
|
|
);
|
|
}
|
|
std::fs::create_dir_all(game_dir)?;
|
|
std::fs::copy(dll_src, game_dir.join("version.dll"))?;
|
|
std::fs::write(game_dir.join("openfut.cfg"), cfg_contents)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Update only openfut.cfg without redeploying the DLL. `cfg_contents` is the
|
|
/// full structured `openfut.cfg` body.
|
|
pub fn update_hook_config(game_dir: &Path, cfg_contents: &str) -> anyhow::Result<()> {
|
|
let cfg = game_dir.join("openfut.cfg");
|
|
if !cfg.exists() {
|
|
anyhow::bail!("Hook DLL not deployed yet — deploy first.");
|
|
}
|
|
std::fs::write(cfg, cfg_contents)?;
|
|
Ok(())
|
|
}
|
|
|
|
/// Remove the deployed hook DLL from the FIFA game directory.
|
|
pub fn remove_hook_dll(game_dir: &Path) -> anyhow::Result<()> {
|
|
let dest = game_dir.join("version.dll");
|
|
if dest.exists() {
|
|
std::fs::remove_file(&dest)?;
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
/// Check whether our hook DLL is deployed in the game directory.
|
|
pub fn hook_dll_deployed(game_dir: &Path) -> bool {
|
|
game_dir.join("version.dll").exists()
|
|
}
|
|
|
|
/// The Steam launch options the user needs to paste in to enable the override.
|
|
/// Proton loads local DLLs named in WINEDLLOVERRIDES ahead of system ones.
|
|
pub const STEAM_LAUNCH_OPTIONS: &str = "WINEDLLOVERRIDES=\"version=n,b\" %command%";
|
|
|
|
// ── Game launch ───────────────────────────────────────────────────────────────
|
|
|
|
/// Launch the game via the user-provided shell command. Runs `sh -c <command>`
|
|
/// (optionally from `workdir`), streaming stdout+stderr into `log_buf` on a
|
|
/// background thread. The launcher does not assume Steam vs umu-run vs a custom
|
|
/// script — whatever the user configured is what runs.
|
|
pub fn launch_game(
|
|
command: &str,
|
|
workdir: &str,
|
|
log_buf: std::sync::Arc<std::sync::Mutex<crate::logs::LogBuffer>>,
|
|
) -> anyhow::Result<()> {
|
|
use std::io::{BufRead, BufReader};
|
|
use std::process::{Command, Stdio};
|
|
|
|
if command.trim().is_empty() {
|
|
anyhow::bail!("No game launch command configured (set it in the Config tab).");
|
|
}
|
|
|
|
let mut cmd = Command::new("sh");
|
|
cmd.arg("-c").arg(command);
|
|
if !workdir.trim().is_empty() {
|
|
cmd.current_dir(workdir);
|
|
}
|
|
cmd.stdout(Stdio::piped()).stderr(Stdio::piped());
|
|
|
|
log_buf
|
|
.lock()
|
|
.unwrap()
|
|
.push(format!("[launcher] launching game: {command}"));
|
|
|
|
let mut child = cmd.spawn()?;
|
|
|
|
if let Some(out) = child.stdout.take() {
|
|
let buf = std::sync::Arc::clone(&log_buf);
|
|
std::thread::spawn(move || {
|
|
for line in BufReader::new(out).lines().map_while(Result::ok) {
|
|
buf.lock().unwrap().push(line);
|
|
}
|
|
});
|
|
}
|
|
if let Some(err) = child.stderr.take() {
|
|
let buf = std::sync::Arc::clone(&log_buf);
|
|
std::thread::spawn(move || {
|
|
for line in BufReader::new(err).lines().map_while(Result::ok) {
|
|
buf.lock().unwrap().push(line);
|
|
}
|
|
});
|
|
}
|
|
// Reap the child in the background so a finished game doesn't linger as a
|
|
// zombie; we don't block the UI on it.
|
|
std::thread::spawn(move || {
|
|
let _ = child.wait();
|
|
log_buf
|
|
.lock()
|
|
.unwrap()
|
|
.push("[launcher] game process exited.".to_string());
|
|
});
|
|
|
|
Ok(())
|
|
}
|