From 043b4442a77b81b2a38721927d61a99e93bd63a1 Mon Sep 17 00:00:00 2001 From: goose Date: Tue, 14 Jul 2026 11:43:56 -0300 Subject: [PATCH] fix: HTTPS with self-signed cert for Web Crypto API support Browsers disable crypto.subtle on non-localhost HTTP. The frontend server now generates a self-signed TLS cert at startup and serves over HTTPS by default (FRONTEND_TLS=true). FRONTEND_TLS=0 disables it for localhost dev. Added axum-server (tls-rustls) + rcgen deps to the frontend-server crate. --- web/frontend-server/Cargo.toml | 2 ++ web/frontend-server/src/main.rs | 44 +++++++++++++++++++++++++++------ 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/web/frontend-server/Cargo.toml b/web/frontend-server/Cargo.toml index 00b6189..dcf205e 100644 --- a/web/frontend-server/Cargo.toml +++ b/web/frontend-server/Cargo.toml @@ -12,3 +12,5 @@ axum = "0.7" tokio = { version = "1", features = ["full"] } tower-http = { version = "0.5", features = ["fs", "trace"] } reqwest = { version = "0.12", features = ["json"] } +axum-server = { version = "0.7", features = ["tls-rustls"] } +rcgen = "0.13" diff --git a/web/frontend-server/src/main.rs b/web/frontend-server/src/main.rs index e55427b..7f4a081 100644 --- a/web/frontend-server/src/main.rs +++ b/web/frontend-server/src/main.rs @@ -3,6 +3,8 @@ //! 1. Reverse-proxies `/api/*` requests to the backend container. //! 2. Serves the built Vite SPA bundle from `dist/` (static files). //! 3. Falls back to `index.html` for any unmatched route (SPA history mode). +//! 4. Serves over HTTPS with a self-signed cert (required for Web Crypto API +//! on non-localhost addresses; browsers disable crypto.subtle on plain HTTP). use axum::{ body::Body, @@ -13,6 +15,7 @@ use axum::{ Router, }; use std::env; +use std::sync::Arc; use tower_http::services::{ServeDir, ServeFile}; use tower_http::trace::TraceLayer; @@ -30,8 +33,12 @@ async fn main() { .unwrap_or_else(|_| "8080".to_string()) .parse() .expect("FRONTEND_PORT must be a number"); + let use_tls = env::var("FRONTEND_TLS") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(true); // TLS on by default (Web Crypto requires HTTPS) let client = reqwest::Client::builder() + .danger_accept_invalid_certs(true) // backend is plain HTTP inside Docker .build() .expect("failed to build reqwest client"); @@ -40,25 +47,46 @@ async fn main() { client, }; - // Static file serving with SPA fallback. let serve_dir = ServeDir::new("dist").fallback(ServeFile::new("dist/index.html")); let app = Router::new() - // Proxy /api and everything under /api/ to the backend. .route("/api", any(proxy_to_backend)) .route("/api/*path", any(proxy_to_backend)) .with_state(state) - // Everything else: static files + SPA fallback. .fallback_service(serve_dir) .layer(TraceLayer::new_for_http()); let addr = format!("0.0.0.0:{port}"); - eprintln!("Frontend server listening on {addr}"); - eprintln!("Proxying /api/* to {backend_url}"); - eprintln!("Serving static files from dist/"); - let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind failed"); - axum::serve(listener, app).await.expect("server error"); + if use_tls { + eprintln!("Generating self-signed TLS certificate..."); + let cert = rcgen::generate_simple_self_signed(vec!["localhost", "normogen"]) + .expect("failed to generate cert"); + let cert_der = rcgen::Certificate::der(&cert); + let key_der = rcgen::Certificate::serialize_der_key(&cert); + let rustls_cert = axum_server::tls_rustls::RustlsConfig::from_der( + cert_der.to_vec(), + key_der.to_vec(), + ) + .await + .expect("failed to build rustls config"); + + eprintln!("Frontend server listening on https://{addr} (TLS)"); + eprintln!("Proxying /api/* to {backend_url}"); + eprintln!("Serving static files from dist/"); + + axum_server::bind_rustls(addr.parse().expect("bad addr"), rustls_cert) + .serve(app.into_make_service()) + .await + .expect("server error"); + } else { + eprintln!("Frontend server listening on http://{addr} (no TLS)"); + eprintln!("Proxying /api/* to {backend_url}"); + eprintln!("Serving static files from dist/"); + + let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind failed"); + axum::serve(listener, app).await.expect("server error"); + } } /// Forward a request to the backend, preserving method, headers, body, and