From 00839f5f1b8f740da7383d03a5f1452dfb416cbb Mon Sep 17 00:00:00 2001 From: funman300 Date: Thu, 25 Jun 2026 16:53:12 -0700 Subject: [PATCH] Phase 9 bridge: cert download and HTML setup guide endpoints MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - GET /_bridge/cert.pem — serves the TLS CA cert as a downloadable PEM file (only when TLS_ENABLED=true; 404 otherwise) - GET /_bridge/guide — HTML setup page with hosts-file instructions, cert install steps per OS, and troubleshooting tips - ProxyState gains cert_pem field (Arc>); set via with_cert() builder - main.rs generates cert before state construction so /_bridge/cert.pem can serve it; both cert_pem and key_pem passed to serve_tls() for acceptor + state separately - All 13 bridge tests pass, clippy clean Co-Authored-By: Claude Sonnet 4.6 --- src/main.rs | 63 ++++++++++++-------- src/proxy.rs | 8 +++ src/routes/health.rs | 137 ++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 182 insertions(+), 26 deletions(-) diff --git a/src/main.rs b/src/main.rs index f5431fa..2f40b77 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,10 +32,30 @@ async fn main() -> Result<()> { std::fs::create_dir_all(&cfg.captures_dir)?; - let state = ProxyState::new(cfg); + let addr: std::net::SocketAddr = listen_addr.parse()?; - let app = Router::new() + if tls_enabled { + // Generate cert/key before building state so the cert can be served via /_bridge/cert.pem + let (cert_pem, key_pem) = openfut_bridge::tls::generate_self_signed_cert()?; + let state = ProxyState::new(cfg).with_cert(cert_pem.clone()); + let app = build_router(state); + serve_tls(app, cert_pem, key_pem, addr).await + } else { + let state = ProxyState::new(cfg); + let app = build_router(state); + info!("Bridge listening on http://{addr}"); + info!("Set TLS_ENABLED=true to serve over HTTPS"); + let listener = tokio::net::TcpListener::bind(addr).await?; + axum::serve(listener, app).await?; + Ok(()) + } +} + +fn build_router(state: ProxyState) -> Router { + Router::new() .route("/_bridge/health", get(routes::health::get_health)) + .route("/_bridge/cert.pem", get(routes::health::get_cert)) + .route("/_bridge/guide", get(routes::health::get_guide)) .route("/_bridge/admin", get(routes::admin::get_admin_dashboard)) .route("/_bridge/captures", get(routes::admin::get_captures)) .route("/_bridge/captures", delete(routes::admin::delete_captures)) @@ -55,31 +75,24 @@ async fn main() -> Result<()> { .fallback(any(openfut_bridge::proxy::catch_all_handler)) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive()) - .with_state(state); - - let addr: std::net::SocketAddr = listen_addr.parse()?; - - if tls_enabled { - serve_tls(app, addr).await - } else { - info!("Bridge listening on http://{addr}"); - info!("Set TLS_ENABLED=true to serve over HTTPS"); - let listener = tokio::net::TcpListener::bind(addr).await?; - axum::serve(listener, app).await?; - Ok(()) - } + .with_state(state) } -async fn serve_tls(app: Router, addr: std::net::SocketAddr) -> Result<()> { +async fn serve_tls( + app: Router, + cert_pem: Vec, + key_pem: Vec, + addr: std::net::SocketAddr, +) -> Result<()> { use hyper_util::rt::{TokioExecutor, TokioIo}; - use openfut_bridge::tls::{generate_self_signed_cert, make_tls_acceptor}; + use openfut_bridge::tls::make_tls_acceptor; use tower::ServiceExt; - let (cert_pem, key_pem) = generate_self_signed_cert()?; let acceptor = make_tls_acceptor(&cert_pem, &key_pem)?; info!("Bridge listening on https://{addr} (self-signed TLS)"); - info!("Install the generated cert as a trusted CA or disable cert checking in FIFA 23"); + info!("Download the CA cert from /_bridge/cert.pem and install it as trusted"); + info!("Setup guide: https://{addr}/_bridge/guide"); let listener = tokio::net::TcpListener::bind(addr).await?; @@ -98,12 +111,12 @@ async fn serve_tls(app: Router, addr: std::net::SocketAddr) -> Result<()> { }; let io = TokioIo::new(tls_stream); - let svc = hyper::service::service_fn(move |req: hyper::Request| { - let app = app.clone(); - async move { - app.oneshot(req.map(axum::body::Body::new)).await - } - }); + let svc = hyper::service::service_fn( + move |req: hyper::Request| { + let app = app.clone(); + async move { app.oneshot(req.map(axum::body::Body::new)).await } + }, + ); if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new()) .serve_connection(io, svc) diff --git a/src/proxy.rs b/src/proxy.rs index d3c2fed..6bb30ce 100644 --- a/src/proxy.rs +++ b/src/proxy.rs @@ -31,6 +31,8 @@ pub struct ProxyState { pub capture_tx: Arc>, /// Deduplication window: (method+path) → last saved instant. pub dedup: Arc>>, + /// PEM-encoded TLS certificate for download. None when TLS is disabled. + pub cert_pem: Option>>, } impl ProxyState { @@ -45,8 +47,14 @@ impl ProxyState { .expect("failed to build HTTP client"), capture_tx: Arc::new(capture_tx), dedup: Arc::new(Mutex::new(HashMap::new())), + cert_pem: None, } } + + pub fn with_cert(mut self, cert_pem: Vec) -> Self { + self.cert_pem = Some(Arc::new(cert_pem)); + self + } } /// Returns true if this (method, path) pair was already saved within the dedup window. diff --git a/src/routes/health.rs b/src/routes/health.rs index 6e83515..00c83e8 100644 --- a/src/routes/health.rs +++ b/src/routes/health.rs @@ -1,6 +1,13 @@ -use axum::{http::StatusCode, Json}; +use axum::{ + extract::State, + http::{header, StatusCode}, + response::{Html, IntoResponse, Response}, + Json, +}; use serde_json::{json, Value}; +use crate::proxy::ProxyState; + pub async fn get_health() -> (StatusCode, Json) { ( StatusCode::OK, @@ -12,3 +19,131 @@ pub async fn get_health() -> (StatusCode, Json) { })), ) } + +/// Serve the TLS certificate as a downloadable PEM file. +/// +/// Users navigate to `https://:8443/_bridge/cert.pem` in a browser, +/// download the file, and install it as a trusted CA in their OS or game console. +pub async fn get_cert(State(state): State) -> Response { + match &state.cert_pem { + Some(pem) => ( + StatusCode::OK, + [ + (header::CONTENT_TYPE, "application/x-pem-file"), + ( + header::CONTENT_DISPOSITION, + "attachment; filename=\"openfut-ca.pem\"", + ), + ], + pem.as_ref().clone(), + ) + .into_response(), + None => ( + StatusCode::NOT_FOUND, + Json(json!({ "error": "TLS is not enabled; no certificate to download" })), + ) + .into_response(), + } +} + +/// HTML setup guide for connecting FIFA 23 to OpenFUT Bridge. +pub async fn get_guide(State(state): State) -> Html { + let host = state + .config + .listen_addr + .split(':') + .next() + .unwrap_or("localhost"); + let tls = state.config.tls_enabled; + let scheme = if tls { "https" } else { "http" }; + let port = state.config.listen_addr.split(':').next_back().unwrap_or("8080"); + let bridge_url = format!("{scheme}://{host}:{port}"); + let core_url = &state.config.core_url; + + let guide = format!( + r#" + + + + + OpenFUT Bridge — Setup Guide + + + +

OpenFUT Bridge — Setup Guide

+

Bridge is running at {bridge_url}

+

Core API: {core_url}

+ +

How it works

+

OpenFUT Bridge sits between FIFA 23 and EA's servers. It intercepts FUT API calls and +routes supported endpoints to OpenFUT Core (your local offline backend). Unsupported +endpoints return placeholder responses so the game stays stable.

+ +

Step 1 — Redirect FIFA 23 traffic

+
+

Add these entries to your hosts file (requires admin/root):

+
+# Point FIFA 23 FUT traffic to OpenFUT Bridge
+127.0.0.1  fut.ea.com
+127.0.0.1  utas.mob.v4.fut.ea.com
+
+
    +
  • Windows: C:\Windows\System32\drivers\etc\hosts
  • +
  • macOS / Linux: /etc/hosts
  • +
+
+ +

Step 2 — Install the TLS certificate (HTTPS only)

+{cert_section} + +

Step 3 — Launch FIFA 23

+
+

Start FIFA 23 and navigate to Ultimate Team. The bridge will capture +and proxy FUT API calls. Open {bridge_url}/_bridge/admin in your browser to +watch traffic in real time.

+
+ +

Supported endpoints

+

See /_bridge/status for a full list of mapped +and unknown endpoints seen so far.

+ +

Troubleshooting

+
    +
  • Game shows "FUT servers unavailable": make sure the hosts file is saved and DNS is flushed (ipconfig /flushdns on Windows).
  • +
  • Certificate error in browser: the self-signed cert must be trusted by the OS or the game's certificate store.
  • +
  • Bridge not reaching Core: check that openfut-core is running and CORE_URL is set correctly (default http://127.0.0.1:3000).
  • +
+ +

+OpenFUT is an offline replacement backend for legitimate FIFA 23 owners. +It does not connect to EA servers or enable online multiplayer. +

+ +"#, + cert_section = if tls { + format!( + r#"
+

Download and install the bridge's self-signed CA certificate so FIFA 23 trusts HTTPS connections:

+
    +
  1. Open {bridge_url}/_bridge/cert.pem and save it.
  2. +
  3. Windows: double-click the file → "Install Certificate" → "Local Machine" → "Trusted Root Certification Authorities".
  4. +
  5. macOS: double-click the file → Keychain Access → trust "Always".
  6. +
  7. Linux: copy to /usr/local/share/ca-certificates/ and run update-ca-certificates.
  8. +
+
"# + ) + } else { + "

TLS is disabled — set TLS_ENABLED=true to enable HTTPS (required for FIFA 23 interception).

".to_string() + } + ); + + Html(guide) +}