fix: HTTPS with self-signed cert for Web Crypto API support
Some checks failed
Lint and Build / format (push) Successful in 43s
Lint and Build / clippy (push) Successful in 2m42s
Lint and Build / build (push) Successful in 3m44s
Lint and Build / test (push) Failing after 1s

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.
This commit is contained in:
goose 2026-07-14 11:43:56 -03:00
parent 47000beef4
commit 043b4442a7
2 changed files with 38 additions and 8 deletions

View file

@ -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"

View file

@ -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