diff --git a/Cargo.lock b/Cargo.lock index aa5e84d507..7f86e52ec7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1336,12 +1336,12 @@ dependencies = [ "deno_net", "deno_websocket", "flate2", - "fly-accept-encoding", "http", "http-body-util", "httparse", "hyper 0.14.27", "hyper 1.0.0-rc.4", + "itertools", "memmem", "mime", "once_cell", @@ -2425,17 +2425,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "fly-accept-encoding" -version = "0.2.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3afa7516fdcfd8e5e93a938f8fec857785ced190a1f62d842d1fe1ffbe22ba8" -dependencies = [ - "http", - "itertools", - "thiserror", -] - [[package]] name = "fnv" version = "1.0.7" diff --git a/ext/http/Cargo.toml b/ext/http/Cargo.toml index 8426b9e141..fe4ed0c975 100644 --- a/ext/http/Cargo.toml +++ b/ext/http/Cargo.toml @@ -31,11 +31,11 @@ deno_core.workspace = true deno_net.workspace = true deno_websocket.workspace = true flate2.workspace = true -fly-accept-encoding = "0.2.0" http.workspace = true httparse.workspace = true hyper = { workspace = true, features = ["server", "stream", "http1", "http2", "runtime"] } hyper1 = { package = "hyper", features = ["full"], version = "=1.0.0-rc.4" } +itertools = "0.10" memmem.workspace = true mime = "0.3.16" once_cell.workspace = true diff --git a/ext/http/fly_accept_encoding.rs b/ext/http/fly_accept_encoding.rs new file mode 100644 index 0000000000..f3de9bee41 --- /dev/null +++ b/ext/http/fly_accept_encoding.rs @@ -0,0 +1,214 @@ +// Copyright 2018 Yoshua Wuyts. All rights reserved. MIT license. +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +// Forked from https://github.com/superfly/accept-encoding/blob/1cded757ec7ff3916e5bfe7441db76cdc48170dc/ +// Forked to support both http 0.3 and http 1.0 crates. + +use http::header::HeaderMap; +use http::header::ACCEPT_ENCODING; +use itertools::Itertools; + +/// A list enumerating the categories of errors in this crate. +/// +/// This list is intended to grow over time and it is not recommended to +/// exhaustively match against it. +/// +/// It is used with the [`Error`] struct. +/// +/// [`Error`]: std.struct.Error.html +#[derive(Debug, thiserror::Error)] +pub enum EncodingError { + /// Invalid header encoding. + #[error("Invalid header encoding.")] + InvalidEncoding, + /// The encoding scheme is unknown. + #[error("Unknown encoding scheme.")] + UnknownEncoding, +} + +/// Encodings to use. +#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash)] +pub enum Encoding { + /// The Gzip encoding. + Gzip, + /// The Deflate encoding. + Deflate, + /// The Brotli encoding. + Brotli, + /// The Zstd encoding. + Zstd, + /// No encoding. + Identity, +} + +impl Encoding { + /// Parses a given string into its corresponding encoding. + fn parse(s: &str) -> Result, EncodingError> { + match s { + "gzip" => Ok(Some(Encoding::Gzip)), + "deflate" => Ok(Some(Encoding::Deflate)), + "br" => Ok(Some(Encoding::Brotli)), + "zstd" => Ok(Some(Encoding::Zstd)), + "identity" => Ok(Some(Encoding::Identity)), + "*" => Ok(None), + _ => Err(EncodingError::UnknownEncoding), + } + } +} + +/// Select the encoding with the largest qval or the first with qval ~= 1 +pub fn preferred( + encodings: impl Iterator, f32), EncodingError>>, +) -> Result, EncodingError> { + let mut preferred_encoding = None; + let mut max_qval = 0.0; + + for r in encodings { + let (encoding, qval) = r?; + if (qval - 1.0f32).abs() < 0.01 { + return Ok(encoding); + } else if qval > max_qval { + preferred_encoding = encoding; + max_qval = qval; + } + } + + Ok(preferred_encoding) +} + +/// Parse a set of HTTP headers into an iterator containing tuples of options containing encodings and their corresponding q-values. +pub fn encodings_iter( + headers: &HeaderMap, +) -> impl Iterator, f32), EncodingError>> + '_ { + headers + .get_all(ACCEPT_ENCODING) + .iter() + .map(|hval| hval.to_str().map_err(|_| EncodingError::InvalidEncoding)) + .map_ok(|s| s.split(',').map(str::trim)) + .flatten_ok() + .filter_map_ok(|v| { + let (e, q) = match v.split_once(";q=") { + Some((e, q)) => (e, q), + None => return Some(Ok((Encoding::parse(v).ok()?, 1.0f32))), + }; + let encoding = Encoding::parse(e).ok()?; // ignore unknown encodings + let qval = match q.parse() { + Ok(f) if f > 1.0 => return Some(Err(EncodingError::InvalidEncoding)), // q-values over 1 are unacceptable, + Ok(f) => f, + Err(_) => return Some(Err(EncodingError::InvalidEncoding)), + }; + Some(Ok((encoding, qval))) + }) + .map(|r| r?) // flatten Result Result, f32)>, EncodingError> { + encodings_iter(headers).collect() + } + + fn parse(headers: &HeaderMap) -> Result, EncodingError> { + preferred(encodings_iter(headers)) + } + + #[test] + fn single_encoding() { + let mut headers = HeaderMap::new(); + headers.insert(ACCEPT_ENCODING, HeaderValue::from_str("gzip").unwrap()); + + let encoding = parse(&headers).unwrap().unwrap(); + assert_eq!(encoding, Encoding::Gzip); + } + + #[test] + fn multiple_encodings() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("gzip, deflate, br").unwrap(), + ); + + let encoding = parse(&headers).unwrap().unwrap(); + assert_eq!(encoding, Encoding::Gzip); + } + + #[test] + fn single_encoding_with_qval() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("deflate;q=1.0").unwrap(), + ); + + let encoding = parse(&headers).unwrap().unwrap(); + assert_eq!(encoding, Encoding::Deflate); + } + + #[test] + fn multiple_encodings_with_qval_1() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("deflate, gzip;q=1.0, *;q=0.5").unwrap(), + ); + + let encoding = parse(&headers).unwrap().unwrap(); + assert_eq!(encoding, Encoding::Deflate); + } + + #[test] + fn multiple_encodings_with_qval_2() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("gzip;q=0.5, deflate;q=1.0, *;q=0.5").unwrap(), + ); + + let encoding = parse(&headers).unwrap().unwrap(); + assert_eq!(encoding, Encoding::Deflate); + } + + #[test] + fn multiple_encodings_with_qval_3() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("gzip;q=0.5, deflate;q=0.75, *;q=1.0").unwrap(), + ); + + let encoding = parse(&headers).unwrap(); + assert!(encoding.is_none()); + } + + #[test] + fn list_encodings() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("zstd;q=1.0, deflate;q=0.8, br;q=0.9").unwrap(), + ); + + let encodings = encodings(&headers).unwrap(); + assert_eq!(encodings[0], (Some(Encoding::Zstd), 1.0)); + assert_eq!(encodings[1], (Some(Encoding::Deflate), 0.8)); + assert_eq!(encodings[2], (Some(Encoding::Brotli), 0.9)); + } + + #[test] + fn list_encodings_ignore_unknown() { + let mut headers = HeaderMap::new(); + headers.insert( + ACCEPT_ENCODING, + HeaderValue::from_str("zstd;q=1.0, unknown;q=0.8, br;q=0.9").unwrap(), + ); + + let encodings = encodings(&headers).unwrap(); + assert_eq!(encodings[0], (Some(Encoding::Zstd), 1.0)); + assert_eq!(encodings[1], (Some(Encoding::Brotli), 0.9)); + } +} diff --git a/ext/http/http_next.rs b/ext/http/http_next.rs index 217ae1c39f..c1cb2df660 100644 --- a/ext/http/http_next.rs +++ b/ext/http/http_next.rs @@ -44,7 +44,6 @@ use deno_core::ResourceId; use deno_net::ops_tls::TlsStream; use deno_net::raw::NetworkStream; use deno_websocket::ws_create_server_stream; -use fly_accept_encoding::Encoding; use http::header::ACCEPT_ENCODING; use http::header::CACHE_CONTROL; use http::header::CONTENT_ENCODING; @@ -72,6 +71,9 @@ use std::pin::Pin; use std::ptr::null; use std::rc::Rc; +use super::fly_accept_encoding; +use fly_accept_encoding::Encoding; + use tokio::io::AsyncReadExt; use tokio::io::AsyncWriteExt; diff --git a/ext/http/lib.rs b/ext/http/lib.rs index af0f0f0c94..f4fb55062b 100644 --- a/ext/http/lib.rs +++ b/ext/http/lib.rs @@ -41,7 +41,6 @@ use deno_net::raw::NetworkStream; use deno_websocket::ws_create_server_stream; use flate2::write::GzEncoder; use flate2::Compression; -use fly_accept_encoding::Encoding; use hyper::body::Bytes; use hyper::body::HttpBody; use hyper::body::SizeHint; @@ -79,6 +78,7 @@ use crate::reader_stream::ExternallyAbortableReaderStream; use crate::reader_stream::ShutdownHandle; pub mod compressible; +mod fly_accept_encoding; mod http_next; mod hyper_util_tokioio; mod network_buffered_stream; @@ -89,6 +89,7 @@ mod response_body; mod service; mod websocket_upgrade; +use fly_accept_encoding::Encoding; pub use request_properties::DefaultHttpPropertyExtractor; pub use request_properties::HttpConnectionProperties; pub use request_properties::HttpListenProperties;