Files
OpenFUT/openfut-autopatch/src/logging.rs
T
funman300 750d6c2e18 feat(companions): port the launcher's two Python services to Rust
The launcher spawned `python3 lsx_responder_v2.py` and `python3 autopatch.py`. Both are
now Rust workspace crates, and the launcher spawns the binaries (gitlink 1cd4f18).

openfut-lsx (2244 lines, 57 tests) — EA Origin LSX emulator on loopback 4216.
Dependency-light on purpose: `aes` for the one security-shaped primitive, parking_lot
per the project lock rule. AES-128-ECB is the whole cipher requirement, so the
surrounding framing (PKCS7, lowercase hex, NUL-termination) stays explicit and separate
because it is protocol, not cryptography.

openfut-autopatch (43 tests) — ProtoSSL cert gates plus the CardsDLL store patches,
applied over /proc/<pid>/mem. Deliberately dependency-free: a tool that writes another
process's memory should be auditable end to end without a dependency tree. std has no
getuid and no local-time formatting, so it carries a small TZif reader rather than
pulling in chrono to reproduce Python's strftime('%H:%M:%S').

The Python remains in fifa17-recon/tools. It is NOT dead: the docker entrypoint,
client_arm.sh, the runbooks and test_autopatch_guard.py still use it. Only the
launcher's dependency on Python is gone, which is what was asked for; deleting the
recon toolchain's implementation would have broken unrelated workflows.

VERIFICATION — the ports are checked against the Python, not against themselves:

* Crypto parity across THREE implementations. The Rust tests assert the Rust's own
  constants, which proves consistency, not parity, and the Python cannot run here
  (pycryptodome absent) with the client host unreachable. So the LCG and key derivation
  were transcribed from the Python and run as plain arithmetic, and every AES value came
  from the openssl CLI. All agree: msvcr_rand(7)==61, _TAIL_CONST
  954f64f2e4e86e9eee82d20216684899, the 96-hex emu challenge shape, the derived session
  key 6a9da3e78615153cc2f10eec25ae6382, the framing rule at both boundaries (an aligned
  payload gains a whole block), and the port's pinned 4-block login-frame ciphertext.
* LSX end to end on the real port. 4216 here is a docker forward into the production
  netns, so the smoke test runs under `unshare -n` — the real binary on the port the
  client actually dials, with no port-override hack and no risk to production. A
  hand-written client read the unprompted <Challenge>, completed the handshake, and
  decrypted the GetProfileResponse (PersonaId 33068179, Persona CAGE) with a session key
  derived INDEPENDENTLY of the Rust, then observed the Login pushes across all three
  candidate senders.
* autopatch behaviourally. The startup banner, the --launcher-pid watchdog exiting with
  the exact Python message, dual stdout+logfile output, and a missing value rejected
  with Python's own "invalid --launcher-pid". The subagent additionally cross-checked
  every constant by executing the Python module and drove the binary against a synthetic
  client (correct comm, a CardsDLL mapping, gates mmapped at their absolute VAs),
  confirming all eleven patches byte-exact in table order.
* The `[store-guard] verified capability …` line is byte-identical to openfut-launcher's
  own parser fixture, so backend capability registration still works.

Workspace builds; openfut-lsx 57, openfut-autopatch 43, openfut-launcher 74 tests green.
2026-08-18 05:31:00 +00:00

104 lines
3.4 KiB
Rust

//! `[HH:MM:SS] <msg>` to stdout (flushed per line) and appended to the log file.
//!
//! The launcher pipes this process's stdout into its own log buffer and *parses*
//! some of these lines, so the per-line flush and the line shapes are a contract,
//! not cosmetics.
use std::fs::OpenOptions;
use std::io::{self, Write};
use std::path::PathBuf;
use crate::localtime;
use crate::procmem;
/// Environment override for the log file path.
pub const LOG_PATH_ENV: &str = "OPENFUT_AUTOPATCH_LOG";
pub struct Logger {
path: PathBuf,
}
impl Logger {
/// `$OPENFUT_AUTOPATCH_LOG`, defaulting to `/tmp/openfut-autopatch-<uid>.log`.
///
/// Resolved once at startup, exactly like the Python's module-level `LOG`, so
/// a later environment change cannot move the file mid-run.
pub fn from_env() -> io::Result<Self> {
let path = match std::env::var_os(LOG_PATH_ENV) {
Some(path) if !path.is_empty() => PathBuf::from(path),
_ => PathBuf::from(format!(
"/tmp/openfut-autopatch-{}.log",
procmem::current_uid()?
)),
};
Ok(Self { path })
}
pub fn path(&self) -> &std::path::Path {
&self.path
}
/// Emit one line. Timestamped in local time.
pub fn log(&self, msg: &str) {
let (h, m, s) = localtime::now_hms();
let line = format!("[{h:02}:{m:02}:{s:02}] {msg}");
let mut stdout = io::stdout().lock();
// Ignore a broken pipe: the launcher may have stopped reading, and dying
// here would leave FIFA's store patches unenforced.
let _ = writeln!(stdout, "{line}");
let _ = stdout.flush();
drop(stdout);
if let Err(e) = self.append(&line) {
// The Python lets a failing log write kill the process. Patching the
// running client matters more than the transcript, so report once to
// stderr (the launcher captures it too) and carry on.
let _ = writeln!(io::stderr(), "autopatch: cannot append to {}: {e}", self.path.display());
}
}
fn append(&self, line: &str) -> io::Result<()> {
let mut file = OpenOptions::new().create(true).append(true).open(&self.path)?;
file.write_all(line.as_bytes())?;
file.write_all(b"\n")
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn appends_a_timestamped_line_to_the_configured_path() {
let path = std::env::temp_dir().join(format!(
"openfut-autopatch-test-{}.log",
std::process::id()
));
let _ = std::fs::remove_file(&path);
let logger = Logger {
path: path.clone(),
};
logger.log("pid 4242: PATCHED cert gates");
logger.log("second line");
let body = std::fs::read_to_string(&path).unwrap();
let lines: Vec<&str> = body.lines().collect();
assert_eq!(lines.len(), 2);
assert_eq!(&lines[0][..1], "[");
assert_eq!(&lines[0][3..4], ":");
assert_eq!(&lines[0][6..7], ":");
assert_eq!(&lines[0][9..], "] pid 4242: PATCHED cert gates");
assert!(lines[1].ends_with("] second line"));
let _ = std::fs::remove_file(&path);
}
#[test]
fn a_bad_log_path_does_not_kill_the_patcher() {
let logger = Logger {
path: PathBuf::from("/proc/definitely/not/writable.log"),
};
logger.log("still running");
}
}