diff --git a/include/tquic.h b/include/tquic.h index 973bbd1e4..1eac383fd 100644 --- a/include/tquic.h +++ b/include/tquic.h @@ -425,7 +425,7 @@ typedef struct quic_conn_stats_t { typedef struct http3_methods_t { /** - * Called when the stream got headers. + * Called when the stream receives a header or trailer field section. */ void (*on_stream_headers)(void *ctx, uint64_t stream_id, @@ -1445,7 +1445,7 @@ int http3_stream_set_priority(struct http3_conn_t *conn, const struct http3_priority_t *priority); /** - * Send HTTP/3 request or response headers on the given stream. + * Send initial HTTP/3 request or response headers on the given stream. */ int http3_send_headers(struct http3_conn_t *conn, struct quic_conn_t *quic_conn, @@ -1454,6 +1454,21 @@ int http3_send_headers(struct http3_conn_t *conn, size_t headers_len, bool fin); +/** + * Send an additional HTTP/3 field section on the given stream. + * + * Clients can only send trailer sections. Servers can also send additional + * response field sections before the response body. Once a trailer section is + * sent, no more HEADERS or DATA frames can be sent on the stream. + */ +int http3_send_additional_headers(struct http3_conn_t *conn, + struct quic_conn_t *quic_conn, + uint64_t stream_id, + const struct http3_header_t *headers, + size_t headers_len, + bool is_trailer_section, + bool fin); + /** * Send HTTP/3 request or response body on the given stream. */ diff --git a/src/ffi.rs b/src/ffi.rs index ae5d7314c..ada0a8fca 100644 --- a/src/ffi.rs +++ b/src/ffi.rs @@ -2349,7 +2349,7 @@ pub struct Header { value_len: usize, } -/// Send HTTP/3 request or response headers on the given stream. +/// Send initial HTTP/3 request or response headers on the given stream. #[cfg(feature = "h3")] #[no_mangle] pub extern "C" fn http3_send_headers( @@ -2368,6 +2368,30 @@ pub extern "C" fn http3_send_headers( } } +/// Send an additional HTTP/3 field section on the given stream. +/// +/// Clients can only send trailer sections. Servers can also send additional +/// response field sections before the response body. Once a trailer section is +/// sent, no more HEADERS or DATA frames can be sent on the stream. +#[cfg(feature = "h3")] +#[no_mangle] +pub extern "C" fn http3_send_additional_headers( + conn: &mut Http3Connection, + quic_conn: &mut Connection, + stream_id: u64, + headers: *const Header, + headers_len: size_t, + is_trailer_section: bool, + fin: bool, +) -> c_int { + let h3_headers = headers_from_ptr(headers, headers_len); + + match conn.send_additional_headers(quic_conn, stream_id, &h3_headers, is_trailer_section, fin) { + Ok(_) => 0, + Err(e) => e.to_errno() as c_int, + } +} + /// Send HTTP/3 request or response body on the given stream. #[cfg(feature = "h3")] #[no_mangle] @@ -2516,7 +2540,7 @@ pub extern "C" fn quic_set_logger( #[cfg(feature = "h3")] #[repr(C)] pub struct Http3Methods { - /// Called when the stream got headers. + /// Called when the stream receives a header or trailer field section. pub on_stream_headers: Option, diff --git a/src/h3/connection.rs b/src/h3/connection.rs index 7962b37b6..8eb2704a8 100644 --- a/src/h3/connection.rs +++ b/src/h3/connection.rs @@ -474,8 +474,8 @@ impl Http3Connection { Ok(()) } - /// Write HTTP/3 headers to quic stream buffer. - pub fn send_headers( + /// Encode and write an HTTP/3 HEADERS frame to a request stream. + fn send_headers_frame( &mut self, conn: &mut Connection, stream_id: u64, @@ -497,6 +497,71 @@ impl Http3Connection { self.send_header_block(conn, stream_id, header_block, fin) } + /// Write the initial HTTP/3 request or response headers to a request stream. + pub fn send_headers( + &mut self, + conn: &mut Connection, + stream_id: u64, + headers: &[T], + fin: bool, + ) -> Result<()> { + match self.streams.get(&stream_id) { + Some(stream) if !stream.local_initialized() && !stream.write_finished() => (), + _ => return Err(Http3Error::FrameUnexpected), + } + + self.send_headers_frame(conn, stream_id, headers, fin) + } + + /// Write an additional HTTP/3 field section to a request stream. + /// + /// This can be used to send a trailer section, or by a server to send an + /// informational response followed by another response field section. + /// Clients can only use this method to send trailers. Once a trailer has + /// been sent, no more HEADERS or DATA frames can be sent on the stream. + /// + /// If the underlying QUIC stream does not have enough capacity, this method + /// returns [`Http3Error::StreamBlocked`] and the application should retry it + /// after the stream becomes writable. + /// + /// See [Section 4.1 of RFC 9114] for the HTTP message frame sequence. + /// + /// [Section 4.1 of RFC 9114]: https://www.rfc-editor.org/rfc/rfc9114.html#section-4.1 + pub fn send_additional_headers( + &mut self, + conn: &mut Connection, + stream_id: u64, + headers: &[T], + is_trailer_section: bool, + fin: bool, + ) -> Result<()> { + // A request cannot contain informational field sections. + if !self.is_server && !is_trailer_section { + return Err(Http3Error::FrameUnexpected); + } + + match self.streams.get(&stream_id) { + Some(stream) + if stream.local_initialized() + && !stream.write_finished() + && !stream.trailers_sent() + && (is_trailer_section || !stream.data_sent()) => {} + _ => return Err(Http3Error::FrameUnexpected), + } + + self.send_headers_frame(conn, stream_id, headers, fin)?; + + if is_trailer_section { + // The stream can be removed after a successful write when both + // directions are finished, in which case no further writes are possible. + if let Some(stream) = self.streams.get_mut(&stream_id) { + stream.mark_trailers_sent(); + } + } + + Ok(()) + } + /// Write request or response body into quic transport stream's send buffer. pub fn send_body( &mut self, @@ -516,6 +581,16 @@ impl Http3Connection { .get_mut(&stream_id) .ok_or(Http3Error::FrameUnexpected)?; + // A cached block on an initialized stream is an additional field section + // waiting for flow-control capacity. It must be retried through + // send_additional_headers(), not bypassed with a DATA frame. + if stream.write_finished() + || stream.trailers_sent() + || (stream.local_initialized() && stream.has_header_block()) + { + return Err(Http3Error::FrameUnexpected); + } + if let Some((header_block, write_fin)) = stream.take_header_block() { // We should update fin flag if the application send empty body with fin. let write_fin = write_fin || (fin && body.is_empty()); @@ -595,6 +670,10 @@ impl Http3Connection { // Write the DATA frame payload. let written = conn.stream_write(stream_id, body, fin)?; + if let Some(stream) = self.streams.get_mut(&stream_id) { + stream.mark_data_sent(); + } + trace!( "{:?} stream {} send DATA frame written {} body_len {} fin {}", conn.trace_id(), @@ -1137,6 +1216,10 @@ impl Http3Connection { } }; + if let Some(stream) = self.streams.get_mut(&stream_id) { + stream.increment_headers_received(); + } + let headers_event = Http3Event::Headers { headers, fin: conn.stream_finished(stream_id), @@ -3088,6 +3171,364 @@ mod tests { assert_eq!(s.client_poll(), Err(Http3Error::Done)); } + // Client and server exchange content followed by trailer sections. + #[test] + fn request_and_response_with_trailers() { + let mut s = Session::new().unwrap(); + + let (stream_id, req_headers) = s.send_request(false).unwrap(); + let req_body = s.client_send_body(stream_id, false).unwrap(); + let req_trailers = vec![Header::new(b"request-checksum", b"abc123")]; + + s.client + .send_additional_headers(&mut s.pair.client, stream_id, &req_trailers, true, true) + .unwrap(); + s.move_forward().ok(); + + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_headers, + fin: false, + }, + )) + ); + assert_eq!(s.server_poll(), Ok((stream_id, Http3Event::Data))); + + let mut recv_buf = vec![0; req_body.len()]; + assert_eq!( + s.server_recv_body(stream_id, &mut recv_buf), + Ok(req_body.len()) + ); + assert_eq!(recv_buf, req_body); + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_trailers, + fin: true, + }, + )) + ); + assert_eq!(s.server_poll(), Ok((stream_id, Http3Event::Finished))); + + let resp_headers = s.send_response(stream_id, false).unwrap(); + let resp_body = s.server_send_body(stream_id, false).unwrap(); + let resp_trailers = vec![Header::new(b"response-checksum", b"def456")]; + + assert_eq!( + s.server.send_additional_headers( + &mut s.pair.server, + stream_id, + &resp_trailers, + false, + false, + ), + Err(Http3Error::FrameUnexpected) + ); + + s.server + .send_additional_headers(&mut s.pair.server, stream_id, &resp_trailers, true, true) + .unwrap(); + s.move_forward().ok(); + + assert_eq!( + s.client_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: resp_headers, + fin: false, + }, + )) + ); + assert_eq!(s.client_poll(), Ok((stream_id, Http3Event::Data))); + + let mut recv_buf = vec![0; resp_body.len()]; + assert_eq!( + s.client_recv_body(stream_id, &mut recv_buf), + Ok(resp_body.len()) + ); + assert_eq!(recv_buf, resp_body); + assert_eq!( + s.client_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: resp_trailers, + fin: true, + }, + )) + ); + assert_eq!(s.client_poll(), Ok((stream_id, Http3Event::Finished))); + } + + // Trailer sections are valid even when the message has no content. + #[test] + fn trailers_without_body() { + let mut s = Session::new().unwrap(); + + let (stream_id, req_headers) = s.send_request(false).unwrap(); + let req_trailers = vec![Header::new(b"request-complete", b"true")]; + + s.client + .send_additional_headers(&mut s.pair.client, stream_id, &req_trailers, true, true) + .unwrap(); + s.move_forward().ok(); + + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_headers, + fin: false, + }, + )) + ); + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_trailers, + fin: true, + }, + )) + ); + assert_eq!(s.server_poll(), Ok((stream_id, Http3Event::Finished))); + } + + // RFC 9114 forbids HEADERS and DATA frames after a trailer section. + #[test] + fn trailer_sequence_is_enforced() { + let mut s = Session::new().unwrap(); + let stream_id = s.client.stream_new(&mut s.pair.client).unwrap(); + let req_headers = Session::default_request_headers(); + let req_trailers = vec![Header::new(b"request-complete", b"true")]; + + assert_eq!( + s.client.send_additional_headers( + &mut s.pair.client, + stream_id, + &req_trailers, + true, + false, + ), + Err(Http3Error::FrameUnexpected) + ); + s.client + .send_headers(&mut s.pair.client, stream_id, &req_headers, false) + .unwrap(); + assert_eq!( + s.client + .send_headers(&mut s.pair.client, stream_id, &req_headers, false), + Err(Http3Error::FrameUnexpected) + ); + assert_eq!( + s.client.send_additional_headers( + &mut s.pair.client, + stream_id, + &req_trailers, + false, + false, + ), + Err(Http3Error::FrameUnexpected) + ); + + s.client + .send_additional_headers(&mut s.pair.client, stream_id, &req_trailers, true, false) + .unwrap(); + + assert_eq!( + s.client.send_additional_headers( + &mut s.pair.client, + stream_id, + &req_trailers, + true, + false, + ), + Err(Http3Error::FrameUnexpected) + ); + assert_eq!( + s.client.send_body( + &mut s.pair.client, + stream_id, + Bytes::from_static(b"invalid"), + false, + ), + Err(Http3Error::FrameUnexpected) + ); + + s.move_forward().ok(); + s.client_send_frame( + stream_id, + frame::Http3Frame::Data { + data: b"invalid".to_vec(), + }, + true, + ) + .unwrap(); + + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_headers, + fin: false, + }, + )) + ); + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_trailers, + fin: false, + }, + )) + ); + assert_eq!(s.server_poll(), Err(Http3Error::FrameUnexpected)); + } + + // A blocked trailer is retried through the additional-headers API and + // cannot be bypassed by writing a DATA frame. + #[test] + fn trailer_flow_control_retry() { + let h3_config = Http3Config::new().unwrap(); + let mut h3_client = Http3Connection::new(&h3_config, false).unwrap(); + let req_headers = Session::default_request_headers(); + let req_trailers = vec![Header::new(b"request-checksum", b"abc123")]; + let headers_frame_size = + Session::calculate_headers_frame_size(&mut h3_client, &req_headers).unwrap(); + let trailers_frame_size = + Session::calculate_headers_frame_size(&mut h3_client, &req_trailers).unwrap(); + + let mut client_config = Session::new_test_config(false).unwrap(); + let mut server_config = Session::new_test_config(true).unwrap(); + // The critical streams consume 5 bytes. Leave the request stream one + // byte short of the capacity required for its trailer frame. + server_config + .set_initial_max_data((5 + headers_frame_size + trailers_frame_size - 1) as u64); + + let mut s = + Session::new_with_test_config(&mut client_config, &mut server_config, &h3_config) + .unwrap(); + let (stream_id, req_headers) = s.send_request(false).unwrap(); + + assert_eq!( + s.client.send_additional_headers( + &mut s.pair.client, + stream_id, + &req_trailers, + true, + true, + ), + Err(Http3Error::StreamBlocked) + ); + assert!(s.client.streams.get(&stream_id).unwrap().has_header_block()); + assert_eq!( + s.client.send_body( + &mut s.pair.client, + stream_id, + Bytes::from_static(b"invalid"), + false, + ), + Err(Http3Error::FrameUnexpected) + ); + + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_headers, + fin: false, + }, + )) + ); + assert_eq!(s.server_poll(), Err(Http3Error::Done)); + s.move_forward().unwrap(); + + s.client + .send_additional_headers(&mut s.pair.client, stream_id, &req_trailers, true, true) + .unwrap(); + assert!(!s.client.streams.get(&stream_id).unwrap().has_header_block()); + s.move_forward().unwrap(); + + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_trailers, + fin: true, + }, + )) + ); + assert_eq!(s.server_poll(), Ok((stream_id, Http3Event::Finished))); + } + + // Servers can send informational responses before the final response field section. + #[test] + fn informational_response_uses_additional_headers() { + let mut s = Session::new().unwrap(); + + let (stream_id, req_headers) = s.send_request(true).unwrap(); + assert_eq!( + s.server_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: req_headers, + fin: true, + }, + )) + ); + assert_eq!(s.server_poll(), Ok((stream_id, Http3Event::Finished))); + + let informational = vec![ + Header::new(b":status", b"103"), + Header::new(b"link", b"; rel=preload"), + ]; + let final_headers = Session::default_response_headers(); + + s.server + .send_headers(&mut s.pair.server, stream_id, &informational, false) + .unwrap(); + s.server + .send_additional_headers(&mut s.pair.server, stream_id, &final_headers, false, true) + .unwrap(); + s.move_forward().ok(); + + assert_eq!( + s.client_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: informational, + fin: false, + }, + )) + ); + assert_eq!( + s.client_poll(), + Ok(( + stream_id, + Http3Event::Headers { + headers: final_headers, + fin: true, + }, + )) + ); + assert_eq!(s.client_poll(), Ok((stream_id, Http3Event::Finished))); + } + // Client and server send body with empty data block with or without FIN flag. #[test] fn send_body_with_empty_data_block() { diff --git a/src/h3/h3.rs b/src/h3/h3.rs index 7313e57b3..83b04b23a 100644 --- a/src/h3/h3.rs +++ b/src/h3/h3.rs @@ -68,13 +68,13 @@ impl Http3Config { /// An HTTP/3 connection event. #[derive(Clone, Debug, PartialEq, Eq)] pub enum Http3Event { - /// HTTP/3 headers were received on request stream. + /// An HTTP/3 header or trailer field section was received on a request stream. Headers { /// HTTP/3 header fields are represented as a list of name-value pairs. /// Note that the application is responsible for validating the headers. headers: Vec
, - /// Whether the stream consists of only headers and no data. + /// Whether the peer's send side finished with this field section. fin: bool, }, @@ -188,7 +188,7 @@ impl NameValue for HeaderRef<'_> { /// The Http3Handler lists the callbacks used by the Http3Connection to /// communicate with the user application code. pub trait Http3Handler { - /// Called when the stream got headers. + /// Called when the stream receives a header or trailer field section. fn on_stream_headers(&self, stream_id: u64, event: &mut Http3Event); /// Called when the stream has buffered data to read. diff --git a/src/h3/stream.rs b/src/h3/stream.rs index 63a8b55f7..7581b1fcc 100644 --- a/src/h3/stream.rs +++ b/src/h3/stream.rs @@ -56,6 +56,21 @@ pub struct Http3Stream { /// Whether the stream has been initialized by the local endpoint. local_initialized: bool, + /// The number of HEADERS frames received on this stream. + headers_received_count: usize, + + /// Whether a DATA frame has been sent on this stream. + data_sent: bool, + + /// Whether a DATA frame has been received on this stream. + data_received: bool, + + /// Whether a trailing HEADERS frame has been sent on this stream. + trailers_sent: bool, + + /// Whether a trailing HEADERS frame has been received on this stream. + trailers_received: bool, + /// Whether all the application data with fin has been written to quic stream buffer. write_finished: bool, @@ -97,6 +112,11 @@ impl Http3Stream { peer_initialized: false, local_initialized: false, + headers_received_count: 0, + data_sent: false, + data_received: false, + trailers_sent: false, + trailers_received: false, write_finished: false, data_event_triggered: false, priority_initialized: false, @@ -144,10 +164,32 @@ impl Http3Stream { (frame::HEADERS_FRAME_TYPE, false) => self.peer_initialized = true, // Receipt of an invalid sequence of frames MUST be treated as a connection error of type H3_FRAME_UNEXPECTED. - // In particular, a DATA frame before any HEADERS frame, or a HEADERS or DATA frame after the trailing HEADERS - // frame, is considered invalid. + // In particular, a DATA frame before any HEADERS frame is considered invalid. (frame::DATA_FRAME_TYPE, false) => return Err(Http3Error::FrameUnexpected), + // A request has at most one initial field section and one trailer section. A response + // can have informational field sections before the final field section, so a HEADERS + // frame is unambiguously a trailer only after DATA has been received. For a peer-created + // request stream, the second HEADERS frame is always the trailer, even without content. + (frame::HEADERS_FRAME_TYPE, true) => { + if self.trailers_received { + return Err(Http3Error::FrameUnexpected); + } + + if self.data_received || (!self.local && self.headers_received_count() > 0) { + self.trailers_received = true; + } + } + + // A HEADERS or DATA frame after the trailing HEADERS frame is invalid. + (frame::DATA_FRAME_TYPE, true) => { + if self.trailers_received { + return Err(Http3Error::FrameUnexpected); + } + + self.data_received = true; + } + // RFC9114 7. Table 1: HTTP/3 Frames and Stream Type Overview // `CANCEL_PUSH`, `SETTINGS`, `GOAWAY`, and `MAX_PUSH_ID` frames MUST NOT be sent on the request stream. (frame::CANCEL_PUSH_FRAME_TYPE, _) => return Err(Http3Error::FrameUnexpected), @@ -624,6 +666,36 @@ impl Http3Stream { self.local_initialized = true } + /// Increment the number of HEADERS frames received on this stream. + pub fn increment_headers_received(&mut self) { + self.headers_received_count = self.headers_received_count.saturating_add(1); + } + + /// Return the number of HEADERS frames received on this stream. + pub fn headers_received_count(&self) -> usize { + self.headers_received_count + } + + /// Return true if a DATA frame has been sent on this stream. + pub fn data_sent(&self) -> bool { + self.data_sent + } + + /// Mark that a DATA frame has been sent on this stream. + pub fn mark_data_sent(&mut self) { + self.data_sent = true; + } + + /// Return true if a trailing HEADERS frame has been sent on this stream. + pub fn trailers_sent(&self) -> bool { + self.trailers_sent + } + + /// Mark that a trailing HEADERS frame has been sent on this stream. + pub fn mark_trailers_sent(&mut self) { + self.trailers_sent = true; + } + /// Return true if the stream's priority has been initialized. pub fn priority_initialized(&self) -> bool { self.priority_initialized @@ -1257,6 +1329,7 @@ mod tests { Ok(()) ); assert_eq!(stream.peer_initialized, true); + stream.increment_headers_received(); // RFC9114 7. Table 1: HTTP/3 Frames and Stream Type Overview // `CANCEL_PUSH`, `SETTINGS`, `GOAWAY`, and `MAX_PUSH_ID` frames MUST NOT be sent on the request stream. @@ -1278,6 +1351,31 @@ mod tests { Err(Http3Error::FrameUnexpected) ); } + + if local { + // A locally-created request stream receives a response. After DATA, + // the next HEADERS frame is the response trailer section. + assert_eq!( + stream.check_frame_on_request_stream(frame::DATA_FRAME_TYPE), + Ok(()) + ); + assert!(stream.data_received); + } + + // For a peer-created request stream, the second HEADERS frame is a + // request trailer even when there was no DATA frame. + assert_eq!( + stream.check_frame_on_request_stream(frame::HEADERS_FRAME_TYPE), + Ok(()) + ); + assert!(stream.trailers_received); + + for frame_type in vec![frame::HEADERS_FRAME_TYPE, frame::DATA_FRAME_TYPE] { + assert_eq!( + stream.check_frame_on_request_stream(frame_type), + Err(Http3Error::FrameUnexpected) + ); + } } }