Skip to content

attempt: split-brain auth — maxima-cli serve + /authorize HTTP (TF2 still WIP)#6

Merged
AA-EION merged 4 commits into
masterfrom
attempt/serve-mode-and-authorize-http
May 19, 2026
Merged

attempt: split-brain auth — maxima-cli serve + /authorize HTTP (TF2 still WIP)#6
AA-EION merged 4 commits into
masterfrom
attempt/serve-mode-and-authorize-http

Conversation

@AA-EION
Copy link
Copy Markdown
Owner

@AA-EION AA-EION commented May 19, 2026

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 /authorize endpoint, instead of bootstrap re-spawning a fresh maxima-cli launch on every link2ea:// 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:

  1. `feat: split-brain auth — maxima-cli serve + /authorize HTTP`

    • New `maxima-lib::auth_server` module (HTTP server, `GET /` liveness + `POST /authorize` → calls `launch::start_game`).
    • New `maxima-lib::steam` module (lifted from `maxima-cli`, adds reverse lookup by Origin offer).
    • `Maxima::start_lsx` now probes before binding (cooperate with an existing server).
    • `maxima-bootstrap` rewrites `link2ea://` / `origin2://` to probe + HTTP forward, falling back to the legacy `maxima-cli launch` spawn.
    • `maxima-cli` gains `Mode::Serve { no_rtm }`.
  2. `refactor(launch): centralize Steam-Play handling; OPAQUE→JWS fallback; LSX consistency`

    • `LaunchOptions.steam_launch: bool` → `steam_app_id: Option`. All Steam-Play env vars (`SteamAppId`, `SteamGameId`, etc.) and arg injection (`-noOriginStartup -multiple`) moved from the CLI into `launch::start_game`, set on the spawned `Command` (not via `env::set_var` on the parent).
    • `request_opaque_ooa_token` falls back to the JWS `access_token` when EA rejects the JWS→OPAQUE exchange. EA's rejection is structural under Wine (no session cookies), see open issue.
    • `ActiveGameContext.steam_app_id` plumbs the launch context into the LSX handlers so `GetProfileResponse.IsSteamSubscriber` and `GetAllGameInfoResponse.EntitlementSource` agree. Removes the previous mismatch (EntitlementSource hardcoded `STEAM` + IsSteamSubscriber `false`) which is a likely DRM-tamper trigger.
    • `handle_license_request` no longer panics on `maxima.playing()=None` (catornot pattern extended).
  3. `diag + docs: verbose LSX trace, real close reasons, CLAUDE.md/README rewrite`

    • LSX `Received` / `Queuing` log entries: `debug` → `info` (full LSX trace in `maxima-cli.log` by default).
    • `service::"LSX connection closed"` now includes the underlying `LSXConnectionError` (distinguishes clean EOF from transport / parse failures).
    • `MAXIMA_SKIP_LICENSE_WRITE` env override (skips `request_and_save_license` entirely) for testing the .dlf-hash hypothesis.
    • CLAUDE.md restructured to put current state first, history at the bottom. README marked honestly as beta (TF2 doesn't reliably launch yet).

What works now

  • Bootstrap probes the auth server reliably; falls back to spawn when absent.
  • `maxima-cli serve` keeps login + LSX + `/authorize` warm across multiple TF2 launches.
  • Steam App ID URL → Origin offer ID translation in `/authorize`.
  • Steam install path discovery (registry + libraryfolders.vdf) for Steam-only owners.
  • Cross-compiles clean on Linux + Windows + macOS in CI.

What's NOT working (open issues, documented in CLAUDE.md)

  1. 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.

  2. "Engine Error: File corruption detected" after manual Origin login. Confirmed empirically:

    • NOT the LSX `RequestLicense` response (tested via `MAXIMA_DENUVO_TOKEN`).
    • NOT Steam DRM IPC (tested with `steam.exe` running in the bottle).
    • NOT the `.dlf` license file on disk (tested via `MAXIMA_SKIP_LICENSE_WRITE`).
    • NOT Northstar interference (user is launching vanilla).
    • LSX flow completes Challenge → GetConfig → GetProfile → GetSetting → GetGameInfo → GetAllGameInfo → IsProgressiveInstallationAvailable, then TF2 closes the socket with clean EOF and shows the corruption error. The full LSX trace lands in `maxima-cli.log` thanks to the diag commit.
    • Untested hypothesis: TF2 download source under Wine matters. User reports a finding that Maxima-downloaded TF2 works under Wine where Steam-downloaded TF2 doesn't (on Windows it doesn't matter). Suspects: missing EA-style install artifacts, registry keys, or DLLs that EA Desktop's installer would have placed.

Next steps (not in this PR)

  • Cookie persistence in `nucleus_auth_exchange` so the OPAQUE OOA swap stops requiring fresh login → would eliminate the in-game Origin window.
  • Install TF2 via Maxima's downloader and compare the install tree against Steam's. Identify what to replicate.
  • Maybe write a test harness that compares the LSX trace of a working Windows session against our Wine session.

Test plan

  • `cargo +nightly check --target x86_64-pc-windows-gnu -p maxima-lib -p maxima-cli -p maxima-bootstrap` passes.
  • `cargo +nightly check --target x86_64-pc-windows-gnu -p maxima-ui -p maxima-tui` passes.
  • In a clean bottle: install MaximaSetup, register helper, log in via `maxima-cli.exe` interactive once, then `maxima-cli.exe serve`. Verify the three "listening" log lines appear.
  • Steam emits link2ea, bootstrap forwards, `serve` log shows `Authorize request for slug '1237970'` → `Steam App ID '1237970' resolved to Origin offer ID 'Origin.OFR.50.0001456'` → `Game launched for offer ...`.
  • TF2 reaches the Origin login dialog (currently expected). After manual login, full LSX trace (Challenge → ... → IsProgressiveInstallationAvailable) lands in `maxima-cli.log`. (This is where we are blocked on the file-corruption symptom.)

🤖 Generated with Claude Code

AA-EION and others added 3 commits May 18, 2026 19:33
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>
Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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();
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

security-medium medium

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.

Comment thread maxima-lib/src/core/mod.rs Outdated
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)) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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.

Comment thread maxima-bootstrap/src/main.rs Outdated
addr.parse()
.ok()
.and_then(|target| {
std::net::TcpStream::connect_timeout(
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Similar to the issue in maxima-lib, using synchronous TcpStream::connect_timeout inside an async context blocks the executor thread. Since this is a tokio application, it's preferable to use tokio::net::TcpStream to keep the probe non-blocking.

….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>
@AA-EION AA-EION merged commit 5e9a524 into master May 19, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant