attempt: split-brain auth — maxima-cli serve + /authorize HTTP (TF2 still WIP)#6
Conversation
Bootstrap rewrites from "always spawn maxima-cli launch" to a router that probes for a long-running Maxima and forwards link2ea/origin2 over HTTP when found. Falls back to the legacy spawn so users who never run `serve` keep working. Aligned with upstream issue ArmchairDevelopers#27's design intent (long- running auth server + thin protocol-handler shim). Pieces: - New module `maxima-lib/src/auth_server.rs` (~280 lines). Plain tokio::net::TcpListener + manual HTTP parse. Two routes: `GET /` liveness probe, `POST /authorize?offer_id=X[&cmd_params=...]` which validates login, resolves the offer (with Steam App ID translation via `STEAM_GAMES`), calls `launch::start_game` so the spawned game has the full EA env context (EALsxPort / EAGenericAuthToken / etc.). Default port 13219, override via MAXIMA_AUTHORIZE_PORT. - New module `maxima-lib/src/steam.rs` — `STEAM_GAMES` table, lookup_steam_game / lookup_steam_game_by_offer (reverse: Origin offer → Steam entry), resolve_steam_install_path (registry + libraryfolders.vdf). Lifted out of `maxima-cli/src/main.rs` so the auth server can use it. `winreg` dep dropped from `maxima-cli/Cargo.toml`, moved to maxima-lib. - `Maxima::start_auth_server` companion to `start_lsx`. - `Maxima::start_lsx` now probes 127.0.0.1:<port> with a 200ms TCP connect_timeout before binding. If something else (e.g. `serve`) is already listening, the new instance skips its bind. Avoids the bootstrap-spawned `maxima-cli launch` from racing the existing `serve` for the LSX socket under Wine. - `maxima-bootstrap` link2ea/origin2 handlers deduplicated into a single `handle_protocol_authorize(offer_id, cmd_params, protocol_name)` helper: validate offer-id shape, probe :13219, POST /authorize with 60s timeout on success, fall through to `maxima-cli launch <offer>` on no auth-server. AUTHORIZE_PORT imported from maxima-lib (no duplicated constant). `log_event` helper writes structured lines to %TEMP%/maxima_execution.log for diagnostics. - `maxima-cli` gains `Mode::Serve { no_rtm }` — passive auth-only mode. Logs in, starts LSX, starts the authorize HTTP server, logs into RTM (optional), and parks. `serve_lsx` polls `maxima.update()` once a second so game-exit detection runs cloud-save sync and clears `maxima.playing`. Steam env-var setting + arg injection removed from Mode::Launch (moves to launch::start_game in commit 2). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…; LSX consistency
LaunchOptions field `steam_launch: bool` → `steam_app_id: Option<String>`.
When set, `launch::start_game` now handles the entire Steam-Play context
in one place (instead of bits scattered across maxima-cli + the UI):
1. EAEntitlementSource / EAExternalSource / EALaunchOwner → "Steam"
(vs hardcoded "EA"). Required so TF2's DRM stub sees a launch
context consistent with where the game is being run from.
2. SteamAppId / SteamGameId / SteamClientLaunch / SteamPath env vars
set on the spawned child's `Command` directly. Previously
maxima-cli set them via `env::set_var` on its own process,
polluting the parent. Setting them on `Command` only is correct
and lets `maxima-cli serve` invoke `launch::start_game` many
times across its lifetime without leaking state.
3. `-noOriginStartup -multiple` auto-injected into args (the
`-noOriginStartup` flag skips Northstar/TF2's Origin-startup
wait that hangs in Wine; `-multiple` avoids the "another
instance already running" race during the Steam handoff).
Also in this commit:
- `request_opaque_ooa_token` is now best-effort: when EA rejects the
JWS→OPAQUE exchange (typical under Wine — EA bounces to
`signin.ea.com/p/juno/login?fid=...` because we have no session
cookies), fall back to the JWS access_token for
EALaunchUserAuthToken. That's the pre-upstream-PR-ArmchairDevelopers#34 behavior and
lets the launch proceed past auth instead of bailing. The game's
Origin SDK still doesn't accept it as SSO and shows its built-in
login dialog — open issue tracked in CLAUDE.md.
- `ActiveGameContext` gains `steam_app_id: Option<String>` plumbed
from `LaunchOptions.steam_app_id`. LSX handlers now read it from
`maxima.playing()` instead of `env::var("SteamAppId")` (which
doesn't work for the auth_server case where env vars are set on
the spawned child, not the parent serve process).
- `GetProfileResponse.IsSubscriber` / `IsSteamSubscriber` and
`GetAllGameInfoResponse.EntitlementSource` now agree: both `true`
/ `"STEAM"` when steam_app_id is Some, both `false` / `"EA"`
otherwise. The previous mismatch (EntitlementSource hardcoded
STEAM + IsSteamSubscriber=false) was a likely tamper-detection
trigger for TF2's DRM stub.
- `handle_license_request` (LSX RequestLicense) now defensively
returns an empty token when `maxima.playing()=None` instead of
panicking on `.unwrap()`. Catornot's pattern, extended to the
license path.
- `maxima-ui/src/bridge/start_game.rs` follows the renamed field
(`steam_app_id: None`, since UI launches are EA-Desktop-style).
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
…rewrite Diagnostics added so the next iteration of this attempt has a usable trace by default (no rebuild for debug logs): - `lsx::connection::process_message` "Received LSX Message" log: debug → info. The XML payload is typically <500 bytes per message and we get ~15 messages per launch, so volume is fine. Lets us see the full LSX request sequence in `maxima-cli.log`. - `lsx::connection::queue_message` "Queuing LSX Message" log: debug → info (same rationale, gives us the response side of each exchange). - `lsx::service` "LSX connection closed" now includes the underlying `LSXConnectionError`. `Closed` means clean EOF (peer closed socket); anything else is a transport / parse failure. The previous opaque message hid the difference. Docs: - CLAUDE.md restructured to put "state of the world" first, history at the bottom. Added "Current architecture: two launch paths" section with sequence diagrams (Path A = `serve` + bootstrap forward, Path B = legacy `maxima-cli launch` fallback). New "Operator recipes" section. Open issues consolidated: "File corruption detected" still open, with current understanding of why the Origin in-game login window appears (OPAQUE OOA exchange rejected by EA), the OPAQUE→JWS fallback we landed, and remaining hypotheses for the corruption symptom itself. - README rewritten to be honest about status: "TF2 doesn't reliably launch yet" warning prominent at the top, "What works / What's not confirmed working / Known limitations" section, recipe for the new `serve` mode flow. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
There was a problem hiding this comment.
Code Review
This pull request introduces a long-running authentication server mode (maxima-cli serve) and an HTTP-based authorization endpoint to allow the protocol handler to forward requests to an existing session. It significantly enhances Steam-Play support for Titanfall 2 on macOS/CrossOver by automating environment variable setup and launch argument injection. Additionally, the changes include defensive LSX request handling for external game launches and improved diagnostic logging. Feedback identifies a potential local Denial of Service vulnerability in the unauthenticated HTTP server and performance issues caused by using blocking synchronous TCP probes within asynchronous contexts.
| // We only need the request line — the body is empty for our endpoints | ||
| // and headers carry nothing we care about. Drain enough to keep the | ||
| // peer's send buffer happy, then respond. | ||
| let mut request_line = String::new(); |
There was a problem hiding this comment.
The use of read_line without a length limit on an unauthenticated network socket is a potential security risk (local Denial of Service). A malicious local process could send an extremely long line to exhaust memory and crash the server. Consider using .take(limit) on the reader or a fixed-size buffer to bound the amount of data read per request. Additionally, the entire handle_connection call should ideally be wrapped in a tokio::time::timeout to prevent slow-client attacks from hanging tasks indefinitely.
| use std::time::Duration as StdDuration; | ||
| let probe_addr = format!("127.0.0.1:{}", lsx_port); | ||
| if let Ok(probe_target) = probe_addr.parse() { | ||
| match std::net::TcpStream::connect_timeout(&probe_target, StdDuration::from_millis(200)) { |
There was a problem hiding this comment.
Using std::net::TcpStream::connect_timeout inside an async function blocks the Tokio executor thread. This can lead to performance degradation as it prevents other tasks from running on that thread for the duration of the timeout (up to 200ms). Consider using tokio::net::TcpStream combined with tokio::time::timeout for a non-blocking connection probe.
| addr.parse() | ||
| .ok() | ||
| .and_then(|target| { | ||
| std::net::TcpStream::connect_timeout( |
….5.0 Addresses three review comments from gemini-code-assist on PR #6: 1. **auth_server: bound request size + per-request timeout** (security-medium). Wraps `handle_connection` in a 30s `tokio::time::timeout` so a slow / hostile peer can't pin a task indefinitely, and limits the total request-head bytes our BufReader will surface to 8 KiB via `read_half.take(MAX_REQUEST_HEAD_BYTES)`. Both bounds are new constants at the top of the module with comments explaining the chosen numbers (30s ≫ normal /authorize <2s; 8 KiB matches Nginx's `large_client_header_buffers` default). 2. **`Maxima::start_lsx` probe: tokio instead of std::net** (perf-medium). The previous `std::net::TcpStream::connect_timeout` was parking an executor worker for up to 200ms. Swapped to `tokio::time::timeout(.., tokio::net::TcpStream::connect(..))` which yields cleanly while it waits. 3. **`maxima-bootstrap::auth_server_alive` same change** (perf-medium). Function is now `async fn` and the single call site at `handle_protocol_authorize` awaits it. Also bumps versions 0.4.0 → 0.5.0 in `maxima-cli`, `maxima-bootstrap`, `maxima-service` Cargo.toml files and `installer/maxima-setup.nsi` PRODUCT_VERSION. Caught a missing call site in `maxima-tui/src/main.rs` still referencing the renamed `LaunchOptions.steam_launch`; updated to `steam_app_id: None` to match `maxima-ui`. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
Warning
This is an attempt, not a confirmed solution. TF2 on macOS/CrossOver still does NOT launch end-to-end. The plumbing is correct in principle (aligned with upstream issue #27), and Maxima now goes much further than it did before, but two open issues remain that are documented in CLAUDE.md and need more investigation. Do not merge this expecting the game to suddenly run — merge it only as a strictly better-than-the-previous-state baseline if/when we want to.
Summary
Rewrites the bootstrap → auth flow as a long-running auth provider (
maxima-cli serve) with an HTTP/authorizeendpoint, instead of bootstrap re-spawning a freshmaxima-cli launchon everylink2ea://invocation. Aligned with upstream's design intent in #27 ("Support launching Maxima from Epic/Steam") and depends on the catornot external-LSX patch already in this fork.Three commits:
`feat: split-brain auth — maxima-cli serve + /authorize HTTP`
`refactor(launch): centralize Steam-Play handling; OPAQUE→JWS fallback; LSX consistency`
`diag + docs: verbose LSX trace, real close reasons, CLAUDE.md/README rewrite`
What works now
What's NOT working (open issues, documented in CLAUDE.md)
Origin in-game login window appears. TF2's embedded Origin SDK rejects our `EAGenericAuthToken` / `EAAccessTokenJWS` and falls back to its built-in login. Root cause: EA's `nucleus_auth_exchange` rejects the JWS→OPAQUE swap (no session cookies on our reqwest client) → we fall back to JWS for `EALaunchUserAuthToken` → SDK doesn't trust JWS as OPAQUE. Workaround: user logs in manually through that window.
"Engine Error: File corruption detected" after manual Origin login. Confirmed empirically:
Next steps (not in this PR)
Test plan
🤖 Generated with Claude Code