use bytes::Bytes;
use flowscope::FlowSide;
use flowscope::http2::{HpackEncoder, Http2Event, Http2Parser, PREFACE, StreamHead, write_headers};
fn field(name: &str, value: &str) -> (Bytes, Bytes) {
(
Bytes::copy_from_slice(name.as_bytes()),
Bytes::copy_from_slice(value.as_bytes()),
)
}
fn client_request(enc: &mut HpackEncoder, path: &str, token: &str) -> Vec<u8> {
let fields = vec![
field(":method", "GET"),
field(":scheme", "https"),
field(":authority", "api.example"),
field(":path", path),
field("user-agent", "demo-client/1.0"),
field("authorization", token),
];
enc.encode(&fields).expect("a valid request")
}
fn rewrite(head: &StreamHead) -> Vec<(Bytes, Bytes)> {
let mut out: Vec<(Bytes, Bytes)> = head
.fields
.iter()
.filter(|(n, _)| n.as_ref() != b"user-agent")
.cloned()
.collect();
out.push(field("x-forwarded-for", "203.0.113.7"));
out
}
fn main() {
let mut client_enc = HpackEncoder::new();
let mut inbound = Http2Parser::new();
let mut upstream_enc = HpackEncoder::new();
let mut upstream = Http2Parser::new();
inbound.push(FlowSide::Initiator, &Bytes::from_static(PREFACE));
upstream.push(FlowSide::Initiator, &Bytes::from_static(PREFACE));
let requests = [
("/v1/orders", "Bearer aaaaaaaaaaaaaaaaaaaa"),
("/v1/orders?page=2", "Bearer aaaaaaaaaaaaaaaaaaaa"),
("/v1/customers", "Bearer aaaaaaaaaaaaaaaaaaaa"),
];
for (i, (path, token)) in requests.iter().enumerate() {
let stream = i as u32 * 2 + 1;
let block = client_request(&mut client_enc, path, token);
let wire = write_headers(stream, &block, true, 16_384).expect("framable");
inbound.push(FlowSide::Initiator, &Bytes::from(wire));
let Some(Http2Event::Head(head)) = inbound.next_event() else {
panic!("expected a head on stream {stream}");
};
while inbound.next_event().is_some() {}
let rewritten = rewrite(&head);
let out_block = upstream_enc
.encode(&rewritten)
.expect("the rewrite is still a legal header list");
let max_frame = upstream.max_frame_size(FlowSide::Initiator);
let out_wire = write_headers(stream, &out_block, true, max_frame).expect("framable");
println!(
"stream {stream} {} in {:>3} B out {:>3} B encoder table {:>3} B",
head.path().unwrap_or("?"),
block.len(),
out_block.len(),
upstream_enc.table_size(),
);
upstream.push(FlowSide::Initiator, &Bytes::from(out_wire));
let Some(Http2Event::Head(seen)) = upstream.next_event() else {
panic!("the upstream parser rejected our own output");
};
while upstream.next_event().is_some() {}
assert_eq!(seen.fields, rewritten, "what we sent is what arrives");
assert!(seen.field("user-agent").is_none(), "the header was dropped");
assert_eq!(seen.field("x-forwarded-for"), Some(&b"203.0.113.7"[..]));
}
println!(
"\nThe first block out is *larger* than the one that came in — it\n\
gained a header, and its fields are new to a fresh table. From\n\
the second onward they are one-byte indices, so the block\n\
shrinks even though the request did not. That only works\n\
because the encoder's table is a *model of the peer's decoder*:\n\
build a block and then drop it instead of sending it, and the\n\
two go permanently out of step."
);
let auth_len = requests[0].1.len();
println!(
"\n`authorization` ({auth_len} B) is never indexed, so it costs\n\
those bytes on every request rather than one byte after the\n\
first. An indexed repeat would prove the value recurred, which\n\
is exactly the signal a CRIME-family attack reads — and it\n\
matters most when one upstream connection is shared across\n\
clients. `HpackEncoder::with_sensitivity` overrides it if your\n\
deployment does not have that shape."
);
let big: Vec<(Bytes, Bytes)> = std::iter::once(field(":method", "GET"))
.chain((0..80).map(|i| field(&format!("x-pad-{i}"), &"v".repeat(300))))
.collect();
let block = HpackEncoder::new()
.with_max_block_bytes(1024 * 1024)
.encode(&big)
.expect("encodable");
let frames = write_headers(1, &block, true, 16_384).expect("framable");
let mut kinds = Vec::new();
let mut at = 0usize;
while at + 9 <= frames.len() {
let len =
(frames[at] as usize) << 16 | (frames[at + 1] as usize) << 8 | frames[at + 2] as usize;
kinds.push(frames[at + 3]);
at += 9 + len;
}
let continuations = kinds.iter().filter(|&&k| k == 0x9).count();
println!(
"\nA {} B block does not fit one 16 KiB frame: write_headers\n\
split it into 1 HEADERS + {continuations} CONTINUATION. Sending it as a\n\
single frame instead is a FRAME_SIZE_ERROR at the far end —\n\
fatal to the whole connection, and reported nowhere near the\n\
cause.",
block.len(),
);
}