#![cfg(feature = "http2")]
use bytes::Bytes;
use flowscope::FlowSide;
use flowscope::http2::{
HeaderSensitivity, HpackEncoder, Http2Config, Http2Event, Http2Parser, PREFACE, write_headers,
};
use proptest::prelude::*;
fn frame(kind: u8, flags: u8, stream: u32, payload: &[u8]) -> Vec<u8> {
let mut v = Vec::new();
let len = payload.len() as u32;
v.extend_from_slice(&[(len >> 16) as u8, (len >> 8) as u8, len as u8]);
v.push(kind);
v.push(flags);
v.extend_from_slice(&stream.to_be_bytes());
v.extend_from_slice(payload);
v
}
fn drain(p: &mut Http2Parser) -> Vec<Http2Event> {
let mut out = Vec::new();
while let Some(ev) = p.next_event() {
out.push(ev);
}
out
}
fn shape(evs: &[Http2Event]) -> Vec<String> {
evs.iter()
.map(|e| match e {
Http2Event::Head(h) => format!(
"head {} {:?} {:?} {}",
h.stream_id,
h.method(),
h.path(),
h.end_stream
),
Http2Event::Body {
stream_id, data, ..
} => format!("body {} {}", stream_id, data.len()),
Http2Event::Trailers {
stream_id, fields, ..
} => format!("trailers {} {}", stream_id, fields.len()),
Http2Event::End { stream_id, .. } => format!("end {stream_id}"),
Http2Event::StreamReset { stream_id, .. } => format!("rst {stream_id}"),
Http2Event::GoAway { last_stream_id, .. } => format!("goaway {last_stream_id}"),
other => format!("{other:?}"),
})
.collect()
}
fn connection() -> impl Strategy<Value = Vec<u8>> {
proptest::collection::vec(
(
prop_oneof![Just(0x0u8), Just(0x1), Just(0x3), Just(0x4), Just(0x8)],
any::<u8>(),
1u32..8,
proptest::collection::vec(any::<u8>(), 0..24),
),
0..12,
)
.prop_map(|frames| {
let mut wire = PREFACE.to_vec();
for (kind, flags, stream, payload) in frames {
wire.extend(frame(kind, flags, stream * 2 - 1, &payload));
}
wire
})
}
proptest! {
#[test]
fn split_feeds_produce_the_same_events(wire in connection(), split in 1usize..400) {
let mut whole = Http2Parser::new();
whole.push(FlowSide::Initiator, &Bytes::from(wire.clone()));
let a = drain(&mut whole);
let at = split.min(wire.len().saturating_sub(1)).max(1);
let mut parts = Http2Parser::new();
let mut b = Vec::new();
for chunk in [&wire[..at], &wire[at..]] {
parts.push(FlowSide::Initiator, &Bytes::copy_from_slice(chunk));
b.extend(drain(&mut parts));
}
prop_assert_eq!(shape(&a), shape(&b));
prop_assert_eq!(whole.is_failed(), parts.is_failed());
}
#[test]
fn tracked_streams_never_exceed_the_cap(wire in connection()) {
let mut p = Http2Parser::with_config(
Http2Config::default().with_max_concurrent_streams(4),
);
p.push(FlowSide::Initiator, &Bytes::from(wire));
while p.next_event().is_some() {
prop_assert!(p.tracked_streams() <= 4);
}
prop_assert!(p.tracked_streams() <= 4);
}
#[test]
fn failure_is_terminal(wire in connection(), extra in proptest::collection::vec(any::<u8>(), 0..64)) {
let mut p = Http2Parser::new();
p.push(FlowSide::Initiator, &Bytes::from(wire));
while p.next_event().is_some() {}
if p.is_failed() {
let before = p.error();
prop_assert_eq!(p.push(FlowSide::Initiator, &Bytes::from(extra)), 0);
prop_assert!(p.next_event().is_none());
prop_assert_eq!(p.error(), before, "the first failure is the reported one");
}
}
#[test]
fn buffering_is_bounded(chunks in proptest::collection::vec(
proptest::collection::vec(any::<u8>(), 0..512), 0..12,
)) {
let cap = 8192;
let mut p = Http2Parser::with_config(
Http2Config::default().with_max_buffered_bytes(cap),
);
p.push(FlowSide::Initiator, &Bytes::from_static(PREFACE));
for c in chunks {
p.push(FlowSide::Initiator, &Bytes::from(c));
while p.next_event().is_some() {}
prop_assert!(p.buffered(FlowSide::Initiator) <= cap);
}
}
#[test]
fn never_panics(bytes in proptest::collection::vec(any::<u8>(), 0..512)) {
let mut p = Http2Parser::new();
p.push(FlowSide::Initiator, &Bytes::from_static(PREFACE));
p.push(FlowSide::Initiator, &Bytes::copy_from_slice(&bytes));
p.push(FlowSide::Responder, &Bytes::copy_from_slice(&bytes));
while p.next_event().is_some() {}
let mut bare = Http2Parser::new();
bare.push(FlowSide::Initiator, &Bytes::from(bytes));
while bare.next_event().is_some() {}
}
#[test]
fn encoded_blocks_round_trip_through_the_parser(blocks in block_sequence()) {
fn index_everything(_: &[u8], _: &[u8]) -> HeaderSensitivity {
HeaderSensitivity::Indexable
}
let mut enc = HpackEncoder::new().with_sensitivity(index_everything);
let mut p = Http2Parser::new();
p.push(FlowSide::Initiator, &Bytes::from_static(PREFACE));
for (i, fields) in blocks.iter().enumerate() {
let block = match enc.encode(fields) {
Ok(b) => b,
Err(_) => continue,
};
let stream = i as u32 * 2 + 1;
let wire = write_headers(stream, &block, true, 16_384).unwrap();
p.push(FlowSide::Initiator, &Bytes::from(wire));
let mut got = None;
while let Some(ev) = p.next_event() {
if let Http2Event::Head(h) = ev {
got = Some(h);
}
}
let head = got.expect("a head per block");
prop_assert_eq!(&head.fields, fields, "block {} changed in flight", i);
}
prop_assert!(!p.is_failed(), "{:?}", p.error());
}
#[test]
fn the_encoder_table_stays_bounded(blocks in block_sequence()) {
const CAP: usize = 512;
let mut enc = HpackEncoder::new().with_max_table_size(CAP);
for fields in &blocks {
let _ = enc.encode(fields);
prop_assert!(
enc.table_size() <= CAP,
"table grew to {}",
enc.table_size()
);
}
}
}
fn field_pair() -> impl Strategy<Value = (Bytes, Bytes)> {
let name = prop_oneof![
Just(":method".to_string()),
Just(":scheme".to_string()),
Just(":authority".to_string()),
Just("accept".to_string()),
Just("user-agent".to_string()),
"[a-z][a-z0-9-]{0,12}",
];
let value = prop_oneof![
Just("GET".to_string()),
Just("https".to_string()),
"[a-zA-Z0-9/._-]{0,40}",
];
(name, value).prop_map(|(n, v)| {
(
Bytes::copy_from_slice(n.as_bytes()),
Bytes::copy_from_slice(v.as_bytes()),
)
})
}
fn block_sequence() -> impl Strategy<Value = Vec<Vec<(Bytes, Bytes)>>> {
proptest::collection::vec(
proptest::collection::vec(field_pair(), 0..8).prop_map(|mut fields| {
fields.sort_by_key(|(n, _)| !n.starts_with(b":"));
fields
}),
0..6,
)
}