mirror of
https://github.com/fafhrd91/actix-web
synced 2025-02-17 10:13:30 +01:00
Fix quality parse error in Accept-Encoding HTTP header (#2344)
This commit is contained in:
parent
373b3f91df
commit
ddc8c16cb3
@ -4,10 +4,15 @@
|
|||||||
### Added
|
### Added
|
||||||
* Re-export actix-service `ServiceFactory` in `dev` module. [#2325]
|
* Re-export actix-service `ServiceFactory` in `dev` module. [#2325]
|
||||||
|
|
||||||
### Changes
|
### Changed
|
||||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
* Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344]
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
* Fix quality parse error in Accept-Encoding header. [#2344]
|
||||||
|
|
||||||
[#2325]: https://github.com/actix/actix-web/pull/2325
|
[#2325]: https://github.com/actix/actix-web/pull/2325
|
||||||
|
[#2344]: https://github.com/actix/actix-web/pull/2344
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.8 - 2021-06-26
|
## 4.0.0-beta.8 - 2021-06-26
|
||||||
|
@ -1,22 +1,23 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
### Changes
|
### Changed
|
||||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
|
||||||
* Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
* Remove `Into<Error>` bound on `Encoder` body types. [#2375]
|
||||||
|
* Fix quality parse error in Accept-Encoding header. [#2344]
|
||||||
|
|
||||||
[#2364]: https://github.com/actix/actix-web/pull/2364
|
[#2364]: https://github.com/actix/actix-web/pull/2364
|
||||||
[#2375]: https://github.com/actix/actix-web/pull/2375
|
[#2375]: https://github.com/actix/actix-web/pull/2375
|
||||||
|
[#2344]: https://github.com/actix/actix-web/pull/2344
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.8 - 2021-08-09
|
## 3.0.0-beta.8 - 2021-08-09
|
||||||
### Fixed
|
### Fixed
|
||||||
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.8 - 2021-06-26
|
## 3.0.0-beta.8 - 2021-06-26
|
||||||
### Changed
|
### Changed
|
||||||
* Change compression algorithm features flags. [#2250]
|
* Change compression algorithm features flags. [#2250]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
//! Stream decoders.
|
//! Stream decoders.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
convert::TryFrom,
|
||||||
future::Future,
|
future::Future,
|
||||||
io::{self, Write as _},
|
io::{self, Write as _},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
@ -80,7 +81,7 @@ where
|
|||||||
let encoding = headers
|
let encoding = headers
|
||||||
.get(&CONTENT_ENCODING)
|
.get(&CONTENT_ENCODING)
|
||||||
.and_then(|val| val.to_str().ok())
|
.and_then(|val| val.to_str().ok())
|
||||||
.map(ContentEncoding::from)
|
.and_then(|x| ContentEncoding::try_from(x).ok())
|
||||||
.unwrap_or(ContentEncoding::Identity);
|
.unwrap_or(ContentEncoding::Identity);
|
||||||
|
|
||||||
Self::new(stream, encoding)
|
Self::new(stream, encoding)
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
use std::{convert::Infallible, str::FromStr};
|
use std::{convert::TryFrom, error, fmt, str::FromStr};
|
||||||
|
|
||||||
use http::header::InvalidHeaderValue;
|
use http::header::InvalidHeaderValue;
|
||||||
|
|
||||||
@ -8,6 +8,20 @@ use crate::{
|
|||||||
HttpMessage,
|
HttpMessage,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// Error return when a content encoding is unknown.
|
||||||
|
///
|
||||||
|
/// Example: 'compress'
|
||||||
|
#[derive(Debug)]
|
||||||
|
pub struct ContentEncodingParseError;
|
||||||
|
|
||||||
|
impl fmt::Display for ContentEncodingParseError {
|
||||||
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
|
write!(f, "Unsupported content encoding")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl error::Error for ContentEncodingParseError {}
|
||||||
|
|
||||||
/// Represents a supported content encoding.
|
/// Represents a supported content encoding.
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
pub enum ContentEncoding {
|
pub enum ContentEncoding {
|
||||||
@ -37,7 +51,7 @@ impl ContentEncoding {
|
|||||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert content encoding to string
|
/// Convert content encoding to string.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_str(self) -> &'static str {
|
pub fn as_str(self) -> &'static str {
|
||||||
match self {
|
match self {
|
||||||
@ -48,18 +62,6 @@ impl ContentEncoding {
|
|||||||
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Default Q-factor (quality) value.
|
|
||||||
#[inline]
|
|
||||||
pub fn quality(self) -> f64 {
|
|
||||||
match self {
|
|
||||||
ContentEncoding::Br => 1.1,
|
|
||||||
ContentEncoding::Gzip => 1.0,
|
|
||||||
ContentEncoding::Deflate => 0.9,
|
|
||||||
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
|
|
||||||
ContentEncoding::Zstd => 0.0,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ContentEncoding {
|
impl Default for ContentEncoding {
|
||||||
@ -69,31 +71,33 @@ impl Default for ContentEncoding {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl FromStr for ContentEncoding {
|
impl FromStr for ContentEncoding {
|
||||||
type Err = Infallible;
|
type Err = ContentEncodingParseError;
|
||||||
|
|
||||||
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(Self::from(val))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl From<&str> for ContentEncoding {
|
|
||||||
fn from(val: &str) -> ContentEncoding {
|
|
||||||
let val = val.trim();
|
let val = val.trim();
|
||||||
|
|
||||||
if val.eq_ignore_ascii_case("br") {
|
if val.eq_ignore_ascii_case("br") {
|
||||||
ContentEncoding::Br
|
Ok(ContentEncoding::Br)
|
||||||
} else if val.eq_ignore_ascii_case("gzip") {
|
} else if val.eq_ignore_ascii_case("gzip") {
|
||||||
ContentEncoding::Gzip
|
Ok(ContentEncoding::Gzip)
|
||||||
} else if val.eq_ignore_ascii_case("deflate") {
|
} else if val.eq_ignore_ascii_case("deflate") {
|
||||||
ContentEncoding::Deflate
|
Ok(ContentEncoding::Deflate)
|
||||||
} else if val.eq_ignore_ascii_case("zstd") {
|
} else if val.eq_ignore_ascii_case("zstd") {
|
||||||
ContentEncoding::Zstd
|
Ok(ContentEncoding::Zstd)
|
||||||
} else {
|
} else {
|
||||||
ContentEncoding::default()
|
Err(ContentEncodingParseError)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl TryFrom<&str> for ContentEncoding {
|
||||||
|
type Error = ContentEncodingParseError;
|
||||||
|
|
||||||
|
fn try_from(val: &str) -> Result<Self, Self::Error> {
|
||||||
|
val.parse()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl IntoHeaderValue for ContentEncoding {
|
impl IntoHeaderValue for ContentEncoding {
|
||||||
type Error = InvalidHeaderValue;
|
type Error = InvalidHeaderValue;
|
||||||
|
|
||||||
|
@ -1,11 +1,14 @@
|
|||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
convert::{TryFrom, TryInto},
|
convert::{TryFrom, TryInto},
|
||||||
fmt, str,
|
fmt,
|
||||||
|
str::{self, FromStr},
|
||||||
};
|
};
|
||||||
|
|
||||||
use derive_more::{Display, Error};
|
use derive_more::{Display, Error};
|
||||||
|
|
||||||
|
use crate::error::ParseError;
|
||||||
|
|
||||||
const MAX_QUALITY: u16 = 1000;
|
const MAX_QUALITY: u16 = 1000;
|
||||||
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
||||||
|
|
||||||
@ -113,12 +116,12 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
impl<T: FromStr> FromStr for QualityItem<T> {
|
||||||
type Err = crate::error::ParseError;
|
type Err = ParseError;
|
||||||
|
|
||||||
fn from_str(qitem_str: &str) -> Result<QualityItem<T>, crate::error::ParseError> {
|
fn from_str(qitem_str: &str) -> Result<Self, Self::Err> {
|
||||||
if !qitem_str.is_ascii() {
|
if !qitem_str.is_ascii() {
|
||||||
return Err(crate::error::ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set defaults used if parsing fails.
|
// Set defaults used if parsing fails.
|
||||||
@ -139,7 +142,7 @@ impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
|||||||
if parts[0].len() < 2 {
|
if parts[0].len() < 2 {
|
||||||
// Can't possibly be an attribute since an attribute needs at least a name followed
|
// Can't possibly be an attribute since an attribute needs at least a name followed
|
||||||
// by an equals sign. And bare identifiers are forbidden.
|
// by an equals sign. And bare identifiers are forbidden.
|
||||||
return Err(crate::error::ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
let start = &parts[0][0..2];
|
let start = &parts[0][0..2];
|
||||||
@ -148,25 +151,21 @@ impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
|||||||
let q_val = &parts[0][2..];
|
let q_val = &parts[0][2..];
|
||||||
if q_val.len() > 5 {
|
if q_val.len() > 5 {
|
||||||
// longer than 5 indicates an over-precise q-factor
|
// longer than 5 indicates an over-precise q-factor
|
||||||
return Err(crate::error::ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
let q_value = q_val
|
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
|
||||||
.parse::<f32>()
|
|
||||||
.map_err(|_| crate::error::ParseError::Header)?;
|
|
||||||
|
|
||||||
if (0f32..=1f32).contains(&q_value) {
|
if (0f32..=1f32).contains(&q_value) {
|
||||||
quality = q_value;
|
quality = q_value;
|
||||||
raw_item = parts[1];
|
raw_item = parts[1];
|
||||||
} else {
|
} else {
|
||||||
return Err(crate::error::ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let item = raw_item
|
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
|
||||||
.parse::<T>()
|
|
||||||
.map_err(|_| crate::error::ParseError::Header)?;
|
|
||||||
|
|
||||||
// we already checked above that the quality is within range
|
// we already checked above that the quality is within range
|
||||||
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
||||||
@ -224,7 +223,7 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl str::FromStr for Encoding {
|
impl FromStr for Encoding {
|
||||||
type Err = crate::error::ParseError;
|
type Err = crate::error::ParseError;
|
||||||
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
||||||
use Encoding::*;
|
use Encoding::*;
|
||||||
|
@ -2,10 +2,10 @@
|
|||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cmp,
|
cmp,
|
||||||
|
convert::TryFrom,
|
||||||
future::Future,
|
future::Future,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
str::FromStr,
|
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -13,16 +13,18 @@ use actix_http::{
|
|||||||
body::{MessageBody, ResponseBody},
|
body::{MessageBody, ResponseBody},
|
||||||
encoding::Encoder,
|
encoding::Encoder,
|
||||||
http::header::{ContentEncoding, ACCEPT_ENCODING},
|
http::header::{ContentEncoding, ACCEPT_ENCODING},
|
||||||
|
StatusCode,
|
||||||
};
|
};
|
||||||
use actix_service::{Service, Transform};
|
use actix_service::{Service, Transform};
|
||||||
use actix_utils::future::{ok, Ready};
|
use actix_utils::future::{ok, Either, Ready};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
|
use once_cell::sync::Lazy;
|
||||||
use pin_project::pin_project;
|
use pin_project::pin_project;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
dev::BodyEncoding,
|
dev::BodyEncoding,
|
||||||
service::{ServiceRequest, ServiceResponse},
|
service::{ServiceRequest, ServiceResponse},
|
||||||
Error,
|
Error, HttpResponse,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Middleware for compressing response payloads.
|
/// Middleware for compressing response payloads.
|
||||||
@ -78,34 +80,78 @@ pub struct CompressMiddleware<S> {
|
|||||||
encoding: ContentEncoding,
|
encoding: ContentEncoding,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
|
||||||
|
let mut encoding = vec![];
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-brotli")]
|
||||||
|
{
|
||||||
|
encoding.push("br");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-gzip")]
|
||||||
|
{
|
||||||
|
encoding.push("gzip");
|
||||||
|
encoding.push("deflate");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(feature = "compress-zstd")]
|
||||||
|
encoding.push("zstd");
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
!encoding.is_empty(),
|
||||||
|
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
|
||||||
|
);
|
||||||
|
|
||||||
|
encoding.join(", ")
|
||||||
|
});
|
||||||
|
|
||||||
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
|
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
|
||||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||||
|
B: MessageBody,
|
||||||
{
|
{
|
||||||
type Response = ServiceResponse<ResponseBody<Encoder<B>>>;
|
type Response = ServiceResponse<ResponseBody<Encoder<B>>>;
|
||||||
type Error = Error;
|
type Error = Error;
|
||||||
type Future = CompressResponse<S, B>;
|
type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>;
|
||||||
|
|
||||||
actix_service::forward_ready!(service);
|
actix_service::forward_ready!(service);
|
||||||
|
|
||||||
#[allow(clippy::borrow_interior_mutable_const)]
|
#[allow(clippy::borrow_interior_mutable_const)]
|
||||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||||
// negotiate content-encoding
|
// negotiate content-encoding
|
||||||
let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) {
|
let encoding_result = req
|
||||||
if let Ok(enc) = val.to_str() {
|
.headers()
|
||||||
AcceptEncoding::parse(enc, self.encoding)
|
.get(&ACCEPT_ENCODING)
|
||||||
} else {
|
.and_then(|val| val.to_str().ok())
|
||||||
ContentEncoding::Identity
|
.map(|enc| AcceptEncoding::try_parse(enc, self.encoding));
|
||||||
}
|
|
||||||
} else {
|
|
||||||
ContentEncoding::Identity
|
|
||||||
};
|
|
||||||
|
|
||||||
CompressResponse {
|
match encoding_result {
|
||||||
encoding,
|
// Missing header => fallback to identity
|
||||||
fut: self.service.call(req),
|
None => Either::left(CompressResponse {
|
||||||
_phantom: PhantomData,
|
encoding: ContentEncoding::Identity,
|
||||||
|
fut: self.service.call(req),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// Valid encoding
|
||||||
|
Some(Ok(encoding)) => Either::left(CompressResponse {
|
||||||
|
encoding,
|
||||||
|
fut: self.service.call(req),
|
||||||
|
_phantom: PhantomData,
|
||||||
|
}),
|
||||||
|
|
||||||
|
// There is an HTTP header but we cannot match what client as asked for
|
||||||
|
Some(Err(_)) => {
|
||||||
|
let res = HttpResponse::with_body(
|
||||||
|
StatusCode::NOT_ACCEPTABLE,
|
||||||
|
SUPPORTED_ALGORITHM_NAMES.as_str(),
|
||||||
|
);
|
||||||
|
let enc = ContentEncoding::Identity;
|
||||||
|
|
||||||
|
Either::right(ok(req.into_response(res.map_body(move |head, body| {
|
||||||
|
Encoder::response(enc, head, ResponseBody::Other(body.into()))
|
||||||
|
}))))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -114,7 +160,6 @@ where
|
|||||||
pub struct CompressResponse<S, B>
|
pub struct CompressResponse<S, B>
|
||||||
where
|
where
|
||||||
S: Service<ServiceRequest>,
|
S: Service<ServiceRequest>,
|
||||||
B: MessageBody,
|
|
||||||
{
|
{
|
||||||
#[pin]
|
#[pin]
|
||||||
fut: S::Future,
|
fut: S::Future,
|
||||||
@ -151,6 +196,7 @@ where
|
|||||||
|
|
||||||
struct AcceptEncoding {
|
struct AcceptEncoding {
|
||||||
encoding: ContentEncoding,
|
encoding: ContentEncoding,
|
||||||
|
// TODO: use Quality or QualityItem<ContentEncoding>
|
||||||
quality: f64,
|
quality: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -177,26 +223,56 @@ impl PartialOrd for AcceptEncoding {
|
|||||||
|
|
||||||
impl PartialEq for AcceptEncoding {
|
impl PartialEq for AcceptEncoding {
|
||||||
fn eq(&self, other: &AcceptEncoding) -> bool {
|
fn eq(&self, other: &AcceptEncoding) -> bool {
|
||||||
self.quality == other.quality
|
self.encoding == other.encoding && self.quality == other.quality
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Parse q-factor from quality strings.
|
||||||
|
///
|
||||||
|
/// If parse fail, then fallback to default value which is 1.
|
||||||
|
/// More details available here: <https://developer.mozilla.org/en-US/docs/Glossary/Quality_values>
|
||||||
|
fn parse_quality(parts: &[&str]) -> f64 {
|
||||||
|
for part in parts {
|
||||||
|
if part.trim().starts_with("q=") {
|
||||||
|
return part[2..].parse().unwrap_or(1.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
1.0
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
enum AcceptEncodingError {
|
||||||
|
/// This error occurs when client only support compressed response and server do not have any
|
||||||
|
/// algorithm that match client accepted algorithms.
|
||||||
|
CompressionAlgorithmMismatch,
|
||||||
|
}
|
||||||
|
|
||||||
impl AcceptEncoding {
|
impl AcceptEncoding {
|
||||||
fn new(tag: &str) -> Option<AcceptEncoding> {
|
fn new(tag: &str) -> Option<AcceptEncoding> {
|
||||||
let parts: Vec<&str> = tag.split(';').collect();
|
let parts: Vec<&str> = tag.split(';').collect();
|
||||||
let encoding = match parts.len() {
|
let encoding = match parts.len() {
|
||||||
0 => return None,
|
0 => return None,
|
||||||
_ => ContentEncoding::from(parts[0]),
|
_ => match ContentEncoding::try_from(parts[0]) {
|
||||||
};
|
Err(_) => return None,
|
||||||
let quality = match parts.len() {
|
Ok(x) => x,
|
||||||
1 => encoding.quality(),
|
},
|
||||||
_ => f64::from_str(parts[1]).unwrap_or(0.0),
|
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let quality = parse_quality(&parts[1..]);
|
||||||
|
if quality <= 0.0 || quality > 1.0 {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
Some(AcceptEncoding { encoding, quality })
|
Some(AcceptEncoding { encoding, quality })
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Parse a raw Accept-Encoding header value into an ordered list.
|
/// Parse a raw Accept-Encoding header value into an ordered list then return the best match
|
||||||
pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding {
|
/// based on middleware configuration.
|
||||||
|
pub fn try_parse(
|
||||||
|
raw: &str,
|
||||||
|
encoding: ContentEncoding,
|
||||||
|
) -> Result<ContentEncoding, AcceptEncodingError> {
|
||||||
let mut encodings = raw
|
let mut encodings = raw
|
||||||
.replace(' ', "")
|
.replace(' ', "")
|
||||||
.split(',')
|
.split(',')
|
||||||
@ -206,13 +282,90 @@ impl AcceptEncoding {
|
|||||||
encodings.sort();
|
encodings.sort();
|
||||||
|
|
||||||
for enc in encodings {
|
for enc in encodings {
|
||||||
if encoding == ContentEncoding::Auto {
|
if encoding == ContentEncoding::Auto || encoding == enc.encoding {
|
||||||
return enc.encoding;
|
return Ok(enc.encoding);
|
||||||
} else if encoding == enc.encoding {
|
|
||||||
return encoding;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ContentEncoding::Identity
|
// Special case if user cannot accept uncompressed data.
|
||||||
|
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
||||||
|
// TODO: account for whitespace
|
||||||
|
if raw.contains("*;q=0") || raw.contains("identity;q=0") {
|
||||||
|
return Err(AcceptEncodingError::CompressionAlgorithmMismatch);
|
||||||
|
}
|
||||||
|
|
||||||
|
Ok(ContentEncoding::Identity)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
macro_rules! assert_parse_eq {
|
||||||
|
($raw:expr, $result:expr) => {
|
||||||
|
assert_eq!(
|
||||||
|
AcceptEncoding::try_parse($raw, ContentEncoding::Auto),
|
||||||
|
Ok($result)
|
||||||
|
);
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
macro_rules! assert_parse_fail {
|
||||||
|
($raw:expr) => {
|
||||||
|
assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err());
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_encoding() {
|
||||||
|
// Test simple case
|
||||||
|
assert_parse_eq!("br", ContentEncoding::Br);
|
||||||
|
assert_parse_eq!("gzip", ContentEncoding::Gzip);
|
||||||
|
assert_parse_eq!("deflate", ContentEncoding::Deflate);
|
||||||
|
assert_parse_eq!("zstd", ContentEncoding::Zstd);
|
||||||
|
|
||||||
|
// Test space, trim, missing values
|
||||||
|
assert_parse_eq!("br,,,,", ContentEncoding::Br);
|
||||||
|
assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip);
|
||||||
|
|
||||||
|
// Test float number parsing
|
||||||
|
assert_parse_eq!("br;q=1 ,", ContentEncoding::Br);
|
||||||
|
assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br);
|
||||||
|
|
||||||
|
// Test wildcard
|
||||||
|
assert_parse_eq!("*", ContentEncoding::Identity);
|
||||||
|
assert_parse_eq!("*;q=1.0", ContentEncoding::Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_encoding_qfactor_ordering() {
|
||||||
|
assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip);
|
||||||
|
assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd);
|
||||||
|
|
||||||
|
assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br);
|
||||||
|
assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_encoding_qfactor_invalid() {
|
||||||
|
// Out of range
|
||||||
|
assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity);
|
||||||
|
assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity);
|
||||||
|
|
||||||
|
// Disabled
|
||||||
|
assert_parse_eq!("gzip;q=0", ContentEncoding::Identity);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_compression_required() {
|
||||||
|
// Check we fallback to identity if there is an unsupported compression algorithm
|
||||||
|
assert_parse_eq!("compress", ContentEncoding::Identity);
|
||||||
|
|
||||||
|
// User do not want any compression
|
||||||
|
assert_parse_fail!("compress, identity;q=0");
|
||||||
|
assert_parse_fail!("compress, identity;q=0.0");
|
||||||
|
assert_parse_fail!("compress, *;q=0");
|
||||||
|
assert_parse_fail!("compress, *;q=0.0");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1077,3 +1077,22 @@ async fn test_data_drop() {
|
|||||||
|
|
||||||
assert_eq!(num.load(Ordering::SeqCst), 0);
|
assert_eq!(num.load(Ordering::SeqCst), 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_accept_encoding_no_match() {
|
||||||
|
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Compress::default())
|
||||||
|
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish())))
|
||||||
|
});
|
||||||
|
|
||||||
|
let response = srv
|
||||||
|
.get("/")
|
||||||
|
.append_header((ACCEPT_ENCODING, "compress, identity;q=0"))
|
||||||
|
.no_decompress()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
|
||||||
|
assert_eq!(response.status().as_u16(), 406);
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user