use anyhow::{Context, Result}; use axum::body::Body; use axum::extract::Path; use axum::http::{header, HeaderValue, StatusCode}; use axum::response::{IntoResponse, Response}; use axum::routing::get; use axum::Router; use mime_guess::from_path; use rust_embed::RustEmbed; #[derive(RustEmbed)] #[folder = "../backchannel-web/dist"] struct WebAssets; pub async fn serve(bind_addr: &str) -> Result<()> { let app = Router::new() .route("/", get(index)) .route("/*path", get(asset_or_index)); let listener = tokio::net::TcpListener::bind(bind_addr) .await .with_context(|| format!("Failed to bind HTTP server to {bind_addr}"))?; tracing::info!("BackChannel web UI listening on http://{bind_addr}"); axum::serve(listener, app).await.context("HTTP server failed") } async fn index() -> Response { render_asset("index.html") } async fn asset_or_index(Path(path): Path) -> Response { if path.is_empty() { return render_asset("index.html"); } render_asset(&path) } fn render_asset(path: &str) -> Response { let canonical = path.trim_start_matches('/'); if let Some(content) = WebAssets::get(canonical) { return build_response(canonical, content.data.as_ref(), StatusCode::OK); } if let Some(index) = WebAssets::get("index.html") { return build_response("index.html", index.data.as_ref(), StatusCode::OK); } (StatusCode::NOT_FOUND, "Web UI asset not found").into_response() } fn build_response(path: &str, body: &[u8], status: StatusCode) -> Response { let mime = from_path(path).first_or_octet_stream(); let mut response = Response::new(Body::from(body.to_vec())); *response.status_mut() = status; response.headers_mut().insert( header::CONTENT_TYPE, HeaderValue::from_str(mime.as_ref()).unwrap_or(HeaderValue::from_static("application/octet-stream")), ); response }