feat: Phase 6 — proxy polish, TLS, admin UI, replay CLI
## TLS (#11) - Self-signed cert generated at startup via rcgen (covers localhost, 127.0.0.1, fut.ea.com, utas.mob.v4.fut.ea.com); activated with TLS_ENABLED=true - Custom accept loop: tokio-rustls acceptor → hyper-util auto Builder → axum Router (no axum-server dependency — uses hyper 1.x natively) ## Replay CLI (#12) - New binary: openfut-bridge-replay <file.json|dir> [bridge-url] - Replays single capture or entire directory against Bridge - Accepts self-signed certs automatically ## Capture quality (#13, #14) - DELETE /_bridge/captures — wipe all capture files from disk - Deduplication: same method+path within 1 s is skipped (configurable constant) ## Admin UI (#21, #22, #23) - GET /_bridge/admin — embedded HTML dashboard; auto-refresh every 10 s Shows: live stats, SSE log of incoming traffic, endpoint status table, recent captures list with delete button - GET /_bridge/status — per-endpoint mapped/known/unknown status - GET /_bridge/captures/stream — SSE stream; event: capture on each request Uses tokio::sync::broadcast channel (capacity 256) in ProxyState ## Tests (#24, #25) - 9 new tests: placeholder format, full HTTP integration (health, placeholder, captures list, delete captures, status), TLS cert generation + acceptor build - Total bridge tests: 13/13 passing Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
+65
-5
@@ -1,6 +1,6 @@
|
||||
use anyhow::Result;
|
||||
use axum::{
|
||||
routing::{any, get},
|
||||
routing::{any, delete, get},
|
||||
Router,
|
||||
};
|
||||
use openfut_bridge::{config::Config, proxy::ProxyState, routes};
|
||||
@@ -22,11 +22,13 @@ async fn main() -> Result<()> {
|
||||
|
||||
let cfg = Config::from_env()?;
|
||||
let listen_addr = cfg.listen_addr.clone();
|
||||
let tls_enabled = cfg.tls_enabled;
|
||||
|
||||
info!("OpenFUT Bridge starting on {listen_addr}");
|
||||
info!("Core URL: {}", cfg.core_url);
|
||||
info!("Placeholder mode: {}", cfg.placeholder_mode);
|
||||
info!("Captures dir: {}", cfg.captures_dir);
|
||||
info!("TLS enabled: {tls_enabled}");
|
||||
|
||||
std::fs::create_dir_all(&cfg.captures_dir)?;
|
||||
|
||||
@@ -34,19 +36,77 @@ async fn main() -> Result<()> {
|
||||
|
||||
let app = Router::new()
|
||||
.route("/_bridge/health", get(routes::health::get_health))
|
||||
.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))
|
||||
.route(
|
||||
"/_bridge/captures/stream",
|
||||
get(routes::admin::get_captures_stream),
|
||||
)
|
||||
.route(
|
||||
"/_bridge/unknown",
|
||||
get(routes::admin::get_unknown_endpoints),
|
||||
)
|
||||
.route("/_bridge/status", get(routes::admin::get_endpoint_status))
|
||||
.fallback(any(openfut_bridge::proxy::catch_all_handler))
|
||||
.layer(TraceLayer::new_for_http())
|
||||
.layer(CorsLayer::permissive())
|
||||
.with_state(state);
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(&listen_addr).await?;
|
||||
info!("Bridge listening on http://{listen_addr}");
|
||||
axum::serve(listener, app).await?;
|
||||
let addr: std::net::SocketAddr = listen_addr.parse()?;
|
||||
|
||||
Ok(())
|
||||
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(())
|
||||
}
|
||||
}
|
||||
|
||||
async fn serve_tls(app: Router, addr: std::net::SocketAddr) -> Result<()> {
|
||||
use hyper_util::rt::{TokioExecutor, TokioIo};
|
||||
use openfut_bridge::tls::{generate_self_signed_cert, 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");
|
||||
|
||||
let listener = tokio::net::TcpListener::bind(addr).await?;
|
||||
|
||||
loop {
|
||||
let (tcp, _peer) = listener.accept().await?;
|
||||
let acceptor = acceptor.clone();
|
||||
let app = app.clone();
|
||||
|
||||
tokio::spawn(async move {
|
||||
let tls_stream = match acceptor.accept(tcp).await {
|
||||
Ok(s) => s,
|
||||
Err(e) => {
|
||||
tracing::warn!("TLS handshake failed: {e}");
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
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
|
||||
}
|
||||
});
|
||||
|
||||
if let Err(e) = hyper_util::server::conn::auto::Builder::new(TokioExecutor::new())
|
||||
.serve_connection(io, svc)
|
||||
.await
|
||||
{
|
||||
tracing::debug!("TLS connection closed: {e}");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user