use anyhow::Result; use axum::{ routing::{any, delete, get, post}, Router, }; use openfut_bridge::{config::Config, proxy::ProxyState, routes}; use tower_http::{cors::CorsLayer, trace::TraceLayer}; use tracing::info; use tracing_subscriber::{layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; #[tokio::main] async fn main() -> Result<()> { dotenvy::dotenv().ok(); tracing_subscriber::registry() .with( EnvFilter::try_from_default_env() .unwrap_or_else(|_| "openfut_bridge=debug,tower_http=info".into()), ) .with(tracing_subscriber::fmt::layer()) .init(); 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)?; let addr: std::net::SocketAddr = listen_addr.parse()?; 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/dashboard", get(routes::health::get_dashboard)) .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)) .route( "/_bridge/captures/diff", get(routes::admin::get_capture_diff), ) .route( "/_bridge/captures/:id/replay", post(routes::admin::post_replay_capture), ) .fallback(any(openfut_bridge::proxy::catch_all_handler)) .layer(TraceLayer::new_for_http()) .layer(CorsLayer::permissive()) .with_state(state) } 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::make_tls_acceptor; use tower::ServiceExt; let acceptor = make_tls_acceptor(&cert_pem, &key_pem)?; info!("Bridge listening on https://{addr} (self-signed TLS)"); 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?; 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| { 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}"); } }); } }