Skip to main content

tachyon_web/ws/
deflate.rs

1//! `permessage-deflate` (RFC 7692): negotiation of the `Sec-WebSocket-Extensions` offer/response,
2//! and the actual per-message DEFLATE compressor/decompressor built on raw (headerless) deflate
3//! streams via `flate2`.
4//!
5//! Compression is negotiated per RFC 7692 §7 and applied to the *payload of a full message* (all
6//! fragments concatenated) rather than per-frame: RSV1 is only ever set on the first frame of a
7//! message, and continuation frames carry no RSV1 of their own. The wire format for a compressed
8//! message is produced with `Z_SYNC_FLUSH` and then has its trailing 4-byte empty-block marker
9//! (`00 00 ff ff`) stripped per §7.2.1; the decompressor puts that marker back before inflating.
10
11use flate2::{Compress, Compression, Decompress, FlushCompress, FlushDecompress};
12use hyper::header::{HeaderMap, HeaderValue, SEC_WEBSOCKET_EXTENSIONS};
13
14/// The 4 bytes a `Z_SYNC_FLUSH` block ends with, which RFC 7692 requires senders to strip and
15/// receivers to restore before inflating.
16const DEFLATE_TAIL: [u8; 4] = [0x00, 0x00, 0xff, 0xff];
17
18/// Server-side tuning for the `permessage-deflate` extension.
19///
20/// Constructed via [`Default`] and passed to
21/// [`WebSocketUpgrade::deflate_config`](super::WebSocketUpgrade::deflate_config).
22#[derive(Debug, Clone, Copy)]
23#[non_exhaustive]
24pub struct DeflateConfig {
25    /// Reset our own compression context after every message instead of reusing the sliding
26    /// window across messages. Lowers compression ratio, lowers memory use. Default: `false`.
27    pub server_no_context_takeover: bool,
28    /// Require the client to reset its compression context after every message. This is only a
29    /// request; correctness on our end does not depend on the client honoring it. Default: `false`.
30    pub client_no_context_takeover: bool,
31    /// The base-2 logarithm of the LZ77 window we use to compress outgoing messages, `9..=15`.
32    /// Values are clamped into that range. Default: `15` (32 KiB window, maximum compression).
33    pub server_max_window_bits: u8,
34}
35
36impl Default for DeflateConfig {
37    fn default() -> Self {
38        Self {
39            server_no_context_takeover: false,
40            client_no_context_takeover: false,
41            server_max_window_bits: 15,
42        }
43    }
44}
45
46/// One `permessage-deflate` offer parsed out of a `Sec-WebSocket-Extensions` request header.
47#[derive(Debug, Default, Clone, Copy)]
48pub(super) struct Offer {
49    server_no_context_takeover: bool,
50    client_no_context_takeover: bool,
51    /// Whether the client's offer named `server_max_window_bits` at all (with or without a
52    /// value) — distinguishes "absent" from "present with the default window size". We don't
53    /// honor `client_max_window_bits` (our decompressor always uses the maximum window, which
54    /// is always compatible with whatever smaller window the client's compressor might use), so
55    /// there's nothing to record for it beyond validating it during parsing.
56    server_max_window_bits_offered: bool,
57}
58
59/// The agreed-upon parameters after negotiating an [`Offer`] against a [`DeflateConfig`].
60#[derive(Debug, Clone, Copy)]
61pub(super) struct Agreement {
62    pub(super) server_no_context_takeover: bool,
63    pub(super) client_no_context_takeover: bool,
64    pub(super) server_max_window_bits: u8,
65    /// Whether `server_max_window_bits` should be echoed in the response — only valid if the
66    /// client's offer itself named the parameter.
67    echo_server_max_window_bits: bool,
68}
69
70/// Extracts every `permessage-deflate` offer from the request's `Sec-WebSocket-Extensions`
71/// header(s), in the order they appeared. Unrecognized extensions and unrecognized/malformed
72/// parameters on an otherwise-recognized offer are skipped per-offer (per RFC 7692 §7, a server
73/// declines an individual malformed offer rather than failing the whole negotiation).
74pub(super) fn parse_offers(headers: &HeaderMap) -> Vec<Offer> {
75    let mut offers = Vec::new();
76    for header in headers.get_all(SEC_WEBSOCKET_EXTENSIONS) {
77        let Ok(text) = header.to_str() else { continue };
78        for extension in text.split(',') {
79            let mut parts = extension.split(';').map(str::trim);
80            let Some(name) = parts.next() else { continue };
81            if !name.eq_ignore_ascii_case("permessage-deflate") {
82                continue;
83            }
84            if let Some(offer) = parse_params(parts) {
85                offers.push(offer);
86            }
87        }
88    }
89    offers
90}
91
92/// Parses one `window_bits` parameter's optional value, validating it against RFC 7692's
93/// `9..=15` range when present. Returns `Err` if the offer should be declined outright.
94fn parse_window_bits(value: Option<&str>) -> Result<(), ()> {
95    match value.map(str::parse::<u8>) {
96        None | Some(Ok(9..=15)) => Ok(()),
97        Some(_) => Err(()),
98    }
99}
100
101fn parse_params<'a>(params: impl Iterator<Item = &'a str>) -> Option<Offer> {
102    let mut offer = Offer::default();
103    for param in params {
104        if param.is_empty() {
105            continue;
106        }
107        let (key, value) = match param.split_once('=') {
108            Some((k, v)) => (k.trim(), Some(v.trim().trim_matches('"'))),
109            None => (param, None),
110        };
111        match key {
112            "server_no_context_takeover" if value.is_none() => {
113                offer.server_no_context_takeover = true;
114            }
115            "client_no_context_takeover" if value.is_none() => {
116                offer.client_no_context_takeover = true;
117            }
118            "server_max_window_bits" => {
119                parse_window_bits(value).ok()?;
120                offer.server_max_window_bits_offered = true;
121            }
122            "client_max_window_bits" => {
123                parse_window_bits(value).ok()?;
124            }
125            // Unrecognized parameter, or a value on a no-value-only flag: decline this offer.
126            _ => return None,
127        }
128    }
129    Some(offer)
130}
131
132/// Picks the first offer we can accept and applies `config`'s server-side preferences to it.
133/// Returns `None` if there is nothing to negotiate (no offers at all).
134pub(super) fn negotiate(offers: &[Offer], config: DeflateConfig) -> Option<Agreement> {
135    let offer = offers.first()?;
136    let server_max_window_bits = config.server_max_window_bits.clamp(9, 15);
137    Some(Agreement {
138        server_no_context_takeover: config.server_no_context_takeover
139            || offer.server_no_context_takeover,
140        client_no_context_takeover: config.client_no_context_takeover
141            || offer.client_no_context_takeover,
142        server_max_window_bits,
143        echo_server_max_window_bits: offer.server_max_window_bits_offered
144            && server_max_window_bits < 15,
145    })
146}
147
148/// Builds the `Sec-WebSocket-Extensions` response header value for an accepted [`Agreement`].
149pub(super) fn agreement_header_value(agreement: Agreement) -> HeaderValue {
150    let mut value = String::from("permessage-deflate");
151    if agreement.server_no_context_takeover {
152        value.push_str("; server_no_context_takeover");
153    }
154    if agreement.client_no_context_takeover {
155        value.push_str("; client_no_context_takeover");
156    }
157    if agreement.echo_server_max_window_bits {
158        value.push_str("; server_max_window_bits=");
159        value.push_str(&agreement.server_max_window_bits.to_string());
160    }
161    HeaderValue::from_str(&value).unwrap_or_else(|_| HeaderValue::from_static("permessage-deflate"))
162}
163
164/// Per-connection compressor/decompressor for an agreed `permessage-deflate` extension.
165pub(super) struct PerMessageDeflate {
166    compress: Compress,
167    decompress: Decompress,
168    server_no_context_takeover: bool,
169    client_no_context_takeover: bool,
170}
171
172impl PerMessageDeflate {
173    pub(super) fn new(agreement: Agreement) -> Self {
174        Self {
175            compress: Compress::new_with_window_bits(
176                Compression::default(),
177                false,
178                agreement.server_max_window_bits,
179            ),
180            decompress: Decompress::new_with_window_bits(false, 15),
181            server_no_context_takeover: agreement.server_no_context_takeover,
182            client_no_context_takeover: agreement.client_no_context_takeover,
183        }
184    }
185
186    /// Compresses `data`, but only when the result actually comes out smaller — RFC 7692 leaves
187    /// per-message compression up to the sender (RSV1 is just left unset for the ones we skip),
188    /// and deflate's per-block overhead means a small or already-dense payload can come out
189    /// *larger* compressed. Returns `None` when the caller should send `data` verbatim instead.
190    ///
191    /// When we skip, the compressor's context is reset regardless of `server_no_context_takeover`:
192    /// RFC 7692 requires an unsent-compressed message not to affect the compression context, but
193    /// `flate2` gives no way to "undo" the trial `compress_vec` call already made while sizing up
194    /// the candidate. Resetting is the only way back to a self-consistent state — deflate back-
195    /// references are entirely encoder-side, so a reset simply means future messages don't
196    /// reference data the client's decompressor never saw; the client's own (untouched, larger)
197    /// window carrying unused extra history is harmless.
198    pub(super) fn compress_if_smaller(
199        &mut self,
200        data: &[u8],
201    ) -> Result<Option<Vec<u8>>, crate::http::error::Error> {
202        let compressed = self.compress_raw(data)?;
203        if compressed.len() < data.len() {
204            if self.server_no_context_takeover {
205                self.compress.reset();
206            }
207            Ok(Some(compressed))
208        } else {
209            self.compress.reset();
210            Ok(None)
211        }
212    }
213
214    /// Compresses one full message payload, stripping the trailing sync-flush marker per §7.2.1.
215    fn compress_raw(&mut self, data: &[u8]) -> Result<Vec<u8>, crate::http::error::Error> {
216        let total_in_before = self.compress.total_in();
217        let mut out = Vec::with_capacity(data.len() + 32);
218        loop {
219            grow(&mut out, 1024.max(data.len()));
220            self.compress
221                .compress_vec(data, &mut out, FlushCompress::Sync)
222                .map_err(|e| crate::http::error::Error::Internal(e.to_string()))?;
223            let consumed =
224                usize::try_from(self.compress.total_in() - total_in_before).unwrap_or(usize::MAX);
225            if consumed >= data.len() {
226                break;
227            }
228        }
229        out.truncate(out.len().saturating_sub(DEFLATE_TAIL.len()));
230        Ok(out)
231    }
232
233    /// Decompresses one full message payload, restoring the trailing sync-flush marker first.
234    pub(super) fn decompress(
235        &mut self,
236        data: &[u8],
237        max_size: Option<usize>,
238    ) -> Result<Vec<u8>, crate::http::error::Error> {
239        let max_size = max_size.unwrap_or(usize::MAX);
240        let mut input = Vec::with_capacity(data.len() + DEFLATE_TAIL.len());
241        input.extend_from_slice(data);
242        input.extend_from_slice(&DEFLATE_TAIL);
243
244        let total_in_before = self.decompress.total_in();
245        let mut out = Vec::with_capacity((data.len() * 3 + 32).min(max_size));
246        loop {
247            grow(&mut out, 1024);
248            let consumed_before =
249                usize::try_from(self.decompress.total_in() - total_in_before).unwrap_or(usize::MAX);
250            self.decompress
251                .decompress_vec(&input[consumed_before..], &mut out, FlushDecompress::Sync)
252                .map_err(|e| crate::http::error::Error::Internal(e.to_string()))?;
253            if out.len() > max_size {
254                return Err(crate::http::error::Error::Internal(
255                    "decompressed message exceeds the configured maximum size".to_string(),
256                ));
257            }
258            let consumed =
259                usize::try_from(self.decompress.total_in() - total_in_before).unwrap_or(usize::MAX);
260            if consumed >= input.len() {
261                break;
262            }
263        }
264        if self.client_no_context_takeover {
265            self.decompress.reset(false);
266        }
267        Ok(out)
268    }
269}
270
271/// Reserves more spare capacity in `out`, doubling what's already there (or `min_initial` on the
272/// first call) — geometric growth means a large payload needing several `compress_vec`/
273/// `decompress_vec` rounds costs O(log n) reallocations instead of O(n).
274fn grow(out: &mut Vec<u8>, min_initial: usize) {
275    let additional = out.capacity().max(min_initial);
276    out.reserve(additional);
277}