From c300a859a038fb3e336fd60f7c2e06649cd55149 Mon Sep 17 00:00:00 2001 From: Philippe Antoine Date: Wed, 30 Sep 2020 23:29:36 +0200 Subject: [PATCH] http2: keep track of dynamic headers table size And evict entries accordingly to maximum size --- rust/src/http2/http2.rs | 44 +++++++++++++++++-- rust/src/http2/parser.rs | 91 ++++++++++++++++++++++++---------------- 2 files changed, 96 insertions(+), 39 deletions(-) diff --git a/rust/src/http2/http2.rs b/rust/src/http2/http2.rs index 86218f923e..c7b55c3f60 100644 --- a/rust/src/http2/http2.rs +++ b/rust/src/http2/http2.rs @@ -58,6 +58,8 @@ const HTTP2_FRAME_GOAWAY_LEN: usize = 4; const HTTP2_FRAME_RSTSTREAM_LEN: usize = 4; const HTTP2_FRAME_PRIORITY_LEN: usize = 1; const HTTP2_FRAME_WINDOWUPDATE_LEN: usize = 4; +//TODO make this configurable +pub const HTTP2_MAX_TABLESIZE: u32 = 0x10000; // 65536 #[repr(u8)] #[derive(Copy, Clone, PartialOrd, PartialEq, Debug)] @@ -271,12 +273,30 @@ impl HTTP2Event { } } +pub struct HTTP2DynTable { + pub table: Vec, + pub current_size: usize, + pub max_size: usize, + pub overflow: u8, +} + +impl HTTP2DynTable { + pub fn new() -> Self { + Self { + table: Vec::with_capacity(64), + current_size: 0, + max_size: 4096, //default value + overflow: 0, + } + } +} + pub struct HTTP2State { tx_id: u64, request_frame_size: u32, response_frame_size: u32, - dynamic_headers_ts: Vec, - dynamic_headers_tc: Vec, + dynamic_headers_ts: HTTP2DynTable, + dynamic_headers_tc: HTTP2DynTable, transactions: Vec, progress: HTTP2ConnectionState, pub files: HTTP2Files, @@ -291,8 +311,8 @@ impl HTTP2State { // the headers are encoded on one byte // with a fixed number of static headers, and // a variable number of dynamic headers - dynamic_headers_ts: Vec::with_capacity(256 - parser::HTTP2_STATIC_HEADERS_NUMBER), - dynamic_headers_tc: Vec::with_capacity(256 - parser::HTTP2_STATIC_HEADERS_NUMBER), + dynamic_headers_ts: HTTP2DynTable::new(), + dynamic_headers_tc: HTTP2DynTable::new(), transactions: Vec::new(), progress: HTTP2ConnectionState::Http2StateInit, files: HTTP2Files::new(), @@ -444,6 +464,22 @@ impl HTTP2State { Some(parser::HTTP2FrameType::SETTINGS) => { match parser::http2_parse_frame_settings(input) { Ok((_, set)) => { + for i in 0..set.len() { + if set[i].id == parser::HTTP2SettingsId::SETTINGSHEADERTABLESIZE { + //set for both endpoints ? to be tested + self.dynamic_headers_tc.max_size = set[i].value as usize; + self.dynamic_headers_ts.max_size = set[i].value as usize; + if set[i].value > HTTP2_MAX_TABLESIZE { + //mark potential overflow + self.dynamic_headers_tc.overflow = 1; + self.dynamic_headers_ts.overflow = 1; + } else { + //reset in case peer set a lower value, to be tested + self.dynamic_headers_tc.overflow = 0; + self.dynamic_headers_ts.overflow = 0; + } + } + } //we could set an event on remaining data return HTTP2FrameTypeData::SETTINGS(set); } diff --git a/rust/src/http2/parser.rs b/rust/src/http2/parser.rs index 2555fd6322..d0e3b3aaa9 100644 --- a/rust/src/http2/parser.rs +++ b/rust/src/http2/parser.rs @@ -16,6 +16,7 @@ */ use super::huffman; +use crate::http2::http2::{HTTP2DynTable, HTTP2_MAX_TABLESIZE}; use nom::character::complete::digit1; use nom::combinator::rest; use nom::error::ErrorKind; @@ -209,9 +210,7 @@ named!(pub http2_parse_headers_priority, pub const HTTP2_STATIC_HEADERS_NUMBER: usize = 61; -fn http2_frame_header_static( - n: u64, dyn_headers: &Vec, -) -> Option { +fn http2_frame_header_static(n: u64, dyn_headers: &HTTP2DynTable) -> Option { let (name, value) = match n { 1 => (":authority", ""), 2 => (":method", "GET"), @@ -292,7 +291,7 @@ fn http2_frame_header_static( error: HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeIndex0, sizeupdate: 0, }); - } else if dyn_headers.len() + HTTP2_STATIC_HEADERS_NUMBER < n as usize { + } else if dyn_headers.table.len() + HTTP2_STATIC_HEADERS_NUMBER < n as usize { return Some(HTTP2FrameHeaderBlock { name: Vec::new(), value: Vec::new(), @@ -300,10 +299,10 @@ fn http2_frame_header_static( sizeupdate: 0, }); } else { - let indyn = dyn_headers.len() - (n as usize - HTTP2_STATIC_HEADERS_NUMBER); + let indyn = dyn_headers.table.len() - (n as usize - HTTP2_STATIC_HEADERS_NUMBER); let headcopy = HTTP2FrameHeaderBlock { - name: dyn_headers[indyn].name.to_vec(), - value: dyn_headers[indyn].value.to_vec(), + name: dyn_headers.table[indyn].name.to_vec(), + value: dyn_headers.table[indyn].value.to_vec(), error: HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeSuccess, sizeupdate: 0, }; @@ -338,7 +337,7 @@ pub struct HTTP2FrameHeaderBlock { } fn http2_parse_headers_block_indexed<'a>( - input: &'a [u8], dyn_headers: &Vec, + input: &'a [u8], dyn_headers: &HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { fn parser(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits!( @@ -379,7 +378,7 @@ fn http2_parse_headers_block_string(input: &[u8]) -> IResult<&[u8], Vec> { } fn http2_parse_headers_block_literal_common<'a>( - input: &'a [u8], index: u64, dyn_headers: &Vec, + input: &'a [u8], index: u64, dyn_headers: &HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { let (i3, name, error) = if index == 0 { match http2_parse_headers_block_string(input) { @@ -413,7 +412,7 @@ fn http2_parse_headers_block_literal_common<'a>( } fn http2_parse_headers_block_literal_incindex<'a>( - input: &'a [u8], dyn_headers: &mut Vec, + input: &'a [u8], dyn_headers: &mut HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { fn parser(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits!( @@ -438,16 +437,28 @@ fn http2_parse_headers_block_literal_incindex<'a>( error: head.error, sizeupdate: 0, }; - dyn_headers.push(headcopy); - let mut dynsize = 0; - //we may spend less CPU by storing in memory - for i in 0..dyn_headers.len() { - dynsize += 32 + dyn_headers[i].name.len() + dyn_headers[i].value.len(); - } - //TODO keep track of settings + updates instead of magic default value - while dynsize > 4096 { - dynsize -= 32 + dyn_headers[0].name.len() + dyn_headers[0].value.len(); - dyn_headers.remove(0); + if head.error == HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeSuccess { + dyn_headers.current_size += 32 + headcopy.name.len() + headcopy.value.len(); + //in case of overflow, best effort is to keep first headers + if dyn_headers.overflow > 0 { + if dyn_headers.overflow == 1 { + if dyn_headers.current_size <= (HTTP2_MAX_TABLESIZE as usize) { + //overflow had not yet happened + dyn_headers.table.push(headcopy); + } else if dyn_headers.current_size > dyn_headers.max_size { + //overflow happens, we cannot replace evicted headers + dyn_headers.overflow = 2; + } + } + } else { + dyn_headers.table.push(headcopy); + } + while dyn_headers.current_size > dyn_headers.max_size && dyn_headers.table.len() > 0 + { + dyn_headers.current_size -= + 32 + dyn_headers.table[0].name.len() + dyn_headers.table[0].value.len(); + dyn_headers.table.remove(0); + } } return Ok((r, head)); } @@ -458,7 +469,7 @@ fn http2_parse_headers_block_literal_incindex<'a>( } fn http2_parse_headers_block_literal_noindex<'a>( - input: &'a [u8], dyn_headers: &Vec, + input: &'a [u8], dyn_headers: &HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { fn parser(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits!( @@ -479,7 +490,7 @@ fn http2_parse_headers_block_literal_noindex<'a>( } fn http2_parse_headers_block_literal_neverindex<'a>( - input: &'a [u8], dyn_headers: &Vec, + input: &'a [u8], dyn_headers: &HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { fn parser(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits!( @@ -521,7 +532,9 @@ fn http2_parse_var_uint(input: &[u8], value: u64, max: u64) -> IResult<&[u8], u6 return Ok((i3, varval)); } -fn http2_parse_headers_block_dynamic_size(input: &[u8]) -> IResult<&[u8], HTTP2FrameHeaderBlock> { +fn http2_parse_headers_block_dynamic_size<'a>( + input: &'a [u8], dyn_headers: &mut HTTP2DynTable, +) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { fn parser(input: &[u8]) -> IResult<&[u8], (u8, u8)> { bits!( input, @@ -544,6 +557,15 @@ fn http2_parse_headers_block_dynamic_size(input: &[u8]) -> IResult<&[u8], HTTP2F }, )); } + if (maxsize2 as usize) < dyn_headers.max_size { + dyn_headers.max_size = maxsize2 as usize; + //may evict entries + while dyn_headers.current_size > dyn_headers.max_size { + dyn_headers.current_size -= + 32 + dyn_headers.table[0].name.len() + dyn_headers.table[0].value.len(); + dyn_headers.table.remove(0); + } + } return Ok(( i3, HTTP2FrameHeaderBlock { @@ -556,7 +578,7 @@ fn http2_parse_headers_block_dynamic_size(input: &[u8]) -> IResult<&[u8], HTTP2F } fn http2_parse_headers_block<'a>( - input: &'a [u8], dyn_headers: &mut Vec, + input: &'a [u8], dyn_headers: &mut HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaderBlock> { //caller garantees o have at least one byte if input[0] & 0x80 != 0 { @@ -564,7 +586,7 @@ fn http2_parse_headers_block<'a>( } else if input[0] & 0x40 != 0 { return http2_parse_headers_block_literal_incindex(input, dyn_headers); } else if input[0] & 0x20 != 0 { - return http2_parse_headers_block_dynamic_size(input); + return http2_parse_headers_block_dynamic_size(input, dyn_headers); } else if input[0] & 0x10 != 0 { return http2_parse_headers_block_literal_neverindex(input, dyn_headers); } else { @@ -586,7 +608,7 @@ const HTTP2_FLAG_HEADER_PADDED: u8 = 0x8; const HTTP2_FLAG_HEADER_PRIORITY: u8 = 0x20; pub fn http2_parse_frame_headers<'a>( - input: &'a [u8], flags: u8, dyn_headers: &mut Vec, + input: &'a [u8], flags: u8, dyn_headers: &mut HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameHeaders> { let (i2, padlength) = cond!(input, flags & HTTP2_FLAG_HEADER_PADDED != 0, be_u8)?; let (mut i3, priority) = cond!( @@ -630,7 +652,7 @@ pub struct HTTP2FramePushPromise { } pub fn http2_parse_frame_push_promise<'a>( - input: &'a [u8], flags: u8, dyn_headers: &mut Vec, + input: &'a [u8], flags: u8, dyn_headers: &mut HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FramePushPromise> { let (i2, padlength) = cond!(input, flags & HTTP2_FLAG_HEADER_PADDED != 0, be_u8)?; let (mut i3, stream_id) = bits!(i2, tuple!(take_bits!(1u8), take_bits!(31u32)))?; @@ -668,7 +690,7 @@ pub struct HTTP2FrameContinuation { } pub fn http2_parse_frame_continuation<'a>( - input: &'a [u8], dyn_headers: &mut Vec, + input: &'a [u8], dyn_headers: &mut HTTP2DynTable, ) -> IResult<&'a [u8], HTTP2FrameContinuation> { let mut i3 = input; let mut blocks = Vec::new(); @@ -897,8 +919,7 @@ mod tests { #[test] fn test_http2_parse_header() { let buf0: &[u8] = &[0x82]; - let mut dynh: Vec = - Vec::with_capacity(255 - HTTP2_STATIC_HEADERS_NUMBER); + let mut dynh = HTTP2DynTable::new(); let r0 = http2_parse_headers_block(buf0, &mut dynh); match r0 { Ok((remainder, hd)) => { @@ -924,7 +945,7 @@ mod tests { assert_eq!(hd.value, "*/*".as_bytes().to_vec()); // And we should have no bytes left. assert_eq!(remainder.len(), 0); - assert_eq!(dynh.len(), 1); + assert_eq!(dynh.table.len(), 1); } Err(Err::Incomplete(_)) => { panic!("Result should not have been incomplete."); @@ -944,7 +965,7 @@ mod tests { assert_eq!(hd.value, "localhost:3000".as_bytes().to_vec()); // And we should have no bytes left. assert_eq!(remainder.len(), 0); - assert_eq!(dynh.len(), 2); + assert_eq!(dynh.table.len(), 2); } Err(Err::Incomplete(_)) => { panic!("Result should not have been incomplete."); @@ -962,7 +983,7 @@ mod tests { assert_eq!(hd.value, "localhost:3000".as_bytes().to_vec()); // And we should have no bytes left. assert_eq!(remainder.len(), 0); - assert_eq!(dynh.len(), 2); + assert_eq!(dynh.table.len(), 2); } Err(Err::Incomplete(_)) => { panic!("Result should not have been incomplete."); @@ -977,7 +998,7 @@ mod tests { Ok((remainder, hd)) => { assert_eq!(hd.error, HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeIndex0); assert_eq!(remainder.len(), 0); - assert_eq!(dynh.len(), 2); + assert_eq!(dynh.table.len(), 2); } Err(Err::Incomplete(_)) => { panic!("Result should not have been incomplete."); @@ -998,7 +1019,7 @@ mod tests { assert_eq!(hd.value, "/doc/manual/html/index.html".as_bytes().to_vec()); // And we should have no bytes left. assert_eq!(remainder.len(), 0); - assert_eq!(dynh.len(), 2); + assert_eq!(dynh.table.len(), 2); } Err(Err::Incomplete(_)) => { panic!("Result should not have been incomplete.");