Skip to content

Commit

Permalink
h3i: add example (#1878)
Browse files Browse the repository at this point in the history
The example can be run with `cargo r --example content_length_mismatch`.
TLS keys will be generated at `h3i/h3i-example`, which can be used to
decrypt a corresponding PCAP if one is captured separately.
  • Loading branch information
evanrittenhouse authored Dec 3, 2024
1 parent 684bc41 commit 15606d2
Show file tree
Hide file tree
Showing 3 changed files with 190 additions and 0 deletions.
19 changes: 19 additions & 0 deletions h3i/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,25 @@ h3i is also provided as a library, which allows programmatic control over HTTP/3

The key components of the library are actions, client runner, connection summary, and stream map.

## Example
h3i currently has one example, which can be run with:

```shell
cargo run --example content_length_mismatch
```

It can be useful to observe the packets on the wire that are exchanged when h3i
is run. Since QUIC is an encrypted transport protocol, tools such as Wireshark
need access to the session keys in order to dissect the packets. A common
approach is to log these keys in response to the `SSLKEYLOGFILE` environment
variable and configure Wireshark to use them; [see
more](https://wiki.wireshark.org/TLS#using-the-pre-master-secret). For example,
to log to the file `h3i-example.keys`:

```shell
SSLKEYLOGFILE="h3i-example.keys" cargo run --example content_length_mismatch
```

## 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.
Expand Down
82 changes: 82 additions & 0 deletions h3i/examples/content_length_mismatch.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use h3i::actions::h3::Action;
use h3i::client::sync_client;
use h3i::config::Config;
use quiche::h3::frame::Frame;
use quiche::h3::Header;
use quiche::h3::NameValue;

/// 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;

/// Send a request with a Content-Length header that specifies 5 bytes, but a
/// body that is only 4 bytes long. This verifies https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.2-3 for
/// blog.cloudflare.com.
fn main() {
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(),
},
},
];

let summary =
sync_client::connect(config, &actions).expect("connection failed");

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)
}
89 changes: 89 additions & 0 deletions h3i/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,10 +38,99 @@
//! 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 comprised of 4 bytes. This is classified as a
//! [malformed request], and the server should respond with a 400 Bad Request
//! response.
//!
//! ```no_run
//! use h3i::actions::h3::Action;
//! 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(),
//! },
//! },
//! ];
//!
//! let summary =
//! sync_client::connect(config, &actions).expect("connection failed");
//!
//! 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)
//! }
//! ```
//! [RFC 9000]: https://www.rfc-editor.org/rfc/rfc9000.html
//! [RFC 9110]: https://www.rfc-editor.org/rfc/rfc9110.html
//! [RFC 9114]: https://www.rfc-editor.org/rfc/rfc9114.html
//! [RFC 9204]: https://www.rfc-editor.org/rfc/rfc9204.html
//! [malformed request]: https://datatracker.ietf.org/doc/html/rfc9114#section-4.1.2-3
use qlog::events::quic::PacketHeader;
use qlog::events::quic::PacketSent;
Expand Down

0 comments on commit 15606d2

Please sign in to comment.