98 lines
3.5 KiB
Rust
98 lines
3.5 KiB
Rust
//! 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(())
|
|
}
|