v0.24.5
This commit is contained in:
commit
5e23e9af02
240 files changed
+99957
No files matched your search
+574
@@ -0,0 +1,574 @@
|
||||
//! RFC 9114 transport adapter: Quinn (QUIC/TLS 1.3) + h3 (HTTP/3/QPACK).
|
||||
//! Every request uses the SAME router, ACLs, auth, cache and response policies.
|
||||
//! No full-body buffering or 0-RTT replay. RFC 9220 WebSocket Extended CONNECT is supported; WebTransport is not.
|
||||
use crate::{
|
||||
metrics::Metrics,
|
||||
runtime::App,
|
||||
server::{LocalAddr, PeerAddr, ServerShutdown, Transport},
|
||||
vhost::Listener,
|
||||
};
|
||||
use anyhow::{ensure, Context as _, Result};
|
||||
use axum::{body::Body, Router};
|
||||
use bytes::{Buf, Bytes};
|
||||
use h3::error::Code;
|
||||
use http::{HeaderMap, HeaderValue, Method, Version};
|
||||
use http_body::{Body as HttpBody, Frame, SizeHint};
|
||||
use http_body_util::BodyExt;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
io,
|
||||
net::SocketAddr,
|
||||
pin::Pin,
|
||||
sync::{atomic::Ordering, Arc},
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
use tokio::{
|
||||
sync::{OwnedSemaphorePermit, Semaphore},
|
||||
task::JoinSet,
|
||||
};
|
||||
use tower::ServiceExt;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[serde(default, deny_unknown_fields)]
|
||||
pub struct Settings {
|
||||
pub alt_svc_max_age: u64,
|
||||
pub max_concurrent_connections: u32,
|
||||
pub max_concurrent_bidistream: u32,
|
||||
pub max_concurrent_unistream: u32,
|
||||
/// QUIC idle timeout in seconds; zero disables the transport idle timeout.
|
||||
pub max_idle_timeout: u64,
|
||||
}
|
||||
impl Default for Settings {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
alt_svc_max_age: 3600,
|
||||
max_concurrent_connections: 512,
|
||||
max_concurrent_bidistream: 100,
|
||||
max_concurrent_unistream: 100,
|
||||
max_idle_timeout: 10,
|
||||
}
|
||||
}
|
||||
}
|
||||
impl Settings {
|
||||
pub fn validate(&self) -> Result<()> {
|
||||
ensure!(
|
||||
self.alt_svc_max_age <= 604800,
|
||||
"HTTP/3 Alt-Svc lifetime must be at most seven days"
|
||||
);
|
||||
ensure!(
|
||||
(1..=4096).contains(&self.max_concurrent_connections),
|
||||
"HTTP/3 connections per listener must be 1..4096"
|
||||
);
|
||||
ensure!(
|
||||
(1..=1024).contains(&self.max_concurrent_bidistream),
|
||||
"HTTP/3 bidirectional streams must be 1..1024"
|
||||
);
|
||||
ensure!((3..=1024).contains(&self.max_concurrent_unistream),"HTTP/3 requires at least three peer unidirectional streams (control and QPACK), maximum 1024");
|
||||
ensure!(
|
||||
self.max_idle_timeout <= 86400,
|
||||
"HTTP/3 idle timeout must be 0..86400 seconds"
|
||||
);
|
||||
Ok(())
|
||||
}
|
||||
pub fn transport(&self) -> Result<quinn::TransportConfig> {
|
||||
self.validate()?;
|
||||
let mut transport = quinn::TransportConfig::default();
|
||||
transport
|
||||
.max_concurrent_bidi_streams(self.max_concurrent_bidistream.into())
|
||||
.max_concurrent_uni_streams(self.max_concurrent_unistream.into())
|
||||
.max_idle_timeout(if self.max_idle_timeout == 0 {
|
||||
None
|
||||
} else {
|
||||
Some(Duration::from_secs(self.max_idle_timeout).try_into()?)
|
||||
})
|
||||
.stream_receive_window((256 * 1024u32).into())
|
||||
.receive_window((8 * 1024 * 1024u32).into())
|
||||
.send_window(8 * 1024 * 1024)
|
||||
.datagram_receive_buffer_size(None);
|
||||
Ok(transport)
|
||||
}
|
||||
}
|
||||
pub fn crypto(
|
||||
tls: &Arc<rustls::ServerConfig>,
|
||||
policy: &crate::tls::ProtocolPolicy,
|
||||
) -> Result<Arc<quinn::crypto::rustls::QuicServerConfig>> {
|
||||
ensure!(
|
||||
policy.allows_http3(),
|
||||
"HTTP/3 requires TLS 1.3 and a TLS 1.3 cipher suite"
|
||||
);
|
||||
let mut provider = rustls::crypto::ring::default_provider();
|
||||
if !policy.cipher_suites.is_empty() {
|
||||
provider.cipher_suites.retain(|suite| {
|
||||
let name = format!("{:?}", suite.suite());
|
||||
policy
|
||||
.cipher_suites
|
||||
.iter()
|
||||
.any(|selected| selected == &name)
|
||||
});
|
||||
}
|
||||
let mut quic = rustls::ServerConfig::builder_with_provider(Arc::new(provider))
|
||||
.with_protocol_versions(&[&rustls::version::TLS13])?
|
||||
.with_no_client_auth()
|
||||
.with_cert_resolver(tls.cert_resolver.clone());
|
||||
quic.alpn_protocols = vec![b"h3".to_vec()];
|
||||
quic.max_early_data_size = 0;
|
||||
Ok(Arc::new(quinn::crypto::rustls::QuicServerConfig::try_from(
|
||||
quic,
|
||||
)?))
|
||||
}
|
||||
pub struct Bound {
|
||||
endpoint: quinn::Endpoint,
|
||||
server_config: quinn::ServerConfig,
|
||||
address: SocketAddr,
|
||||
settings: Settings,
|
||||
}
|
||||
pub fn bind(listener: Listener, state: &App) -> Result<Bound> {
|
||||
let settings = listener
|
||||
.http3
|
||||
.context("HTTP/3 is disabled on this listener")?;
|
||||
settings.validate()?;
|
||||
ensure!(listener.tls, "HTTP/3 listener requires TLS");
|
||||
let crypto = state
|
||||
.config
|
||||
.load()
|
||||
.quic_tls
|
||||
.clone()
|
||||
.context("HTTP/3 certificate unavailable")?;
|
||||
let mut server_config = quinn::ServerConfig::with_crypto(crypto);
|
||||
server_config
|
||||
.transport_config(Arc::new(settings.transport()?))
|
||||
.migration(false)
|
||||
.max_incoming(settings.max_concurrent_connections as usize)
|
||||
.incoming_buffer_size(64 * 1024)
|
||||
.incoming_buffer_size_total(1024 * 1024);
|
||||
let endpoint = quinn::Endpoint::server(server_config.clone(), listener.address)
|
||||
.with_context(|| format!("cannot bind HTTP/3 UDP {}", listener.address))?;
|
||||
Ok(Bound {
|
||||
endpoint,
|
||||
server_config,
|
||||
address: listener.address,
|
||||
settings,
|
||||
})
|
||||
}
|
||||
/// Derive the advertised endpoint from ACTUALLY bound sockets, never saved intent
|
||||
/// or a backend Alt-Svc header. A pending H3 enable is not advertised before restart.
|
||||
pub fn advertise(
|
||||
headers: &mut HeaderMap,
|
||||
local: Option<SocketAddr>,
|
||||
tls: bool,
|
||||
active: &[Listener],
|
||||
) {
|
||||
headers.remove("alt-svc");
|
||||
if !tls {
|
||||
return;
|
||||
}
|
||||
let Some(local) = local else {
|
||||
return;
|
||||
};
|
||||
if let Some(settings) = active
|
||||
.iter()
|
||||
.find(|binding| binding.matches(local, true))
|
||||
.and_then(|binding| binding.http3)
|
||||
{
|
||||
let value = format!("h3=\":{}\"; ma={}", local.port(), settings.alt_svc_max_age);
|
||||
if let Ok(value) = HeaderValue::from_str(&value) {
|
||||
headers.insert("alt-svc", value);
|
||||
}
|
||||
}
|
||||
}
|
||||
struct Lease {
|
||||
_permit: OwnedSemaphorePermit,
|
||||
metrics: Arc<Metrics>,
|
||||
}
|
||||
impl Drop for Lease {
|
||||
fn drop(&mut self) {
|
||||
self.metrics
|
||||
.http3_active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
self.metrics
|
||||
.active_connections
|
||||
.fetch_sub(1, Ordering::Relaxed);
|
||||
}
|
||||
}
|
||||
pub async fn serve(bound: Bound, router: Router, state: Arc<App>, grace: Duration) -> Result<()> {
|
||||
let slots = Arc::new(Semaphore::new(
|
||||
bound.settings.max_concurrent_connections as usize,
|
||||
));
|
||||
let mut tasks = JoinSet::new();
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_=state.shutdown.cancelled()=>break,
|
||||
_=tasks.join_next(),if !tasks.is_empty()=>{},
|
||||
incoming=bound.endpoint.accept()=>{
|
||||
let Some(incoming)=incoming else {break;};
|
||||
if !incoming.remote_address_validated() {let _=incoming.retry();continue;}
|
||||
let Ok(permit)=slots.clone().try_acquire_owned() else {
|
||||
state.metrics.http3_rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.rejected_connections.fetch_add(1,Ordering::Relaxed);
|
||||
incoming.refuse();continue;
|
||||
};
|
||||
let mut config=bound.server_config.clone();
|
||||
let Some(crypto)=state.config.load().quic_tls.clone() else {incoming.refuse();continue;};
|
||||
// Certificate changes apply to new handshakes without changing
|
||||
// active transport limits or regenerating the Retry token key.
|
||||
config.crypto=crypto;
|
||||
let connecting=match incoming.accept_with(Arc::new(config)) {Ok(c)=>c,Err(_)=>continue};
|
||||
let router=router.clone();let state=state.clone();let address=bound.address;
|
||||
let limit=bound.settings.max_concurrent_bidistream as usize;
|
||||
tasks.spawn(async move {
|
||||
let connection=tokio::select! {
|
||||
_=state.shutdown.cancelled()=>return,
|
||||
result=tokio::time::timeout(Duration::from_secs(10),connecting)=>match result {
|
||||
Ok(Ok(connection))=>connection,
|
||||
_=>{state.metrics.tls_handshake_errors.fetch_add(1,Ordering::Relaxed);return;},
|
||||
}
|
||||
};
|
||||
// A wildcard without destination metadata must fail closed.
|
||||
let local=match connection.local_ip() {
|
||||
Some(ip)=>SocketAddr::new(ip,address.port()),
|
||||
None if !address.ip().is_unspecified()=>address,
|
||||
None=>{connection.close(0x102u32.into(),b"local destination unavailable");return;},
|
||||
};
|
||||
state.metrics.connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.active_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.http3_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.http3_active_connections.fetch_add(1,Ordering::Relaxed);
|
||||
state.metrics.tls_connections.fetch_add(1,Ordering::Relaxed);
|
||||
let _lease=Lease {_permit:permit,metrics:state.metrics.clone()};
|
||||
if let Err(error)=connection_loop(connection,local,router,state,limit,grace).await {
|
||||
tracing::debug!(%error,"HTTP/3 connection ended");
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
// Connection tasks send GOAWAY and drain; only close the endpoint after the
|
||||
// grace period, not immediately after a response FIN (which can lose data).
|
||||
let _ = tokio::time::timeout(grace, async { while tasks.join_next().await.is_some() {} }).await;
|
||||
tasks.abort_all();
|
||||
while tasks.join_next().await.is_some() {}
|
||||
bound.endpoint.close(0x100u32.into(), b"shutdown");
|
||||
let _ = tokio::time::timeout(Duration::from_secs(1), bound.endpoint.wait_idle()).await;
|
||||
Ok(())
|
||||
}
|
||||
async fn connection_loop(
|
||||
connection: quinn::Connection,
|
||||
local: SocketAddr,
|
||||
router: Router,
|
||||
state: Arc<App>,
|
||||
limit: usize,
|
||||
grace: Duration,
|
||||
) -> Result<()> {
|
||||
let mut builder = h3::server::builder();
|
||||
builder.max_field_section_size(32 * 1024);
|
||||
// RFC 9220 remains an explicit experimental opt-in. Avoid advertising
|
||||
// Extended CONNECT on listeners that only use classic HTTP/1.1 WebSockets.
|
||||
builder.enable_extended_connect(state.config.load().websocket_rfc9220_on(local));
|
||||
let mut h3 = builder
|
||||
.build(h3::quinn::Connection::new(connection.clone()))
|
||||
.await?;
|
||||
let mut requests = JoinSet::new();
|
||||
let mut draining = false;
|
||||
let deadline = tokio::time::sleep(Duration::from_secs(86400));
|
||||
tokio::pin!(deadline);
|
||||
loop {
|
||||
tokio::select! {
|
||||
biased;
|
||||
_=state.shutdown.cancelled(),if !draining=>{
|
||||
draining=true;deadline.as_mut().reset(tokio::time::Instant::now()+grace);
|
||||
h3.shutdown(0).await?;
|
||||
},
|
||||
_=&mut deadline,if draining=>break,
|
||||
_=requests.join_next(),if !requests.is_empty()=>{},
|
||||
next=h3.accept()=>match next {
|
||||
Ok(Some(resolver))=>{
|
||||
// Completed JoinSet entries are drained before accepting more.
|
||||
if requests.len()>=limit {drop(resolver);continue;}
|
||||
let router=router.clone();let state=state.clone();let peer=connection.remote_address();
|
||||
requests.spawn(async move {
|
||||
let Ok(Ok((mut request,stream)))=tokio::time::timeout(state.header_timeout,resolver.resolve_request()).await else {return;};
|
||||
*request.version_mut()=Version::HTTP_3;
|
||||
let protocol=request.extensions().get::<h3::ext::Protocol>().map(|protocol|protocol.as_str().to_owned());
|
||||
let extended_connect=request.method()==Method::CONNECT && protocol.is_some();
|
||||
let websocket=extended_connect && protocol.as_deref()==Some("websocket");
|
||||
let (mut send,receive)=stream.split();
|
||||
if invalid_headers(request.headers()) {
|
||||
send.stop_stream(Code::H3_MESSAGE_ERROR);return;
|
||||
}
|
||||
let expected=request.headers().get("content-length").and_then(|v|v.to_str().ok()).and_then(|v|v.parse::<u64>().ok());
|
||||
let mut tunnel_receive=None;
|
||||
let mut request=if websocket {
|
||||
// RFC 9220 DATA belongs to the WebSocket byte stream, not
|
||||
// to an HTTP request body. Keep the receive half for the
|
||||
// transport bridge after the router accepts the tunnel.
|
||||
tunnel_receive=Some(receive);
|
||||
request.map(|_|Body::empty())
|
||||
} else if extended_connect {
|
||||
// Unsupported Extended CONNECT protocols are answered by
|
||||
// the router (501). They are not interpreted as HTTP bodies.
|
||||
drop(receive);
|
||||
request.map(|_|Body::empty())
|
||||
} else {
|
||||
request.map(|_|Body::new(ReceiveBody {stream:receive,expected,received:0,data_done:false,done:false}))
|
||||
};
|
||||
request.extensions_mut().insert(PeerAddr(peer));request.extensions_mut().insert(LocalAddr(local));
|
||||
request.extensions_mut().insert(Transport {tls:true});
|
||||
request.extensions_mut().insert(ServerShutdown(state.shutdown.clone()));
|
||||
let response=match router.oneshot(request).await {Ok(response)=>response,Err(never)=>match never {}};
|
||||
let (mut parts,mut body)=response.into_parts();
|
||||
let tunnel=parts.extensions.remove::<crate::proxy::websocket::H3Tunnel>();
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut parts.headers);
|
||||
parts.version=Version::HTTP_3;
|
||||
let response=http::Response::from_parts(parts,());
|
||||
let sent=async {
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_response(response)).await??;
|
||||
if let Some(tunnel)=tunnel {
|
||||
let Some(mut receive)=tunnel_receive.take() else {
|
||||
send.stop_stream(Code::H3_INTERNAL_ERROR);
|
||||
return Err(anyhow::anyhow!("HTTP/3 tunnel response without a WebSocket Extended CONNECT request"));
|
||||
};
|
||||
drop(body);
|
||||
if let Err(error)=crate::proxy::websocket::run_h3(state.clone(),&mut send,&mut receive,tunnel).await {
|
||||
// run_h3 owns RFC 9220 tunnel shutdown/reset semantics.
|
||||
// Do not overwrite its H3_REQUEST_CANCELLED with a
|
||||
// second generic H3_INTERNAL_ERROR here.
|
||||
tracing::debug!(%error,"HTTP/3 WebSocket tunnel closed");
|
||||
}
|
||||
return Ok::<(),anyhow::Error>(());
|
||||
}
|
||||
if let Some(mut receive)=tunnel_receive.take() {
|
||||
// The WebSocket bootstrap was rejected before becoming
|
||||
// a tunnel. Stop accepting request DATA after sending the
|
||||
// ordinary HTTP error response.
|
||||
receive.stop_sending(Code::H3_REQUEST_CANCELLED);
|
||||
}
|
||||
while let Some(frame)=body.frame().await {
|
||||
let frame=frame?;
|
||||
match frame.into_data() {
|
||||
Ok(data)=>{if !data.is_empty() {tokio::time::timeout(state.body_idle_timeout,send.send_data(data)).await??;}},
|
||||
Err(frame)=>{if let Ok(mut trailers)=frame.into_trailers() {
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut trailers);
|
||||
tokio::time::timeout(state.body_idle_timeout,send.send_trailers(trailers)).await??;
|
||||
}},
|
||||
}
|
||||
}
|
||||
tokio::time::timeout(state.body_idle_timeout,send.finish()).await??;
|
||||
Ok::<(),anyhow::Error>(())
|
||||
}.await;
|
||||
if let Err(error)=sent {tracing::debug!(%error,"HTTP/3 response interrupted");send.stop_stream(Code::H3_INTERNAL_ERROR);}
|
||||
});
|
||||
},
|
||||
Ok(None)=>break,
|
||||
Err(error)=>{requests.abort_all();return Err(error.into());},
|
||||
}
|
||||
}
|
||||
}
|
||||
let _ = tokio::time::timeout(grace, async {
|
||||
while requests.join_next().await.is_some() {}
|
||||
})
|
||||
.await;
|
||||
requests.abort_all();
|
||||
while requests.join_next().await.is_some() {}
|
||||
// Retain the transport until the client closes or the bounded drain expires.
|
||||
if draining {
|
||||
let _ = tokio::time::timeout(grace, connection.closed()).await;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
fn invalid_headers(headers: &HeaderMap) -> bool {
|
||||
[
|
||||
"connection",
|
||||
"keep-alive",
|
||||
"proxy-connection",
|
||||
"transfer-encoding",
|
||||
"upgrade",
|
||||
]
|
||||
.iter()
|
||||
.any(|name| headers.contains_key(*name))
|
||||
|| headers
|
||||
.get_all("te")
|
||||
.iter()
|
||||
.any(|value| value.as_bytes() != b"trailers")
|
||||
|| headers.get_all("content-length").iter().count() > 1
|
||||
|| headers.get("content-length").is_some_and(|value| {
|
||||
value
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|s| s.parse::<u64>().ok())
|
||||
.is_none()
|
||||
})
|
||||
}
|
||||
struct ReceiveBody {
|
||||
stream: h3::server::RequestStream<h3::quinn::RecvStream, Bytes>,
|
||||
expected: Option<u64>,
|
||||
received: u64,
|
||||
data_done: bool,
|
||||
done: bool,
|
||||
}
|
||||
impl HttpBody for ReceiveBody {
|
||||
type Data = Bytes;
|
||||
type Error = io::Error;
|
||||
fn poll_frame(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<std::result::Result<Frame<Bytes>, io::Error>>> {
|
||||
let this = self.get_mut();
|
||||
if this.done {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
if !this.data_done {
|
||||
match this.stream.poll_recv_data(cx) {
|
||||
Poll::Pending => return Poll::Pending,
|
||||
Poll::Ready(Err(error)) => {
|
||||
this.done = true;
|
||||
return Poll::Ready(Some(Err(io::Error::other(error.to_string()))));
|
||||
}
|
||||
Poll::Ready(Ok(Some(mut data))) => {
|
||||
let length = data.remaining();
|
||||
this.received = this.received.saturating_add(length as u64);
|
||||
if this
|
||||
.expected
|
||||
.is_some_and(|expected| this.received > expected)
|
||||
{
|
||||
drop(data);
|
||||
this.done = true;
|
||||
this.stream.stop_sending(Code::H3_MESSAGE_ERROR);
|
||||
return Poll::Ready(Some(Err(io::Error::other(
|
||||
"HTTP/3 Content-Length exceeded",
|
||||
))));
|
||||
}
|
||||
return Poll::Ready(Some(Ok(Frame::data(data.copy_to_bytes(length)))));
|
||||
}
|
||||
Poll::Ready(Ok(None)) => this.data_done = true,
|
||||
}
|
||||
}
|
||||
match this.stream.poll_recv_trailers(cx) {
|
||||
Poll::Pending => Poll::Pending,
|
||||
Poll::Ready(Err(error)) => {
|
||||
this.done = true;
|
||||
Poll::Ready(Some(Err(io::Error::other(error.to_string()))))
|
||||
}
|
||||
Poll::Ready(Ok(trailers)) => {
|
||||
this.done = true;
|
||||
if this
|
||||
.expected
|
||||
.is_some_and(|expected| expected != this.received)
|
||||
{
|
||||
this.stream.stop_sending(Code::H3_MESSAGE_ERROR);
|
||||
return Poll::Ready(Some(Err(io::Error::other(
|
||||
"HTTP/3 Content-Length mismatch",
|
||||
))));
|
||||
}
|
||||
Poll::Ready(trailers.map(|mut headers| {
|
||||
crate::proxy::headers::strip_hop_by_hop(&mut headers);
|
||||
Ok(Frame::trailers(headers))
|
||||
}))
|
||||
}
|
||||
}
|
||||
}
|
||||
fn is_end_stream(&self) -> bool {
|
||||
self.done
|
||||
}
|
||||
fn size_hint(&self) -> SizeHint {
|
||||
// A declared zero is not proof that the peer ended its stream. Unknown
|
||||
// lengths are verified by the cache's bounded empty-body probe.
|
||||
let mut hint = SizeHint::new();
|
||||
if self.done {
|
||||
hint.set_exact(0);
|
||||
} else if let Some(remaining) = self
|
||||
.expected
|
||||
.map(|n| n.saturating_sub(self.received))
|
||||
.filter(|n| *n > 0)
|
||||
{
|
||||
hint.set_upper(remaining);
|
||||
}
|
||||
hint
|
||||
}
|
||||
}
|
||||
impl Drop for ReceiveBody {
|
||||
fn drop(&mut self) {
|
||||
// Dropping the request stream cancels the receive side at QUIC level.
|
||||
// Explicit STOP_SENDING is used only from poll_frame paths where the
|
||||
// stream handle is available and a protocol error was detected.
|
||||
}
|
||||
}
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn requested_defaults_and_infinite_idle_are_valid() {
|
||||
let s = Settings::default();
|
||||
assert_eq!(
|
||||
(
|
||||
s.alt_svc_max_age,
|
||||
s.max_concurrent_connections,
|
||||
s.max_concurrent_bidistream,
|
||||
s.max_concurrent_unistream,
|
||||
s.max_idle_timeout
|
||||
),
|
||||
(3600, 512, 100, 100, 10)
|
||||
);
|
||||
assert!(s.transport().is_ok());
|
||||
assert!(Settings {
|
||||
max_idle_timeout: 0,
|
||||
..s
|
||||
}
|
||||
.transport()
|
||||
.is_ok());
|
||||
}
|
||||
#[test]
|
||||
fn critical_streams_and_resource_limits_are_validated() {
|
||||
assert!(Settings {
|
||||
max_concurrent_unistream: 2,
|
||||
..Default::default()
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
assert!(Settings {
|
||||
max_concurrent_connections: 0,
|
||||
..Default::default()
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
assert!(Settings {
|
||||
max_concurrent_bidistream: 0,
|
||||
..Default::default()
|
||||
}
|
||||
.validate()
|
||||
.is_err());
|
||||
}
|
||||
#[test]
|
||||
fn alt_svc_only_for_actual_tls_udp_listener() {
|
||||
let binding = Listener {
|
||||
address: "127.0.0.1:8443".parse().unwrap(),
|
||||
tls: true,
|
||||
http2: true,
|
||||
http2_settings: Some(crate::settings::Http2Connections::default()),
|
||||
h2c: false,
|
||||
http3: Some(Settings::default()),
|
||||
};
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert("alt-svc", "h3=\":9999\"".parse().unwrap());
|
||||
advertise(&mut h, Some(binding.address), true, &[binding]);
|
||||
assert_eq!(h["alt-svc"], "h3=\":8443\"; ma=3600");
|
||||
advertise(&mut h, Some(binding.address), false, &[binding]);
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
advertise(
|
||||
&mut h,
|
||||
Some("127.0.0.2:8443".parse().unwrap()),
|
||||
true,
|
||||
&[binding],
|
||||
);
|
||||
assert!(!h.contains_key("alt-svc"));
|
||||
}
|
||||
#[test]
|
||||
fn h3_rejects_http1_connection_specific_headers() {
|
||||
for name in ["connection", "transfer-encoding", "upgrade"] {
|
||||
let mut h = HeaderMap::new();
|
||||
h.insert(name, "anything".parse().unwrap());
|
||||
assert!(invalid_headers(&h));
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user