Skip to main content

tungstenite/extensions/
mod.rs

1//! WebSocket extensions.
2// Only `permessage-deflate` is supported at the moment.
3
4use bytes::Bytes;
5use thiserror::Error;
6
7use crate::extensions::compression::{
8    CompressionError, DecompressionError, PerMessageCompressionContext,
9};
10#[cfg(feature = "handshake")]
11use crate::extensions::headers::{SecWebsocketExtensions, WebsocketProtocolExtension};
12use crate::protocol::Role;
13
14pub mod compression;
15#[cfg(feature = "headers")]
16pub(crate) mod headers;
17
18/// Container for configured extensions for a connection.
19#[derive(Debug, Default)]
20#[allow(missing_copy_implementations)]
21pub struct Extensions {
22    /// The Per-Message Compression extension configured for the connection, if
23    /// any.
24    per_message_compression: Option<PerMessageCompressionContext>,
25}
26
27/// Configuration for extensions for a connection.
28#[derive(Copy, Clone, Default, Debug)]
29#[non_exhaustive]
30pub struct ExtensionsConfig {
31    /// Configuration for the `permessage-deflate` PMCE as specified by [RFC 7692].
32    ///
33    /// [RFC 7692]: https://tools.ietf.org/html/rfc7692
34    #[cfg(feature = "deflate")]
35    pub permessage_deflate: Option<compression::deflate::DeflateConfig>,
36}
37
38/// Error encountered while handling extensions.
39#[derive(Debug, Error, PartialEq, Eq, Clone)]
40pub enum ExtensionsError {
41    /// The header included an invalid extension.
42    #[error("Extension header had invalid extension: {0}")]
43    InvalidExtension(Box<str>),
44    /// The negotiation response included an extension more than once.
45    #[error("Extension negotiation response had conflicting extension: {0}")]
46    ExtensionConflict(Box<str>),
47    /// The header included an unparsable extension.
48    #[error("Extension negotiation response had malformed extension: {0}")]
49    MalformedExtension(&'static str),
50}
51
52#[cfg(feature = "handshake")]
53impl ExtensionsConfig {
54    pub(crate) fn generate_offers(&self) -> impl Iterator<Item = WebsocketProtocolExtension> {
55        let Self {
56            #[cfg(feature = "deflate")]
57            permessage_deflate,
58        } = self;
59
60        #[allow(unused_mut, unused_assignments)]
61        let mut permessage_compression_offer = None;
62
63        #[cfg(feature = "deflate")]
64        {
65            permessage_compression_offer = permessage_deflate.as_ref().map(|p| p.as_offer().into());
66        }
67
68        permessage_compression_offer.into_iter()
69    }
70
71    /// Checks that the given extensions are compatible with the given config
72    /// (if any).
73    ///
74    /// Receives a [`SecWebsocketExtensions`] header in a handshake response and
75    /// evaluates it against the given local configuration. Returns a new
76    /// `Extensions` that can be used for the connection for the completed
77    /// handshake.
78    pub(crate) fn verify_agreed_on(
79        &self,
80        agreed: SecWebsocketExtensions,
81    ) -> Result<Extensions, ExtensionsError> {
82        #[cfg_attr(not(feature = "deflate"), allow(unused_mut))]
83        let mut per_message_compression = None;
84
85        for extension in agreed.iter() {
86            match extension.name() {
87                #[cfg(feature = "deflate")]
88                compression::deflate::EXTENSION_NAME => {
89                    use compression::deflate::{
90                        DeflateContext, DeflateParameterError, PermessageDeflateConfig,
91                        EXTENSION_NAME,
92                    };
93
94                    // Already had PMCE configured
95                    if per_message_compression.is_some() {
96                        return Err(ExtensionsError::ExtensionConflict(EXTENSION_NAME.into()));
97                    }
98
99                    let deflate = self
100                        .permessage_deflate
101                        .ok_or_else(|| ExtensionsError::InvalidExtension(EXTENSION_NAME.into()))?;
102
103                    let extension: PermessageDeflateConfig = PermessageDeflateConfig::parse_params(
104                        extension.params(),
105                    )
106                    .map_err(|_: DeflateParameterError| {
107                        ExtensionsError::MalformedExtension(EXTENSION_NAME)
108                    })?;
109
110                    let deflate_config = deflate.accept_response(extension).map_err(|e| {
111                        ExtensionsError::InvalidExtension(format!("{EXTENSION_NAME}: {e}").into())
112                    })?;
113
114                    per_message_compression =
115                        Some(DeflateContext::new(Role::Client, deflate_config).into());
116                }
117                name => return Err(ExtensionsError::InvalidExtension(name.into())),
118            }
119        }
120
121        Ok(Extensions { per_message_compression })
122    }
123
124    /// Checks whether the given extension headers are compatible with the given
125    /// config (if any).
126    ///
127    /// Recieves a [`SecWebsocketExtensions`] header in a handshake request and
128    /// evaluates it against the given local configuration. Returns a
129    /// `SecWebsocketExtensions` header to be sent in the handshake response to
130    /// the client, and a `Extensions` value to be used for the connection, once
131    /// it is established.
132    pub(crate) fn accept_offers(
133        &self,
134        extensions: &SecWebsocketExtensions,
135    ) -> Result<(Extensions, Option<SecWebsocketExtensions>), ExtensionsError> {
136        #[cfg_attr(not(feature = "deflate"), allow(unused_mut))]
137        let mut per_message_compression = None;
138
139        for extension in extensions.iter() {
140            // Only one extension is currently supported. If that changes,
141            // this will need to be updated to apply the extensions in the correct order.
142            match extension.name() {
143                #[cfg(feature = "deflate")]
144                compression::deflate::EXTENSION_NAME => {
145                    use compression::deflate::{
146                        DeflateContext, PermessageDeflateConfig, EXTENSION_NAME,
147                    };
148
149                    let deflate = match self.permessage_deflate {
150                        Some(deflate) => deflate,
151                        None => continue,
152                    };
153
154                    let extension = match PermessageDeflateConfig::parse_params(extension.params())
155                    {
156                        Ok(extension) => extension,
157                        Err(e) => {
158                            // Per RFC 7692 Section 7:
159                            //
160                            //  A server MUST decline an extension negotiation
161                            //  offer for this extension if any of the following
162                            //  conditions are met:
163                            //
164                            //   o  The negotiation offer contains an extension
165                            //   parameter not defined for use in an offer.
166                            //
167                            // Declining instead of rejecting the request
168                            // outright allows clients that conform to a
169                            // (currently hypothetical) RFC that supersedes RFC
170                            // 7692 to fall back to requesting to the behavior
171                            // specified in the latter.
172                            log::debug!("{EXTENSION_NAME} extension: {e}");
173                            continue;
174                        }
175                    };
176                    // Per RFC 7692 Section 5:
177                    //
178                    //   A client may also offer multiple PMCE choices to the server
179                    //   by including multiple elements in the
180                    //   "Sec-WebSocket-Extensions" header, one for each PMCE
181                    //   offered.  This set of elements MAY include multiple PMCEs
182                    //   with the same extension name to offer the possibility to
183                    //   use the same algorithm with different configuration
184                    //   parameters.  The order of elements is important as it
185                    //   specifies the client's preference.  An element preceding
186                    //   another element has higher preference.  It is recommended
187                    //   that a server accepts PMCEs with higher preference if the
188                    //   server supports them.
189                    //
190                    // Follow the RFC recommendation by not overwriting a PMCE that
191                    // is already configured.
192                    if per_message_compression.is_some() {
193                        continue;
194                    }
195
196                    if let Some((config, response)) = deflate.accept_offer(extension) {
197                        per_message_compression = Some((
198                            DeflateContext::new(Role::Server, config).into(),
199                            response.into(),
200                        ));
201                    }
202                }
203                // Ignore any unknown extensions in the offer.
204                _ => {}
205            }
206        }
207
208        let (per_message_compression, response) = match per_message_compression {
209            Some((a, b)) => (Some(a), Some(b)),
210            None => (None, None),
211        };
212
213        Ok((
214            Extensions { per_message_compression },
215            response.map(|response| SecWebsocketExtensions::new(std::iter::once(response))),
216        ))
217    }
218}
219
220impl ExtensionsConfig {
221    /// Bypasses negotiation of extension parameters and enables those that have
222    /// been configured.
223    ///
224    /// Returns an [`Extensions`] that has all the extensions enabled that this
225    /// [`ExtensionsConfig`] was configured with.
226    pub(crate) fn into_unnegotiated_context(self, role: Role) -> Extensions {
227        // This can only be infallible while only one per-message compression
228        // extension is supported. If more are added there will need to be some
229        // resolution strategy for picking which one takes precedence.
230        let Self {
231            #[cfg(feature = "deflate")]
232            permessage_deflate,
233        } = self;
234
235        #[cfg_attr(feature = "deflate", allow(unused_assignments))]
236        #[cfg_attr(not(feature = "deflate"), allow(unused_mut))]
237        let mut per_message_compression = None;
238        #[cfg(feature = "deflate")]
239        {
240            per_message_compression = permessage_deflate
241                .map(|deflate| compression::deflate::DeflateContext::new(role, deflate).into());
242        }
243        let _ = role;
244
245        Extensions { per_message_compression }
246    }
247}
248
249impl Extensions {
250    /// Returns a function that, if present, compresses a message payload.
251    ///
252    /// The returned value will only be `Some` if a per-message compression
253    /// extension, as specified by [RFC 7692], was configured for the connection
254    /// state to which this `Extensions` applies.
255    ///
256    /// [RFC 7692]: https://tools.ietf.org/html/rfc7692
257    #[inline]
258    pub(crate) fn per_message_compressor<'s>(
259        &'s mut self,
260    ) -> Option<impl FnOnce(&Bytes) -> Result<Bytes, CompressionError> + 's> {
261        let Self { per_message_compression } = self;
262
263        per_message_compression.as_mut().map(PerMessageCompressionContext::compressor)
264    }
265
266    /// Returns a function that, if present, decompresses a frame payload.
267    ///
268    /// The returned value will only be `Some` if a per-message compression
269    /// extension, as specified by [RFC 7692], was configured for the connection
270    /// state to which this `Extensions` applies. The closure takes as arguments
271    /// the frame payload, in bytes, a boolean indicating whether the frame is
272    /// the final one for a message, and the maximum number of uncompressed
273    /// bytes to produce before returning an error.
274    ///
275    /// [RFC 7692]: https://tools.ietf.org/html/rfc7692
276    #[inline]
277    pub(crate) fn per_message_decompressor<'s>(
278        &'s mut self,
279    ) -> Option<impl FnMut(&Bytes, bool, usize) -> Result<Bytes, DecompressionError> + 's> {
280        let Self { per_message_compression } = self;
281        per_message_compression.as_mut().map(PerMessageCompressionContext::decompressor)
282    }
283}
284
285#[cfg(feature = "handshake")]
286#[cfg(test)]
287mod test {
288    use super::*;
289
290    #[test]
291    fn accept_offers_ignores_unknown_extensions() {
292        let (Extensions { per_message_compression }, response) = ExtensionsConfig::default()
293            .accept_offers(&SecWebsocketExtensions::new([
294                "unknown-1".parse().unwrap(),
295                "other-unknown; a=5; b=3".parse().unwrap(),
296            ]))
297            .unwrap();
298
299        assert!(matches!(per_message_compression, None));
300        assert_eq!(response, None);
301    }
302
303    #[test]
304    fn accept_offers_with_deflate_disabled() {
305        let extensions = ExtensionsConfig::default();
306
307        // With or without #[cfg(feature = "deflate")], the extension should be ignored.
308        let (Extensions { per_message_compression }, response) =
309            extensions.accept_offers(&SecWebsocketExtensions::new([])).unwrap();
310
311        assert!(matches!(per_message_compression, None));
312        assert_eq!(response, None);
313    }
314
315    #[cfg(feature = "deflate")]
316    #[test]
317    fn accept_offers_with_deflate_enabled() {
318        let extensions = ExtensionsConfig { permessage_deflate: Some(Default::default()) };
319
320        {
321            // If the client doesn't offer permessage-deflate support, the response
322            // shouldn't include it.
323            let (Extensions { per_message_compression }, response) =
324                extensions.accept_offers(&SecWebsocketExtensions::new([])).unwrap();
325            assert!(matches!(per_message_compression, None));
326            assert_eq!(response, None);
327        }
328
329        {
330            // If the client does offer support, the response should include it.
331            let (Extensions { per_message_compression }, response) = extensions
332                .accept_offers(&SecWebsocketExtensions::new([
333                    WebsocketProtocolExtension::new(compression::deflate::EXTENSION_NAME, []),
334                    WebsocketProtocolExtension::new("some-other-extension", []),
335                ]))
336                .unwrap();
337
338            assert!(matches!(per_message_compression, Some(_)));
339            assert_eq!(
340                response,
341                Some(SecWebsocketExtensions::new([WebsocketProtocolExtension::new(
342                    compression::deflate::EXTENSION_NAME,
343                    []
344                )]))
345            );
346        }
347    }
348
349    #[cfg(feature = "deflate")]
350    #[test]
351    fn accept_offers_picks_first_acceptable_offer() {
352        use compression::deflate::*;
353        let extensions = ExtensionsConfig {
354            permessage_deflate: Some(
355                DeflateConfig::new().set_max_window_bits(Role::Client, 11).unwrap(),
356            ),
357        };
358
359        let (Extensions { per_message_compression }, response) = extensions
360            .accept_offers(&SecWebsocketExtensions::new([
361                // These two offers are declined because they doesn't indicate
362                // support for client_max_window_bits, which the server is
363                // configured to require.
364                "permessage-deflate".parse().unwrap(),
365                "permessage-deflate; server_max_window_bits=12".parse().unwrap(),
366                // This offer would be acceptable but it has a parameter that the server doesn't recognize.
367                "permessage-deflate; client_max_window_bits=11; parameter-from-the-future=3"
368                    .parse()
369                    .unwrap(),
370                // This offer is accepted.
371                "permessage-deflate; client_no_context_takeover; client_max_window_bits=11"
372                    .parse()
373                    .unwrap(),
374                // This offer is ignored since an earlier one was accepted.
375                "permessage-deflate; client_max_window_bits=10".parse().unwrap(),
376            ]))
377            .unwrap();
378
379        assert!(matches!(per_message_compression, Some(PerMessageCompressionContext::Deflate(_))));
380        assert_eq!(
381            response,
382            Some(SecWebsocketExtensions::new([DeflateConfig::new()
383                .set_no_context_takeover(Role::Client, true)
384                .set_max_window_bits(Role::Client, 11)
385                .unwrap()
386                .as_offer()
387                .into()]))
388        )
389    }
390
391    #[cfg(feature = "deflate")]
392    #[test]
393    fn verify_agreed_on_deflate_then_garbage() {
394        let extensions = ExtensionsConfig { permessage_deflate: Some(Default::default()) };
395
396        let result = extensions.verify_agreed_on(SecWebsocketExtensions::new([
397            WebsocketProtocolExtension::new(compression::deflate::EXTENSION_NAME, []),
398            WebsocketProtocolExtension::new("unrecognized", []),
399        ]));
400
401        assert_eq!(result.unwrap_err(), ExtensionsError::InvalidExtension("unrecognized".into()));
402    }
403
404    #[cfg(feature = "deflate")]
405    #[test]
406    fn verify_agreed_on_deflate_multiple_times() {
407        let extensions = ExtensionsConfig { permessage_deflate: Some(Default::default()) };
408
409        let result = extensions.verify_agreed_on(SecWebsocketExtensions::new([
410            WebsocketProtocolExtension::new(compression::deflate::EXTENSION_NAME, []),
411            WebsocketProtocolExtension::new(
412                compression::deflate::EXTENSION_NAME,
413                ["client_no_context_takeover".parse().unwrap()],
414            ),
415        ]));
416
417        assert_eq!(
418            result.unwrap_err(),
419            ExtensionsError::ExtensionConflict(compression::deflate::EXTENSION_NAME.into())
420        );
421    }
422}