Crate h3i

Source
Expand description

h3i - low-level HTTP/3 debug and testing

HTTP/3 (RFC 9114) is the wire format for HTTP semantics (RFC 9110). The RFCs contain a range of requirements about how Request or Response messages are generated, serialized, sent, received, parsed, and consumed. QUIC (RFC 9000) streams are used for these messages along with other control and QPACK (RFC 9204) header compression instructions.

h3i provides a highly configurable HTTP/3 client that can bend RFC rules in order to test the behavior of servers. QUIC streams can be opened, fin’d, stopped or reset at any point in time. HTTP/3 frames can be sent on any stream, in any order, containing user-controlled content (both legal and illegal).

§Example

The following example sends a request with its Content-Length header set to 5, but with its body only consisting of 4 bytes. This is classified as a malformed request, and the server should respond with a 400 Bad Request response. Once h3i receives the response, it will close the connection.

use h3i::actions::h3::Action;
use h3i::actions::h3::StreamEvent;
use h3i::actions::h3::StreamEventType;
use h3i::actions::h3::WaitType;
use h3i::client::sync_client;
use h3i::config::Config;
use quiche::h3::frame::Frame;
use quiche::h3::Header;
use quiche::h3::NameValue;

fn main() {
   /// The QUIC stream to send the frames on. See
   /// https://datatracker.ietf.org/doc/html/rfc9000#name-streams and
   /// https://datatracker.ietf.org/doc/html/rfc9114#request-streams for more.
   const STREAM_ID: u64 = 0;

   let config = Config::new()
       .with_host_port("blog.cloudflare.com".to_string())
       .with_idle_timeout(2000)
       .build()
       .unwrap();

   let headers = vec![
       Header::new(b":method", b"POST"),
       Header::new(b":scheme", b"https"),
       Header::new(b":authority", b"blog.cloudflare.com"),
       Header::new(b":path", b"/"),
       // We say that we're going to send a body with 5 bytes...
       Header::new(b"content-length", b"5"),
   ];

   let header_block = encode_header_block(&headers).unwrap();

   let actions = vec![
       Action::SendHeadersFrame {
           stream_id: STREAM_ID,
           fin_stream: false,
           headers,
           frame: Frame::Headers { header_block },
       },
       Action::SendFrame {
           stream_id: STREAM_ID,
           fin_stream: true,
           frame: Frame::Data {
               // ...but, in actuality, we only send 4 bytes. This should yield a
               // 400 Bad Request response from an RFC-compliant
               // server: https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.2-3
               payload: b"test".to_vec(),
           },
       },
       Action::Wait {
           wait_type: WaitType::StreamEvent(StreamEvent {
               stream_id: STREAM_ID,
               event_type: StreamEventType::Headers,
           }),
       },
       Action::ConnectionClose {
           error: quiche::ConnectionError {
               is_app: true,
               error_code: quiche::h3::WireErrorCode::NoError as u64,
               reason: vec![],
           },
       },
   ];

   // This example doesn't use close trigger frames, since we manually close the connection upon
   // receiving a HEADERS frame on stream 0.
   let close_trigger_frames = None;
   let summary = sync_client::connect(config, &actions, close_trigger_frames);

   println!(
       "=== received connection summary! ===\n\n{}",
       serde_json::to_string_pretty(&summary).unwrap_or_else(|e| e.to_string())
   );
}

// SendHeadersFrame requires a QPACK-encoded header block. h3i provides a
// `send_headers_frame` helper function to abstract this, but for clarity, we do
// it here.
fn encode_header_block(
    headers: &[quiche::h3::Header],
) -> std::result::Result<Vec<u8>, String> {
    let mut encoder = quiche::h3::qpack::Encoder::new();

    let headers_len = headers
        .iter()
        .fold(0, |acc, h| acc + h.value().len() + h.name().len() + 32);

    let mut header_block = vec![0; headers_len];
    let len = encoder
        .encode(headers, &mut header_block)
        .map_err(|_| "Internal Error")?;

    header_block.truncate(len);

    Ok(header_block)
}

Re-exports§

pub use quiche;

Modules§

actions
Actions are small operations such as sending HTTP/3 frames or managing QUIC streams. Each independent use case for h3i requires its own collection of Actions, that h3i iterates over in sequence and executes.
client
The main h3i client runner.
config
Configuration for the h3i client.
frame
Helpers for dealing with quiche stream events and HTTP/3 frames.
frame_parser
Stateful parsing of QUIC streams into HTTP/3 frames.
prompts
A collection of interactive CLI prompts based on [inquire].
recordreplay
Support for recording h3i Actions and replaying them.

Constants§

HTTP3_CONTROL_STREAM_TYPE_ID
The ID for an HTTP/3 control stream type.
HTTP3_PUSH_STREAM_TYPE_ID
The ID for an HTTP/3 push stream type.
QPACK_DECODER_STREAM_TYPE_ID
The ID for a QPACK decoder stream type.
QPACK_ENCODER_STREAM_TYPE_ID
The ID for a QPACK encoder stream type.