#![cfg(feature = "http3")]
use crate::app::App;
use crate::body::Body;
use bytes::{Buf, Bytes};
use http::{HeaderMap, Method, Request, Uri};
use std::io;
use std::net::SocketAddr;
use std::sync::Arc;
const DEFAULT_IDLE_MS: u64 = 75_000;
pub fn server_config_from_pem(cert_path: &str, key_path: &str) -> io::Result<quinn::ServerConfig> {
let certs = load_certs(cert_path)?;
let key = load_key(key_path)?;
let mut tls = rustls::ServerConfig::builder_with_protocol_versions(&[&rustls::version::TLS13])
.with_no_client_auth()
.with_single_cert(certs, key)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
tls.alpn_protocols = vec![b"h3".to_vec()];
let quic = quinn::crypto::rustls::QuicServerConfig::try_from(tls)
.map_err(|e| io::Error::new(io::ErrorKind::InvalidInput, e))?;
let mut config = quinn::ServerConfig::with_crypto(Arc::new(quic));
let mut transport = quinn::TransportConfig::default();
transport.max_idle_timeout(Some(
std::time::Duration::from_millis(DEFAULT_IDLE_MS)
.try_into()
.expect("idle timeout fits in a QUIC varint"),
));
config.transport_config(Arc::new(transport));
Ok(config)
}
fn load_certs(path: &str) -> io::Result<Vec<rustls::pki_types::CertificateDer<'static>>> {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
rustls_pemfile::certs(&mut reader).collect::<Result<Vec<_>, _>>()
}
fn load_key(path: &str) -> io::Result<rustls::pki_types::PrivateKeyDer<'static>> {
let file = std::fs::File::open(path)?;
let mut reader = std::io::BufReader::new(file);
rustls_pemfile::private_key(&mut reader)?
.ok_or_else(|| io::Error::new(io::ErrorKind::InvalidInput, "no private key found"))
}
pub async fn serve(app: App, addr: SocketAddr, cert_path: &str, key_path: &str) -> io::Result<()> {
let config = server_config_from_pem(cert_path, key_path)?;
serve_with_config(app, addr, config).await
}
pub async fn serve_with_config(
app: App,
addr: SocketAddr,
config: quinn::ServerConfig,
) -> io::Result<()> {
Http3Server::bind(addr, config)?.serve(app).await;
Ok(())
}
pub struct Http3Server {
endpoint: quinn::Endpoint,
}
impl std::fmt::Debug for Http3Server {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Http3Server")
.field("local_addr", &self.endpoint.local_addr().ok())
.finish_non_exhaustive()
}
}
impl Http3Server {
pub fn bind(addr: SocketAddr, config: quinn::ServerConfig) -> io::Result<Self> {
Ok(Self {
endpoint: quinn::Endpoint::server(config, addr)?,
})
}
pub fn local_addr(&self) -> io::Result<SocketAddr> {
self.endpoint.local_addr()
}
pub async fn serve(self, app: App) {
accept_loop(app, self.endpoint).await;
}
}
async fn accept_loop(app: App, endpoint: quinn::Endpoint) {
let slots = (app.config().max_connections > 0)
.then(|| std::sync::Arc::new(tokio::sync::Semaphore::new(app.config().max_connections)));
while let Some(incoming) = endpoint.accept().await {
let app = app.clone();
let permit = match &slots {
Some(sem) => sem.clone().acquire_owned().await.ok(),
None => None,
};
tokio::spawn(async move {
let _permit = permit;
match incoming.await {
Ok(connection) => {
if let Err(e) = serve_connection(app, connection).await {
tracing::debug!(error = %e, "http3 connection ended");
}
}
Err(e) => tracing::debug!(error = %e, "http3 handshake failed"),
}
});
}
}
async fn serve_connection(
app: App,
connection: quinn::Connection,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>> {
let peer = connection.remote_address();
let mut h3 = h3::server::builder()
.max_field_section_size(app.config().h2_max_header_list_size as u64)
.build(h3_quinn::Connection::new(connection))
.await?;
loop {
match h3.accept().await {
Ok(Some(resolver)) => {
let app = app.clone();
tokio::spawn(async move {
let (request, stream) = match resolver.resolve_request().await {
Ok(pair) => pair,
Err(e) => {
tracing::debug!(error = %e, "http3 request headers failed");
return;
}
};
if let Err(e) = serve_request(app, request, stream, peer).await {
tracing::debug!(error = %e, "http3 request failed");
}
});
}
Ok(None) => return Ok(()),
Err(e) => return Err(e.into()),
}
}
}
async fn serve_request<S>(
app: App,
request: Request<()>,
mut stream: h3::server::RequestStream<S, Bytes>,
peer: SocketAddr,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
S: h3::quic::BidiStream<Bytes>,
{
let (parts, _) = request.into_parts();
let max_body = app.config().max_body_bytes;
let timeout_ms = app.config().request_timeout_ms;
let deadline = (timeout_ms > 0)
.then(|| tokio::time::Instant::now() + std::time::Duration::from_millis(timeout_ms));
let read = async { read_body(&mut stream, max_body).await };
let read = match deadline {
Some(at) => match tokio::time::timeout_at(at, read).await {
Ok(r) => r,
Err(_) => {
return send_status(&app, &mut stream, http::StatusCode::REQUEST_TIMEOUT).await;
}
},
None => read.await,
};
let body = match read {
Ok(body) => body,
Err(BodyRefused::TooLarge) => {
return send_status(&app, &mut stream, http::StatusCode::PAYLOAD_TOO_LARGE).await;
}
Err(BodyRefused::Incomplete(e)) => {
tracing::debug!(error = %e, "http3 request body ended before it was complete");
stream.stop_stream(h3::error::Code::H3_REQUEST_INCOMPLETE);
return Ok(());
}
};
let mut extensions = http::Extensions::new();
extensions.insert(crate::call::PeerAddr(peer));
let process = app.process_with_extensions(
parts.method.clone(),
normalise_uri(&parts.uri, &parts.method),
parts.headers.clone(),
body,
extensions,
);
let response = match deadline {
None => process.await,
Some(at) => match tokio::time::timeout_at(at, process).await {
Ok(res) => res,
Err(_) => crate::response::Response::text("Request Timeout")
.with_status(http::StatusCode::REQUEST_TIMEOUT),
},
};
send_response(&app, &mut stream, response, parts.method == Method::HEAD).await
}
async fn send_status<S>(
app: &App,
stream: &mut h3::server::RequestStream<S, Bytes>,
status: http::StatusCode,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
S: h3::quic::BidiStream<Bytes>,
{
let mut response = http::Response::builder().status(status).body(())?;
app.apply_security_headers(response.headers_mut(), true);
stream.send_response(response).await?;
stream.finish().await?;
Ok(())
}
enum BodyRefused {
TooLarge,
Incomplete(h3::error::StreamError),
}
async fn read_body<S>(
stream: &mut h3::server::RequestStream<S, Bytes>,
max_body: usize,
) -> Result<Bytes, BodyRefused>
where
S: h3::quic::BidiStream<Bytes>,
{
let mut buf = bytes::BytesMut::new();
loop {
match stream.recv_data().await {
Ok(Some(mut chunk)) => {
let piece = chunk.copy_to_bytes(chunk.remaining());
if buf.len() + piece.len() > max_body {
return Err(BodyRefused::TooLarge);
}
buf.extend_from_slice(&piece);
}
Ok(None) => return Ok(buf.freeze()),
Err(e) => return Err(BodyRefused::Incomplete(e)),
}
}
}
fn normalise_uri(uri: &Uri, method: &Method) -> Uri {
if method == Method::CONNECT || uri.path() == "*" {
return uri.clone();
}
let Some(path_and_query) = uri.path_and_query() else {
return uri.clone();
};
path_and_query
.as_str()
.parse()
.unwrap_or_else(|_| uri.clone())
}
async fn send_response<S>(
app: &App,
stream: &mut h3::server::RequestStream<S, Bytes>,
response: crate::Response,
head_only: bool,
) -> Result<(), Box<dyn std::error::Error + Send + Sync>>
where
S: h3::quic::BidiStream<Bytes>,
{
let mut builder = http::Response::builder().status(response.status);
if let Some(headers) = builder.headers_mut() {
*headers = sanitise(response.headers);
app.apply_security_headers(headers, true);
}
stream.send_response(builder.body(())?).await?;
if !head_only {
match response.body {
Body::Bytes(bytes) => {
if !bytes.is_empty() {
stream.send_data(bytes).await?;
}
}
Body::Stream(mut chunks) => {
use futures_util::StreamExt;
while let Some(chunk) = chunks.next().await {
match chunk {
Ok(bytes) => stream.send_data(bytes).await?,
Err(e) => {
tracing::debug!(error = %e, "http3 response body failed mid-stream");
stream.stop_stream(h3::error::Code::H3_INTERNAL_ERROR);
return Ok(());
}
}
}
}
}
}
stream.finish().await?;
Ok(())
}
fn sanitise(mut headers: HeaderMap) -> HeaderMap {
for name in [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
] {
headers.remove(name);
}
headers
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn an_absolute_target_becomes_origin_form() {
let uri: Uri = "https://example.com/a/b?x=1".parse().unwrap();
assert_eq!(
normalise_uri(&uri, &Method::GET).to_string(),
"/a/b?x=1",
"a handler should see the same target on every transport"
);
}
#[test]
fn an_origin_form_target_is_left_alone() {
let uri: Uri = "/a/b".parse().unwrap();
assert_eq!(normalise_uri(&uri, &Method::GET).to_string(), "/a/b");
}
#[test]
fn a_connect_target_is_left_alone() {
let uri: Uri = "example.com:443".parse().unwrap();
assert_eq!(
normalise_uri(&uri, &Method::CONNECT).to_string(),
"example.com:443"
);
}
#[test]
fn connection_specific_headers_are_dropped() {
let mut headers = HeaderMap::new();
headers.insert("connection", "keep-alive".parse().unwrap());
headers.insert("transfer-encoding", "chunked".parse().unwrap());
headers.insert("content-type", "text/plain".parse().unwrap());
let clean = sanitise(headers);
assert!(clean.get("connection").is_none());
assert!(clean.get("transfer-encoding").is_none());
assert_eq!(clean.get("content-type").unwrap(), "text/plain");
}
#[test]
fn a_missing_certificate_is_reported_rather_than_panicking() {
match server_config_from_pem("/nonexistent/cert.pem", "/nonexistent/key.pem") {
Ok(_) => panic!("expected an error for missing files"),
Err(e) => assert_eq!(e.kind(), io::ErrorKind::NotFound),
}
}
}