This commit is contained in:
valenti
2026-10-16 11:36:02 +01:00
commit 236fd509ca
59 changed files with 18858 additions and 0 deletions
+97
View File
@@ -0,0 +1,97 @@
//! 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<Arc<ServerConfig>> {
let certs = CertificateDer::pem_file_iter(cert_path)
.with_context(|| format!("opening certificate {}", cert_path.display()))?
.collect::<Result<Vec<_>, _>>()
.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<ServerConfig>,
shutdown: impl std::future::Future<Output = ()>,
) -> 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<hyper::body::Incoming>| {
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(())
}