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
+38 -25
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 app = app.clone();
async move {
app.oneshot(req.map(axum::body::Body::new)).await
}
});
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 }
},
);
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
.serve_connection(io, svc)