Skip to main content

specter/transport/h2/
hpack.rs

1//! HPACK header compression with custom pseudo-header ordering.
2//!
3//! This module provides a custom HPACK implementation with:
4//! - Custom pseudo-header ordering (Chrome uses `:method, :scheme, :authority, :path`)
5//! - Full control over header encoding for fingerprint accuracy
6//! - Complete Huffman encoding support
7
8use crate::headers::Headers;
9use crate::transport::h2::hpack_impl::{Decoder, Encoder};
10use bytes::Bytes;
11
12fn bytes_eq_ignore_ascii_case(a: &[u8], b: &[u8]) -> bool {
13    a.len() == b.len() && a.iter().zip(b).all(|(x, y)| x.eq_ignore_ascii_case(y))
14}
15
16/// Pseudo-header ordering for HTTP/2 fingerprinting.
17///
18/// Different browsers/clients send pseudo-headers in different orders.
19/// This order is visible in the Akamai HTTP/2 fingerprint.
20#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
21pub enum PseudoHeaderOrder {
22    /// Chrome order: :method, :scheme, :authority, :path (m,s,a,p)
23    #[default]
24    Chrome,
25    /// Firefox order: :method, :path, :authority, :scheme (m,p,a,s)
26    Firefox,
27    /// Safari order: :method, :scheme, :path, :authority (m,s,p,a)
28    Safari,
29    /// Legacy order: :method, :authority, :scheme, :path (m,a,s,p)
30    Standard,
31    /// Custom order specified by indices (0=method, 1=authority, 2=scheme, 3=path)
32    Custom([u8; 4]),
33}
34
35impl PseudoHeaderOrder {
36    /// Get the order as array indices.
37    /// Input array is [method(0), authority(1), scheme(2), path(3)].
38    /// Returns indices to select in output order.
39    fn order(&self) -> [usize; 4] {
40        match self {
41            // Chrome: m,s,a,p -> method, scheme, authority, path
42            Self::Chrome => [0, 2, 1, 3], // m=0, s=2, a=1, p=3
43            // Firefox: m,p,a,s
44            Self::Firefox => [0, 3, 1, 2], // m=0, p=3, a=1, s=2
45            // Safari: m,s,p,a
46            Self::Safari => [0, 2, 3, 1], // m=0, s=2, p=3, a=1
47            // Legacy: m,a,s,p (old incorrect Chrome assumption)
48            Self::Standard => [0, 1, 2, 3], // m=0, a=1, s=2, p=3
49            Self::Custom(order) => [
50                order[0] as usize,
51                order[1] as usize,
52                order[2] as usize,
53                order[3] as usize,
54            ],
55        }
56    }
57
58    /// Get the Akamai fingerprint string for this order.
59    pub fn akamai_string(&self) -> &'static str {
60        match self {
61            Self::Chrome => "m,s,a,p",
62            Self::Firefox => "m,p,a,s",
63            Self::Safari => "m,s,p,a",
64            Self::Standard => "m,a,s,p",
65            Self::Custom(_) => "custom",
66        }
67    }
68}
69
70/// HPACK encoder with custom pseudo-header ordering.
71pub struct HpackEncoder {
72    encoder: Encoder,
73    pseudo_order: PseudoHeaderOrder,
74}
75
76impl HpackEncoder {
77    /// Create a new encoder with the specified pseudo-header order.
78    pub fn new(pseudo_order: PseudoHeaderOrder) -> Self {
79        Self {
80            encoder: Encoder::new(),
81            pseudo_order,
82        }
83    }
84
85    /// Create encoder with Chrome pseudo-header order (default).
86    pub fn chrome() -> Self {
87        Self::new(PseudoHeaderOrder::Chrome)
88    }
89
90    /// Set the dynamic table size.
91    pub fn set_max_table_size(&mut self, size: usize) {
92        self.encoder.set_max_table_size(size);
93    }
94
95    /// Encode headers for an HTTP/2 request.
96    ///
97    /// Pseudo-headers are ordered according to the configured order.
98    /// Regular headers follow in the order provided.
99    pub fn encode_request(
100        &mut self,
101        method: &str,
102        scheme: &str,
103        authority: &str,
104        path: &str,
105        headers: impl Into<Headers>,
106    ) -> Bytes {
107        let headers = headers.into();
108        // Build pseudo-headers in configured order
109        let pseudo_headers: [(&[u8], &[u8]); 4] = [
110            (b":method", method.as_bytes()),
111            (b":authority", authority.as_bytes()),
112            (b":scheme", scheme.as_bytes()),
113            (b":path", path.as_bytes()),
114        ];
115
116        // Collect all headers in the correct order
117        let mut all_headers: Vec<(&[u8], &[u8])> = Vec::new();
118
119        // Storage for processed valid headers (lowercased name, value ref)
120        // We need this intermediate storage to ensure the Strings live long enough
121        // and to avoid borrow checker issues (references into a growing Vec).
122        let mut valid_headers: Vec<(Vec<u8>, &[u8])> = Vec::with_capacity(headers.len());
123
124        for (name, value) in headers.iter_bytes() {
125            if name.first() == Some(&b':') {
126                continue;
127            }
128
129            if name.is_empty() {
130                continue;
131            }
132            if name.iter().any(|&b| b < 0x21 || (b > 0x7E && b != 0x7F)) {
133                continue;
134            }
135
136            let name_lower = if name.iter().all(|b| b.is_ascii_lowercase()) {
137                name.to_vec()
138            } else {
139                name.iter().map(|b| b.to_ascii_lowercase()).collect()
140            };
141
142            if name_lower == b"connection"
143                || name_lower == b"keep-alive"
144                || name_lower == b"proxy-connection"
145                || name_lower == b"transfer-encoding"
146                || name_lower == b"upgrade"
147            {
148                continue;
149            }
150
151            if name_lower == b"te" && !bytes_eq_ignore_ascii_case(value, b"trailers") {
152                continue;
153            }
154
155            valid_headers.push((name_lower, value));
156        }
157
158        // Add pseudo-headers in the specified order
159        let order = self.pseudo_order.order();
160        for &idx in &order {
161            all_headers.push(pseudo_headers[idx]);
162        }
163
164        // Add regular headers from the validated list
165        for (n, v) in &valid_headers {
166            all_headers.push((n.as_slice(), *v));
167        }
168
169        // Encode all headers
170        let encoded = self.encoder.encode(&all_headers);
171        Bytes::from(encoded)
172    }
173
174    /// Encode RFC 8441 Extended CONNECT headers for WebSocket over HTTP/2.
175    ///
176    /// The pseudo-header order is deterministic and spec-compliant for RFC 8441;
177    /// it is not claimed to be Chrome-exact.
178    pub fn encode_extended_connect_websocket(
179        &mut self,
180        authority: &str,
181        scheme: &str,
182        path: &str,
183        headers: impl Into<Headers>,
184    ) -> Result<Bytes, String> {
185        let headers = headers.into();
186        if authority.is_empty() {
187            return Err(":authority must not be empty".to_string());
188        }
189        if scheme.is_empty() {
190            return Err(":scheme must not be empty".to_string());
191        }
192        if path.is_empty() {
193            return Err(":path must not be empty".to_string());
194        }
195
196        let pseudo_headers: [(&[u8], &[u8]); 5] = [
197            (b":method", b"CONNECT"),
198            (b":protocol", b"websocket"),
199            (b":scheme", scheme.as_bytes()),
200            (b":path", path.as_bytes()),
201            (b":authority", authority.as_bytes()),
202        ];
203
204        let mut valid_headers: Vec<(Vec<u8>, &[u8])> = Vec::with_capacity(headers.len());
205
206        for (name, value) in headers.iter_bytes() {
207            if name.first() == Some(&b':') {
208                return Err(format!(
209                    "RFC 8441 user pseudo-header rejected: {}",
210                    String::from_utf8_lossy(name)
211                ));
212            }
213
214            if name.is_empty() {
215                return Err("RFC 8441 header name must not be empty".to_string());
216            }
217            if name.iter().any(|&b| b < 0x21 || (b > 0x7E && b != 0x7F)) {
218                return Err(format!(
219                    "RFC 8441 invalid header name rejected: {}",
220                    String::from_utf8_lossy(name)
221                ));
222            }
223
224            let name_lower = if name.iter().all(|b| b.is_ascii_lowercase()) {
225                name.to_vec()
226            } else {
227                name.iter().map(|b| b.to_ascii_lowercase()).collect()
228            };
229            if matches!(
230                name_lower.as_slice(),
231                b"connection"
232                    | b"upgrade"
233                    | b"host"
234                    | b"sec-websocket-key"
235                    | b"sec-websocket-accept"
236                    | b"sec-websocket-extensions"
237                    | b"keep-alive"
238                    | b"proxy-connection"
239                    | b"transfer-encoding"
240            ) {
241                return Err(format!(
242                    "RFC 8441 forbidden header rejected: {}",
243                    String::from_utf8_lossy(&name_lower)
244                ));
245            }
246
247            if name_lower == b"te" && !bytes_eq_ignore_ascii_case(value, b"trailers") {
248                return Err("RFC 8441 forbids TE values other than trailers".to_string());
249            }
250
251            valid_headers.push((name_lower, value));
252        }
253
254        let mut all_headers: Vec<(&[u8], &[u8])> =
255            Vec::with_capacity(pseudo_headers.len() + valid_headers.len());
256        all_headers.extend_from_slice(&pseudo_headers);
257        for (name, value) in &valid_headers {
258            all_headers.push((name.as_slice(), *value));
259        }
260
261        let encoded = self.encoder.encode(&all_headers);
262        Ok(Bytes::from(encoded))
263    }
264
265    /// Split an encoded header block into chunks if it exceeds max_frame_size.
266    /// Returns (first_chunk, remaining_chunks).
267    ///
268    /// This is used when header blocks exceed MAX_FRAME_SIZE and must be
269    /// split across HEADERS + CONTINUATION frames per RFC 9113 Section 6.10.
270    ///
271    /// Use this after calling encode_request() to chunk the result if needed.
272    pub fn chunk_encoded(encoded: Bytes, max_frame_size: usize) -> (Bytes, Vec<Bytes>) {
273        if encoded.len() <= max_frame_size {
274            // Fits in single frame
275            return (encoded, Vec::new());
276        }
277
278        // Split into chunks
279        let mut chunks: Vec<Bytes> = encoded
280            .chunks(max_frame_size)
281            .map(Bytes::copy_from_slice)
282            .collect();
283
284        let first = chunks.remove(0);
285        (first, chunks)
286    }
287}
288
289/// HPACK decoder.
290pub struct HpackDecoder {
291    decoder: Decoder,
292}
293
294impl HpackDecoder {
295    /// Create a new decoder.
296    pub fn new() -> Self {
297        Self {
298            decoder: Decoder::new(),
299        }
300    }
301
302    /// Set the maximum dynamic table size.
303    pub fn set_max_table_size(&mut self, size: usize) {
304        self.decoder.set_max_table_size(size);
305    }
306
307    /// Decode a header block into a list of headers.
308    pub fn decode(&mut self, data: &[u8]) -> Result<Vec<(String, String)>, String> {
309        let mut headers = Vec::new();
310
311        self.decoder
312            .decode_with_cb(data, |name, value| {
313                let name_str = String::from_utf8_lossy(name).into_owned();
314                let value_str = String::from_utf8_lossy(value).into_owned();
315                headers.push((name_str, value_str));
316            })
317            .map_err(|e| format!("HPACK decode error: {:?}", e))?;
318
319        Ok(headers)
320    }
321}
322
323impl Default for HpackDecoder {
324    fn default() -> Self {
325        Self::new()
326    }
327}
328
329#[cfg(test)]
330mod tests {
331    use super::*;
332
333    #[test]
334    fn test_pseudo_order_chrome() {
335        let order = PseudoHeaderOrder::Chrome;
336        assert_eq!(order.akamai_string(), "m,s,a,p");
337    }
338
339    #[test]
340    fn test_pseudo_order_standard() {
341        let order = PseudoHeaderOrder::Standard;
342        assert_eq!(order.akamai_string(), "m,a,s,p");
343    }
344
345    #[test]
346    fn test_encoder_creates_valid_block() {
347        let mut encoder = HpackEncoder::chrome();
348        let block = encoder.encode_request(
349            "GET",
350            "https",
351            "example.com",
352            "/",
353            &Headers::from(vec![("user-agent".to_string(), "test".to_string())]),
354        );
355
356        // Block should be non-empty
357        assert!(!block.is_empty());
358
359        // Decode and verify
360        let mut decoder = HpackDecoder::new();
361        let headers = decoder.decode(&block).unwrap();
362
363        // Should have 5 headers (4 pseudo + 1 regular)
364        assert_eq!(headers.len(), 5);
365
366        // Verify Chrome order: m,s,a,p
367        assert_eq!(headers[0].0, ":method");
368        assert_eq!(headers[0].1, "GET");
369        assert_eq!(headers[1].0, ":scheme");
370        assert_eq!(headers[1].1, "https");
371        assert_eq!(headers[2].0, ":authority");
372        assert_eq!(headers[2].1, "example.com");
373        assert_eq!(headers[3].0, ":path");
374        assert_eq!(headers[3].1, "/");
375        assert_eq!(headers[4].0, "user-agent");
376        assert_eq!(headers[4].1, "test");
377    }
378
379    #[test]
380    fn test_encoder_standard_order() {
381        let mut encoder = HpackEncoder::new(PseudoHeaderOrder::Standard);
382        let block = encoder.encode_request("GET", "https", "example.com", "/", &Headers::new());
383
384        let mut decoder = HpackDecoder::new();
385        let headers = decoder.decode(&block).unwrap();
386
387        // Verify Standard/legacy order: m,a,s,p
388        assert_eq!(headers[0].0, ":method");
389        assert_eq!(headers[1].0, ":authority");
390        assert_eq!(headers[2].0, ":scheme");
391        assert_eq!(headers[3].0, ":path");
392    }
393
394    #[test]
395    fn test_encoder_filters_connection_headers() {
396        let mut encoder = HpackEncoder::chrome();
397        let block = encoder.encode_request(
398            "GET",
399            "https",
400            "example.com",
401            "/",
402            &Headers::from(vec![
403                ("connection".to_string(), "keep-alive".to_string()),
404                ("keep-alive".to_string(), "timeout=5".to_string()),
405                ("user-agent".to_string(), "test".to_string()),
406            ]),
407        );
408
409        let mut decoder = HpackDecoder::new();
410        let headers = decoder.decode(&block).unwrap();
411
412        // Should only have pseudo-headers + user-agent (connection headers filtered)
413        assert_eq!(headers.len(), 5);
414        assert_eq!(headers[4].0, "user-agent");
415    }
416}