cellos_supervisor/sni_proxy/h2/hpack/
mod.rs1pub mod dynamic_table;
19pub mod huffman;
20pub mod integer;
21pub mod static_table;
22pub mod string;
23
24use super::error::H2ParseError;
25use dynamic_table::DynamicTable;
26use integer::decode_integer;
27use static_table::{STATIC_INDEX_AUTHORITY, STATIC_TABLE_MAX};
28use string::decode_string;
29
30pub const DEFAULT_HEADER_TABLE_SIZE: usize = 4096;
32
33pub struct HpackDecoder {
40 dynamic_table: DynamicTable,
41}
42
43impl Default for HpackDecoder {
44 fn default() -> Self {
45 Self::new()
46 }
47}
48
49impl HpackDecoder {
50 pub fn new() -> Self {
52 Self {
53 dynamic_table: DynamicTable::new(DEFAULT_HEADER_TABLE_SIZE)
55 .expect("default table size is within bound"),
56 }
57 }
58
59 #[allow(dead_code)] pub fn reset(&mut self) {
63 self.dynamic_table = DynamicTable::new(DEFAULT_HEADER_TABLE_SIZE)
64 .expect("default table size is within bound");
65 }
66
67 pub fn decode_block(&mut self, block: &[u8]) -> Result<Option<DecodedAuthority>, H2ParseError> {
76 let mut cursor = block;
77 let mut found: Option<DecodedAuthority> = None;
78
79 while !cursor.is_empty() {
80 let first = cursor[0];
81
82 if first & 0b1000_0000 != 0 {
83 let (index, rest) = decode_integer(cursor, 7)?;
85 cursor = rest;
86 let (name, value) = self
87 .dynamic_table
88 .lookup(index)
89 .ok_or(H2ParseError::HpackInvalidIndex { index })?;
90 if found.is_none() && eq_authority(name.as_bytes()) && !value.is_empty() {
91 let provenance = if index <= STATIC_TABLE_MAX {
92 AuthorityProvenance::StaticIndexed
100 } else {
101 AuthorityProvenance::DynamicIndexed
102 };
103 found = Some(DecodedAuthority {
104 value: normalise_authority(value.as_bytes()),
105 provenance,
106 });
107 }
108 continue;
109 }
110
111 if first & 0b1100_0000 == 0b0100_0000 {
112 let (name, value, name_index, value_was_huffman, after) =
115 self.parse_literal(cursor, 6)?;
116 cursor = after;
117 let is_authority = if name_index == 0 {
118 eq_authority(name.as_bytes())
119 } else {
120 name_index == STATIC_INDEX_AUTHORITY
121 || self
122 .dynamic_table
123 .lookup(name_index)
124 .map(|(n, _)| eq_authority(n.as_bytes()))
125 .unwrap_or(false)
126 };
127 if found.is_none() && is_authority && !value.is_empty() {
128 let provenance = match (name_index, value_was_huffman) {
129 (_, true) => AuthorityProvenance::Huffman,
130 (0, false) => AuthorityProvenance::StaticLiteral,
131 (i, false) if i <= STATIC_TABLE_MAX => AuthorityProvenance::StaticLiteral,
132 (_, false) => AuthorityProvenance::DynamicIndexed,
133 };
134 found = Some(DecodedAuthority {
135 value: normalise_authority(value.as_bytes()),
136 provenance,
137 });
138 }
139 let stored_name = if name_index == 0 {
141 name
142 } else {
143 self.dynamic_table
144 .lookup(name_index)
145 .ok_or(H2ParseError::HpackInvalidIndex { index: name_index })?
146 .0
147 .to_string()
148 };
149 self.dynamic_table.insert(stored_name, value);
150 continue;
151 }
152
153 if first & 0b1111_0000 == 0b0000_0000 || first & 0b1111_0000 == 0b0001_0000 {
154 let (name, value, name_index, value_was_huffman, after) =
157 self.parse_literal(cursor, 4)?;
158 cursor = after;
159 let is_authority = if name_index == 0 {
160 eq_authority(name.as_bytes())
161 } else {
162 name_index == STATIC_INDEX_AUTHORITY
163 || self
164 .dynamic_table
165 .lookup(name_index)
166 .map(|(n, _)| eq_authority(n.as_bytes()))
167 .unwrap_or(false)
168 };
169 if found.is_none() && is_authority && !value.is_empty() {
170 let provenance = if value_was_huffman {
171 AuthorityProvenance::Huffman
172 } else if name_index == 0 || name_index <= STATIC_TABLE_MAX {
173 AuthorityProvenance::StaticLiteral
174 } else {
175 AuthorityProvenance::DynamicIndexed
176 };
177 found = Some(DecodedAuthority {
178 value: normalise_authority(value.as_bytes()),
179 provenance,
180 });
181 }
182 continue;
184 }
185
186 if first & 0b1110_0000 == 0b0010_0000 {
187 let (new_max, rest) = decode_integer(cursor, 5)?;
189 cursor = rest;
190 let new_max_usize =
191 usize::try_from(new_max).map_err(|_| H2ParseError::MalformedHeaders)?;
192 self.dynamic_table.update_max_size(new_max_usize)?;
193 continue;
194 }
195
196 return Err(H2ParseError::MalformedHeaders);
198 }
199
200 Ok(found)
201 }
202
203 fn parse_literal<'a>(
209 &mut self,
210 buf: &'a [u8],
211 prefix_bits: u32,
212 ) -> Result<(String, String, u64, bool, &'a [u8]), H2ParseError> {
213 let (name_index, rest) = decode_integer(buf, prefix_bits)?;
214 let (name, after_name) = if name_index == 0 {
215 let (n, r) = decode_string(rest)?;
216 (n, r)
217 } else {
218 (String::new(), rest)
219 };
220 let value_was_huffman = !after_name.is_empty() && (after_name[0] & 0x80) != 0;
221 let (value, after_value) = decode_string(after_name)?;
222 Ok((name, value, name_index, value_was_huffman, after_value))
223 }
224}
225
226#[derive(Debug, PartialEq, Eq, Clone, Copy)]
232pub enum AuthorityProvenance {
233 StaticIndexed,
236 StaticLiteral,
240 DynamicIndexed,
245 Huffman,
248}
249
250#[derive(Debug, Clone)]
252pub struct DecodedAuthority {
253 pub value: String,
254 pub provenance: AuthorityProvenance,
255}
256
257fn eq_authority(name: &[u8]) -> bool {
259 if name.len() != b":authority".len() {
260 return false;
261 }
262 name.iter()
263 .zip(b":authority".iter())
264 .all(|(a, b)| a.eq_ignore_ascii_case(b))
265}
266
267pub(crate) fn normalise_authority(raw: &[u8]) -> String {
272 let trimmed = trim_ascii(raw);
273 let host = if trimmed.first() == Some(&b'[') {
274 if let Some(close) = trimmed.iter().position(|&b| b == b']') {
275 &trimmed[..=close]
276 } else {
277 trimmed
278 }
279 } else if let Some(colon) = trimmed.iter().position(|&b| b == b':') {
280 &trimmed[..colon]
281 } else {
282 trimmed
283 };
284 let mut s = String::from_utf8_lossy(host).to_string();
285 s.make_ascii_lowercase();
286 if s.ends_with('.') {
287 s.pop();
288 }
289 s
290}
291
292fn trim_ascii(s: &[u8]) -> &[u8] {
293 let mut start = 0;
294 while start < s.len() && (s[start] == b' ' || s[start] == b'\t') {
295 start += 1;
296 }
297 let mut end = s.len();
298 while end > start && (s[end - 1] == b' ' || s[end - 1] == b'\t') {
299 end -= 1;
300 }
301 &s[start..end]
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307
308 fn lit_indexed_name(name_index: u8, value: &str) -> Vec<u8> {
309 let mut out = Vec::new();
310 out.push(0x40 | (name_index & 0x3F));
311 out.push(value.len() as u8); out.extend_from_slice(value.as_bytes());
313 out
314 }
315
316 fn lit_indexed_name_huffman(name_index: u8, value: &str) -> Vec<u8> {
317 let mut out = Vec::new();
318 out.push(0x40 | (name_index & 0x3F));
319 let payload = huffman::encode(value);
320 assert!(payload.len() < 0x7F);
321 out.push(0x80 | payload.len() as u8);
322 out.extend_from_slice(&payload);
323 out
324 }
325
326 #[test]
327 fn extracts_authority_via_static_literal() {
328 let mut d = HpackDecoder::new();
329 let block = lit_indexed_name(1, "api.example.com");
330 let result = d.decode_block(&block).unwrap().unwrap();
331 assert_eq!(result.value, "api.example.com");
332 assert_eq!(result.provenance, AuthorityProvenance::StaticLiteral);
333 assert_eq!(d.dynamic_table.entry_count(), 1);
335 }
336
337 #[test]
338 fn extracts_authority_via_huffman_literal() {
339 let mut d = HpackDecoder::new();
340 let block = lit_indexed_name_huffman(1, "api.example.com");
341 let result = d.decode_block(&block).unwrap().unwrap();
342 assert_eq!(result.value, "api.example.com");
343 assert_eq!(result.provenance, AuthorityProvenance::Huffman);
344 }
345
346 #[test]
347 fn extracts_authority_via_dynamic_table_reference() {
348 let mut d = HpackDecoder::new();
349 let block1 = lit_indexed_name(1, "api.example.com");
352 let r1 = d.decode_block(&block1).unwrap().unwrap();
353 assert_eq!(r1.value, "api.example.com");
354 let block2 = vec![0x80 | 62];
357 let r2 = d.decode_block(&block2).unwrap().unwrap();
358 assert_eq!(r2.value, "api.example.com");
359 assert_eq!(r2.provenance, AuthorityProvenance::DynamicIndexed);
360 }
361
362 #[test]
363 fn rejects_invalid_dynamic_index() {
364 let mut d = HpackDecoder::new();
365 let block = vec![0x80 | 62];
367 let err = d.decode_block(&block).unwrap_err();
368 assert!(matches!(err, H2ParseError::HpackInvalidIndex { .. }));
369 }
370
371 #[test]
372 fn dynamic_table_size_update_is_honoured() {
373 let mut d = HpackDecoder::new();
374 let block = vec![0x20]; d.decode_block(&block).unwrap();
377 assert_eq!(d.dynamic_table.max_size(), 0);
378 }
379
380 #[test]
381 fn block_with_no_authority_returns_none() {
382 let mut d = HpackDecoder::new();
383 let block = vec![0x80 | 2];
386 let result = d.decode_block(&block).unwrap();
387 assert!(result.is_none());
388 }
389
390 #[test]
391 fn literal_with_incremental_indexing_persists_across_blocks() {
392 let mut d = HpackDecoder::new();
393 let mut block1 = Vec::new();
395 block1.push(0x40); block1.push(8);
397 block1.extend_from_slice(b"x-custom");
398 block1.push(5);
399 block1.extend_from_slice(b"value");
400 d.decode_block(&block1).unwrap();
401 assert_eq!(d.dynamic_table.entry_count(), 1);
402 let (name, value) = d.dynamic_table.lookup(62).unwrap();
404 assert_eq!(name, "x-custom");
405 assert_eq!(value, "value");
406 }
407}