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"
+ );
+}