use crate::hpack::{Decoder, Encoder, Header}; use http::header::{HeaderName, HeaderValue}; use bytes::BytesMut; use quickcheck::{Arbitrary, Gen, QuickCheck, TestResult}; use rand::distributions::Slice; use rand::rngs::StdRng; use rand::{thread_rng, Rng, SeedableRng}; use std::io::Cursor; 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 set of headers to encode / decode frames: Vec, } #[derive(Debug, Clone)] struct HeaderFrame { resizes: Vec, headers: Vec>>, } impl FuzzHpack { fn new(seed: [u8; 32]) -> 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); } FuzzHpack { frames } } fn run(self) { 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 buf = BytesMut::new(); 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); } encoder.encode(frame.headers, &mut buf); // 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(_: &mut Gen) -> Self { FuzzHpack::new(thread_rng().gen()) } } fn gen_header(g: &mut StdRng) -> Header> { use http::{Method, StatusCode}; if g.gen_ratio(1, 10) { match g.gen_range(0u32..5) { 0 => { let value = gen_string(g, 4, 20); Header::Authority(to_shared(value)) } 1 => { let method = match g.gen_range(0u32..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.sample(Slice::new(b"ABCDEFGHIJKLMNOPQRSTUVWXYZ").unwrap())) .collect(); Method::from_bytes(&bytes).unwrap() } _ => unreachable!(), }; Header::Method(method) } 2 => { let value = match g.gen_range(0u32..2) { 0 => "http", 1 => "https", _ => unreachable!(), }; Header::Scheme(to_shared(value.to_string())) } 3 => { let value = match g.gen_range(0u32..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_ratio(1, 10) { None } else { Some(gen_header_name(g)) }; let mut value = gen_header_value(g); if g.gen_ratio(1, 30) { value.set_sensitive(true); } Header::Field { name, value } } } fn gen_header_name(g: &mut StdRng) -> HeaderName { use http::header; if g.gen_ratio(1, 2) { g.sample( Slice::new(&[ 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.sample(Slice::new(b"ABCDEFGHIJKLMNOPQRSTUVabcdefghilpqrstuvwxyz----").unwrap()) }) .collect(); String::from_utf8(bytes).unwrap() } fn to_shared(src: String) -> crate::hpack::BytesStr { crate::hpack::BytesStr::from(src.as_str()) }