Skip to main content

hpke_http/
compression.rs

1//! Authenticated protocol body coding, separate from HTTP `Content-Encoding`.
2//!
3//! Request compression uses a marker. Response records name their body coding.
4
5use std::io::{Read, Write};
6
7use flate2::{Compression, bufread::MultiGzDecoder, write::GzEncoder};
8use ruzstd::{
9    decoding::StreamingDecoder,
10    encoding::{CompressionLevel, compress_to_vec},
11};
12
13use crate::{
14    Error, Limits,
15    message::{self, Request},
16};
17
18pub(crate) const MARKER: u8 = 0x04;
19const MIN_BODY_LEN: usize = 64;
20// Standard encoders can declare a larger history window than the decoded
21// payload. Keep a 1 MiB interoperability floor, still far below ruzstd's
22// 100 MiB default, and cap even the 64 MiB body configuration at 8 MiB.
23const MIN_ZSTD_WINDOW: usize = 1024 * 1024;
24const MAX_ZSTD_WINDOW: usize = 8 * 1024 * 1024;
25const MAX_ZSTD_FRAMES: usize = 16;
26
27/// Opt-in, authenticated protocol body coding. This is not HTTP `Content-Encoding`.
28#[derive(Clone, Copy, Debug, Eq, PartialEq)]
29pub enum CompressionCoding {
30    /// RFC 1952 gzip members.
31    Gzip,
32    /// RFC 8878 Zstandard frames without a dictionary.
33    Zstd,
34}
35
36impl CompressionCoding {
37    pub(crate) const fn wire_id(self) -> u8 {
38        match self {
39            Self::Gzip => 1,
40            Self::Zstd => 2,
41        }
42    }
43
44    pub(crate) const fn from_wire_id(id: u8) -> Result<Option<Self>, Error> {
45        match id {
46            0 => Ok(None),
47            1 => Ok(Some(Self::Gzip)),
48            2 => Ok(Some(Self::Zstd)),
49            _ => Err(Error::MalformedEnvelope),
50        }
51    }
52}
53
54pub(crate) fn encode_request(
55    request: &Request,
56    limits: Limits,
57    preferred: Option<CompressionCoding>,
58) -> Result<Vec<u8>, Error> {
59    let Some(preferred) = preferred else {
60        return message::encode_request(request, limits);
61    };
62    let coded = maybe_compress(&request.body, preferred)?;
63    let body_coding = coded.as_ref().map(|_| preferred);
64    let mut output = vec![
65        MARKER,
66        body_coding.map_or(0, CompressionCoding::wire_id),
67        preferred.wire_id(),
68    ];
69    let bhttp = message::encode_request_with_body(
70        request,
71        coded.as_deref().unwrap_or(&request.body),
72        limits,
73    )?;
74    output.extend_from_slice(&bhttp);
75    Ok(output)
76}
77
78pub(crate) fn decode_request(
79    input: &[u8],
80    limits: Limits,
81    enabled: bool,
82) -> Result<
83    (
84        Request,
85        Option<CompressionCoding>,
86        Option<CompressionCoding>,
87    ),
88    Error,
89> {
90    if !input.starts_with(&[MARKER]) {
91        return Ok((message::decode_request(input, limits)?, None, None));
92    }
93    if !enabled || input.len() < 4 {
94        return Err(Error::MalformedEnvelope);
95    }
96    let body_coding = CompressionCoding::from_wire_id(input[1])?;
97    let response_coding = CompressionCoding::from_wire_id(input[2])?;
98    if body_coding.is_none() && response_coding.is_none() {
99        return Err(Error::MalformedEnvelope);
100    }
101    let request = if body_coding.is_some() {
102        message::decode_coded_request(&input[3..], limits)?
103    } else {
104        message::decode_request(&input[3..], limits)?
105    };
106    Ok((request, body_coding, response_coding))
107}
108
109pub(crate) fn finish_request(
110    mut request: Request,
111    coding: Option<CompressionCoding>,
112    limits: Limits,
113) -> Result<Request, Error> {
114    if let Some(coding) = coding {
115        request.body = decompress(&request.body, coding, limits.max_body_len)?;
116        message::validate_decoded_request_body(&request)?;
117    }
118    Ok(request)
119}
120
121pub(crate) fn maybe_compress(
122    body: &[u8],
123    coding: CompressionCoding,
124) -> Result<Option<Vec<u8>>, Error> {
125    if body.len() < MIN_BODY_LEN {
126        return Ok(None);
127    }
128    let coded = compress(body, coding)?;
129    Ok((coded.len() < body.len()).then_some(coded))
130}
131
132pub(crate) fn compress(body: &[u8], coding: CompressionCoding) -> Result<Vec<u8>, Error> {
133    let coded = match coding {
134        CompressionCoding::Gzip => {
135            let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
136            encoder
137                .write_all(body)
138                .map_err(|_| Error::CompressionFailure)?;
139            encoder.finish().map_err(|_| Error::CompressionFailure)?
140        }
141        CompressionCoding::Zstd => compress_to_vec(body, CompressionLevel::Fastest),
142    };
143    Ok(coded)
144}
145
146pub(crate) fn decompress(
147    coded: &[u8],
148    coding: CompressionCoding,
149    maximum_body_len: usize,
150) -> Result<Vec<u8>, Error> {
151    if coded.is_empty() {
152        return Err(Error::MalformedEnvelope);
153    }
154    match coding {
155        CompressionCoding::Gzip => {
156            let mut decoder = MultiGzDecoder::new(coded);
157            let body = read_bounded(&mut decoder, maximum_body_len)?;
158            if !decoder.into_inner().is_empty() {
159                return Err(Error::MalformedEnvelope);
160            }
161            Ok(body)
162        }
163        CompressionCoding::Zstd => {
164            let mut remaining = coded;
165            let mut body = Vec::new();
166            let mut decoded_frames = 0_usize;
167            let mut seen_frames = 0_usize;
168            while !remaining.is_empty() {
169                seen_frames += 1;
170                if seen_frames > MAX_ZSTD_FRAMES {
171                    return Err(Error::LimitExceeded);
172                }
173                // RFC 8878 skippable frames can precede or separate data frames.
174                if let Some(skip_len) = skippable_frame_len(remaining)? {
175                    remaining = &remaining[skip_len..];
176                    continue;
177                }
178                let mut decoder = StreamingDecoder::new_with_max_window_size(
179                    remaining,
180                    maximum_body_len.clamp(MIN_ZSTD_WINDOW, MAX_ZSTD_WINDOW) as u64,
181                )
182                .map_err(|_| Error::MalformedEnvelope)?;
183                let declared_size = decoder.decoder.content_size();
184                if declared_size > maximum_body_len.saturating_sub(body.len()) as u64 {
185                    return Err(Error::LimitExceeded);
186                }
187                let decoded_before = body.len();
188                read_bounded_into(&mut decoder, &mut body, maximum_body_len)?;
189                if declared_size != 0 && declared_size != (body.len() - decoded_before) as u64 {
190                    return Err(Error::MalformedEnvelope);
191                }
192                if decoder.decoder.get_checksum_from_data().is_some()
193                    && decoder.decoder.get_checksum_from_data()
194                        != decoder.decoder.get_calculated_checksum()
195                {
196                    return Err(Error::MalformedEnvelope);
197                }
198                remaining = decoder.into_inner();
199                decoded_frames += 1;
200            }
201            if decoded_frames == 0 {
202                return Err(Error::MalformedEnvelope);
203            }
204            Ok(body)
205        }
206    }
207}
208
209fn skippable_frame_len(input: &[u8]) -> Result<Option<usize>, Error> {
210    if input.len() < 4 {
211        return Err(Error::MalformedEnvelope);
212    }
213    let magic = u32::from_le_bytes([input[0], input[1], input[2], input[3]]);
214    if !(0x184d_2a50..=0x184d_2a5f).contains(&magic) {
215        return Ok(None);
216    }
217    if input.len() < 8 {
218        return Err(Error::MalformedEnvelope);
219    }
220    let length = u32::from_le_bytes([input[4], input[5], input[6], input[7]]) as usize;
221    let total = length.checked_add(8).ok_or(Error::MalformedEnvelope)?;
222    if total > input.len() {
223        return Err(Error::MalformedEnvelope);
224    }
225    Ok(Some(total))
226}
227
228fn read_bounded(reader: &mut impl Read, maximum_body_len: usize) -> Result<Vec<u8>, Error> {
229    let mut output = Vec::new();
230    read_bounded_into(reader, &mut output, maximum_body_len)?;
231    Ok(output)
232}
233
234fn read_bounded_into(
235    reader: &mut impl Read,
236    output: &mut Vec<u8>,
237    maximum_body_len: usize,
238) -> Result<(), Error> {
239    let mut buffer = [0_u8; 8192];
240    loop {
241        let count = reader
242            .read(&mut buffer)
243            .map_err(|_| Error::MalformedEnvelope)?;
244        if count == 0 {
245            return Ok(());
246        }
247        if count > maximum_body_len.saturating_sub(output.len()) {
248            return Err(Error::LimitExceeded);
249        }
250        output.extend_from_slice(&buffer[..count]);
251    }
252}
253
254#[cfg(test)]
255mod tests {
256    use super::{CompressionCoding, decompress, encode_request, maybe_compress};
257    use crate::{Error, Limits, Method, Request};
258
259    // Produced independently with `printf ... | gzip -n -c` and
260    // `printf ... | zstd -q -1 -c`, not by this crate's encoders.
261    const GZIP: &[u8] = &[
262        0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xcb, 0x48, 0xcd, 0xc9, 0xc9,
263        0x57, 0x48, 0x2b, 0xca, 0xcf, 0x55, 0x48, 0xcc, 0x53, 0xc8, 0xcc, 0x4b, 0x49, 0x2d, 0x48,
264        0x05, 0x12, 0x79, 0x25, 0x0a, 0xe9, 0x55, 0x99, 0x05, 0x0a, 0xa9, 0x79, 0xc9, 0xf9, 0x29,
265        0xa9, 0x45, 0x0a, 0x19, 0x44, 0x29, 0x03, 0x00, 0x61, 0xc4, 0x34, 0x58, 0x4d, 0x00, 0x00,
266        0x00,
267    ];
268    const ZSTD: &[u8] = &[
269        0x28, 0xb5, 0x2f, 0xfd, 0x04, 0x48, 0x7d, 0x01, 0x00, 0x74, 0x02, 0x68, 0x65, 0x6c, 0x6c,
270        0x6f, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x61, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x65, 0x70,
271        0x65, 0x6e, 0x64, 0x65, 0x6e, 0x74, 0x20, 0x7a, 0x73, 0x74, 0x64, 0x20, 0x65, 0x6e, 0x63,
272        0x6f, 0x64, 0x65, 0x72, 0x20, 0x01, 0x00, 0xaf, 0x1e, 0xf5, 0x04, 0x2a, 0x5c, 0xa5, 0xa6,
273    ];
274
275    #[test]
276    fn independent_gzip_members_and_strict_trailer() {
277        let expected =
278            b"hello from an independent gzip encoder hello from an independent gzip encoder";
279        assert_eq!(
280            decompress(GZIP, CompressionCoding::Gzip, 1024),
281            Ok(expected.to_vec())
282        );
283        let mut joined = GZIP.to_vec();
284        joined.extend_from_slice(GZIP);
285        assert_eq!(
286            decompress(&joined, CompressionCoding::Gzip, 1024),
287            Ok(expected.repeat(2))
288        );
289        assert_eq!(
290            decompress(&GZIP[..GZIP.len() - 1], CompressionCoding::Gzip, 1024),
291            Err(Error::MalformedEnvelope)
292        );
293        let mut bad_checksum = GZIP.to_vec();
294        bad_checksum[GZIP.len() - 8] ^= 1;
295        assert_eq!(
296            decompress(&bad_checksum, CompressionCoding::Gzip, 1024),
297            Err(Error::MalformedEnvelope)
298        );
299        let mut garbage = GZIP.to_vec();
300        garbage.push(0xff);
301        assert_eq!(
302            decompress(&garbage, CompressionCoding::Gzip, 1024),
303            Err(Error::MalformedEnvelope)
304        );
305    }
306
307    #[test]
308    fn independent_zstd_frames_and_skippable_frame() {
309        let expected =
310            b"hello from an independent zstd encoder hello from an independent zstd encoder";
311        assert_eq!(
312            decompress(ZSTD, CompressionCoding::Zstd, 1024),
313            Ok(expected.to_vec())
314        );
315        let mut joined = ZSTD.to_vec();
316        joined.extend_from_slice(ZSTD);
317        assert_eq!(
318            decompress(&joined, CompressionCoding::Zstd, 1024),
319            Ok(expected.repeat(2))
320        );
321        let mut skipped = vec![0x50, 0x2a, 0x4d, 0x18, 3, 0, 0, 0, b'x', b'y', b'z'];
322        skipped.extend_from_slice(ZSTD);
323        assert_eq!(
324            decompress(&skipped, CompressionCoding::Zstd, 1024),
325            Ok(expected.to_vec())
326        );
327        assert_eq!(
328            decompress(&ZSTD[..ZSTD.len() - 1], CompressionCoding::Zstd, 1024),
329            Err(Error::MalformedEnvelope)
330        );
331        let mut bad_checksum = ZSTD.to_vec();
332        bad_checksum[ZSTD.len() - 1] ^= 1;
333        assert_eq!(
334            decompress(&bad_checksum, CompressionCoding::Zstd, 1024),
335            Err(Error::MalformedEnvelope)
336        );
337        let mut garbage = ZSTD.to_vec();
338        garbage.push(0xff);
339        assert_eq!(
340            decompress(&garbage, CompressionCoding::Zstd, 1024),
341            Err(Error::MalformedEnvelope)
342        );
343        assert_eq!(
344            decompress(&ZSTD.repeat(17), CompressionCoding::Zstd, 4096),
345            Err(Error::LimitExceeded)
346        );
347        let mut oversized_window = ZSTD.to_vec();
348        oversized_window[5] = 0x90;
349        assert_eq!(
350            decompress(&oversized_window, CompressionCoding::Zstd, 64 * 1024 * 1024),
351            Err(Error::MalformedEnvelope)
352        );
353        // `zstd --stream-size=77` emits the same block with a one-byte frame
354        // content size. Mutating that authenticated-format field must fail.
355        let mut sized = ZSTD.to_vec();
356        sized[4] = 0x24;
357        sized[5] = 77;
358        assert_eq!(
359            decompress(&sized, CompressionCoding::Zstd, 1024),
360            Ok(expected.to_vec())
361        );
362        sized[5] = 76;
363        assert_eq!(
364            decompress(&sized, CompressionCoding::Zstd, 1024),
365            Err(Error::MalformedEnvelope)
366        );
367        sized[5] = 200;
368        assert_eq!(
369            decompress(&sized, CompressionCoding::Zstd, 100),
370            Err(Error::LimitExceeded)
371        );
372    }
373
374    #[test]
375    fn decoded_length_is_bounded_during_expansion() -> Result<(), Error> {
376        for coding in [CompressionCoding::Gzip, CompressionCoding::Zstd] {
377            let encoded =
378                maybe_compress(&vec![b'a'; 4096], coding)?.ok_or(Error::CompressionFailure)?;
379            assert_eq!(decompress(&encoded, coding, 128), Err(Error::LimitExceeded));
380        }
381        Ok(())
382    }
383
384    #[test]
385    fn body_coding_extension_has_literal_wire_prefixes() -> Result<(), Error> {
386        let limits = Limits::default();
387        let request = Request {
388            method: Method::Post,
389            authority: b"api.example.test".to_vec(),
390            path: b"/compressed".to_vec(),
391            headers: Vec::new(),
392            body: vec![b'a'; 4096],
393        };
394        for (coding, request_prefix) in [
395            (CompressionCoding::Gzip, [0x04, 0x01, 0x01]),
396            (CompressionCoding::Zstd, [0x04, 0x02, 0x02]),
397        ] {
398            assert!(encode_request(&request, limits, Some(coding))?.starts_with(&request_prefix));
399
400            let mut small_request = request.clone();
401            small_request.body.clear();
402            assert!(
403                encode_request(&small_request, limits, Some(coding))?.starts_with(&[
404                    0x04,
405                    0x00,
406                    request_prefix[2]
407                ])
408            );
409        }
410        Ok(())
411    }
412}