eae3e72fd9
Single-page club management UI served by the bridge. The Core URL is injected server-side so client JS calls Core directly without routing through the proxy. Vanilla HTML/CSS/JS, dark theme matching admin.html. Sections: Club (name/manager/coins/stats edit), Collection (filterable card grid with stat bars + chemistry/training pills), Packs (open with modal, history), Objectives (progress bars + one-click claim), Division (W/D/L record, season progress, rivals weekly claim), FUT Champions (current session with in-browser match simulation, claim, history), Notifications (badge count in nav). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
135 lines
4.6 KiB
Rust
135 lines
4.6 KiB
Rust
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<u8>,
|
|
key_pem: Vec<u8>,
|
|
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<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}");
|
|
}
|
|
});
|
|
}
|
|
}
|