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:
+37
-24
@@ -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)
|
||||
|
||||
@@ -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
@@ -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">✓</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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user