parent
73ef209ac6
commit
40eb9235d9
@ -1,81 +1,131 @@ |
|||||||
//! WebSocket extensions.
|
//! WebSocket extensions.
|
||||||
// Only `permessage-deflate` is supported at the moment.
|
// Only `permessage-deflate` is supported at the moment.
|
||||||
|
|
||||||
use std::borrow::Cow; |
|
||||||
|
|
||||||
mod compression; |
mod compression; |
||||||
pub use compression::deflate::{DeflateConfig, DeflateContext, DeflateError}; |
pub use compression::deflate::{DeflateConfig, DeflateContext, DeflateError}; |
||||||
|
use http::HeaderValue; |
||||||
|
|
||||||
/// Extension parameter.
|
/// Iterator of all extension offers/responses in `Sec-WebSocket-Extensions` values.
|
||||||
#[derive(Clone, Debug, Eq, PartialEq)] |
pub(crate) fn iter_all<'a>( |
||||||
pub(crate) struct Param<'a> { |
values: impl Iterator<Item = &'a HeaderValue>, |
||||||
name: Cow<'a, str>, |
) -> impl Iterator<Item = (&'a str, impl Iterator<Item = (&'a str, Option<&'a str>)>)> { |
||||||
value: Option<Cow<'a, str>>, |
values |
||||||
|
.filter_map(|h| h.to_str().ok()) |
||||||
|
.map(|value_str| { |
||||||
|
split_iter(value_str, ',').filter_map(|offer| { |
||||||
|
// Parameters are separted by semicolons.
|
||||||
|
// The first element is the name of the extension.
|
||||||
|
let mut iter = split_iter(offer.trim(), ';').map(str::trim); |
||||||
|
let name = iter.next()?; |
||||||
|
let params = iter.filter_map(|kv| { |
||||||
|
let mut it = kv.splitn(2, '='); |
||||||
|
let key = it.next()?.trim(); |
||||||
|
let val = it.next().map(|v| v.trim().trim_matches('"')); |
||||||
|
Some((key, val)) |
||||||
|
}); |
||||||
|
Some((name, params)) |
||||||
|
}) |
||||||
|
}) |
||||||
|
.flatten() |
||||||
} |
} |
||||||
|
|
||||||
impl<'a> Param<'a> { |
fn split_iter(input: &str, sep: char) -> impl Iterator<Item = &str> { |
||||||
/// Create a new parameter with name.
|
let mut in_quotes = false; |
||||||
pub fn new(name: impl Into<Cow<'a, str>>) -> Self { |
let mut prev = None; |
||||||
Param { name: name.into(), value: None } |
input.split(move |c| { |
||||||
} |
if in_quotes { |
||||||
|
if c == '"' && prev != Some('\\') { |
||||||
|
in_quotes = false; |
||||||
|
} |
||||||
|
prev = Some(c); |
||||||
|
false |
||||||
|
} else if c == sep { |
||||||
|
prev = Some(c); |
||||||
|
true |
||||||
|
} else { |
||||||
|
if c == '"' { |
||||||
|
in_quotes = true; |
||||||
|
} |
||||||
|
prev = Some(c); |
||||||
|
false |
||||||
|
} |
||||||
|
}) |
||||||
|
} |
||||||
|
|
||||||
/// Consume itself to create a parameter with value.
|
#[cfg(test)] |
||||||
pub fn with_value(mut self, value: impl Into<Cow<'a, str>>) -> Self { |
mod tests { |
||||||
self.value = Some(value.into()); |
use http::{header::SEC_WEBSOCKET_EXTENSIONS, HeaderMap}; |
||||||
self |
|
||||||
} |
|
||||||
|
|
||||||
/// Get the name of the parameter.
|
use super::*; |
||||||
pub fn name(&self) -> &str { |
|
||||||
&self.name |
// Make sure comma separated offers and multiple headers are equivalent
|
||||||
} |
fn test_iteration<'a>( |
||||||
|
mut iter: impl Iterator<Item = (&'a str, impl Iterator<Item = (&'a str, Option<&'a str>)>)>, |
||||||
|
) { |
||||||
|
let (name, mut params) = iter.next().unwrap(); |
||||||
|
assert_eq!(name, "permessage-deflate"); |
||||||
|
assert_eq!(params.next(), Some(("client_max_window_bits", None))); |
||||||
|
assert_eq!(params.next(), Some(("server_max_window_bits", Some("10")))); |
||||||
|
assert!(params.next().is_none()); |
||||||
|
|
||||||
/// Get the optional value of the parameter.
|
let (name, mut params) = iter.next().unwrap(); |
||||||
pub fn value(&self) -> Option<&str> { |
assert_eq!(name, "permessage-deflate"); |
||||||
self.value.as_ref().map(|v| v.as_ref()) |
assert_eq!(params.next(), Some(("client_max_window_bits", None))); |
||||||
|
assert!(params.next().is_none()); |
||||||
|
|
||||||
|
assert!(iter.next().is_none()); |
||||||
} |
} |
||||||
} |
|
||||||
|
|
||||||
// NOTE This doesn't support quoted values
|
#[test] |
||||||
/// Parse `Sec-WebSocket-Extensions` offer/response.
|
fn iter_single() { |
||||||
pub(crate) fn parse_header(exts: &str) -> Vec<(Cow<'_, str>, Vec<Param<'_>>)> { |
let mut hm = HeaderMap::new(); |
||||||
let mut collected = Vec::new(); |
hm.append( |
||||||
// ext-name; a; b=c, ext-name; x, y=z
|
SEC_WEBSOCKET_EXTENSIONS, |
||||||
for ext in exts.split(',') { |
HeaderValue::from_static( |
||||||
let mut parts = ext.split(';'); |
"permessage-deflate; client_max_window_bits; server_max_window_bits=10, permessage-deflate; client_max_window_bits", |
||||||
if let Some(name) = parts.next().map(str::trim) { |
), |
||||||
let mut params = Vec::new(); |
); |
||||||
for p in parts { |
test_iteration(iter_all(std::iter::once(hm.get(SEC_WEBSOCKET_EXTENSIONS).unwrap()))); |
||||||
let mut kv = p.splitn(2, '='); |
|
||||||
if let Some(key) = kv.next().map(str::trim) { |
|
||||||
let param = if let Some(value) = kv.next().map(str::trim) { |
|
||||||
Param::new(key).with_value(value) |
|
||||||
} else { |
|
||||||
Param::new(key) |
|
||||||
}; |
|
||||||
params.push(param); |
|
||||||
} |
|
||||||
} |
|
||||||
collected.push((Cow::from(name), params)); |
|
||||||
} |
|
||||||
} |
} |
||||||
collected |
|
||||||
} |
|
||||||
|
|
||||||
#[test] |
#[test] |
||||||
fn test_parse_extensions() { |
fn iter_multiple() { |
||||||
let extensions = "permessage-deflate; client_max_window_bits; server_max_window_bits=10, permessage-deflate; client_max_window_bits"; |
let mut hm = HeaderMap::new(); |
||||||
assert_eq!( |
hm.append( |
||||||
parse_header(extensions), |
SEC_WEBSOCKET_EXTENSIONS, |
||||||
vec![ |
HeaderValue::from_static( |
||||||
( |
"permessage-deflate; client_max_window_bits; server_max_window_bits=10", |
||||||
Cow::from("permessage-deflate"), |
|
||||||
vec![ |
|
||||||
Param::new("client_max_window_bits"), |
|
||||||
Param::new("server_max_window_bits").with_value("10") |
|
||||||
] |
|
||||||
), |
), |
||||||
(Cow::from("permessage-deflate"), vec![Param::new("client_max_window_bits")]) |
); |
||||||
] |
hm.append( |
||||||
); |
SEC_WEBSOCKET_EXTENSIONS, |
||||||
|
HeaderValue::from_static("permessage-deflate; client_max_window_bits"), |
||||||
|
); |
||||||
|
test_iteration(iter_all(hm.get_all(SEC_WEBSOCKET_EXTENSIONS).iter())); |
||||||
|
} |
||||||
} |
} |
||||||
|
|
||||||
|
// TODO More strict parsing
|
||||||
|
// https://datatracker.ietf.org/doc/html/rfc6455#section-4.3
|
||||||
|
// Sec-WebSocket-Extensions = extension-list
|
||||||
|
// extension-list = 1#extension
|
||||||
|
// extension = extension-token *( ";" extension-param )
|
||||||
|
// extension-token = registered-token
|
||||||
|
// registered-token = token
|
||||||
|
// extension-param = token [ "=" (token | quoted-string) ]
|
||||||
|
// ;When using the quoted-string syntax variant, the value
|
||||||
|
// ;after quoted-string unescaping MUST conform to the
|
||||||
|
// ;'token' ABNF.
|
||||||
|
//
|
||||||
|
// token = 1*<any CHAR except CTLs or separators>
|
||||||
|
// CHAR = <any US-ASCII character (octets 0 - 127)>
|
||||||
|
// CTL = <any US-ASCII control character (octets 0 - 31) and DEL (127)>
|
||||||
|
// separators = "(" | ")" | "<" | ">" | "@"
|
||||||
|
// | "," | ";" | ":" | "\" | <">
|
||||||
|
// | "/" | "[" | "]" | "?" | "="
|
||||||
|
// | "{" | "}" | SP | HT
|
||||||
|
// SP = <US-ASCII SP, space (32)>
|
||||||
|
// HT = <US-ASCII HT, horizontal-tab (9)>
|
||||||
|
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
|
||||||
|
// qdtext = <any TEXT except <">>
|
||||||
|
// quoted-pair = "\" CHAR
|
||||||
|
Loading…
Reference in new issue