Phase 9 bridge: cert download and HTML setup guide endpoints

- 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<Vec<u8>>); 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 <noreply@anthropic.com>
This commit is contained in:
funman300
2026-06-25 16:53:12 -07:00
parent f3ff291ff8
commit 00839f5f1b
3 changed files with 182 additions and 26 deletions
+37 -24
View File
@@ -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<u8>,
key_pem: Vec<u8>,
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<hyper::body::Incoming>| {
let svc = hyper::service::service_fn(
move |req: hyper::Request<hyper::body::Incoming>| {
let app = app.clone();
async move {
app.oneshot(req.map(axum::body::Body::new)).await
}
});
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)
+8
View File
@@ -31,6 +31,8 @@ pub struct ProxyState {
pub capture_tx: Arc<broadcast::Sender<CapturedRequest>>,
/// Deduplication window: (method+path) → last saved instant.
pub dedup: Arc<Mutex<HashMap<String, Instant>>>,
/// PEM-encoded TLS certificate for download. None when TLS is disabled.
pub cert_pem: Option<Arc<Vec<u8>>>,
}
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<u8>) -> Self {
self.cert_pem = Some(Arc::new(cert_pem));
self
}
}
/// Returns true if this (method, path) pair was already saved within the dedup window.
+136 -1
View File
@@ -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<Value>) {
(
StatusCode::OK,
@@ -12,3 +19,131 @@ pub async fn get_health() -> (StatusCode, Json<Value>) {
})),
)
}
/// Serve the TLS certificate as a downloadable PEM file.
///
/// Users navigate to `https://<bridge-ip>: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<ProxyState>) -> 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<ProxyState>) -> Html<String> {
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#"<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>OpenFUT Bridge — Setup Guide</title>
<style>
body {{ font-family: system-ui, sans-serif; max-width: 780px; margin: 0 auto; padding: 2rem; background: #0a0a0a; color: #e0e0e0; }}
h1 {{ color: #f5a623; }} h2 {{ color: #a0c4ff; border-bottom: 1px solid #333; padding-bottom: 4px; }}
code {{ background: #1c1c1c; padding: 2px 6px; border-radius: 3px; font-family: monospace; }}
pre {{ background: #1c1c1c; padding: 1rem; border-radius: 6px; overflow-x: auto; }}
.ok {{ color: #5fbb5f; }} .warn {{ color: #f5a623; }}
a {{ color: #a0c4ff; }}
.step {{ margin: 1.2rem 0; padding: 1rem; background: #141414; border-left: 3px solid #f5a623; border-radius: 0 6px 6px 0; }}
</style>
</head>
<body>
<h1>OpenFUT Bridge — Setup Guide</h1>
<p><span class="ok">&#x2713;</span> Bridge is running at <code>{bridge_url}</code></p>
<p>Core API: <code>{core_url}</code></p>
<h2>How it works</h2>
<p>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.</p>
<h2>Step 1 — Redirect FIFA 23 traffic</h2>
<div class="step">
<p>Add these entries to your <strong>hosts file</strong> (requires admin/root):</p>
<pre>
# 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
</pre>
<ul>
<li>Windows: <code>C:\Windows\System32\drivers\etc\hosts</code></li>
<li>macOS / Linux: <code>/etc/hosts</code></li>
</ul>
</div>
<h2>Step 2 — Install the TLS certificate (HTTPS only)</h2>
{cert_section}
<h2>Step 3 — Launch FIFA 23</h2>
<div class="step">
<p>Start FIFA 23 and navigate to <strong>Ultimate Team</strong>. The bridge will capture
and proxy FUT API calls. Open <code>{bridge_url}/_bridge/admin</code> in your browser to
watch traffic in real time.</p>
</div>
<h2>Supported endpoints</h2>
<p>See <code><a href="/_bridge/status">/_bridge/status</a></code> for a full list of mapped
and unknown endpoints seen so far.</p>
<h2>Troubleshooting</h2>
<ul>
<li><strong>Game shows "FUT servers unavailable":</strong> make sure the hosts file is saved and DNS is flushed (<code>ipconfig /flushdns</code> on Windows).</li>
<li><strong>Certificate error in browser:</strong> the self-signed cert must be trusted by the OS or the game's certificate store.</li>
<li><strong>Bridge not reaching Core:</strong> check that <code>openfut-core</code> is running and <code>CORE_URL</code> is set correctly (default <code>http://127.0.0.1:3000</code>).</li>
</ul>
<p style="color:#555; font-size:0.85em; margin-top:3rem;">
OpenFUT is an <strong>offline</strong> replacement backend for legitimate FIFA 23 owners.
It does not connect to EA servers or enable online multiplayer.
</p>
</body>
</html>"#,
cert_section = if tls {
format!(
r#"<div class="step">
<p>Download and install the bridge's self-signed CA certificate so FIFA 23 trusts HTTPS connections:</p>
<ol>
<li>Open <a href="/_bridge/cert.pem"><code>{bridge_url}/_bridge/cert.pem</code></a> and save it.</li>
<li><strong>Windows:</strong> double-click the file → "Install Certificate" → "Local Machine" → "Trusted Root Certification Authorities".</li>
<li><strong>macOS:</strong> double-click the file → Keychain Access → trust "Always".</li>
<li><strong>Linux:</strong> copy to <code>/usr/local/share/ca-certificates/</code> and run <code>update-ca-certificates</code>.</li>
</ol>
</div>"#
)
} else {
"<div class=\"step\"><p class=\"warn\">TLS is disabled — set <code>TLS_ENABLED=true</code> to enable HTTPS (required for FIFA 23 interception).</p></div>".to_string()
}
);
Html(guide)
}