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
26 changes: 26 additions & 0 deletions h3-quinn/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,12 +338,22 @@ where
}
}

impl<B> quic::Is0rtt for BidiStream<B>
where
B: Buf,
{
fn is_0rtt(&self) -> bool {
self.recv.is_0rtt()
}
}

/// Quinn-backed receive stream
///
/// Implements a [`quic::RecvStream`] backed by a [`quinn::RecvStream`].
pub struct RecvStream {
stream: Option<quinn::RecvStream>,
read_chunk_fut: ReadChunkFuture,
is_0rtt: bool,
}

type ReadChunkFuture = ReusableBoxFuture<
Expand All @@ -356,12 +366,22 @@ type ReadChunkFuture = ReusableBoxFuture<

impl RecvStream {
fn new(stream: quinn::RecvStream) -> Self {
let is_0rtt = stream.is_0rtt();
Self {
stream: Some(stream),
// Should only allocate once the first time it's used
read_chunk_fut: ReusableBoxFuture::new(async { unreachable!() }),
is_0rtt,
}
}

/// Check if this stream has been opened during 0-RTT.
///
/// In which case any non-idempotent request should be considered dangerous at the application
/// level. Because read data is subject to replay attacks.
pub fn is_0rtt(&self) -> bool {
self.is_0rtt
}
}

impl quic::RecvStream for RecvStream {
Expand Down Expand Up @@ -403,6 +423,12 @@ impl quic::RecvStream for RecvStream {
}
}

impl quic::Is0rtt for RecvStream {
fn is_0rtt(&self) -> bool {
self.is_0rtt
}
}

fn convert_read_error_to_stream_error(error: ReadError) -> StreamErrorIncoming {
match error {
ReadError::Reset(var_int) => StreamErrorIncoming::StreamTerminated {
Expand Down
7 changes: 7 additions & 0 deletions h3/src/frame.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,13 @@ impl<S, B> FrameStream<S, B> {
pub fn into_inner(self) -> BufRecvStream<S, B> {
self.stream
}

pub(crate) fn is_0rtt(&self) -> bool
where
S: crate::quic::Is0rtt,
{
self.stream.is_0rtt()
}
}

impl<S, B> FrameStream<S, B>
Expand Down
16 changes: 16 additions & 0 deletions h3/src/quic.rs
Original file line number Diff line number Diff line change
Expand Up @@ -230,3 +230,19 @@ pub trait BidiStream<B: Buf>: SendStream<B> + RecvStream {
/// Split this stream into two halves.
fn split(self) -> (Self::SendStream, Self::RecvStream);
}

/// Trait for QUIC streams that support 0-RTT detection.
///
/// This allows detection of streams opened during the 0-RTT phase of a QUIC connection.
/// 0-RTT data is vulnerable to replay attacks, so applications should be cautious when
/// processing non-idempotent requests on such streams.
///
/// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2)
/// for guidance on handling 0-RTT data in HTTP/3.
pub trait Is0rtt {
/// Check if this stream was opened during 0-RTT.
///
/// Returns `true` if the stream was opened during the 0-RTT phase,
/// `false` otherwise.
fn is_0rtt(&self) -> bool;
}
22 changes: 22 additions & 0 deletions h3/src/server/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -105,6 +105,28 @@ where
pub fn id(&self) -> StreamId {
self.inner.stream.id()
}

/// Check if this stream was opened during 0-RTT.
///
/// See [RFC 8470 Section 5.2](https://www.rfc-editor.org/rfc/rfc8470.html#section-5.2).
///
/// # Example
///
/// ```no_run
/// # use h3::server::RequestStream;
/// # async fn example(mut stream: RequestStream<impl h3::quic::BidiStream<bytes::Bytes>, bytes::Bytes>) {
/// if stream.is_0rtt() {
/// // Reject non-idempotent methods (e.g., POST, PUT, DELETE)
/// // to prevent replay attacks
/// }
/// # }
/// ```
pub fn is_0rtt(&self) -> bool
where
S: quic::Is0rtt,
{
self.inner.stream.is_0rtt()
}
}

impl<S, B> RequestStream<S, B>
Expand Down
7 changes: 7 additions & 0 deletions h3/src/stream.rs
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,13 @@ impl<S, B> BufRecvStream<S, B> {
_marker: PhantomData,
}
}

pub(crate) fn is_0rtt(&self) -> bool
where
S: crate::quic::Is0rtt,
{
self.stream.is_0rtt()
}
}

impl<B, S: RecvStream> BufRecvStream<S, B> {
Expand Down