//! Optional TLS termination (HTTPS / WSS) for the Axum server. //! //! Enabled by setting both `TLS_CERT` and `TLS_KEY` (PEM files) in the //! environment / `.env`. It serves the exact same [`Router`] as the plain-HTTP //! path, but over `tokio-rustls`, so the browser sees a *secure context* — //! which is what `getUserMedia()` (the microphone) requires when the page is //! not on `localhost`. use anyhow::{Context, Result}; use axum::body::Body; use axum::Router; use hyper::server::conn::http1; use hyper::service::service_fn; use hyper_util::rt::TokioIo; use rustls::ServerConfig; use rustls_pki_types::pem::PemObject; use rustls_pki_types::{CertificateDer, PrivateKeyDer}; use std::path::Path; use std::sync::Arc; use tokio::net::TcpListener; use tokio_rustls::TlsAcceptor; use tower::Service; use tracing::{debug, info}; /// Load a PEM certificate chain + private key and build a rustls server config. pub fn load_server_config(cert_path: &Path, key_path: &Path) -> Result> { let certs = CertificateDer::pem_file_iter(cert_path) .with_context(|| format!("opening certificate {}", cert_path.display()))? .collect::, _>>() .with_context(|| format!("parsing certificate {}", cert_path.display()))?; if certs.is_empty() { anyhow::bail!("no certificates found in {}", cert_path.display()); } let key = PrivateKeyDer::from_pem_file(key_path) .with_context(|| format!("parsing private key {}", key_path.display()))?; let config = ServerConfig::builder() .with_no_client_auth() .with_single_cert(certs, key) .context("building rustls server config")?; Ok(Arc::new(config)) } /// Accept connections on `listener` and serve `app` over TLS until `shutdown` /// resolves. Each connection is handled on its own task; HTTP/1 upgrades are /// enabled so WebSocket handshakes work. pub async fn serve( listener: TcpListener, app: Router, tls_config: Arc, shutdown: impl std::future::Future, ) -> Result<()> { let acceptor = TlsAcceptor::from(tls_config); tokio::pin!(shutdown); loop { let (stream, peer) = tokio::select! { _ = &mut shutdown => break, accepted = listener.accept() => accepted?, }; let acceptor = acceptor.clone(); let app = app.clone(); tokio::spawn(async move { let stream = match acceptor.accept(stream).await { Ok(s) => s, Err(e) => { debug!(%peer, error = %e, "TLS handshake failed"); return; } }; let io = TokioIo::new(stream); // Bridge hyper's request type (with `Incoming` body) to the Axum // router, which expects `axum::body::Body`. let service = service_fn(move |req: hyper::Request| { let mut router = app.clone(); async move { let (parts, body) = req.into_parts(); let req = hyper::Request::from_parts(parts, Body::new(body)); router.call(req).await } }); if let Err(e) = http1::Builder::new() .serve_connection(io, service) .with_upgrades() .await { debug!(%peer, error = %e, "connection error"); } }); } info!("TLS listener stopped"); Ok(()) }