use crate::hpack::{Decoder, Encode, Encoder, Header}; use http::header::{HeaderName, HeaderValue}; use bytes::{buf::BufMutExt, Bytes, BytesMut}; use quickcheck::{Arbitrary, Gen, QuickCheck, TestResult}; use rand::{Rng, SeedableRng, StdRng}; use std::io::Cursor; const MIN_CHUNK: usize = 16; const MAX_CHUNK: usize = 2 * 1024; #[test] fn hpack_fuzz() { let _ = env_logger::try_init(); fn prop(fuzz: FuzzHpack) -> TestResult { fuzz.run(); TestResult::from_bool(true) } QuickCheck::new() .tests(100) .quickcheck(prop as fn(FuzzHpack) -> TestResult) } /* // If wanting to test with a specific feed, uncomment and fill in the seed. #[test] fn hpack_fuzz_seeded() { let _ = env_logger::try_init(); let seed = [/* fill me in*/]; FuzzHpack::new(seed).run(); } */ #[derive(Debug, Clone)] struct FuzzHpack { // The magic seed that makes the test case reproducible seed: [usize; 4], // The set of headers to encode / decode frames: Vec, // The list of chunk sizes to do it in chunks: Vec, // Number of times reduced reduced: usize, } #[derive(Debug, Clone)] struct HeaderFrame { resizes: Vec, headers: Vec>>, } impl FuzzHpack { fn new(seed: [usize; 4]) -> FuzzHpack { // Seed the RNG let mut rng = StdRng::from_seed(&seed); // Generates a bunch of source headers let mut source: Vec>> = vec![]; for _ in 0..2000 { source.push(gen_header(&mut rng)); } // Actual test run headers let num: usize = rng.gen_range(40, 500); let mut frames: Vec = vec![]; let mut added = 0; let skew: i32 = rng.gen_range(1, 5); // Rough number of headers to add while added < num { let mut frame = HeaderFrame { resizes: vec![], headers: vec![], }; match rng.gen_range(0, 20) { 0 => { // Two resizes let high = rng.gen_range(128, MAX_CHUNK * 2); let low = rng.gen_range(0, high); frame.resizes.extend(&[low, high]); } 1..=3 => { frame.resizes.push(rng.gen_range(128, MAX_CHUNK * 2)); } _ => {} } let mut is_name_required = true; for _ in 0..rng.gen_range(1, (num - added) + 1) { let x: f64 = rng.gen_range(0.0, 1.0); let x = x.powi(skew); let i = (x * source.len() as f64) as usize; let header = &source[i]; match header { Header::Field { name: None, .. } => { if is_name_required { continue; } } Header::Field { .. } => { is_name_required = false; } _ => { // pseudos can't be followed by a header with no name is_name_required = true; } } frame.headers.push(header.clone()); added += 1; } frames.push(frame); } // Now, generate the buffer sizes used to encode let mut chunks = vec![]; for _ in 0..rng.gen_range(0, 100) { chunks.push(rng.gen_range(MIN_CHUNK, MAX_CHUNK)); } FuzzHpack { seed: seed, frames: frames, chunks: chunks, reduced: 0, } } fn run(self) { let mut chunks = self.chunks; let frames = self.frames; let mut expect = vec![]; let mut encoder = Encoder::default(); let mut decoder = Decoder::default(); for frame in frames { // build "expected" frames, such that decoding headers always // includes a name let mut prev_name = None; for header in &frame.headers { match header.clone().reify() { Ok(h) => { prev_name = match h { Header::Field { ref name, .. } => Some(name.clone()), _ => None, }; expect.push(h); } Err(value) => { expect.push(Header::Field { name: prev_name.as_ref().cloned().expect("previous header name"), value, }); } } } let mut input = frame.headers.into_iter(); let mut index = None; let mut max_chunk = chunks.pop().unwrap_or(MAX_CHUNK); let mut buf = BytesMut::with_capacity(max_chunk); if let Some(max) = frame.resizes.iter().max() { decoder.queue_size_update(*max); } // Apply resizes for resize in &frame.resizes { encoder.update_max_size(*resize); } loop { match encoder.encode(index.take(), &mut input, &mut (&mut buf).limit(max_chunk)) { Encode::Full => break, Encode::Partial(i) => { index = Some(i); // Decode the chunk! decoder .decode(&mut Cursor::new(&mut buf), |h| { let e = expect.remove(0); assert_eq!(h, e); }) .expect("partial decode"); max_chunk = chunks.pop().unwrap_or(MAX_CHUNK); buf = BytesMut::with_capacity(max_chunk); } } } // Decode the chunk! decoder .decode(&mut Cursor::new(&mut buf), |h| { let e = expect.remove(0); assert_eq!(h, e); }) .expect("full decode"); } assert_eq!(0, expect.len()); } } impl Arbitrary for FuzzHpack { fn arbitrary(g: &mut G) -> Self { FuzzHpack::new(quickcheck::Rng::gen(g)) } } fn gen_header(g: &mut StdRng) -> Header> { use http::{Method, StatusCode}; if g.gen_weighted_bool(10) { match g.next_u32() % 5 { 0 => { let value = gen_string(g, 4, 20); Header::Authority(to_shared(value)) } 1 => { let method = match g.next_u32() % 6 { 0 => Method::GET, 1 => Method::POST, 2 => Method::PUT, 3 => Method::PATCH, 4 => Method::DELETE, 5 => { let n: usize = g.gen_range(3, 7); let bytes: Vec = (0..n) .map(|_| g.choose(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap().clone()) .collect(); Method::from_bytes(&bytes).unwrap() } _ => unreachable!(), }; Header::Method(method) } 2 => { let value = match g.next_u32() % 2 { 0 => "http", 1 => "https", _ => unreachable!(), }; Header::Scheme(to_shared(value.to_string())) } 3 => { let value = match g.next_u32() % 100 { 0 => "/".to_string(), 1 => "/index.html".to_string(), _ => gen_string(g, 2, 20), }; Header::Path(to_shared(value)) } 4 => { let status = (g.gen::() % 500) + 100; Header::Status(StatusCode::from_u16(status).unwrap()) } _ => unreachable!(), } } else { let name = if g.gen_weighted_bool(10) { None } else { Some(gen_header_name(g)) }; let mut value = gen_header_value(g); if g.gen_weighted_bool(30) { value.set_sensitive(true); } Header::Field { name, value } } } fn gen_header_name(g: &mut StdRng) -> HeaderName { use http::header; if g.gen_weighted_bool(2) { g.choose(&[ header::ACCEPT, header::ACCEPT_CHARSET, header::ACCEPT_ENCODING, header::ACCEPT_LANGUAGE, header::ACCEPT_RANGES, header::ACCESS_CONTROL_ALLOW_CREDENTIALS, header::ACCESS_CONTROL_ALLOW_HEADERS, header::ACCESS_CONTROL_ALLOW_METHODS, header::ACCESS_CONTROL_ALLOW_ORIGIN, header::ACCESS_CONTROL_EXPOSE_HEADERS, header::ACCESS_CONTROL_MAX_AGE, header::ACCESS_CONTROL_REQUEST_HEADERS, header::ACCESS_CONTROL_REQUEST_METHOD, header::AGE, header::ALLOW, header::ALT_SVC, header::AUTHORIZATION, header::CACHE_CONTROL, header::CONNECTION, header::CONTENT_DISPOSITION, header::CONTENT_ENCODING, header::CONTENT_LANGUAGE, header::CONTENT_LENGTH, header::CONTENT_LOCATION, header::CONTENT_RANGE, header::CONTENT_SECURITY_POLICY, header::CONTENT_SECURITY_POLICY_REPORT_ONLY, header::CONTENT_TYPE, header::COOKIE, header::DNT, header::DATE, header::ETAG, header::EXPECT, header::EXPIRES, header::FORWARDED, header::FROM, header::HOST, header::IF_MATCH, header::IF_MODIFIED_SINCE, header::IF_NONE_MATCH, header::IF_RANGE, header::IF_UNMODIFIED_SINCE, header::LAST_MODIFIED, header::LINK, header::LOCATION, header::MAX_FORWARDS, header::ORIGIN, header::PRAGMA, header::PROXY_AUTHENTICATE, header::PROXY_AUTHORIZATION, header::PUBLIC_KEY_PINS, header::PUBLIC_KEY_PINS_REPORT_ONLY, header::RANGE, header::REFERER, header::REFERRER_POLICY, header::REFRESH, header::RETRY_AFTER, header::SERVER, header::SET_COOKIE, header::STRICT_TRANSPORT_SECURITY, header::TE, header::TRAILER, header::TRANSFER_ENCODING, header::USER_AGENT, header::UPGRADE, header::UPGRADE_INSECURE_REQUESTS, header::VARY, header::VIA, header::WARNING, header::WWW_AUTHENTICATE, header::X_CONTENT_TYPE_OPTIONS, header::X_DNS_PREFETCH_CONTROL, header::X_FRAME_OPTIONS, header::X_XSS_PROTECTION, ]) .unwrap() .clone() } else { let value = gen_string(g, 1, 25); HeaderName::from_bytes(value.as_bytes()).unwrap() } } fn gen_header_value(g: &mut StdRng) -> HeaderValue { let value = gen_string(g, 0, 70); HeaderValue::from_bytes(value.as_bytes()).unwrap() } fn gen_string(g: &mut StdRng, min: usize, max: usize) -> String { let bytes: Vec<_> = (min..max) .map(|_| { // Chars to pick from g.choose(b"ABCDEFGHIJKLMNOPQRSTUVabcdefghilpqrstuvwxyz----") .unwrap() .clone() }) .collect(); String::from_utf8(bytes).unwrap() } fn to_shared(src: String) -> crate::hpack::BytesStr { let b: Bytes = src.into(); unsafe { crate::hpack::BytesStr::from_utf8_unchecked(b) } }