This commit is contained in:
Mateusz Gruszczyński committed 2026-10-05 09:30:21 +02:00
1 parent 1dea7f55c8
commit c819f0a335
151 files changed
+7610 -4264

No files matched your search

+52 -15
View File
@@ -3,6 +3,7 @@ use anyhow::{Context, Result};
use axum::{body::Body, Router};
use base64::{engine::general_purpose::URL_SAFE_NO_PAD, Engine};
use bytes::{Buf, Bytes, BytesMut};
use http_body::Body as HttpBody;
use hyper::{
body::Incoming,
server::conn::{http1, http2},
@@ -10,7 +11,6 @@ use hyper::{
Request, Response,
};
use hyper_util::rt::{TokioExecutor, TokioIo, TokioTimer};
use http_body::Body as HttpBody;
use std::{
convert::Infallible,
io,
@@ -32,7 +32,6 @@ use tokio::{
use tokio_util::sync::CancellationToken;
use tower::ServiceExt;
const HTTP2_PRIOR_KNOWLEDGE_PREFACE: &[u8; 24] = b"PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n";
const H2_FRAME_HEADER_LEN: usize = 9;
@@ -45,7 +44,10 @@ fn header_has_token(headers: &http::HeaderMap, name: &str, token: &str) -> bool
.any(|value| value.trim().eq_ignore_ascii_case(token))
}
fn single_header<'a>(headers: &'a http::HeaderMap, name: &str) -> std::result::Result<&'a str, &'static str> {
fn single_header<'a>(
headers: &'a http::HeaderMap,
name: &str,
) -> std::result::Result<&'a str, &'static str> {
let mut values = headers.get_all(name).iter();
let value = values.next().ok_or("missing required h2c header")?;
if values.next().is_some() {
@@ -97,9 +99,15 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
if request.version() != http::Version::HTTP_11 {
return Err("h2c upgrade requires HTTP/1.1");
}
if !header_has_token(request.headers(), http::header::CONNECTION.as_str(), "upgrade")
|| !header_has_token(request.headers(), http::header::CONNECTION.as_str(), "http2-settings")
{
if !header_has_token(
request.headers(),
http::header::CONNECTION.as_str(),
"upgrade",
) || !header_has_token(
request.headers(),
http::header::CONNECTION.as_str(),
"http2-settings",
) {
return Err("Connection must nominate Upgrade and HTTP2-Settings");
}
if !single_header(request.headers(), http::header::UPGRADE.as_str())?
@@ -114,10 +122,19 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
if settings.len() > 36 || settings.len() % 6 != 0 {
return Err("invalid HTTP2-Settings payload");
}
if request.headers().contains_key(http::header::TRANSFER_ENCODING) {
if request
.headers()
.contains_key(http::header::TRANSFER_ENCODING)
{
return Err("h2c upgrade request body is not supported");
}
if request.headers().get_all(http::header::CONTENT_LENGTH).iter().count() > 1 {
if request
.headers()
.get_all(http::header::CONTENT_LENGTH)
.iter()
.count()
> 1
{
return Err("duplicate Content-Length on h2c upgrade");
}
if request
@@ -132,7 +149,12 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
.uri()
.authority()
.map(|value| value.as_str())
.or_else(|| request.headers().get(http::header::HOST).and_then(|value| value.to_str().ok()))
.or_else(|| {
request
.headers()
.get(http::header::HOST)
.and_then(|value| value.to_str().ok())
})
.ok_or("h2c upgrade requires Host/:authority")?;
let path = request
.uri()
@@ -167,7 +189,9 @@ fn h2_headers_frame<B>(request: &Request<B>) -> std::result::Result<Bytes, &'sta
if name == "te" && !value.as_bytes().eq_ignore_ascii_case(b"trailers") {
continue;
}
let value = value.to_str().map_err(|_| "non-text header in h2c bootstrap")?;
let value = value
.to_str()
.map_err(|_| "non-text header in h2c bootstrap")?;
hpack_literal(&mut block, name, value);
}
if block.len() > 32_768 {
@@ -215,15 +239,21 @@ impl<I> H2cUpgradeIo<I> {
}
fn try_finish_bootstrap(&mut self) -> io::Result<()> {
if self.bootstrap_done || self.bootstrap.len() < HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() + H2_FRAME_HEADER_LEN {
if self.bootstrap_done
|| self.bootstrap.len() < HTTP2_PRIOR_KNOWLEDGE_PREFACE.len() + H2_FRAME_HEADER_LEN
{
return Ok(());
}
if &self.bootstrap[..HTTP2_PRIOR_KNOWLEDGE_PREFACE.len()] != HTTP2_PRIOR_KNOWLEDGE_PREFACE {
return Err(io::Error::new(io::ErrorKind::InvalidData, "invalid HTTP/2 client preface after h2c upgrade"));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"invalid HTTP/2 client preface after h2c upgrade",
));
}
let offset = HTTP2_PRIOR_KNOWLEDGE_PREFACE.len();
let header = &self.bootstrap[offset..offset + H2_FRAME_HEADER_LEN];
let length = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
let length =
((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
if length > 16_384
|| header[3] != 0x4
|| header[4] != 0
@@ -232,7 +262,10 @@ impl<I> H2cUpgradeIo<I> {
|| header[7] != 0
|| header[8] != 0
{
return Err(io::Error::new(io::ErrorKind::InvalidData, "h2c client preface must begin with a non-ACK SETTINGS frame"));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"h2c client preface must begin with a non-ACK SETTINGS frame",
));
}
let end = offset + H2_FRAME_HEADER_LEN + length;
if self.bootstrap.len() < end {
@@ -296,7 +329,11 @@ impl<I: AsyncRead + Unpin> AsyncRead for H2cUpgradeIo<I> {
}
impl<I: AsyncWrite + Unpin> AsyncWrite for H2cUpgradeIo<I> {
fn poll_write(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>, buf: &[u8]) -> Poll<io::Result<usize>> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut TaskContext<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.inner).poll_write(cx, buf)
}
fn poll_flush(mut self: Pin<&mut Self>, cx: &mut TaskContext<'_>) -> Poll<io::Result<()>> {
+8 -2
View File
@@ -93,8 +93,14 @@ async fn http2_connection<I>(
};
match tokio::time::timeout(drain, &mut conn).await {
Ok(Ok(())) => tracing::debug!(?cause, "HTTP/2 graceful shutdown drained"),
Ok(Err(error)) => tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error"),
Err(_) => tracing::debug!(?cause, drain_seconds = drain.as_secs_f64(), "HTTP/2 drain timeout reached; closing transport"),
Ok(Err(error)) => {
tracing::debug!(?cause, %error, "HTTP/2 graceful shutdown ended with protocol error")
}
Err(_) => tracing::debug!(
?cause,
drain_seconds = drain.as_secs_f64(),
"HTTP/2 drain timeout reached; closing transport"
),
}
}
+50 -18
View File
@@ -55,13 +55,18 @@ mod h2c_tests {
) -> io::Result<(u8, u8, u32, Vec<u8>)> {
let mut header = [0u8; 9];
stream.read_exact(&mut header).await?;
let length = ((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
let length =
((header[0] as usize) << 16) | ((header[1] as usize) << 8) | header[2] as usize;
if length > 64 * 1024 {
return Err(io::Error::new(io::ErrorKind::InvalidData, "unexpectedly large HTTP/2 test frame"));
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"unexpectedly large HTTP/2 test frame",
));
}
let mut payload = vec![0u8; length];
stream.read_exact(&mut payload).await?;
let stream_id = u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7fff_ffff;
let stream_id =
u32::from_be_bytes([header[5], header[6], header[7], header[8]]) & 0x7fff_ffff;
Ok((header[3], header[4], stream_id, payload))
}
@@ -72,7 +77,11 @@ mod h2c_tests {
async fn start_direct_h2(
router: Router,
settings: Http2Connections,
) -> (tokio::net::TcpStream, CancellationToken, tokio::task::JoinHandle<()>) {
) -> (
tokio::net::TcpStream,
CancellationToken,
tokio::task::JoinHandle<()>,
) {
let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
let address = listener.local_addr().unwrap();
let stop = CancellationToken::new();
@@ -100,7 +109,10 @@ mod h2c_tests {
.await;
});
let mut client = tokio::net::TcpStream::connect(address).await.unwrap();
client.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE).await.unwrap();
client
.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE)
.await
.unwrap();
client
.write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0])
.await
@@ -144,7 +156,10 @@ mod h2c_tests {
assert!(response.starts_with("HTTP/1.1 101"), "{response}");
assert!(response.to_ascii_lowercase().contains("upgrade: h2c"));
client.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE).await.unwrap();
client
.write_all(HTTP2_PRIOR_KNOWLEDGE_PREFACE)
.await
.unwrap();
// Client connection preface must begin with a non-ACK SETTINGS frame.
client
.write_all(&[0, 0, 0, 0x4, 0, 0, 0, 0, 0])
@@ -154,12 +169,10 @@ mod h2c_tests {
let mut saw_stream_one_headers = false;
let mut body = Vec::new();
for _ in 0..16 {
let (kind, flags, stream_id, payload) = tokio::time::timeout(
Duration::from_secs(2),
read_h2_frame(&mut client),
)
.await
.expect("HTTP/2 response frame timeout");
let (kind, flags, stream_id, payload) =
tokio::time::timeout(Duration::from_secs(2), read_h2_frame(&mut client))
.await
.expect("HTTP/2 response frame timeout");
if kind == 0x1 && stream_id == 1 {
saw_stream_one_headers = true;
}
@@ -251,7 +264,10 @@ mod h2c_tests {
.header("http2-settings", "")
.body(())
.unwrap();
client.write_all(&h2_headers_frame(&upgrade).unwrap()).await.unwrap();
client
.write_all(&h2_headers_frame(&upgrade).unwrap())
.await
.unwrap();
let saw_goaway_and_close = tokio::time::timeout(Duration::from_secs(4), async {
let mut saw_goaway = false;
@@ -259,7 +275,12 @@ mod h2c_tests {
match try_read_h2_frame(&mut client).await {
Ok((0x7, _, 0, _)) => saw_goaway = true,
Ok(_) => {}
Err(error) if matches!(error.kind(), io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset) => {
Err(error)
if matches!(
error.kind(),
io::ErrorKind::UnexpectedEof | io::ErrorKind::ConnectionReset
) =>
{
return saw_goaway;
}
Err(_) => return saw_goaway,
@@ -268,7 +289,10 @@ mod h2c_tests {
})
.await
.expect("lifetime/drain shutdown exceeded its bound");
assert!(saw_goaway_and_close, "lifetime shutdown must send GOAWAY before drain timeout closes TCP");
assert!(
saw_goaway_and_close,
"lifetime shutdown must send GOAWAY before drain timeout closes TCP"
);
server.await.unwrap();
drop(blocker);
}
@@ -303,7 +327,10 @@ mod h2c_tests {
.header("http2-settings", "")
.body(())
.unwrap();
client.write_all(&h2_headers_frame(&request).unwrap()).await.unwrap();
client
.write_all(&h2_headers_frame(&request).unwrap())
.await
.unwrap();
let release = blocker.clone();
tokio::spawn(async move {
tokio::time::sleep(Duration::from_millis(1_250)).await;
@@ -318,7 +345,9 @@ mod h2c_tests {
Ok((0x7, _, 0, _)) => saw_goaway = true,
Ok((kind, flags, 1, _)) if (kind == 0x0 || kind == 0x1) && flags & 0x1 != 0 => {
saw_end_stream = true;
if saw_goaway { return (true, true); }
if saw_goaway {
return (true, true);
}
}
Ok(_) => {}
Err(_) => return (saw_goaway, saw_end_stream),
@@ -328,7 +357,10 @@ mod h2c_tests {
.await
.expect("active stream did not drain within the configured bound");
assert!(saw_goaway, "lifetime shutdown must send GOAWAY");
assert!(saw_end_stream, "an accepted stream must be allowed to finish during drain");
assert!(
saw_end_stream,
"an accepted stream must be allowed to finish during drain"
);
server.await.unwrap();
}
+8 -4
View File
@@ -302,11 +302,15 @@ async fn connection<I>(
match on_upgrade.await {
Ok(upgraded) => {
if let Some(state) = &upgrade_state_for_task {
state.metrics.http2_connections.fetch_add(1, Ordering::Relaxed);
state
.metrics
.http2_connections
.fetch_add(1, Ordering::Relaxed);
}
let extended_connect = upgrade_state_for_task.as_ref().is_some_and(|state| {
state.config.load().websocket_rfc8441_on(local, false)
});
let extended_connect =
upgrade_state_for_task.as_ref().is_some_and(|state| {
state.config.load().websocket_rfc8441_on(local, false)
});
let io = H2cUpgradeIo::new(TokioIo::new(upgraded), injected_headers);
http2_connection(
io,