diff --git a/beeper/src/dfa.rs b/beeper/src/dfa.rs index 2a0d3f4..8fe621e 100644 --- a/beeper/src/dfa.rs +++ b/beeper/src/dfa.rs @@ -10,9 +10,19 @@ pub const INIT_STATE: StateId = StateId(0); /// appear anywhere in the header block are anchored here. pub const ANY_STATE: StateId = StateId(1); +/// What a transition is matched on: a byte of the message, or [`ANY_INPUT`]. +/// +/// It is wider than a byte so that the two cannot be confused. A pattern is +/// free to spell out any byte there is, [`ANY_INPUT`] included, and the parser +/// program reserves a column of its transition table for the latter. +pub(crate) type Input = u16; + /// The input a state matches any byte with. The parser only follows it if the /// state has no transition for the byte it read. -const ANY_INPUT: u8 = '*' as u8; +/// +/// It is not a byte, so that a pattern holding the byte it used to be spelled +/// with, `*`, matches that byte and nothing else. +const ANY_INPUT: Input = 0x100; /// A single transition of a [`Dfa`]. #[derive(Clone, Copy, Debug, PartialEq, Eq)] @@ -37,13 +47,13 @@ pub struct DfaBuilder<'a, A: Copy + Debug + PartialEq + Eq> { /// The state the pattern has been built up to. state: StateId, - /// Strings that may appear before the next input. They are only built into + /// Inputs that may appear before the next one. They are only built into /// the DFA once that input is known, as each of them has to lead back to /// the state it branched off of. - optional_prefixes: Vec<(String, bool)>, + optional_prefixes: Vec<(Vec, bool)>, /// All edges that lead into [`DfaBuilder::state`]. - last_edges: Vec<(StateId, u8, bool)>, + last_edges: Vec<(StateId, Input, bool)>, } impl DfaBuilder<'_, A> { @@ -56,6 +66,12 @@ impl DfaBuilder<'_, A> { } } + /// Returns the state the pattern has been built up to, so that another one + /// can be anchored at it with [`Dfa::start_pattern`]. + pub fn state(&self) -> StateId { + self.state + } + /// Attaches `action` to every transition that leads into the state the /// pattern has been built up to. pub fn with(&mut self, action: A) -> &mut Self { @@ -66,13 +82,10 @@ impl DfaBuilder<'_, A> { self.push_optional_prefixes(); for (from, input, case_sensitive) in self.last_edges.clone() { - if case_sensitive { - self.dfa.add_action(from, input, action); - } else { - self.dfa - .add_action(from, input.to_ascii_lowercase(), action); - self.dfa - .add_action(from, input.to_ascii_uppercase(), action); + self.dfa.add_action(from, input, action); + + if !case_sensitive && let Some(other) = other_case(input) { + self.dfa.add_action(from, other, action); } } @@ -81,32 +94,35 @@ impl DfaBuilder<'_, A> { /// Builds the optional prefixes that were pushed since the last input, each /// of them leading back into the state the pattern has been built up to. + /// + /// TODO: This creates a self-loop, such that the optional prefix can be repeated + /// multiple times. This is not intended in all cases. fn push_optional_prefixes(&mut self) { let start = self.state; while let Some((optional, case_sensitive)) = self.optional_prefixes.pop() { let mut from = start; - for (i, b) in optional.as_bytes().iter().enumerate() { + for (i, input) in optional.iter().enumerate() { let to = if i == optional.len() - 1 { - self.last_edges.push((from, *b, case_sensitive)); + self.last_edges.push((from, *input, case_sensitive)); Some(start) } else { None }; - from = self.push_edge_from(from, *b, to, case_sensitive); + from = self.push_edge_from(from, *input, to, case_sensitive); } } } /// Appends a single input to the pattern, first building the optional /// prefixes that were pushed since the last input. - fn push_edge(&mut self, input: u8, to: Option, case_sensitive: bool) { + fn push_edge(&mut self, input: Input, to: Option, case_sensitive: bool) { self.push_optional_prefixes(); trace!( "push_edge; state={:?}, input={}, to={:?}", self.state, - (input as char).escape_debug(), + fmt_input(input), to ); @@ -122,22 +138,15 @@ impl DfaBuilder<'_, A> { fn push_edge_from( &mut self, from: StateId, - input: u8, + input: Input, to: Option, case_sensitive: bool, ) -> StateId { let to = to.unwrap_or(self.dfa.next_state(&from, &input)); self.dfa.insert_edge(from, input, to, None); - if !case_sensitive { - let other_case = if input.is_ascii_lowercase() { - input.to_ascii_uppercase() - } else { - input.to_ascii_lowercase() - }; - if other_case != input { - self.dfa.insert_edge(from, other_case, to, None); - } + if !case_sensitive && let Some(other) = other_case(input) { + self.dfa.insert_edge(from, other, to, None); } to @@ -161,27 +170,29 @@ impl DfaBuilder<'_, A> { pub fn push_inner(&mut self, input: &[u8], case_sensitive: bool) -> &mut Self { for b in input { - self.push_edge(*b, None, case_sensitive); + self.push_edge(Input::from(*b), None, case_sensitive); } self } - /// Pushes the [`ANY_INPUT`] character onto the [`Dfa`]. `range` - /// specifies the min and max amount of times any character may + /// Pushes the [`ANY_INPUT`] transition onto the [`Dfa`]. `range` + /// specifies the min and max amount of times any byte may /// appear in the matched string. pub fn push_any>(&mut self, range: R) -> &mut Self { let min_len = match range.start_bound() { - std::ops::Bound::Excluded(n) => *&n.saturating_sub(1), + std::ops::Bound::Excluded(n) => n.saturating_add(1), std::ops::Bound::Included(n) => *n, std::ops::Bound::Unbounded => 0, }; let max_len = match range.end_bound() { - std::ops::Bound::Excluded(n) => *&n.saturating_sub(1), + std::ops::Bound::Excluded(n) => n.saturating_sub(1), std::ops::Bound::Included(n) => *n, std::ops::Bound::Unbounded => min_len, }; + assert!(min_len <= max_len, "Cannot push an empty range"); + trace!( "push_any; state={:?}, min_len={:?}, max_len={:?}", self.state, min_len, max_len @@ -192,9 +203,8 @@ impl DfaBuilder<'_, A> { } // the following transitions are optional and must point to `self.state` - for i in 1..max_len - min_len { - let prefix = ANY_INPUT.to_string().repeat(i); - self.optional_prefixes.push((prefix, true)); + for i in 1..=max_len - min_len { + self.optional_prefixes.push((vec![ANY_INPUT; i], true)); } // the loop leads back into the state the repetition ends in, so it is @@ -207,7 +217,8 @@ impl DfaBuilder<'_, A> { self } - /// Same as [`push_options`], but case insensitive. + /// Adds a set of case-insensitive patterns to the DFA, one of + /// which must match for the DFA to accept an input. pub fn push_options_ci(&mut self, inputs: &[&str]) -> &mut Self { self.push_options_inner(inputs, false) } @@ -244,7 +255,7 @@ impl DfaBuilder<'_, A> { } else { None }; - self.push_edge(*b, to, case_sensitive); + self.push_edge(Input::from(*b), to, case_sensitive); } last_edges.append(&mut self.last_edges); @@ -257,17 +268,18 @@ impl DfaBuilder<'_, A> { /// Appends `input` to the pattern, but allows it to be skipped. pub fn push_optional(&mut self, input: &str) -> &mut Self { - self.optional_prefixes.push((input.to_string(), true)); + let optional = input.bytes().map(Input::from).collect(); + self.optional_prefixes.push((optional, true)); self } /// Matches the given input string but sets the final state /// to the state the DFA would be in if it started from [`ANY_STATE`]. pub fn restart_with(&mut self, input: &str) { - let final_state = input.as_bytes().iter().fold(ANY_STATE, |state, b| { + let final_state = input.bytes().map(Input::from).fold(ANY_STATE, |state, b| { // next state only inserts a state, we also have to ensure an edge exists let next = self.dfa.next_state(&state, &b); - self.push_edge_from(state, *b, Some(next), false); + self.push_edge_from(state, b, Some(next), false); next }); @@ -283,17 +295,37 @@ impl DfaBuilder<'_, A> { } else { None }; - self.push_edge(*b, to, false); + self.push_edge(Input::from(*b), to, false); } } } -type EdgeMap = HashMap>>; +/// Returns the other case of `input`, or `None` if it is not a letter. +fn other_case(input: Input) -> Option { + let byte = u8::try_from(input).ok()?; + let other = if byte.is_ascii_lowercase() { + byte.to_ascii_uppercase() + } else { + byte.to_ascii_lowercase() + }; + + (other != byte).then(|| Input::from(other)) +} + +/// Renders `input` the way it reads in a trace. +pub(crate) fn fmt_input(input: Input) -> String { + match u8::try_from(input) { + Ok(byte) => (byte as char).escape_debug().to_string(), + Err(_) => "".to_string(), + } +} + +type EdgeMap = HashMap>>; /// The DFA the patterns of a [`Parser`](super::Parser) are compiled into. /// /// It is injected into the BPF parser program as a table of transitions, -/// indexed by state and input byte, which is why states are shared between +/// indexed by state and input, which is why states are shared between /// patterns wherever possible. A transition names the action it carries by the /// index it is held under in [`Dfa::actions`], so that an action can say more /// than the 16 bits of a transition have room for. @@ -324,11 +356,6 @@ impl Dfa { self.num_states } - /// Returns the number of edges the DFA has. - pub fn num_edges(&self) -> usize { - self.edges.len() - } - /// Starts a new pattern anchored at `state`, which the caller has to have /// reserved with [`Dfa::with_reserved_states`]. pub fn start_pattern<'a>(&'a mut self, state: StateId) -> DfaBuilder<'a, A> { @@ -345,7 +372,7 @@ impl Dfa { /// Queries the edges to retrieve the next state from given state and /// input character. Creates a new state if none exists. - fn next_state(&mut self, from: &StateId, input: &u8) -> StateId { + fn next_state(&mut self, from: &StateId, input: &Input) -> StateId { self.edges .get(from) .and_then(|es| es.get(input).map(|edge| edge.to)) @@ -363,7 +390,7 @@ impl Dfa { /// Panics if `from` already has an edge for `input` that leads somewhere /// else, as that would make the automaton non-deterministic, or if it /// carries an action `action` cannot be combined with. - pub fn insert_edge(&mut self, from: StateId, input: u8, to: StateId, action: Option) { + pub fn insert_edge(&mut self, from: StateId, input: Input, to: StateId, action: Option) { let edges = self.edges.entry(from).or_default(); let Some(old) = edges.get_mut(&input) else { let _ = edges.insert(input, Edge { to, action }); @@ -393,8 +420,9 @@ impl Dfa { /// /// # Panics /// - /// Panics if the edge does not exist, or already has an action assigned. - fn add_action(&mut self, from: StateId, input: u8, action: A) { + /// Panics if the edge does not exist, or already carries an action `action` + /// cannot be combined with. + fn add_action(&mut self, from: StateId, input: Input, action: A) { let Some(edges) = self.edges.get_mut(&from) else { panic!("State not found"); }; @@ -403,13 +431,22 @@ impl Dfa { panic!("Edge not found"); }; + if let Some(old_action) = edge.action { + assert!( + old_action == action, + "Cannot {action:?} and {old_action:?} on the same transition" + ); + } + edge.action = Some(action); } /// Returns an iterator over the transitions of the DFA, each paired with /// the id of the action it carries: its own if it has one, and the one of /// the state it leads to otherwise. - pub fn iter_transitions(&self) -> impl Iterator)> + '_ { + pub fn iter_transitions( + &self, + ) -> impl Iterator)> + '_ { self.edges.iter().flat_map(move |(from, edges)| { edges .iter() diff --git a/beeper/src/h1/parser.bpf.c b/beeper/src/h1/parser.bpf.c index 1ba68a2..74778ce 100644 --- a/beeper/src/h1/parser.bpf.c +++ b/beeper/src/h1/parser.bpf.c @@ -42,11 +42,12 @@ struct h1_action { u8 mid; }; -// these restrictions are needed to make the verifier happy. All three are -// masked onto an index, so all three have to be powers of two. +// these restrictions are needed to make the verifier happy. `MAX_STATES` and +// `MAX_ACTIONS` are masked onto an index, so both have to be powers of two. #define MAX_STATES 512 -#define MAX_TRANS 128 #define MAX_ACTIONS 256 +#define MAX_TRANS 257 +#define ANY_TRANS 256 // The transition table of the DFA, indexed by state and input byte, and the // actions its transitions carry. User space fills both in before the program is @@ -65,11 +66,12 @@ static __always_inline struct h1_action _action(u16 id) { // none either, back to `s_any`. static __always_inline void _next(u16 state, u8 input, u16 *next_state, u16 *action) { state &= MAX_STATES - 1; - input &= MAX_TRANS - 1; + // `input` is a byte and the row holds a column for every one of them, so it + // needs no bound of its own struct trans t = s2ts[state][input]; if (t.state == 0 && t.action == 0) { - t = s2ts[state]['*']; + t = s2ts[state][ANY_TRANS]; if (t.state == 0 && t.action == 0) { *next_state = s_any; *action = 0; @@ -93,9 +95,10 @@ static __always_inline void _next(u16 state, u8 input, u16 *next_state, u16 *act // Returns the number of bytes it consumed once the DFA is done, or minus the // number of bytes it looked at if the data ran out first. static __always_inline int _parse_from(u8 *data, u8 *data_end, u16 start, struct hdr_match *ms, u32* cidx, u16* s, u16 *null_prefix) { - u32 len = (u32)(data_end - data) & MAX_BYTES; + u32 len = (u32)(data_end - data); + bpf_clamp_uminmax(len, 0, MAX_BYTES); - if (len-start == 0) { + if (start >= len) { return 0; } diff --git a/beeper/src/h1/parser.rs b/beeper/src/h1/parser.rs index 46f5adc..82e4aaa 100644 --- a/beeper/src/h1/parser.rs +++ b/beeper/src/h1/parser.rs @@ -1,7 +1,7 @@ #![allow(unused_imports)] use crate::{ Dfa, MatchId, autoload_and_attach, - dfa::{ANY_STATE, INIT_STATE}, + dfa::{ANY_STATE, INIT_STATE, fmt_input}, h1::action::Action, header::{METHOD, PATH, STATUS}, }; @@ -15,8 +15,12 @@ use xbpf::libbpf::{ skel::{OpenSkel, Skel, SkelBuilder}, }; -/// The sequence terminating the lines of a message. -const CRLF: &str = "\r\n"; +const CR: &str = "\r"; +const LF: &str = "\n"; + +/// The number of ranges a parser can be configured to capture. Must stay in +/// sync with `MAX_MATCHES` of beeper.h. +const MAX_MATCHES: u16 = 32; /// A parser for HTTP/1.x messages. /// @@ -112,7 +116,17 @@ impl Parser { } /// Returns an unused match id. + /// + /// # Panics + /// + /// Panics if the parser is already configured with [`MAX_MATCHES`] matches, + /// as the parser program has no room to tell one more apart from them. fn new_match(&mut self) -> MatchId { + assert!( + self.num_matches < MAX_MATCHES, + "a parser captures at most {MAX_MATCHES} ranges" + ); + let id = MatchId(self.num_matches); self.num_matches += 1; id @@ -136,19 +150,31 @@ impl Parser { } let mid = self.new_match(); - self.dfa - .start_pattern(ANY_STATE) - .push_ci(CRLF) + let mut pattern = self.dfa.start_pattern(ANY_STATE); + pattern + .push(LF) .push_ci(name.as_str()) .push_optional("\t") .push_optional(" ") .push_ci(":") .push_optional("\t") .push_optional(" ") - .with(Action::StartCapture(mid)) + .with(Action::StartCapture(mid)); + + // the value begins here, and it may be empty + let value = pattern.state(); + pattern .push_any(1..) .with(Action::EndCapture(mid)) - .restart_with(CRLF); + .push_optional(CR) + .restart_with(LF); + + // an empty value ends its line where it would have begun, and there is + // nothing in it to capture + self.dfa + .start_pattern(value) + .push_optional(CR) + .restart_with(LF); self } @@ -165,7 +191,10 @@ impl Parser { self.dfa .start_pattern(INIT_STATE) .with(Action::StartCapture(mid)) - .push(&format!("PRI * HTTP/2.0{}{}SM{}{}", CRLF, CRLF, CRLF, CRLF)) + .push(&format!( + "PRI * HTTP/2.0{}{}{}{}SM{}{}{}{}", + CR, LF, CR, LF, CR, LF, CR, LF + )) .with(Action::EndCaptureAndDone(mid)); self @@ -176,8 +205,10 @@ impl Parser { fn done_on_hdr_end(mut self) -> Parser { self.dfa .start_pattern(ANY_STATE) - .push(CRLF) - .push(CRLF) + .push_optional(CR) + .push(LF) + .push_optional(CR) + .push(LF) .with(Action::Done); self @@ -204,7 +235,8 @@ impl Parser { .push(" ") .push_any(1..) .push_ci(" HTTP/1.1") - .restart_with(CRLF); + .push_optional(CR) + .restart_with(LF); } else if name == &PATH { let mid = self.new_match(); self.dfa @@ -215,7 +247,8 @@ impl Parser { .push_any(1..) .with(Action::EndCapture(mid)) .push_ci(" HTTP/1.1") - .restart_with(CRLF); + .push_optional(CR) + .restart_with(LF); } else { panic!( "capture_status_line_hdr called with unsupported header name: {}", @@ -238,7 +271,8 @@ impl Parser { .push_any(3..=3) .with(Action::EndCapture(mid)) .push_any(1..) - .restart_with(CRLF); + .push_optional(CR) + .restart_with(LF); self } @@ -267,7 +301,7 @@ impl Parser { let mut open_skel = skel_builder.open(&mut open_obj)?; if tracing::event_enabled!(Level::TRACE) { open_skel.progs.parse_msg.set_log_level(1); - open_skel.progs.parse_buf.set_log_level(1); + open_skel.progs.parse_skb.set_log_level(1); open_skel.progs.parse_buf.set_log_level(1); } @@ -331,30 +365,35 @@ impl Parser { ); } - let num_edges = self.dfa.num_edges(); - if num_edges > data.a2as.len() { - bail!( - "the patterns take {} edges, the parser holds {}", - num_edges, - data.a2as.len() - ); - } - // action index 0 is reserved for the noop action let mut action_idx = HashMap::new(); action_idx.insert(None, 0usize); for (from, input, to, action) in self.dfa.iter_transitions() { let new_action_idx = action_idx.len(); - let action = action_idx.entry(action).or_insert(new_action_idx); - let action = *action as u16; + let action = *action_idx.entry(action).or_insert(new_action_idx); + if action >= data.a2as.len() { + bail!( + "the patterns take more actions than the {} the parser holds", + data.a2as.len() + ); + } + + let action = action as u16; + let input = input as usize; + if input >= data.s2ts[0].len() { + bail!("the patterns read inputs the parser has no column for: {input}"); + } trace!( "inject; from={} to={} input={} action={}", - from.0, to.0, input as char, action + from.0, + to.0, + fmt_input(input as u16), + action ); - data.s2ts[from.0 as usize][input as usize] = trans { + data.s2ts[from.0 as usize][input] = trans { state: to.0, action, }; diff --git a/beeper/src/h2/hpack.rs b/beeper/src/h2/hpack.rs index ef63423..fd256d3 100644 --- a/beeper/src/h2/hpack.rs +++ b/beeper/src/h2/hpack.rs @@ -1,4 +1,4 @@ -use crate::{Dfa, h2::action::*}; +use crate::{Dfa, dfa::Input, h2::action::*}; use std::collections::HashMap; /// Builds the transitions of every field representation of RFC 7541 into @@ -15,7 +15,8 @@ pub fn dfa() -> Dfa { /// Inserts the transitions of the first byte of a representation, see section 6 /// of RFC 7541. fn insert_field_row(dfa: &mut Dfa) { - let mut edge = |input: u8, to, action| dfa.insert_edge(S_FIELD, input, to, Some(action)); + let mut edge = + |input: u8, to, action| dfa.insert_edge(S_FIELD, Input::from(input), to, Some(action)); // an indexed field, 7 bit prefix. The index 0 is not used edge(0x80, S_DEAD, Action::new(Kind::Err, 0, 0)); @@ -91,11 +92,11 @@ fn insert_length_rows(dfa: &mut Dfa) { for (base, flags, cont) in [(0x00u8, 0, cont), (0x80u8, F_HUFF, cont_huff)] { for len in 0..0x7F { let action = Action::new(kind, len, flags); - dfa.insert_edge(from, base | len as u8, to, Some(action)); + dfa.insert_edge(from, Input::from(base | len as u8), to, Some(action)); } let action = Action::new(Kind::IntStart, 0x7F, 0); - dfa.insert_edge(from, base | 0x7F, cont, Some(action)); + dfa.insert_edge(from, Input::from(base | 0x7F), cont, Some(action)); } } } @@ -118,10 +119,10 @@ fn insert_continuation_rows(dfa: &mut Dfa) { for (from, kind, to, flags) in rows { for input in 0..0x80u8 { let action = Action::new(kind, 0, flags | F_CONT); - dfa.insert_edge(from, input, to, Some(action)); + dfa.insert_edge(from, Input::from(input), to, Some(action)); let action = Action::new(Kind::IntCont, 0, 0); - dfa.insert_edge(from, 0x80 | input, from, Some(action)); + dfa.insert_edge(from, Input::from(0x80 | input), from, Some(action)); } } } @@ -250,7 +251,7 @@ mod tests { let dfa = dfa(); for state in representation_states() { - let inputs: HashSet = dfa + let inputs: HashSet = dfa .iter_transitions() .filter(|(from, ..)| *from == state) .map(|(_, input, _, _)| input) diff --git a/beeper/src/h2/parser.bpf.c b/beeper/src/h2/parser.bpf.c index fc5f99a..59ef4ac 100644 --- a/beeper/src/h2/parser.bpf.c +++ b/beeper/src/h2/parser.bpf.c @@ -386,6 +386,14 @@ static __always_inline u32 _get_dynamic_table_index(const struct dynamic_table_i return (end_idx - idx) + STATIC_TABLE_SIZE; } +// Whether `idx` names an entry either table holds. HPACK numbers the static +// table from 1 and carries on into the dynamic one, so everything above the +// entry added last is out of range, and section 2.3.3 of RFC 7541 has a peer +// answer such an index with a decoding error rather than with an entry. +static __always_inline bool _is_valid_hpack_index(const struct dynamic_table_info *dt_info __arg_nonnull, u32 idx) { + return idx > 0 && idx <= STATIC_TABLE_SIZE + dt_info->count; +} + // Resolves the match `m` into the bytes it refers to, either in the message // itself or, if the peer only referenced the field by index, in the static or // the dynamic table. `is_key` selects the name of a table entry over its value. @@ -403,12 +411,17 @@ static __always_inline void _extract_match(const struct msg_ctx *ctx, const stru if (m->idx > STATIC_TABLE_SIZE) { struct dynamic_table_info *dt_info = bpf_map_lookup_elem(&dynamic_table_info, &ctx->conn); if (dt_info == NULL) return; + if (!_is_valid_hpack_index(dt_info, m->idx)) return; u32 idx = _get_dynamic_table_index(dt_info, m->idx); struct dynamic_table_key key = _new_dynamic_table_key(&ctx->conn, idx); entry = bpf_map_lookup_elem(&dynamic_table, &key); } else { + // the static table is an array, so it answers for every index below its + // size, index 0 included, which HPACK does not use + if (m->idx == 0) return; + u32 key = m->idx; entry = bpf_map_lookup_elem(&static_table, &key); } @@ -451,6 +464,11 @@ static __always_inline void _next(u16 state, u8 input, u16 *next_state, u16 *act // is one of the first `STATIC_TABLE_SIZE` indices and in the dynamic table of // `conn` otherwise. `*hf` is NULL if there is no such entry. static __always_inline void _get_table_entry(const struct ip4_conn *conn __arg_nonnull, const struct dynamic_table_info *dt_info __arg_nonnull, u32 idx, struct header_field **hf) { + if (!_is_valid_hpack_index(dt_info, idx)) { + *hf = NULL; + return; + } + if (idx > STATIC_TABLE_SIZE) { if (dt_info->dirty) { *hf = NULL; @@ -475,20 +493,25 @@ static __always_inline void _get_table_entry(const struct ip4_conn *conn __arg_n // by index, and returns the id of the capture it matched, or -1 if the name // matches no pattern. // +// A pattern only matches the name it spells out, never a name that merely +// starts with it: the walk carries on to the last byte of the name and only +// the capture the last one leads into counts. +// // It is only reached through `_run_action`, so the walk is verified once rather // than as part of every byte of the block the parser reads. static __always_inline int _match_header_key(const u8 *key __arg_nonnull, u16 key__sz) { u16 s = S_NAME; u16 j = 0; + int mid = -1; bpf_for(j, 0, key__sz) { u16 a = 0; _next(s, key[j], &s, &a); struct h2_action act = _action(a); - if (act.kind == H2A_CAPTURE) return act.val & MAX_MATCH_MASK; + mid = (act.kind == H2A_CAPTURE) ? (int)(act.val & MAX_MATCH_MASK) : -1; } - return -1; + return mid; } // Returns the state of `conn`'s dynamic table, creating an empty table with the @@ -571,8 +594,15 @@ __noinline __weak int _add_dynamic_table_entry(const struct msg_ctx *ctx __arg_n _extract_match(ctx, val, false, &val_ptr, &val_len, &val_huff); if (!val_ptr) return -1; - key_len = key_len & HEADER_FIELD_MASK; - val_len = val_len & HEADER_FIELD_MASK; + // an entry only keeps the first `HEADER_FIELD_MAXLEN` bytes of a field, but + // it is sized by everything the peer sent, or the two tables would evict at + // different points + u32 key_wire_len = key_len; + u32 val_wire_len = val_len; + bool key_cut = (key_len > HEADER_FIELD_MAXLEN); + bool val_cut = (val_len > HEADER_FIELD_MAXLEN); + bpf_clamp_uminmax(key_len, 0, HEADER_FIELD_MAXLEN); + bpf_clamp_uminmax(val_len, 0, HEADER_FIELD_MAXLEN); int per_cpu_key = 0; struct dynamic_table_entry *dt_val = bpf_map_lookup_elem(&dynamic_table_entry, &per_cpu_key); @@ -591,8 +621,17 @@ __noinline __weak int _add_dynamic_table_entry(const struct msg_ctx *ctx __arg_n dt_val->field.key_huff = key_huff; dt_val->field.val_huff = val_huff; - u16 key_len_decoded = key_huff ? hpack_huffman_decoded_len(dt_val->field.key, key_len) : key_len; - u16 val_len_decoded = val_huff ? hpack_huffman_decoded_len(dt_val->field.val, val_len) : val_len; + // the decoded length of a Huffman coded field can only be counted off the + // bytes the entry kept, so a coded field that was truncated is one the + // mirrored table can no longer size the way the peer does + if ((key_cut && key_huff) || (val_cut && val_huff)) { + bpf_debug("dt: a Huffman coded field longer than %d bytes, the table has drifted", HEADER_FIELD_MAXLEN); + dt_info->dirty = 1; + return -1; + } + + u32 key_len_decoded = key_huff ? hpack_huffman_decoded_len(dt_val->field.key, key_len) : key_wire_len; + u32 val_len_decoded = val_huff ? hpack_huffman_decoded_len(dt_val->field.val, val_len) : val_wire_len; dt_val->size = key_len_decoded + val_len_decoded + 32; _try_evict_dynamic_table_entries(ctx, dt_info, dt_val->size); @@ -620,8 +659,9 @@ __noinline __weak int _add_dynamic_table_entry(const struct msg_ctx *ctx __arg_n static __always_inline int _parse_stg_from(const struct msg_ctx *ctx, u16 start, u16 end, u16 *s, struct parse_res *pres, u16 *null_prefix) { const u8 *data = ctx->data; const u8 *data_end = ctx->data_end; - u32 len = (u32)(data_end - data) & MAX_BYTES; - if (end < len) len = end & MAX_BYTES; + u32 len = (u32)(data_end - data); + bpf_clamp_uminmax(len, 0, MAX_BYTES); + if (end < len) len = end; if (data + 9 > data_end) return 0; u8 type = data[3]; @@ -656,8 +696,8 @@ static __always_inline int _parse_stg_from(const struct msg_ctx *ctx, u16 start, if (j == 6) { if (id == SETTINGS_HEADER_TABLE_SIZE) { - dt_info->max_size = (u16)val; - bpf_debug("stg: table header size: %u", (u16)val); + dt_info->max_size = val; + bpf_debug("stg: table header size: %u", val); } j = 0; id = 0; @@ -753,20 +793,29 @@ __noinline __weak int _run_action(const struct msg_ctx *ctx __arg_nonnull, struc // matched. Both representations that carry one are handled here, so that the // walk over the entry is only built into the program once if (ps->kind == H2A_INDEXED || ps->kind == H2A_IDX_NAME) { + u32 idx = ps->v; + bool in_range = _is_valid_hpack_index(dt_info, idx); + ps->add_to_dt = (ps->flags & H2F_ADD_DT) != 0; ps->cid = -1; + + // an index in range fits into the match it is reported with, one out + // of range is reported as 0, which names no entry either ps->key = (struct hdr_match) { - .idx = v, + .idx = in_range ? idx : 0, .len = 0, .in_msg = false, .huff = false, }; struct header_field *hf = NULL; - _get_table_entry(&ctx->conn, dt_info, v, &hf); + _get_table_entry(&ctx->conn, dt_info, idx, &hf); if (hf == NULL) return 0; - int mid = _match_header_key(hf->key, hf->key_len & HEADER_FIELD_MASK); + u32 key_len = hf->key_len; + bpf_clamp_uminmax(key_len, 0, HEADER_FIELD_MAXLEN); + + int mid = _match_header_key(hf->key, key_len); if (mid < 0) return 0; if (ps->kind == H2A_IDX_NAME) { @@ -777,7 +826,7 @@ __noinline __weak int _run_action(const struct msg_ctx *ctx __arg_nonnull, struc // both halves of the field are in the table, so the value is reported // as the index it is to be read back with pres->ms[mid & MAX_MATCH_MASK] = (struct hdr_match) { - .idx = v, + .idx = idx, .len = HEADER_FIELD_MASK, .in_msg = false, .huff = false, @@ -831,8 +880,8 @@ __noinline __weak int _run_action(const struct msg_ctx *ctx __arg_nonnull, struc } if (ps->kind == H2A_TABLE_SIZE) { - bpf_debug("hdr: table size update: %u", v); - dt_info->max_size = v; + bpf_debug("hdr: table size update: %u", ps->v); + dt_info->max_size = ps->v; return 0; } @@ -858,13 +907,13 @@ __noinline __weak int _run_action(const struct msg_ctx *ctx __arg_nonnull, struc static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, u16 end, struct dynamic_table_info *dt_info, struct h2_parse_state *ps, struct parse_res *pres, u16 *null_prefix) { const u8 *data = ctx->data; const u8 *data_end = ctx->data_end; - u32 len = (u32)(data_end - data) & MAX_BYTES; - if (end < len) len = end & MAX_BYTES; + + u32 len = (u32)(data_end - data); + bpf_clamp_uminmax(len, 0, MAX_BYTES); + if (end < len) len = end; u32 i = 0; bpf_for(i, start, len+1) { - // the block ends before the message does when the message carries more - // than one frame, so the loop cannot lean on the bounds check alone if (i >= len) break; if (data + i + 1 > data_end) break; u8 c = data[i]; @@ -881,13 +930,10 @@ static __always_inline int _parse_hdr_from(const struct msg_ctx *ctx, u16 start, _next(ps->s, c, &ps->s, &a); struct h2_action act = _action(a); - if (act.kind == H2A_CAPTURE) ps->cid = act.val & MAX_MATCH_MASK; + ps->cid = (act.kind == H2A_CAPTURE) ? (s8)(act.val & MAX_MATCH_MASK) : -1; } ps->skip--; - // the name ended, so the length of the value comes next. A value - // ends on the transition that announced it, which already leads - // back to `S_FIELD` if (ps->skip == 0 && ps->is_key) ps->s = S_VAL_LEN; continue; diff --git a/beeper/src/h2/parser.rs b/beeper/src/h2/parser.rs index e30b882..c05c2e4 100644 --- a/beeper/src/h2/parser.rs +++ b/beeper/src/h2/parser.rs @@ -21,6 +21,10 @@ use xbpf::libbpf::{ extern crate plain; +/// The number of ranges a parser can be configured to capture. Must stay in +/// sync with `MAX_MATCHES` of beeper.h. +const MAX_MATCHES: u16 = 32; + /// A parser for HTTP/2 messages. /// /// The builder methods configure which fields the parser captures and which @@ -147,8 +151,13 @@ impl Parser { /// /// # Errors /// - /// Returns an error if `name` cannot be Huffman encoded. + /// Returns an error if `name` cannot be Huffman encoded, or if the parser + /// already captures as many fields as the parser program has room for. pub fn capture_hdr(mut self, name: &HeaderName) -> Result { + if self.num_matches >= MAX_MATCHES { + bail!("a parser captures at most {MAX_MATCHES} fields"); + } + let mut name_encoded = Vec::new(); huffman::encode(name.as_str().as_bytes(), &mut name_encoded)?; @@ -339,7 +348,12 @@ impl Parser { ); } - data.s2ts[from.0 as usize][input as usize] = trans { + let input = input as usize; + if input >= data.s2ts[0].len() { + bail!("the patterns read inputs the parser has no column for: {input}"); + } + + data.s2ts[from.0 as usize][input] = trans { state: to.0, action: action as u16, }; diff --git a/beeper/tests/h1.rs b/beeper/tests/h1.rs index 84e7d73..5b7aa14 100644 --- a/beeper/tests/h1.rs +++ b/beeper/tests/h1.rs @@ -50,6 +50,44 @@ async fn send_raw(addr: SocketAddr, req: &str) -> String { String::from_utf8_lossy(&buf[..len]).into_owned() } +/// Writes `req` to a raw connection to `addr` and returns the response it reads +/// back. Unlike [`send_raw`], the request does not have to be valid UTF-8. +async fn send_raw_bytes(addr: SocketAddr, req: &[u8]) -> Vec { + let mut stream = TcpStream::connect(addr).await.expect("connect"); + stream.write_all(req).await.expect("write request"); + + let mut buf = [0; 1024]; + let len = stream.read(&mut buf).await.expect("read response"); + + buf[..len].to_vec() +} + +/// Same as [`assert_match_eq`], but compares the raw bytes of the capture, so +/// that a value that is not valid UTF-8 can be asserted on as well. +fn assert_match_bytes_eq(prog: &TestProgram, idx: usize, expected: Option<&[u8]>) { + let actual = prog.get_match(idx).expect("get_match"); + + match expected { + None => assert!( + actual.is_none(), + "get_match({idx}): {:?}, expected: none", + String::from_utf8_lossy(&actual.unwrap()) + ), + Some(expected) => { + assert!( + actual.is_some(), + "get_match({idx}): none, expected: {:?}", + String::from_utf8_lossy(expected) + ); + let actual = actual.unwrap(); + assert_eq!( + String::from_utf8_lossy(&actual), + String::from_utf8_lossy(expected) + ); + } + } +} + fn attach_h1_parser(prog_fd: i32, match_preface: bool, hdrs: &[HeaderName]) -> h1::AttachedParser { let mut h1 = h1::Parser::new(); if match_preface { @@ -283,3 +321,160 @@ async fn parse_status_code() { assert_match_eq(&prog, 1, Some(&status)); assert_match_eq(&prog, 2, Some(&content_length)); } + +#[tokio::test] +async fn ignore_a_preface_that_is_not_one() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); + let _h1 = attach_h1_parser(prog.prog_fd(), true, &[]); + + // the preface asks for the target `*`, which is also the character the DFA + // spells "any byte" with. Anything else in its place is a different message + let req = "PRI X HTTP/2.0\r\n\r\nSM\r\n\r\n"; + _ = send_raw(addr, req).await; + + assert_eq!( + prog.num_upgraded_conns().unwrap(), + 0, + "a request that only looks like the preface upgraded the connection" + ); +} + +#[tokio::test] +async fn match_a_star_in_a_header_name_literally() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); + + // `*` is a legal character of a field name, and one the DFA spells "any + // byte" with + let starred = HeaderName::from_bytes(b"x*y").expect("header name"); + let _h1 = attach_h1_parser(prog.prog_fd(), true, &[starred]); + + let req = format!("GET / HTTP/1.1\r\nHost: {addr}\r\nxzy: beeper\r\n\r\n"); + _ = send_raw(addr, &req).await; + + // no field of the request is named `x*y`, so there is nothing to capture + assert_match_eq(&prog, 1, None); +} + +#[tokio::test] +async fn keep_a_value_that_carries_high_bytes_together() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); + let _h1 = attach_h1_parser(prog.prog_fd(), true, &[header::USER_AGENT]); + + // a field value may carry obs-text, i.e. any byte from 0x80 to 0xFF. 0x8D + // and 0x8A are the two that a parser masking its input down to seven bits + // reads as a CRLF, which is what ends a field value + let value: &[u8] = b"good\x8d\x8auser-agent: evil"; + + let mut req = Vec::new(); + req.extend_from_slice(format!("GET / HTTP/1.1\r\nHost: {addr}\r\nuser-agent: ").as_bytes()); + req.extend_from_slice(value); + req.extend_from_slice(b"\r\n\r\n"); + + _ = send_raw_bytes(addr, &req).await; + + assert_match_bytes_eq(&prog, 1, Some(value)); +} + +#[tokio::test] +async fn capture_nothing_for_an_empty_value() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); + let _h1 = attach_h1_parser(prog.prog_fd(), true, &[header::USER_AGENT]); + + // an empty value is legal, and there is nothing to capture in it. The + // fields behind it are not part of it either + let req = + format!("GET / HTTP/1.1\r\nHost: {addr}\r\nuser-agent:\r\naccept: text/plain\r\n\r\n"); + + let resp = send_raw(addr, &req).await; + assert!( + resp.starts_with("HTTP/1.1 200 OK"), + "unexpected response: {resp}" + ); + + assert_match_eq(&prog, 1, None); +} + +#[tokio::test] +async fn parse_a_header_in_the_first_half_of_a_long_message() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); + let _h1 = attach_h1_parser(prog.prog_fd(), true, &[header::USER_AGENT]); + + // the parser walks the first 0x7FFF bytes of a message. The field below + // sits well inside of them, the padding behind it pushes the message well + // past them + let user_agent = HeaderValue::from_static("beeper"); + let pad = "p".repeat(1000); + + let mut req = format!("GET / HTTP/1.1\r\nHost: {addr}\r\n"); + for i in 0..8 { + req += &format!("x-pad-{i}: {pad}\r\n"); + } + req += "user-agent: beeper\r\n"; + for i in 8..38 { + req += &format!("x-pad-{i}: {pad}\r\n"); + } + req += "\r\n"; + + assert!(req.len() > 0x7FFF, "the message is not long enough"); + _ = send_raw(addr, &req).await; + + assert_match_eq(&prog, 1, Some(&user_agent)); +} + +#[tokio::test] +async fn parse_lf_endings() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); + let _h1 = attach_h1_parser(prog.prog_fd(), true, &[header::USER_AGENT]); + + // section 2.2 of RFC 9112 lets a recipient read a bare LF as a line terminator + let user_agent = HeaderValue::from_static("beeper"); + let req = format!("GET / HTTP/1.1\nHost: {addr}\nuser-agent: beeper\n\n"); + + let resp = send_raw(addr, &req).await; + assert!( + resp.starts_with("HTTP/1.1 200 OK"), + "unexpected response: {resp}" + ); + + assert_match_eq(&prog, 1, Some(&user_agent)); +} + +// TODO: How to inject an invalid response? +// #[tokio::test] +// async fn rejects_multi_cr_endings() { +// let addr = server::launch().await.expect("launch server"); + +// let mut open_obj = OpenObject::new(); +// let prog = TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach"); +// let _h1 = attach_h1_parser(prog.prog_fd(), true, &[header::USER_AGENT]); + +// // section 2.2 of RFC 9112 lets a recipient read a bare LF as a line terminator +// let user_agent = HeaderValue::from_static("beeper"); +// let req = format!("GET / HTTP/1.1\r\nHost: {addr}\nuser-agent: beeper\r\r\n\n"); + +// let resp = send_raw(addr, &req).await; +// assert!( +// resp.starts_with("HTTP/1.1 200 OK"), +// "unexpected response: {resp}" +// ); + +// assert_match_eq(&prog, 1, Some(&user_agent)); +// } diff --git a/beeper/tests/h2.rs b/beeper/tests/h2.rs index 26667ab..02a96e1 100644 --- a/beeper/tests/h2.rs +++ b/beeper/tests/h2.rs @@ -20,6 +20,20 @@ const METHOD_HEADER: HeaderName = HeaderName::from_static("method"); const AUTHORITY_HEADER: HeaderName = HeaderName::from_static("authority"); const PATH_HEADER: HeaderName = HeaderName::from_static("path"); +fn huffman_encode(s: &str) -> Vec { + let mut coded = Vec::new(); + huffman::encode(s.as_bytes(), &mut coded).expect("encode"); + assert!( + coded.len() < 127, + "huffman_encode only encodes a one byte length" + ); + + let mut out = vec![0x80 | coded.len() as u8]; + out.extend_from_slice(&coded); + + out +} + fn huffman_decode(val: &[u8]) -> String { let mut res = Vec::new(); huffman::decode(val, &mut res, huffman::DecoderSpeed::OneBit).unwrap(); @@ -153,6 +167,23 @@ fn raw_str(s: &str) -> Vec { out } +/// Renders an HPACK string of any length, spelled out rather than Huffman +/// coded. The length is written as the two byte integer of section 5.1 of RFC +/// 7541 whenever it does not fit into the seven bit prefix. +fn long_raw_str(s: &str) -> Vec { + let mut out = Vec::new(); + if s.len() < 0x7F { + out.push(s.len() as u8); + } else { + assert!(s.len() < 0x7F + 128, "long_raw_str only encodes two bytes"); + out.push(0x7F); + out.push((s.len() - 0x7F) as u8); + } + out.extend_from_slice(s.as_bytes()); + + out +} + /// A client that writes its own HPACK, which is the only way to send a header /// that is not Huffman coded: `h2`'s encoder always codes. Real clients do send /// them, curl among them. @@ -1060,7 +1091,10 @@ async fn resolve_index_of_entry_added_after_an_eviction() { // decides whether the entries added before it keep the index they were // stored under client - .get(url.clone(), &[(header::USER_AGENT, other_agent_val.clone())]) + .get( + url.clone(), + &[(header::USER_AGENT, other_agent_val.clone())], + ) .await; assert_match_eq(&prog, 1, Some(&other_agent_val)); @@ -1147,3 +1181,242 @@ async fn mark_the_table_as_drifted_when_a_continuation_frame_splits_a_field() { "a block that breaks inside a field left the table looking trustworthy" ); } + +#[tokio::test] +async fn update_dynamic_table_size_past_the_width_of_a_u16() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + // SETTINGS_HEADER_TABLE_SIZE is a 32 bit parameter, and a size above 64KiB + // is one browsers do announce + let client = Client::connect(addr, Some(65536)).await; + client.get(format!("http://{}", addr), &[]).await; + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + assert_eq!(info.max_size, 65536); +} + +#[tokio::test] +async fn resolve_a_captured_index_against_the_table_it_was_read_from() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let _h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let first = HeaderValue::from_static("first"); + + // the first request puts `accept: first` into the dynamic table, where it + // is the entry the next block can address with the first dynamic index + let mut client = RawClient::connect(addr).await; + client + .request(raw_request_block( + &authority, + &[(Some(19), "accept", "first")], + )) + .await; + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(first.as_bytes()) + ); + + // the next block reads that entry by index and then adds one of its own, + // which pushes everything below it down by one. The field it adds is a + // `user-agent`, which matches no pattern, so nothing of it is captured and + // the `accept` read above is still the only capture of the block + let mut block = vec![0x82, 0x86, 0x84]; + block.push(0x01); + block.extend_from_slice(&raw_str(&authority)); + block.push(0x80 | FIRST_DYNAMIC_INDEX); + block.push(0x40 | 58); + block.extend_from_slice(&raw_str("junk")); + + client.request(block).await; + + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(first.as_bytes()), + "the capture followed the index into the entry that took its place" + ); +} + +#[tokio::test] +async fn ignore_a_value_that_runs_into_the_frame_behind_it() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let mut client = RawClient::connect(addr).await; + + // the same lie as in `ignore_header_field_whose_value_runs_past_the_frame`, + // except that a second frame follows in the same write, so the bytes the + // value claims are bytes the parser can read -- they just belong to the + // frame behind it + let mut block = vec![0x82, 0x86, 0x84]; + block.push(0x01); + block.extend_from_slice(&raw_str(&authority)); + block.push(0x40 | 19); + block.push(100); + block.extend_from_slice(b"ab"); + + let mut out = frame(0x01, 0x05, 1, &block); + out.extend_from_slice(&frame(0x01, 0x05, 3, &raw_request_block(&authority, &[]))); + + client.send_raw(&out).await; + + // a field is only ever made of the bytes of its own block + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + assert_eq!( + info.count, 0, + "a value reaching into the next frame was added to the table" + ); + assert_eq!(info.size, 0); +} + +#[tokio::test] +async fn size_a_dynamic_table_entry_that_is_longer_than_an_entry_holds() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let long = "a".repeat(130); + let long_val = HeaderValue::from_str(&long).expect("header value"); + + // the entry does not fit into the 128 bytes the mirrored table keeps of a + // field, but the peer sizes it by everything it sent, and it is that size + // that decides when the two tables evict + let mut block = vec![0x82, 0x86, 0x84]; + block.push(0x01); + block.extend_from_slice(&raw_str(&authority)); + block.push(0x40 | 19); + block.extend_from_slice(&long_raw_str(&long)); + + let mut client = RawClient::connect(addr).await; + client.request(block).await; + + let info = h2 + .dynamic_table_info(client.local_addr, client.remote_addr) + .expect("dynamic_table_info"); + + let expected_dt = &[(header::ACCEPT, long_val.clone())]; + assert_eq!(info.count, expected_dt.len() as u32); + assert_eq!( + info.size, + dynamic_table_size_for_headers(expected_dt), + "a field longer than an entry holds was sized by what was kept of it" + ); +} + +#[tokio::test] +async fn ignore_an_index_that_only_wraps_into_the_table() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let _h2 = attach_h2_parser(prog.prog_fd(), &[header::ACCEPT]); + + let authority = addr.to_string(); + let secret = HeaderValue::from_static("secret"); + + let mut client = RawClient::connect(addr).await; + client + .request(raw_request_block( + &authority, + &[(Some(19), "accept", "secret")], + )) + .await; + assert_eq!( + prog.get_match(0).expect("get_match").as_deref(), + Some(secret.as_bytes()) + ); + + // 32830 is the first dynamic index with the sixteenth bit set. No entry + // sits there, and an index that far past the end of the table is one a + // peer answers with a connection error + let idx: u32 = 0x8000 + FIRST_DYNAMIC_INDEX as u32; + let mut block = vec![0x82, 0x86, 0x84]; + block.push(0x01); + block.extend_from_slice(&raw_str(&authority)); + block.push(0xFF); + let mut rest = idx - 0x7F; + while rest >= 0x80 { + block.push(0x80 | (rest & 0x7F) as u8); + rest >>= 7; + } + block.push(rest as u8); + + client.send_raw(&frame(0x01, 0x05, 3, &block)).await; + + assert_eq!( + prog.get_match(0).expect("get_match"), + None, + "an index past the end of the table resolved to an entry inside it" + ); +} + +#[tokio::test] +async fn match_a_field_name_by_the_whole_name() { + let addr = server::launch().await.expect("launch server"); + + let mut open_obj = OpenObject::new(); + let prog = + TestProgram::attach(addr, &mut open_obj, Direction::Downstream).expect("attach program"); + + // `a&b` is a legal field name whose Huffman code starts with the code of + // `a`, padding and all: the shorter name is a byte prefix of the longer one + let short = HeaderName::from_static("a"); + let mut coded_short = Vec::new(); + huffman::encode(short.as_str().as_bytes(), &mut coded_short).expect("encode"); + let mut coded_long = Vec::new(); + huffman::encode(b"a&b", &mut coded_long).expect("encode"); + assert!(coded_long.starts_with(&coded_short)); + + let _h1 = attach_preface_parser(prog.prog_fd()); + let _h2 = attach_h2_parser(prog.prog_fd(), &[short]); + + let authority = addr.to_string(); + let mut block = vec![0x82, 0x86, 0x84]; + block.push(0x01); + block.extend_from_slice(&raw_str(&authority)); + block.push(0x40); + block.extend_from_slice(&huffman_encode("a&b")); + block.extend_from_slice(&raw_str("not-the-one")); + + let mut client = RawClient::connect(addr).await; + client.send_raw(&frame(0x01, 0x05, 1, &block)).await; + + assert_eq!( + prog.get_match(0).expect("get_match"), + None, + "a field whose name only starts like the pattern was captured" + ); +}