diff options
Diffstat (limited to 'rust/src/http2/http2.rs')
-rw-r--r-- | rust/src/http2/http2.rs | 1408 |
1 files changed, 1408 insertions, 0 deletions
diff --git a/rust/src/http2/http2.rs b/rust/src/http2/http2.rs new file mode 100644 index 0000000..b62ccb9 --- /dev/null +++ b/rust/src/http2/http2.rs @@ -0,0 +1,1408 @@ +/* Copyright (C) 2020-2022 Open Information Security Foundation + * + * You can copy, redistribute or modify this Program under the terms of + * the GNU General Public License version 2 as published by the Free + * Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * version 2 along with this program; if not, write to the Free Software + * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA + * 02110-1301, USA. + */ + +use super::decompression; +use super::detect; +use super::parser; +use super::range; + +use crate::applayer::{self, *}; +use crate::conf::conf_get; +use crate::core::*; +use crate::filecontainer::*; +use crate::filetracker::*; +use nom7::Err; +use std; +use std::collections::VecDeque; +use std::ffi::CString; +use std::fmt; +use std::io; + +static mut ALPROTO_HTTP2: AppProto = ALPROTO_UNKNOWN; + +const HTTP2_DEFAULT_MAX_FRAME_SIZE: u32 = 16384; +const HTTP2_MAX_HANDLED_FRAME_SIZE: usize = 65536; +const HTTP2_MIN_HANDLED_FRAME_SIZE: usize = 256; + +pub static mut SURICATA_HTTP2_FILE_CONFIG: Option<&'static SuricataFileContext> = None; + +#[no_mangle] +pub extern "C" fn rs_http2_init(context: &'static mut SuricataFileContext) { + unsafe { + SURICATA_HTTP2_FILE_CONFIG = Some(context); + } +} + +#[repr(u8)] +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq)] +pub enum HTTP2ConnectionState { + Http2StateInit = 0, + Http2StateMagicDone = 1, +} + +const HTTP2_FRAME_HEADER_LEN: usize = 9; +const HTTP2_MAGIC_LEN: usize = 24; +const HTTP2_FRAME_GOAWAY_LEN: usize = 4; +const HTTP2_FRAME_RSTSTREAM_LEN: usize = 4; +const HTTP2_FRAME_PRIORITY_LEN: usize = 5; +const HTTP2_FRAME_WINDOWUPDATE_LEN: usize = 4; +pub static mut HTTP2_MAX_TABLESIZE: u32 = 65536; // 0x10000 +// maximum size of reassembly for header + continuation +static mut HTTP2_MAX_REASS: usize = 102400; +static mut HTTP2_MAX_STREAMS: usize = 4096; // 0x1000 + +#[repr(u8)] +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Debug)] +pub enum HTTP2FrameUnhandledReason { + UnknownType = 0, + TooLong = 1, + ParsingError = 2, + Incomplete = 3, +} + +impl fmt::Display for HTTP2FrameUnhandledReason { + fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { + write!(f, "{:?}", self) + } +} + +#[derive(Debug)] +pub struct HTTP2FrameUnhandled { + pub reason: HTTP2FrameUnhandledReason, +} + +#[derive(Debug)] +pub enum HTTP2FrameTypeData { + PRIORITY(parser::HTTP2FramePriority), + GOAWAY(parser::HTTP2FrameGoAway), + RSTSTREAM(parser::HTTP2FrameRstStream), + SETTINGS(Vec<parser::HTTP2FrameSettings>), + WINDOWUPDATE(parser::HTTP2FrameWindowUpdate), + HEADERS(parser::HTTP2FrameHeaders), + PUSHPROMISE(parser::HTTP2FramePushPromise), + CONTINUATION(parser::HTTP2FrameContinuation), + PING, + DATA, + //not a defined frame + UNHANDLED(HTTP2FrameUnhandled), +} + +#[repr(u8)] +#[derive(Copy, Clone, PartialOrd, PartialEq, Eq, Debug)] +pub enum HTTP2TransactionState { + HTTP2StateIdle = 0, + HTTP2StateOpen = 1, + HTTP2StateReserved = 2, + HTTP2StateDataClient = 3, + HTTP2StateHalfClosedClient = 4, + HTTP2StateDataServer = 5, + HTTP2StateHalfClosedServer = 6, + HTTP2StateClosed = 7, + //not a RFC-defined state, used for stream 0 frames applying to the global connection + HTTP2StateGlobal = 8, + //not a RFC-defined state, dropping this old tx because we have too many + HTTP2StateTodrop = 9, +} + +#[derive(Debug)] +pub struct HTTP2Frame { + pub header: parser::HTTP2FrameHeader, + pub data: HTTP2FrameTypeData, +} + +#[derive(Debug)] +pub struct HTTP2Transaction { + tx_id: u64, + pub stream_id: u32, + pub state: HTTP2TransactionState, + child_stream_id: u32, + + pub frames_tc: Vec<HTTP2Frame>, + pub frames_ts: Vec<HTTP2Frame>, + + decoder: decompression::HTTP2Decoder, + pub file_range: *mut HttpRangeContainerBlock, + + pub tx_data: AppLayerTxData, + pub ft_tc: FileTransferTracker, + pub ft_ts: FileTransferTracker, + + //temporary escaped header for detection + //must be attached to transaction for memory management (be freed at the right time) + pub escaped: Vec<Vec<u8>>, + pub req_line: Vec<u8>, + pub resp_line: Vec<u8>, +} + +impl Transaction for HTTP2Transaction { + fn id(&self) -> u64 { + self.tx_id + } +} + +impl Default for HTTP2Transaction { + fn default() -> Self { + Self::new() + } +} + +impl HTTP2Transaction { + pub fn new() -> Self { + Self { + tx_id: 0, + stream_id: 0, + child_stream_id: 0, + state: HTTP2TransactionState::HTTP2StateIdle, + frames_tc: Vec::new(), + frames_ts: Vec::new(), + decoder: decompression::HTTP2Decoder::new(), + file_range: std::ptr::null_mut(), + tx_data: AppLayerTxData::new(), + ft_tc: FileTransferTracker::new(), + ft_ts: FileTransferTracker::new(), + escaped: Vec::with_capacity(16), + req_line: Vec::new(), + resp_line: Vec::new(), + } + } + + pub fn free(&mut self) { + if !self.file_range.is_null() { + if let Some(c) = unsafe { SC } { + if let Some(sfcm) = unsafe { SURICATA_HTTP2_FILE_CONFIG } { + //TODO get a file container instead of NULL + (c.HTPFileCloseHandleRange)( + sfcm.files_sbcfg, + std::ptr::null_mut(), + 0, + self.file_range, + std::ptr::null_mut(), + 0, + ); + (c.HttpRangeFreeBlock)(self.file_range); + self.file_range = std::ptr::null_mut(); + } + } + } + } + + pub fn set_event(&mut self, event: HTTP2Event) { + self.tx_data.set_event(event as u8); + } + + fn handle_headers(&mut self, blocks: &[parser::HTTP2FrameHeaderBlock], dir: Direction) { + let mut authority = None; + let mut host = None; + for block in blocks { + if block.name == b"content-encoding" { + self.decoder.http2_encoding_fromvec(&block.value, dir); + } else if block.name.eq_ignore_ascii_case(b":authority") { + authority = Some(&block.value); + if block.value.iter().any(|&x| x == b'@') { + // it is forbidden by RFC 9113 to have userinfo in this field + // when in HTTP1 we can have user:password@domain.com + self.set_event(HTTP2Event::UserinfoInUri); + } + } else if block.name.eq_ignore_ascii_case(b"host") { + host = Some(&block.value); + } + } + if let Some(a) = authority { + if let Some(h) = host { + if !a.eq_ignore_ascii_case(h) { + // The event is triggered only if both headers + // are in the same frame to avoid excessive + // complexity at runtime. + self.set_event(HTTP2Event::AuthorityHostMismatch); + } + } + } + } + + pub fn update_file_flags(&mut self, flow_file_flags: u16) { + self.ft_ts.file_flags = unsafe { FileFlowFlagsToFlags(flow_file_flags, STREAM_TOSERVER) }; + self.ft_tc.file_flags = unsafe { FileFlowFlagsToFlags(flow_file_flags, STREAM_TOCLIENT) }; + } + + fn decompress<'a>( + &'a mut self, input: &'a [u8], dir: Direction, sfcm: &'static SuricataFileContext, over: bool, flow: *const Flow, + ) -> io::Result<()> { + let mut output = Vec::with_capacity(decompression::HTTP2_DECOMPRESSION_CHUNK_SIZE); + let decompressed = self.decoder.decompress(input, &mut output, dir)?; + let xid: u32 = self.tx_id as u32; + if dir == Direction::ToClient { + self.ft_tc.tx_id = self.tx_id - 1; + // Check that we are at the beginning of the file + if !self.ft_tc.is_initialized() { + // we are now sure that new_chunk will open a file + // even if it may close it right afterwards + self.tx_data.incr_files_opened(); + if let Ok(value) = detect::http2_frames_get_header_value_vec( + self, + Direction::ToClient, + "content-range", + ) { + match range::http2_parse_check_content_range(&value) { + Ok((_, v)) => { + range::http2_range_open(self, &v, flow, sfcm, Direction::ToClient, decompressed); + if over && !self.file_range.is_null() { + range::http2_range_close(self, Direction::ToClient, &[]) + } + } + _ => { + self.set_event(HTTP2Event::InvalidRange); + } + } + } + } else if !self.file_range.is_null() { + if over { + range::http2_range_close(self, Direction::ToClient, decompressed) + } else { + range::http2_range_append(sfcm, self.file_range, decompressed) + } + } + self.ft_tc.new_chunk( + sfcm, + b"", + decompressed, + self.ft_tc.tracked, //offset = append + decompressed.len() as u32, + 0, + over, + &xid, + ); + } else { + self.ft_ts.tx_id = self.tx_id - 1; + if !self.ft_ts.file_open { + self.tx_data.incr_files_opened(); + } + self.ft_ts.new_chunk( + sfcm, + b"", + decompressed, + self.ft_ts.tracked, //offset = append + decompressed.len() as u32, + 0, + over, + &xid, + ); + }; + return Ok(()); + } + + fn handle_frame( + &mut self, header: &parser::HTTP2FrameHeader, data: &HTTP2FrameTypeData, dir: Direction, + ) { + //handle child_stream_id changes + match data { + HTTP2FrameTypeData::PUSHPROMISE(hs) => { + if dir == Direction::ToClient { + //we could set an event if self.child_stream_id != 0 + if header.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS == 0 { + self.child_stream_id = hs.stream_id; + } + self.state = HTTP2TransactionState::HTTP2StateReserved; + } + self.handle_headers(&hs.blocks, dir); + } + HTTP2FrameTypeData::CONTINUATION(hs) => { + if dir == Direction::ToClient + && header.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS != 0 + { + self.child_stream_id = 0; + } + self.handle_headers(&hs.blocks, dir); + } + HTTP2FrameTypeData::HEADERS(hs) => { + if dir == Direction::ToClient { + self.child_stream_id = 0; + } + self.handle_headers(&hs.blocks, dir); + } + HTTP2FrameTypeData::RSTSTREAM(_) => { + self.child_stream_id = 0; + } + _ => {} + } + //handle closing state changes + match data { + HTTP2FrameTypeData::HEADERS(_) | HTTP2FrameTypeData::DATA => { + if header.flags & parser::HTTP2_FLAG_HEADER_EOS != 0 { + match self.state { + HTTP2TransactionState::HTTP2StateHalfClosedClient + | HTTP2TransactionState::HTTP2StateDataServer => { + if dir == Direction::ToClient { + self.state = HTTP2TransactionState::HTTP2StateClosed; + } + } + HTTP2TransactionState::HTTP2StateHalfClosedServer => { + if dir == Direction::ToServer { + self.state = HTTP2TransactionState::HTTP2StateClosed; + } + } + // do not revert back to a half closed state + HTTP2TransactionState::HTTP2StateClosed => {} + HTTP2TransactionState::HTTP2StateGlobal => {} + _ => { + if dir == Direction::ToClient { + self.state = HTTP2TransactionState::HTTP2StateHalfClosedServer; + } else { + self.state = HTTP2TransactionState::HTTP2StateHalfClosedClient; + } + } + } + } else if header.ftype == parser::HTTP2FrameType::Data as u8 { + //not end of stream + if dir == Direction::ToServer { + if self.state < HTTP2TransactionState::HTTP2StateDataClient { + self.state = HTTP2TransactionState::HTTP2StateDataClient; + } + } else if self.state < HTTP2TransactionState::HTTP2StateDataServer { + self.state = HTTP2TransactionState::HTTP2StateDataServer; + } + } + } + _ => {} + } + } +} + +impl Drop for HTTP2Transaction { + fn drop(&mut self) { + if let Some(sfcm) = unsafe { SURICATA_HTTP2_FILE_CONFIG } { + self.ft_ts.file.free(sfcm); + self.ft_tc.file.free(sfcm); + } + self.free(); + } +} + +#[derive(AppLayerEvent)] +pub enum HTTP2Event { + InvalidFrameHeader, + InvalidClientMagic, + InvalidFrameData, + InvalidHeader, + InvalidFrameLength, + ExtraHeaderData, + LongFrameData, + StreamIdReuse, + InvalidHTTP1Settings, + FailedDecompression, + InvalidRange, + HeaderIntegerOverflow, + TooManyStreams, + AuthorityHostMismatch, + UserinfoInUri, + ReassemblyLimitReached, +} + +pub struct HTTP2DynTable { + pub table: Vec<parser::HTTP2FrameHeaderBlock>, + pub current_size: usize, + pub max_size: usize, + pub overflow: u8, +} + +impl Default for HTTP2DynTable { + fn default() -> Self { + Self::new() + } +} + +impl HTTP2DynTable { + pub fn new() -> Self { + Self { + table: Vec::with_capacity(64), + current_size: 0, + max_size: 4096, //default value + overflow: 0, + } + } +} + +#[derive(Default)] +struct HTTP2HeaderReassemblyBuffer { + data: Vec<u8>, + stream_id: u32, +} + +pub struct HTTP2State { + state_data: AppLayerStateData, + tx_id: u64, + request_frame_size: u32, + response_frame_size: u32, + dynamic_headers_ts: HTTP2DynTable, + dynamic_headers_tc: HTTP2DynTable, + transactions: VecDeque<HTTP2Transaction>, + progress: HTTP2ConnectionState, + + c2s_buf: HTTP2HeaderReassemblyBuffer, + s2c_buf: HTTP2HeaderReassemblyBuffer, +} + +impl State<HTTP2Transaction> for HTTP2State { + fn get_transaction_count(&self) -> usize { + self.transactions.len() + } + + fn get_transaction_by_index(&self, index: usize) -> Option<&HTTP2Transaction> { + self.transactions.get(index) + } +} + +impl Default for HTTP2State { + fn default() -> Self { + Self::new() + } +} + +impl HTTP2State { + pub fn new() -> Self { + Self { + state_data: AppLayerStateData::new(), + tx_id: 0, + request_frame_size: 0, + response_frame_size: 0, + // the headers are encoded on one byte + // with a fixed number of static headers, and + // a variable number of dynamic headers + dynamic_headers_ts: HTTP2DynTable::new(), + dynamic_headers_tc: HTTP2DynTable::new(), + transactions: VecDeque::new(), + progress: HTTP2ConnectionState::Http2StateInit, + c2s_buf: HTTP2HeaderReassemblyBuffer::default(), + s2c_buf: HTTP2HeaderReassemblyBuffer::default(), + } + } + + pub fn free(&mut self) { + // this should be in HTTP2Transaction::free + // but we need state's file container cf https://redmine.openinfosecfoundation.org/issues/4444 + for tx in &mut self.transactions { + if !tx.file_range.is_null() { + if let Some(c) = unsafe { SC } { + if let Some(sfcm) = unsafe { SURICATA_HTTP2_FILE_CONFIG } { + (c.HTPFileCloseHandleRange)( + sfcm.files_sbcfg, + &mut tx.ft_tc.file, + 0, + tx.file_range, + std::ptr::null_mut(), + 0, + ); + (c.HttpRangeFreeBlock)(tx.file_range); + tx.file_range = std::ptr::null_mut(); + } + } + } + } + self.transactions.clear(); + } + + pub fn set_event(&mut self, event: HTTP2Event) { + let len = self.transactions.len(); + if len == 0 { + return; + } + let tx = &mut self.transactions[len - 1]; + tx.tx_data.set_event(event as u8); + } + + // Free a transaction by ID. + fn free_tx(&mut self, tx_id: u64) { + let len = self.transactions.len(); + let mut found = false; + let mut index = 0; + for i in 0..len { + let tx = &mut self.transactions[i]; + if tx.tx_id == tx_id + 1 { + found = true; + index = i; + // this should be in HTTP2Transaction::free + // but we need state's file container cf https://redmine.openinfosecfoundation.org/issues/4444 + if !tx.file_range.is_null() { + if let Some(c) = unsafe { SC } { + if let Some(sfcm) = unsafe { SURICATA_HTTP2_FILE_CONFIG } { + (c.HTPFileCloseHandleRange)( + sfcm.files_sbcfg, + &mut tx.ft_tc.file, + 0, + tx.file_range, + std::ptr::null_mut(), + 0, + ); + (c.HttpRangeFreeBlock)(tx.file_range); + tx.file_range = std::ptr::null_mut(); + } + } + } + break; + } + } + if found { + self.transactions.remove(index); + } + } + + pub fn get_tx(&mut self, tx_id: u64) -> Option<&HTTP2Transaction> { + for tx in &mut self.transactions { + if tx.tx_id == tx_id + 1 { + tx.tx_data.update_file_flags(self.state_data.file_flags); + tx.update_file_flags(tx.tx_data.file_flags); + return Some(tx); + } + } + return None; + } + + fn find_tx_index(&mut self, sid: u32) -> usize { + for i in 0..self.transactions.len() { + //reverse order should be faster + let idx = self.transactions.len() - 1 - i; + if sid == self.transactions[idx].stream_id { + return idx + 1; + } + } + return 0; + } + + fn find_child_stream_id(&mut self, sid: u32) -> u32 { + for i in 0..self.transactions.len() { + //reverse order should be faster + if sid == self.transactions[self.transactions.len() - 1 - i].stream_id { + if self.transactions[self.transactions.len() - 1 - i].child_stream_id > 0 { + return self.transactions[self.transactions.len() - 1 - i].child_stream_id; + } + return sid; + } + } + return sid; + } + + fn create_global_tx(&mut self) -> &mut HTTP2Transaction { + //special transaction with only one frame + //as it affects the global connection, there is no end to it + let mut tx = HTTP2Transaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + tx.state = HTTP2TransactionState::HTTP2StateGlobal; + tx.tx_data.update_file_flags(self.state_data.file_flags); + // TODO can this tx hold files? + tx.tx_data.file_tx = STREAM_TOSERVER|STREAM_TOCLIENT; // might hold files in both directions + tx.update_file_flags(tx.tx_data.file_flags); + self.transactions.push_back(tx); + return self.transactions.back_mut().unwrap(); + } + + pub fn find_or_create_tx( + &mut self, header: &parser::HTTP2FrameHeader, data: &HTTP2FrameTypeData, dir: Direction, + ) -> Option<&mut HTTP2Transaction> { + if header.stream_id == 0 { + if self.transactions.len() >= unsafe { HTTP2_MAX_STREAMS } { + for tx_old in &mut self.transactions { + if tx_old.state == HTTP2TransactionState::HTTP2StateTodrop { + // loop was already run + break; + } + tx_old.set_event(HTTP2Event::TooManyStreams); + // use a distinct state, even if we do not log it + tx_old.state = HTTP2TransactionState::HTTP2StateTodrop; + } + return None; + } + return Some(self.create_global_tx()); + } + let sid = match data { + //yes, the right stream_id for Suricata is not the header one + HTTP2FrameTypeData::PUSHPROMISE(hs) => hs.stream_id, + HTTP2FrameTypeData::CONTINUATION(_) => { + if dir == Direction::ToClient { + //continuation of a push promise + self.find_child_stream_id(header.stream_id) + } else { + header.stream_id + } + } + _ => header.stream_id, + }; + let index = self.find_tx_index(sid); + if index > 0 { + if self.transactions[index - 1].state == HTTP2TransactionState::HTTP2StateClosed { + //these frames can be received in this state for a short period + if header.ftype != parser::HTTP2FrameType::RstStream as u8 + && header.ftype != parser::HTTP2FrameType::WindowUpdate as u8 + && header.ftype != parser::HTTP2FrameType::Priority as u8 + { + self.set_event(HTTP2Event::StreamIdReuse); + } + } + + let tx = &mut self.transactions[index - 1]; + tx.tx_data.update_file_flags(self.state_data.file_flags); + tx.update_file_flags(tx.tx_data.file_flags); + return Some(tx); + } else { + // do not use SETTINGS_MAX_CONCURRENT_STREAMS as it can grow too much + if self.transactions.len() >= unsafe { HTTP2_MAX_STREAMS } { + for tx_old in &mut self.transactions { + if tx_old.state == HTTP2TransactionState::HTTP2StateTodrop { + // loop was already run + break; + } + tx_old.set_event(HTTP2Event::TooManyStreams); + // use a distinct state, even if we do not log it + tx_old.state = HTTP2TransactionState::HTTP2StateTodrop; + } + return None; + } + let mut tx = HTTP2Transaction::new(); + self.tx_id += 1; + tx.tx_id = self.tx_id; + tx.stream_id = sid; + tx.state = HTTP2TransactionState::HTTP2StateOpen; + tx.tx_data.update_file_flags(self.state_data.file_flags); + tx.update_file_flags(tx.tx_data.file_flags); + tx.tx_data.file_tx = STREAM_TOSERVER|STREAM_TOCLIENT; // might hold files in both directions + self.transactions.push_back(tx); + return Some(self.transactions.back_mut().unwrap()); + } + } + + fn process_headers(&mut self, blocks: &Vec<parser::HTTP2FrameHeaderBlock>, dir: Direction) { + let (mut update, mut sizeup) = (false, 0); + for block in blocks { + if block.error >= parser::HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeError { + self.set_event(HTTP2Event::InvalidHeader); + } else if block.error + == parser::HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeSizeUpdate + { + update = true; + if block.sizeupdate > sizeup { + sizeup = block.sizeupdate; + } + } else if block.error + == parser::HTTP2HeaderDecodeStatus::HTTP2HeaderDecodeIntegerOverflow + { + self.set_event(HTTP2Event::HeaderIntegerOverflow); + } + } + if update { + //borrow checker forbids to pass directly dyn_headers + let dyn_headers = if dir == Direction::ToClient { + &mut self.dynamic_headers_tc + } else { + &mut self.dynamic_headers_ts + }; + dyn_headers.max_size = sizeup as usize; + } + } + + fn parse_frame_data( + &mut self, head: &parser::HTTP2FrameHeader, input: &[u8], complete: bool, dir: Direction, + reass_limit_reached: &mut bool, + ) -> HTTP2FrameTypeData { + let ftype = head.ftype; + let hflags = head.flags; + match num::FromPrimitive::from_u8(ftype) { + Some(parser::HTTP2FrameType::GoAway) => { + if input.len() < HTTP2_FRAME_GOAWAY_LEN { + self.set_event(HTTP2Event::InvalidFrameLength); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::Incomplete, + }); + } + match parser::http2_parse_frame_goaway(input) { + Ok((_, goaway)) => { + return HTTP2FrameTypeData::GOAWAY(goaway); + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + Some(parser::HTTP2FrameType::Settings) => { + match parser::http2_parse_frame_settings(input) { + Ok((_, set)) => { + for e in &set { + if e.id == parser::HTTP2SettingsId::HeaderTableSize { + //reverse order as this is what we accept from the other endpoint + let dyn_headers = if dir == Direction::ToClient { + &mut self.dynamic_headers_ts + } else { + &mut self.dynamic_headers_tc + }; + dyn_headers.max_size = e.value as usize; + if e.value > unsafe { HTTP2_MAX_TABLESIZE } { + //mark potential overflow + dyn_headers.overflow = 1; + } else { + //reset in case peer set a lower value, to be tested + dyn_headers.overflow = 0; + } + } + } + //we could set an event on remaining data + return HTTP2FrameTypeData::SETTINGS(set); + } + Err(Err::Incomplete(_)) => { + if complete { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } else { + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::TooLong, + }); + } + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + Some(parser::HTTP2FrameType::RstStream) => { + if input.len() != HTTP2_FRAME_RSTSTREAM_LEN { + self.set_event(HTTP2Event::InvalidFrameLength); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::Incomplete, + }); + } else { + match parser::http2_parse_frame_rststream(input) { + Ok((_, rst)) => { + return HTTP2FrameTypeData::RSTSTREAM(rst); + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + } + Some(parser::HTTP2FrameType::Priority) => { + if input.len() != HTTP2_FRAME_PRIORITY_LEN { + self.set_event(HTTP2Event::InvalidFrameLength); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::Incomplete, + }); + } else { + match parser::http2_parse_frame_priority(input) { + Ok((_, priority)) => { + return HTTP2FrameTypeData::PRIORITY(priority); + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + } + Some(parser::HTTP2FrameType::WindowUpdate) => { + if input.len() != HTTP2_FRAME_WINDOWUPDATE_LEN { + self.set_event(HTTP2Event::InvalidFrameLength); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::Incomplete, + }); + } else { + match parser::http2_parse_frame_windowupdate(input) { + Ok((_, wu)) => { + return HTTP2FrameTypeData::WINDOWUPDATE(wu); + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + } + Some(parser::HTTP2FrameType::PushPromise) => { + let dyn_headers = if dir == Direction::ToClient { + &mut self.dynamic_headers_tc + } else { + &mut self.dynamic_headers_ts + }; + match parser::http2_parse_frame_push_promise(input, hflags, dyn_headers) { + Ok((_, hs)) => { + self.process_headers(&hs.blocks, dir); + return HTTP2FrameTypeData::PUSHPROMISE(hs); + } + Err(Err::Incomplete(_)) => { + if complete { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } else { + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::TooLong, + }); + } + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + Some(parser::HTTP2FrameType::Data) => { + return HTTP2FrameTypeData::DATA; + } + Some(parser::HTTP2FrameType::Continuation) => { + let buf = if dir == Direction::ToClient { + &mut self.s2c_buf + } else { + &mut self.c2s_buf + }; + if head.stream_id == buf.stream_id { + let max_reass = unsafe { HTTP2_MAX_REASS }; + if buf.data.len() + input.len() < max_reass { + buf.data.extend(input); + } else if buf.data.len() < max_reass { + buf.data.extend(&input[..max_reass - buf.data.len()]); + *reass_limit_reached = true; + } + if head.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS == 0 { + let hs = parser::HTTP2FrameContinuation { + blocks: Vec::new(), + }; + return HTTP2FrameTypeData::CONTINUATION(hs); + } + } // else try to parse anyways + let input_reass = if head.stream_id == buf.stream_id { &buf.data } else { input }; + + let dyn_headers = if dir == Direction::ToClient { + &mut self.dynamic_headers_tc + } else { + &mut self.dynamic_headers_ts + }; + match parser::http2_parse_frame_continuation(input_reass, dyn_headers) { + Ok((_, hs)) => { + if head.stream_id == buf.stream_id { + buf.stream_id = 0; + buf.data.clear(); + } + self.process_headers(&hs.blocks, dir); + return HTTP2FrameTypeData::CONTINUATION(hs); + } + Err(Err::Incomplete(_)) => { + if head.stream_id == buf.stream_id { + buf.stream_id = 0; + buf.data.clear(); + } + if complete { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } else { + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::TooLong, + }); + } + } + Err(_) => { + if head.stream_id == buf.stream_id { + buf.stream_id = 0; + buf.data.clear(); + } + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + Some(parser::HTTP2FrameType::Headers) => { + if head.flags & parser::HTTP2_FLAG_HEADER_END_HEADERS == 0 { + let buf = if dir == Direction::ToClient { + &mut self.s2c_buf + } else { + &mut self.c2s_buf + }; + buf.data.clear(); + buf.data.extend(input); + buf.stream_id = head.stream_id; + let hs = parser::HTTP2FrameHeaders { + padlength: None, + priority: None, + blocks: Vec::new(), + }; + return HTTP2FrameTypeData::HEADERS(hs); + } + let dyn_headers = if dir == Direction::ToClient { + &mut self.dynamic_headers_tc + } else { + &mut self.dynamic_headers_ts + }; + match parser::http2_parse_frame_headers(input, hflags, dyn_headers) { + Ok((hrem, hs)) => { + self.process_headers(&hs.blocks, dir); + if !hrem.is_empty() { + SCLogDebug!("Remaining data for HTTP2 headers"); + self.set_event(HTTP2Event::ExtraHeaderData); + } + return HTTP2FrameTypeData::HEADERS(hs); + } + Err(Err::Incomplete(_)) => { + if complete { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } else { + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::TooLong, + }); + } + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameData); + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::ParsingError, + }); + } + } + } + Some(parser::HTTP2FrameType::Ping) => { + return HTTP2FrameTypeData::PING; + } + _ => { + return HTTP2FrameTypeData::UNHANDLED(HTTP2FrameUnhandled { + reason: HTTP2FrameUnhandledReason::UnknownType, + }); + } + } + } + + fn parse_frames( + &mut self, mut input: &[u8], il: usize, dir: Direction, flow: *const Flow, + ) -> AppLayerResult { + while !input.is_empty() { + match parser::http2_parse_frame_header(input) { + Ok((rem, head)) => { + let hl = head.length as usize; + + //we check for completeness first + if rem.len() < hl { + //but limit ourselves so as not to exhaust memory + if hl < HTTP2_MAX_HANDLED_FRAME_SIZE { + return AppLayerResult::incomplete( + (il - input.len()) as u32, + (HTTP2_FRAME_HEADER_LEN + hl) as u32, + ); + } else if rem.len() < HTTP2_MIN_HANDLED_FRAME_SIZE { + return AppLayerResult::incomplete( + (il - input.len()) as u32, + (HTTP2_FRAME_HEADER_LEN + HTTP2_MIN_HANDLED_FRAME_SIZE) as u32, + ); + } else { + self.set_event(HTTP2Event::LongFrameData); + self.request_frame_size = head.length - (rem.len() as u32); + } + } + + //get a safe length for the buffer + let (hlsafe, complete) = if rem.len() < hl { + (rem.len(), false) + } else { + (hl, true) + }; + + if head.length == 0 && head.ftype == parser::HTTP2FrameType::Settings as u8 { + input = &rem[hlsafe..]; + continue; + } + let mut reass_limit_reached = false; + let txdata = self.parse_frame_data( + &head, + &rem[..hlsafe], + complete, + dir, + &mut reass_limit_reached, + ); + + let tx = self.find_or_create_tx(&head, &txdata, dir); + if tx.is_none() { + return AppLayerResult::err(); + } + let tx = tx.unwrap(); + if reass_limit_reached { + tx.tx_data.set_event(HTTP2Event::ReassemblyLimitReached as u8); + } + tx.handle_frame(&head, &txdata, dir); + let over = head.flags & parser::HTTP2_FLAG_HEADER_EOS != 0; + let ftype = head.ftype; + let sid = head.stream_id; + let padded = head.flags & parser::HTTP2_FLAG_HEADER_PADDED != 0; + if dir == Direction::ToServer { + tx.frames_ts.push(HTTP2Frame { + header: head, + data: txdata, + }); + } else { + tx.frames_tc.push(HTTP2Frame { + header: head, + data: txdata, + }); + } + if ftype == parser::HTTP2FrameType::Data as u8 { + match unsafe { SURICATA_HTTP2_FILE_CONFIG } { + Some(sfcm) => { + //borrow checker forbids to reuse directly tx + let index = self.find_tx_index(sid); + if index > 0 { + let tx_same = &mut self.transactions[index - 1]; + if dir == Direction::ToServer { + tx_same.ft_tc.tx_id = tx_same.tx_id - 1; + } else { + tx_same.ft_ts.tx_id = tx_same.tx_id - 1; + }; + let mut dinput = &rem[..hlsafe]; + if padded && !rem.is_empty() && usize::from(rem[0]) < hlsafe{ + dinput = &rem[1..hlsafe - usize::from(rem[0])]; + } + if tx_same.decompress( + dinput, + dir, + sfcm, + over, + flow).is_err() { + self.set_event(HTTP2Event::FailedDecompression); + } + } + } + None => panic!("no SURICATA_HTTP2_FILE_CONFIG"), + } + } + input = &rem[hlsafe..]; + } + Err(Err::Incomplete(_)) => { + //we may have consumed data from previous records + return AppLayerResult::incomplete( + (il - input.len()) as u32, + HTTP2_FRAME_HEADER_LEN as u32, + ); + } + Err(_) => { + self.set_event(HTTP2Event::InvalidFrameHeader); + return AppLayerResult::err(); + } + } + } + return AppLayerResult::ok(); + } + + fn parse_ts(&mut self, mut input: &[u8], flow: *const Flow) -> AppLayerResult { + //very first : skip magic + let mut magic_consumed = 0; + if self.progress < HTTP2ConnectionState::Http2StateMagicDone { + //skip magic + if input.len() >= HTTP2_MAGIC_LEN { + //skip magic + match std::str::from_utf8(&input[..HTTP2_MAGIC_LEN]) { + Ok("PRI * HTTP/2.0\r\n\r\nSM\r\n\r\n") => { + input = &input[HTTP2_MAGIC_LEN..]; + magic_consumed = HTTP2_MAGIC_LEN; + } + Ok(&_) => { + self.set_event(HTTP2Event::InvalidClientMagic); + } + Err(_) => { + return AppLayerResult::err(); + } + } + self.progress = HTTP2ConnectionState::Http2StateMagicDone; + } else { + //still more buffer + return AppLayerResult::incomplete(0_u32, HTTP2_MAGIC_LEN as u32); + } + } + //first consume frame bytes + let il = input.len(); + if self.request_frame_size > 0 { + let ilen = input.len() as u32; + if self.request_frame_size >= ilen { + self.request_frame_size -= ilen; + return AppLayerResult::ok(); + } else { + let start = self.request_frame_size as usize; + input = &input[start..]; + self.request_frame_size = 0; + } + } + + //then parse all we can + let r = self.parse_frames(input, il, Direction::ToServer, flow); + if r.status == 1 { + //adds bytes consumed by banner to incomplete result + return AppLayerResult::incomplete(r.consumed + magic_consumed as u32, r.needed); + } else { + return r; + } + } + + fn parse_tc(&mut self, mut input: &[u8], flow: *const Flow) -> AppLayerResult { + //first consume frame bytes + let il = input.len(); + if self.response_frame_size > 0 { + let ilen = input.len() as u32; + if self.response_frame_size >= ilen { + self.response_frame_size -= ilen; + return AppLayerResult::ok(); + } else { + let start = self.response_frame_size as usize; + input = &input[start..]; + self.response_frame_size = 0; + } + } + //then parse all we can + return self.parse_frames(input, il, Direction::ToClient, flow); + } +} + +// C exports. + +export_tx_data_get!(rs_http2_get_tx_data, HTTP2Transaction); +export_state_data_get!(rs_http2_get_state_data, HTTP2State); + +/// C entry point for a probing parser. +#[no_mangle] +pub unsafe extern "C" fn rs_http2_probing_parser_tc( + _flow: *const Flow, _direction: u8, input: *const u8, input_len: u32, _rdir: *mut u8, +) -> AppProto { + if !input.is_null() { + let slice = build_slice!(input, input_len as usize); + match parser::http2_parse_frame_header(slice) { + Ok((_, header)) => { + if header.reserved != 0 + || header.length > HTTP2_DEFAULT_MAX_FRAME_SIZE + || header.flags & 0xFE != 0 + || header.ftype != parser::HTTP2FrameType::Settings as u8 + { + return ALPROTO_FAILED; + } + return ALPROTO_HTTP2; + } + Err(Err::Incomplete(_)) => { + return ALPROTO_UNKNOWN; + } + Err(_) => { + return ALPROTO_FAILED; + } + } + } + return ALPROTO_UNKNOWN; +} + +// Extern functions operating on HTTP2. +extern "C" { + pub fn HTTP2MimicHttp1Request( + orig_state: *mut std::os::raw::c_void, new_state: *mut std::os::raw::c_void, + ); +} + +// Suppress the unsafe warning here as creating a state for an app-layer +// is typically not unsafe. +#[no_mangle] +#[allow(clippy::not_unsafe_ptr_arg_deref)] +pub extern "C" fn rs_http2_state_new( + orig_state: *mut std::os::raw::c_void, _orig_proto: AppProto, +) -> *mut std::os::raw::c_void { + let state = HTTP2State::new(); + let boxed = Box::new(state); + let r = Box::into_raw(boxed) as *mut _; + if !orig_state.is_null() { + //we could check ALPROTO_HTTP1 == orig_proto + unsafe { + HTTP2MimicHttp1Request(orig_state, r); + } + } + return r; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_state_free(state: *mut std::os::raw::c_void) { + let mut state: Box<HTTP2State> = Box::from_raw(state as _); + state.free(); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_state_tx_free(state: *mut std::os::raw::c_void, tx_id: u64) { + let state = cast_pointer!(state, HTTP2State); + state.free_tx(tx_id); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_parse_ts( + flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, HTTP2State); + let buf = stream_slice.as_slice(); + return state.parse_ts(buf, flow); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_parse_tc( + flow: *const Flow, state: *mut std::os::raw::c_void, _pstate: *mut std::os::raw::c_void, + stream_slice: StreamSlice, _data: *const std::os::raw::c_void, +) -> AppLayerResult { + let state = cast_pointer!(state, HTTP2State); + let buf = stream_slice.as_slice(); + return state.parse_tc(buf, flow); +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_state_get_tx( + state: *mut std::os::raw::c_void, tx_id: u64, +) -> *mut std::os::raw::c_void { + let state = cast_pointer!(state, HTTP2State); + match state.get_tx(tx_id) { + Some(tx) => { + return tx as *const _ as *mut _; + } + None => { + return std::ptr::null_mut(); + } + } +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_state_get_tx_count(state: *mut std::os::raw::c_void) -> u64 { + let state = cast_pointer!(state, HTTP2State); + return state.tx_id; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_tx_get_state( + tx: *mut std::os::raw::c_void, +) -> HTTP2TransactionState { + let tx = cast_pointer!(tx, HTTP2Transaction); + return tx.state; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_tx_get_alstate_progress( + tx: *mut std::os::raw::c_void, _direction: u8, +) -> std::os::raw::c_int { + return rs_http2_tx_get_state(tx) as i32; +} + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_getfiles( + _state: *mut std::os::raw::c_void, + tx: *mut std::os::raw::c_void, direction: u8, +) -> AppLayerGetFileState { + let tx = cast_pointer!(tx, HTTP2Transaction); + if let Some(sfcm) = { SURICATA_HTTP2_FILE_CONFIG } { + if direction & STREAM_TOSERVER != 0 { + return AppLayerGetFileState { fc: &mut tx.ft_ts.file, cfg: sfcm.files_sbcfg } + } else { + return AppLayerGetFileState { fc: &mut tx.ft_tc.file, cfg: sfcm.files_sbcfg } + } + } + AppLayerGetFileState::err() +} + +// Parser name as a C style string. +const PARSER_NAME: &[u8] = b"http2\0"; + +#[no_mangle] +pub unsafe extern "C" fn rs_http2_register_parser() { + let default_port = CString::new("[80]").unwrap(); + let parser = RustParser { + name: PARSER_NAME.as_ptr() as *const std::os::raw::c_char, + default_port: default_port.as_ptr(), + ipproto: IPPROTO_TCP, + probe_ts: None, // big magic string should be enough + probe_tc: Some(rs_http2_probing_parser_tc), + min_depth: HTTP2_FRAME_HEADER_LEN as u16, + max_depth: HTTP2_MAGIC_LEN as u16, + state_new: rs_http2_state_new, + state_free: rs_http2_state_free, + tx_free: rs_http2_state_tx_free, + parse_ts: rs_http2_parse_ts, + parse_tc: rs_http2_parse_tc, + get_tx_count: rs_http2_state_get_tx_count, + get_tx: rs_http2_state_get_tx, + tx_comp_st_ts: HTTP2TransactionState::HTTP2StateClosed as i32, + tx_comp_st_tc: HTTP2TransactionState::HTTP2StateClosed as i32, + tx_get_progress: rs_http2_tx_get_alstate_progress, + get_eventinfo: Some(HTTP2Event::get_event_info), + get_eventinfo_byid: Some(HTTP2Event::get_event_info_by_id), + localstorage_new: None, + localstorage_free: None, + get_tx_files: Some(rs_http2_getfiles), + get_tx_iterator: Some(applayer::state_get_tx_iterator::<HTTP2State, HTTP2Transaction>), + get_tx_data: rs_http2_get_tx_data, + get_state_data: rs_http2_get_state_data, + apply_tx_config: None, + flags: 0, + truncate: None, + get_frame_id_by_name: None, + get_frame_name_by_id: None, + }; + + let ip_proto_str = CString::new("tcp").unwrap(); + + if AppLayerProtoDetectConfProtoDetectionEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let alproto = AppLayerRegisterProtocolDetection(&parser, 1); + ALPROTO_HTTP2 = alproto; + if AppLayerParserConfParserEnabled(ip_proto_str.as_ptr(), parser.name) != 0 { + let _ = AppLayerRegisterParser(&parser, alproto); + } + if let Some(val) = conf_get("app-layer.protocols.http2.max-streams") { + if let Ok(v) = val.parse::<usize>() { + HTTP2_MAX_STREAMS = v; + } else { + SCLogError!("Invalid value for http2.max-streams"); + } + } + if let Some(val) = conf_get("app-layer.protocols.http2.max-table-size") { + if let Ok(v) = val.parse::<u32>() { + HTTP2_MAX_TABLESIZE = v; + } else { + SCLogError!("Invalid value for http2.max-table-size"); + } + } + if let Some(val) = conf_get("app-layer.protocols.http2.max-reassembly-size") { + if let Ok(v) = val.parse::<u32>() { + HTTP2_MAX_REASS = v as usize; + } else { + SCLogError!("Invalid value for http2.max-reassembly-size"); + } + } + SCLogDebug!("Rust http2 parser registered."); + } else { + SCLogNotice!("Protocol detector and parser disabled for HTTP2."); + } +} |