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.
This commit is contained in:
parent
47000beef4
commit
043b4442a7
2 changed files with 38 additions and 8 deletions
|
|
@ -12,3 +12,5 @@ axum = "0.7"
|
||||||
tokio = { version = "1", features = ["full"] }
|
tokio = { version = "1", features = ["full"] }
|
||||||
tower-http = { version = "0.5", features = ["fs", "trace"] }
|
tower-http = { version = "0.5", features = ["fs", "trace"] }
|
||||||
reqwest = { version = "0.12", features = ["json"] }
|
reqwest = { version = "0.12", features = ["json"] }
|
||||||
|
axum-server = { version = "0.7", features = ["tls-rustls"] }
|
||||||
|
rcgen = "0.13"
|
||||||
|
|
|
||||||
|
|
@ -3,6 +3,8 @@
|
||||||
//! 1. Reverse-proxies `/api/*` requests to the backend container.
|
//! 1. Reverse-proxies `/api/*` requests to the backend container.
|
||||||
//! 2. Serves the built Vite SPA bundle from `dist/` (static files).
|
//! 2. Serves the built Vite SPA bundle from `dist/` (static files).
|
||||||
//! 3. Falls back to `index.html` for any unmatched route (SPA history mode).
|
//! 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::{
|
use axum::{
|
||||||
body::Body,
|
body::Body,
|
||||||
|
|
@ -13,6 +15,7 @@ use axum::{
|
||||||
Router,
|
Router,
|
||||||
};
|
};
|
||||||
use std::env;
|
use std::env;
|
||||||
|
use std::sync::Arc;
|
||||||
use tower_http::services::{ServeDir, ServeFile};
|
use tower_http::services::{ServeDir, ServeFile};
|
||||||
use tower_http::trace::TraceLayer;
|
use tower_http::trace::TraceLayer;
|
||||||
|
|
||||||
|
|
@ -30,8 +33,12 @@ async fn main() {
|
||||||
.unwrap_or_else(|_| "8080".to_string())
|
.unwrap_or_else(|_| "8080".to_string())
|
||||||
.parse()
|
.parse()
|
||||||
.expect("FRONTEND_PORT must be a number");
|
.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()
|
let client = reqwest::Client::builder()
|
||||||
|
.danger_accept_invalid_certs(true) // backend is plain HTTP inside Docker
|
||||||
.build()
|
.build()
|
||||||
.expect("failed to build reqwest client");
|
.expect("failed to build reqwest client");
|
||||||
|
|
||||||
|
|
@ -40,26 +47,47 @@ async fn main() {
|
||||||
client,
|
client,
|
||||||
};
|
};
|
||||||
|
|
||||||
// Static file serving with SPA fallback.
|
|
||||||
let serve_dir = ServeDir::new("dist").fallback(ServeFile::new("dist/index.html"));
|
let serve_dir = ServeDir::new("dist").fallback(ServeFile::new("dist/index.html"));
|
||||||
|
|
||||||
let app = Router::new()
|
let app = Router::new()
|
||||||
// Proxy /api and everything under /api/ to the backend.
|
|
||||||
.route("/api", any(proxy_to_backend))
|
.route("/api", any(proxy_to_backend))
|
||||||
.route("/api/*path", any(proxy_to_backend))
|
.route("/api/*path", any(proxy_to_backend))
|
||||||
.with_state(state)
|
.with_state(state)
|
||||||
// Everything else: static files + SPA fallback.
|
|
||||||
.fallback_service(serve_dir)
|
.fallback_service(serve_dir)
|
||||||
.layer(TraceLayer::new_for_http());
|
.layer(TraceLayer::new_for_http());
|
||||||
|
|
||||||
let addr = format!("0.0.0.0:{port}");
|
let addr = format!("0.0.0.0:{port}");
|
||||||
eprintln!("Frontend server listening on {addr}");
|
|
||||||
|
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!("Proxying /api/* to {backend_url}");
|
||||||
eprintln!("Serving static files from dist/");
|
eprintln!("Serving static files from dist/");
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind failed");
|
let listener = tokio::net::TcpListener::bind(&addr).await.expect("bind failed");
|
||||||
axum::serve(listener, app).await.expect("server error");
|
axum::serve(listener, app).await.expect("server error");
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Forward a request to the backend, preserving method, headers, body, and
|
/// Forward a request to the backend, preserving method, headers, body, and
|
||||||
/// query string. Return the backend's response.
|
/// query string. Return the backend's response.
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue