| 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<HeaderFrame>, |
| } |
| |
| #[derive(Debug, Clone)] |
| struct HeaderFrame { |
| resizes: Vec<usize>, |
| headers: Vec<Header<Option<HeaderName>>>, |
| } |
| |
| 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<Header<Option<HeaderName>>> = 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<HeaderFrame> = 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<Option<HeaderName>> { |
| 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<u8> = (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::<u16>() % 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()) |
| } |