#![cfg(feature = "http3")]
use crate::app::App;
use crate::body::Body;
use crate::pem::{load_certs, load_key};
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;
const DEFAULT_MAX_STREAMS: u32 = 200;
pub fn server_config_from_pem(cert_path: &str, key_path: &str) -> io::Result<quinn::ServerConfig> {
server_config_from_pem_with_limits(cert_path, key_path, TransportLimits::defaults())
}
fn idle_ms_for(keep_alive_ms: u64) -> u64 {
match keep_alive_ms {
0 => DEFAULT_IDLE_MS,
ms => ms,
}
}
fn idle_timeout(idle_ms: u64) -> io::Result<quinn::IdleTimeout> {
std::time::Duration::from_millis(idle_ms)
.try_into()
.map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("keep_alive_ms = {idle_ms} is too large for a QUIC idle timeout: {e}"),
)
})
}
fn max_streams_for(h2_max_concurrent_streams: u32) -> quinn::VarInt {
match h2_max_concurrent_streams {
0 => quinn::VarInt::from_u32(DEFAULT_MAX_STREAMS),
n => quinn::VarInt::from_u32(n),
}
}
struct TransportLimits {
idle_ms: u64,
max_streams: quinn::VarInt,
}
impl TransportLimits {
fn from(cfg: &crate::app::ServerConfig) -> Self {
Self {
idle_ms: idle_ms_for(cfg.keep_alive_ms),
max_streams: max_streams_for(cfg.h2_max_concurrent_streams),
}
}
fn defaults() -> Self {
Self::from(&crate::app::ServerConfig::default())
}
}
fn server_config_from_pem_with_limits(
cert_path: &str,
key_path: &str,
limits: TransportLimits,
) -> 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(idle_timeout(limits.idle_ms)?));
transport.max_concurrent_bidi_streams(limits.max_streams);
config.transport_config(Arc::new(transport));
Ok(config)
}
pub async fn serve(app: App, addr: SocketAddr, cert_path: &str, key_path: &str) -> io::Result<()> {
let limits = TransportLimits::from(app.config());
let config = server_config_from_pem_with_limits(cert_path, key_path, limits)?;
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)));
let handshakes = (app.config().max_tls_handshakes > 0)
.then(|| std::sync::Arc::new(tokio::sync::Semaphore::new(app.config().max_tls_handshakes)));
let handshake_timeout = (app.config().tls_handshake_timeout_ms > 0)
.then(|| std::time::Duration::from_millis(app.config().tls_handshake_timeout_ms));
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,
};
let handshakes = handshakes.clone();
tokio::spawn(async move {
let _permit = permit;
let queue_and_shake = async {
let _handshake_permit = match &handshakes {
Some(sem) => sem.clone().acquire_owned().await.ok(),
None => None,
};
incoming.await
};
let accepted = match handshake_timeout {
Some(limit) => match tokio::time::timeout(limit, queue_and_shake).await {
Ok(res) => res,
Err(_) => {
tracing::debug!("http3 handshake timed out");
return;
}
},
None => queue_and_shake.await,
};
match accepted {
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;
if let Some(field) = connection_specific_field(&parts.headers) {
tracing::debug!(
field,
"rejected an http3 request with a connection-specific field"
);
return send_status(&app, &mut stream, http::StatusCode::BAD_REQUEST).await;
}
let declared: Option<u64> = parts
.headers
.get(http::header::CONTENT_LENGTH)
.and_then(|v| v.to_str().ok())
.and_then(|v| v.parse::<u64>().ok());
if let Some(declared) = declared {
if declared > max_body as u64 {
return send_status(&app, &mut stream, http::StatusCode::PAYLOAD_TOO_LARGE).await;
}
}
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, declared).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::Truncated { declared, got }) => {
tracing::debug!(
declared,
got,
"http3 request body did not match its content-length"
);
return send_status(&app, &mut stream, http::StatusCode::BAD_REQUEST).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 mut headers = parts.headers.clone();
if let Some(authority) = parts.uri.authority() {
if let Ok(value) = http::HeaderValue::from_str(authority.as_str()) {
headers.insert(http::header::HOST, value);
}
}
let process = app.process_with_extensions(
parts.method.clone(),
normalise_uri(&parts.uri, &parts.method),
headers,
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),
Truncated { declared: u64, got: u64 },
}
async fn read_body<S>(
stream: &mut h3::server::RequestStream<S, Bytes>,
max_body: usize,
declared: Option<u64>,
) -> 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) => {
let got = buf.len() as u64;
return match declared {
Some(n) if n != got => Err(BodyRefused::Truncated { declared: n, got }),
_ => 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(())
}
const CONNECTION_SPECIFIC: [&str; 5] = [
"connection",
"keep-alive",
"proxy-connection",
"transfer-encoding",
"upgrade",
];
fn sanitise(mut headers: HeaderMap) -> HeaderMap {
for name in CONNECTION_SPECIFIC {
headers.remove(name);
}
headers
}
fn connection_specific_field(headers: &HeaderMap) -> Option<&'static str> {
for name in CONNECTION_SPECIFIC {
if headers.contains_key(name) {
return Some(name);
}
}
let te_is_allowed = headers
.get_all(http::header::TE)
.iter()
.all(|v| v.as_bytes().eq_ignore_ascii_case(b"trailers"));
(!te_is_allowed).then_some("te")
}
#[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),
}
}
#[test]
fn a_configured_keep_alive_becomes_the_quic_idle_bound() {
assert_eq!(idle_ms_for(5_000), 5_000);
}
#[test]
fn a_zero_keep_alive_keeps_the_default_bound() {
assert_eq!(idle_ms_for(0), DEFAULT_IDLE_MS);
assert_ne!(idle_ms_for(0), 0);
}
#[test]
fn the_default_bound_agrees_with_the_default_keep_alive() {
assert_eq!(
DEFAULT_IDLE_MS,
crate::app::ServerConfig::default().keep_alive_ms
);
}
#[test]
fn a_configured_stream_limit_becomes_the_quic_bidi_cap() {
assert_eq!(max_streams_for(100), quinn::VarInt::from_u32(100));
}
#[test]
fn a_zero_stream_limit_keeps_the_default_rather_than_advertising_a_maximum() {
assert_eq!(
max_streams_for(0),
quinn::VarInt::from_u32(DEFAULT_MAX_STREAMS)
);
assert_ne!(max_streams_for(0), quinn::VarInt::from_u32(0));
assert_ne!(max_streams_for(0), quinn::VarInt::MAX);
assert!(
u64::from(max_streams_for(0)) <= 100_000,
"an advertised stream cap is allocated against eagerly; it must stay small"
);
}
#[test]
fn no_configured_stream_limit_is_large_enough_to_allocate_against() {
for configured in [1u32, 200, 10_000] {
assert!(
u64::from(max_streams_for(configured)) <= 100_000,
"{configured} produced a cap quinn would allocate eagerly against"
);
}
}
#[test]
fn the_transport_defaults_come_from_the_app_defaults() {
let cfg = crate::app::ServerConfig::default();
let limits = TransportLimits::defaults();
assert_eq!(limits.idle_ms, idle_ms_for(cfg.keep_alive_ms));
assert_eq!(
limits.max_streams,
max_streams_for(cfg.h2_max_concurrent_streams)
);
}
#[test]
fn an_ordinary_keep_alive_converts_to_an_idle_timeout() {
assert!(idle_timeout(75_000).is_ok());
}
#[test]
fn a_keep_alive_past_the_varint_range_is_refused_rather_than_panicking() {
match idle_timeout(u64::MAX) {
Ok(_) => panic!("expected an error for an out-of-range idle timeout"),
Err(e) => {
assert_eq!(e.kind(), io::ErrorKind::InvalidInput);
assert!(
e.to_string().contains("keep_alive_ms"),
"the error should name the setting at fault, got: {e}"
);
}
}
}
}