Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion examples/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,7 +112,9 @@ async fn main() -> Result<(), Box<dyn std::error::Error>> {
Ok(conn) => {
info!("new connection established");

let mut h3_conn = h3::server::Connection::new(h3_quinn::Connection::new(conn))
let mut h3_conn = h3::server::builder()
.max_field_section_size(1024 * 16)
.build(h3_quinn::Connection::new(conn))
.await
.unwrap();

Expand Down
2 changes: 0 additions & 2 deletions h3-webtransport/src/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,6 @@ use h3::{
ConnectionError, StreamError,
},
ext::Protocol,
frame::FrameStream,
proto::frame::Frame,
quic::{self, OpenStreams, WriteBuf},
server::{Connection, RequestResolver, RequestStream},
Expand Down Expand Up @@ -189,7 +188,6 @@ where
.await?;

Ok(stream.map(|stream| {
let stream = FrameStream::new(BufRecvStream::new(stream));
let resolver = self.server_conn.lock().unwrap().create_resolver(stream);
BiStreamResolver { resolver }
}))
Expand Down
3 changes: 2 additions & 1 deletion h3/src/client/builder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,7 +80,8 @@ impl Builder {

/// Set the maximum header size this client is willing to accept
///
/// See [header size constraints] section of the specification for details.
/// Defaults to 64 KiB. Measured as decoded name + value + 32 bytes per field,
/// see [header size constraints] section of the specification for details.
///
/// [header size constraints]: https://www.rfc-editor.org/rfc/rfc9114.html#name-header-size-constraints
pub fn max_field_section_size(&mut self, value: u64) -> &mut Self {
Expand Down
5 changes: 4 additions & 1 deletion h3/src/client/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -216,7 +216,10 @@ where

let request_stream = RequestStream {
inner: connection::RequestStream::new(
FrameStream::new(BufRecvStream::new(stream)),
FrameStream::new_for_request(
BufRecvStream::new(stream),
self.max_field_section_size,
),
self.max_field_section_size,
self.conn_state.clone(),
self.send_grease_frame,
Expand Down
13 changes: 4 additions & 9 deletions h3/src/client/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,8 @@ use tracing::instrument;
use crate::{
connection::{self},
error::{
connection_error_creators::{CloseStream, HandleFrameStreamErrorOnRequestStream},
internal_error::InternalConnectionError,
Code, StreamError,
connection_error_creators::CloseStream, internal_error::InternalConnectionError, Code,
StreamError,
},
proto::{frame::Frame, headers::Header},
qpack,
Expand Down Expand Up @@ -99,7 +98,7 @@ where
pub async fn recv_response(&mut self) -> Result<Response<()>, StreamError> {
let mut frame = future::poll_fn(|cx| self.inner.stream.poll_next(cx))
.await
.map_err(|e| self.handle_frame_stream_error_on_request_stream(e))?
.map_err(|e| self.inner.frame_stream_error(e))?
.ok_or_else(|| {
//= https://www.rfc-editor.org/rfc/rfc9114#section-4.1
//# Receipt of an invalid sequence of frames MUST be treated as a
Expand Down Expand Up @@ -221,11 +220,7 @@ where
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<Option<HeaderMap>, StreamError>> {
let res = self.inner.poll_recv_trailers(cx);
if let Poll::Ready(Err(StreamError::HeaderTooBig { .. })) = &res {
self.inner.stream.stop_sending(Code::H3_REQUEST_CANCELLED);
}
res
self.inner.poll_recv_trailers(cx)
}

/// Tell the peer to stop sending into the underlying QUIC stream
Expand Down
21 changes: 15 additions & 6 deletions h3/src/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -22,11 +22,8 @@ pub struct Config {
/// HTTP/3 Settings
#[derive(Debug, Clone, Copy)]
pub struct Settings {
/// The MAX_FIELD_SECTION_SIZE in HTTP/3 refers to the maximum size of the dynamic table used in HPACK compression.
/// HPACK is the compression algorithm used in HTTP/3 to reduce the size of the header fields in HTTP requests and responses.

/// In HTTP/3, the MAX_FIELD_SECTION_SIZE is set to 12.
/// This means that the dynamic table used for HPACK compression can have a maximum size of 2^12 bytes, which is 4KB.
/// SETTINGS_MAX_FIELD_SECTION_SIZE: the largest decoded field section (name + value + 32 bytes
/// per field, <https://www.rfc-editor.org/rfc/rfc9114#section-4.2.2>) this endpoint accepts.
pub(crate) max_field_section_size: u64,

/// https://datatracker.ietf.org/doc/html/draft-ietf-webtrans-http3/#section-3.1
Expand Down Expand Up @@ -134,6 +131,15 @@ impl TryFrom<Config> for frame::Settings {
}
}

/// Default for the field section size this endpoint accepts, see [`Config`].
///
/// Bounds the memory a peer can make this endpoint commit per request or response; a HEADERS
/// frame decodes to many times its wire size, so the limit must be finite by default. Raise it
/// with the client or server builder if peers legitimately send larger headers.
pub const DEFAULT_MAX_FIELD_SECTION_SIZE: u64 = 64 * 1024;

/// The initial values of <https://www.rfc-editor.org/rfc/rfc9114#section-7.2.4.2>, assumed for a
/// peer until its SETTINGS frame arrives. Not the local defaults: those are set by [`Config`].
impl Default for Settings {
fn default() -> Self {
Self {
Expand Down Expand Up @@ -173,7 +179,10 @@ impl Default for Config {
send_grease: true,
#[cfg(test)]
send_settings: true,
settings: Default::default(),
settings: Settings {
max_field_section_size: DEFAULT_MAX_FIELD_SECTION_SIZE,
..Default::default()
},
}
}
}
39 changes: 30 additions & 9 deletions h3/src/connection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -565,6 +565,22 @@ where
),
)));
}
Err(FrameStreamError::PayloadTooLarge {
frame_type: _ty,
actual_size: _actual,
max_size: _max,
}) => {
// Unlike a request stream, there is no message to reject here:
// every frame the control stream may legitimately carry is tens
// of bytes, and the stream itself is critical.
return Poll::Ready(Err(self.handle_connection_error(
InternalConnectionError::new(
Code::H3_EXCESSIVE_LOAD,
"control frame declared a payload larger than this endpoint will buffer"
.to_string(),
),
)));
}
Err(FrameStreamError::Proto(frame_error)) => {
return Poll::Ready(Err(self.handle_connection_error(
InternalConnectionError::got_frame_error(frame_error),
Expand Down Expand Up @@ -851,16 +867,24 @@ where
{
/// Receive some of the request/response body.
#[cfg_attr(feature = "tracing", instrument(skip_all, level = "trace"))]
/// Maps a frame stream error, telling the peer to stop sending when the
/// message was refused for its size: the rest of it is only cost to receive.
pub(crate) fn frame_stream_error(&mut self, error: FrameStreamError) -> StreamError {
let error = self.handle_frame_stream_error_on_request_stream(error);
if matches!(error, StreamError::HeaderTooBig { .. }) {
self.stream.stop_sending(Code::H3_REQUEST_CANCELLED);
}
error
}

pub fn poll_recv_data(
&mut self,
cx: &mut Context<'_>,
) -> Poll<Result<Option<impl Buf>, StreamError>> {
if !self.stream.has_data() {
match ready!(self.stream.poll_next(cx)) {
Err(frame_stream_error) => {
return Poll::Ready(Err(
self.handle_frame_stream_error_on_request_stream(frame_stream_error)
))
return Poll::Ready(Err(self.frame_stream_error(frame_stream_error)));
}
Ok(None) => return Poll::Ready(Ok(None)),
Ok(Some(Frame::Headers(encoded))) => {
Expand Down Expand Up @@ -919,9 +943,7 @@ where
} else {
match ready!(self.stream.poll_next(cx)) {
Err(frame_stream_error) => {
return Poll::Ready(Err(
self.handle_frame_stream_error_on_request_stream(frame_stream_error)
))
return Poll::Ready(Err(self.frame_stream_error(frame_stream_error)));
}
Ok(None) => return Poll::Ready(Ok(None)),
Ok(Some(Frame::Headers(encoded))) => encoded,
Expand Down Expand Up @@ -967,9 +989,7 @@ where

match self.stream.poll_next(cx) {
Poll::Ready(Err(frame_stream_error)) => {
return Poll::Ready(Err(
self.handle_frame_stream_error_on_request_stream(frame_stream_error)
))
return Poll::Ready(Err(self.frame_stream_error(frame_stream_error)));
}
Poll::Ready(Ok(Some(trailing_frame))) => {
// Received a known frame after trailers -> fail.
Expand All @@ -996,6 +1016,7 @@ where
//# An HTTP/3 implementation MAY impose a limit on the maximum size of
//# the message header it will accept on an individual HTTP message.
Err(qpack::DecoderError::HeaderTooLong(cancel_size)) => {
self.stream.stop_sending(Code::H3_REQUEST_CANCELLED);
return Poll::Ready(Err(StreamError::HeaderTooBig {
actual_size: cancel_size,
max_size: self.max_field_section_size,
Expand Down
16 changes: 16 additions & 0 deletions h3/src/error/connection_error_creators.rs
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,22 @@ where
FrameStreamError::Proto(frame_error) => self.handle_connection_error_on_stream(
InternalConnectionError::got_frame_error(frame_error),
),
// A HEADERS limit is per message: rejecting one request must not
// close unrelated streams on the same connection.
FrameStreamError::PayloadTooLarge {
frame_type: crate::proto::frame::FrameType::HEADERS,
actual_size,
max_size,
} => StreamError::HeaderTooBig {
actual_size,
max_size,
},
FrameStreamError::PayloadTooLarge { .. } => {
self.handle_connection_error_on_stream(InternalConnectionError::new(
Code::H3_EXCESSIVE_LOAD,
"frame declared a payload larger than this endpoint will buffer".to_string(),
))
}
FrameStreamError::UnexpectedEnd => {
self.handle_connection_error_on_stream(InternalConnectionError::new(
Code::H3_FRAME_ERROR,
Expand Down
Loading