mirror of https://github.com/fafhrd91/actix-web
Merge branch 'master' into show-listing-with-index
This commit is contained in:
commit
d765e5b051
|
@ -5,17 +5,23 @@
|
||||||
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
|
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
|
||||||
|
* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
|
||||||
|
[#2162]: (https://github.com/actix/actix-web/pull/2162)
|
||||||
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
|
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
|
||||||
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
|
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
|
||||||
* Update `language-tags` to `0.3`.
|
* Update `language-tags` to `0.3`.
|
||||||
* `ServiceResponse::take_body`. [#2201]
|
* `ServiceResponse::take_body`. [#2201]
|
||||||
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
|
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
|
||||||
|
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||||
|
* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
|
* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
|
||||||
|
|
||||||
[#2200]: https://github.com/actix/actix-web/pull/2200
|
[#2200]: https://github.com/actix/actix-web/pull/2200
|
||||||
[#2201]: https://github.com/actix/actix-web/pull/2201
|
[#2201]: https://github.com/actix/actix-web/pull/2201
|
||||||
|
[#2246]: https://github.com/actix/actix-web/pull/2246
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.6 - 2021-04-17
|
## 4.0.0-beta.6 - 2021-04-17
|
||||||
|
|
|
@ -58,7 +58,7 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.4.0"
|
actix-codec = "0.4.0"
|
||||||
actix-macros = "0.2.0"
|
actix-macros = "0.2.1"
|
||||||
actix-router = "0.2.7"
|
actix-router = "0.2.7"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-server = "2.0.0-beta.3"
|
actix-server = "2.0.0-beta.3"
|
||||||
|
@ -101,6 +101,7 @@ brotli2 = "0.3.2"
|
||||||
criterion = "0.3"
|
criterion = "0.3"
|
||||||
env_logger = "0.8"
|
env_logger = "0.8"
|
||||||
flate2 = "1.0.13"
|
flate2 = "1.0.13"
|
||||||
|
zstd = "0.7"
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rcgen = "0.8"
|
rcgen = "0.8"
|
||||||
serde_derive = "1.0"
|
serde_derive = "1.0"
|
||||||
|
|
|
@ -235,6 +235,8 @@ impl NamedFile {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set content encoding for serving this file
|
/// Set content encoding for serving this file
|
||||||
|
///
|
||||||
|
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
|
||||||
self.encoding = Some(enc);
|
self.encoding = Some(enc);
|
||||||
|
|
|
@ -9,6 +9,7 @@
|
||||||
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
|
||||||
* `Response::into_body` that consumes response and returns body type. [#2201]
|
* `Response::into_body` that consumes response and returns body type. [#2201]
|
||||||
* `impl Default` for `Response`. [#2201]
|
* `impl Default` for `Response`. [#2201]
|
||||||
|
* Add zstd support for `ContentEncoding`. [#2244]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
* The `MessageBody` trait now has an associated `Error` type. [#2183]
|
* The `MessageBody` trait now has an associated `Error` type. [#2183]
|
||||||
|
@ -18,6 +19,7 @@
|
||||||
* Update `language-tags` to `0.3`.
|
* Update `language-tags` to `0.3`.
|
||||||
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
|
||||||
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
|
||||||
|
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
|
||||||
|
|
||||||
### Removed
|
### Removed
|
||||||
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
|
||||||
|
@ -35,6 +37,8 @@
|
||||||
[#2201]: https://github.com/actix/actix-web/pull/2201
|
[#2201]: https://github.com/actix/actix-web/pull/2201
|
||||||
[#2205]: https://github.com/actix/actix-web/pull/2205
|
[#2205]: https://github.com/actix/actix-web/pull/2205
|
||||||
[#2215]: https://github.com/actix/actix-web/pull/2215
|
[#2215]: https://github.com/actix/actix-web/pull/2215
|
||||||
|
[#2244]: https://github.com/actix/actix-web/pull/2244
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.6 - 2021-04-17
|
## 3.0.0-beta.6 - 2021-04-17
|
||||||
|
|
|
@ -32,7 +32,7 @@ openssl = ["actix-tls/openssl"]
|
||||||
rustls = ["actix-tls/rustls"]
|
rustls = ["actix-tls/rustls"]
|
||||||
|
|
||||||
# enable compression support
|
# enable compression support
|
||||||
compress = ["flate2", "brotli2"]
|
compress = ["flate2", "brotli2", "zstd"]
|
||||||
|
|
||||||
# trust-dns as client dns resolver
|
# trust-dns as client dns resolver
|
||||||
trust-dns = ["trust-dns-resolver"]
|
trust-dns = ["trust-dns-resolver"]
|
||||||
|
@ -76,6 +76,7 @@ tokio = { version = "1.2", features = ["sync"] }
|
||||||
# compression
|
# compression
|
||||||
brotli2 = { version="0.3.2", optional = true }
|
brotli2 = { version="0.3.2", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
|
zstd = { version = "0.7", optional = true }
|
||||||
|
|
||||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||||
|
|
||||||
|
@ -90,6 +91,7 @@ serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
tls-openssl = { version = "0.10", package = "openssl" }
|
tls-openssl = { version = "0.10", package = "openssl" }
|
||||||
tls-rustls = { version = "0.19", package = "rustls" }
|
tls-rustls = { version = "0.19", package = "rustls" }
|
||||||
|
webpki = { version = "0.21.0" }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
|
|
|
@ -12,6 +12,7 @@ use brotli2::write::BrotliDecoder;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
use zstd::stream::write::Decoder as ZstdDecoder;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
encoding::Writer,
|
encoding::Writer,
|
||||||
|
@ -45,6 +46,12 @@ where
|
||||||
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
|
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
|
||||||
GzDecoder::new(Writer::new()),
|
GzDecoder::new(Writer::new()),
|
||||||
))),
|
))),
|
||||||
|
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
|
||||||
|
ZstdDecoder::new(Writer::new()).expect(
|
||||||
|
"Failed to create zstd decoder. This is a bug. \
|
||||||
|
Please report it to the actix-web repository.",
|
||||||
|
),
|
||||||
|
))),
|
||||||
_ => None,
|
_ => None,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -144,6 +151,9 @@ enum ContentDecoder {
|
||||||
Deflate(Box<ZlibDecoder<Writer>>),
|
Deflate(Box<ZlibDecoder<Writer>>),
|
||||||
Gzip(Box<GzDecoder<Writer>>),
|
Gzip(Box<GzDecoder<Writer>>),
|
||||||
Br(Box<BrotliDecoder<Writer>>),
|
Br(Box<BrotliDecoder<Writer>>),
|
||||||
|
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
|
||||||
|
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
||||||
|
Zstd(Box<ZstdDecoder<'static, Writer>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentDecoder {
|
impl ContentDecoder {
|
||||||
|
@ -186,6 +196,18 @@ impl ContentDecoder {
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
|
||||||
|
Ok(_) => {
|
||||||
|
let b = decoder.get_mut().take();
|
||||||
|
if !b.is_empty() {
|
||||||
|
Ok(Some(b))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +254,20 @@ impl ContentDecoder {
|
||||||
}
|
}
|
||||||
Err(e) => Err(e),
|
Err(e) => Err(e),
|
||||||
},
|
},
|
||||||
|
|
||||||
|
ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
|
||||||
|
Ok(_) => {
|
||||||
|
decoder.flush()?;
|
||||||
|
|
||||||
|
let b = decoder.get_mut().take();
|
||||||
|
if !b.is_empty() {
|
||||||
|
Ok(Some(b))
|
||||||
|
} else {
|
||||||
|
Ok(None)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(e) => Err(e),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,6 +15,7 @@ use derive_more::Display;
|
||||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project::pin_project;
|
use pin_project::pin_project;
|
||||||
|
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
|
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
|
||||||
|
@ -237,6 +238,9 @@ enum ContentEncoder {
|
||||||
Deflate(ZlibEncoder<Writer>),
|
Deflate(ZlibEncoder<Writer>),
|
||||||
Gzip(GzEncoder<Writer>),
|
Gzip(GzEncoder<Writer>),
|
||||||
Br(BrotliEncoder<Writer>),
|
Br(BrotliEncoder<Writer>),
|
||||||
|
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
|
||||||
|
// argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
||||||
|
Zstd(ZstdEncoder<'static, Writer>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ContentEncoder {
|
impl ContentEncoder {
|
||||||
|
@ -253,6 +257,10 @@ impl ContentEncoder {
|
||||||
ContentEncoding::Br => {
|
ContentEncoding::Br => {
|
||||||
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
||||||
}
|
}
|
||||||
|
ContentEncoding::Zstd => {
|
||||||
|
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
|
||||||
|
Some(ContentEncoder::Zstd(encoder))
|
||||||
|
}
|
||||||
_ => None,
|
_ => None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -263,6 +271,7 @@ impl ContentEncoder {
|
||||||
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
||||||
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
||||||
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
||||||
|
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -280,6 +289,10 @@ impl ContentEncoder {
|
||||||
Ok(writer) => Ok(writer.buf.freeze()),
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
Err(err) => Err(err),
|
Err(err) => Err(err),
|
||||||
},
|
},
|
||||||
|
ContentEncoder::Zstd(encoder) => match encoder.finish() {
|
||||||
|
Ok(writer) => Ok(writer.buf.freeze()),
|
||||||
|
Err(err) => Err(err),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -306,6 +319,13 @@ impl ContentEncoder {
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||||
|
Ok(_) => Ok(()),
|
||||||
|
Err(err) => {
|
||||||
|
trace!("Error decoding ztsd encoding: {}", err);
|
||||||
|
Err(err)
|
||||||
|
}
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -171,7 +171,8 @@ mod rustls {
|
||||||
Error = TlsError<io::Error, DispatchError>,
|
Error = TlsError<io::Error, DispatchError>,
|
||||||
InitError = S::InitError,
|
InitError = S::InitError,
|
||||||
> {
|
> {
|
||||||
let protos = vec!["h2".to_string().into()];
|
let mut protos = vec![b"h2".to_vec()];
|
||||||
|
protos.extend_from_slice(&config.alpn_protocols);
|
||||||
config.set_protocols(&protos);
|
config.set_protocols(&protos);
|
||||||
|
|
||||||
Acceptor::new(config)
|
Acceptor::new(config)
|
||||||
|
|
|
@ -8,40 +8,42 @@ use http::header::{HeaderName, InvalidHeaderName};
|
||||||
|
|
||||||
pub trait AsHeaderName: Sealed {}
|
pub trait AsHeaderName: Sealed {}
|
||||||
|
|
||||||
|
pub struct Seal;
|
||||||
|
|
||||||
pub trait Sealed {
|
pub trait Sealed {
|
||||||
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
|
fn try_as_name(&self, seal: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Sealed for HeaderName {
|
impl Sealed for HeaderName {
|
||||||
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
Ok(Cow::Borrowed(self))
|
Ok(Cow::Borrowed(self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl AsHeaderName for HeaderName {}
|
impl AsHeaderName for HeaderName {}
|
||||||
|
|
||||||
impl Sealed for &HeaderName {
|
impl Sealed for &HeaderName {
|
||||||
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
Ok(Cow::Borrowed(*self))
|
Ok(Cow::Borrowed(*self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl AsHeaderName for &HeaderName {}
|
impl AsHeaderName for &HeaderName {}
|
||||||
|
|
||||||
impl Sealed for &str {
|
impl Sealed for &str {
|
||||||
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
HeaderName::from_str(self).map(Cow::Owned)
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl AsHeaderName for &str {}
|
impl AsHeaderName for &str {}
|
||||||
|
|
||||||
impl Sealed for String {
|
impl Sealed for String {
|
||||||
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
HeaderName::from_str(self).map(Cow::Owned)
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
impl AsHeaderName for String {}
|
impl AsHeaderName for String {}
|
||||||
|
|
||||||
impl Sealed for &String {
|
impl Sealed for &String {
|
||||||
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
|
||||||
HeaderName::from_str(self).map(Cow::Owned)
|
HeaderName::from_str(self).map(Cow::Owned)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -213,7 +213,7 @@ impl HeaderMap {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> {
|
fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> {
|
||||||
match key.try_as_name().ok()? {
|
match key.try_as_name(super::as_name::Seal).ok()? {
|
||||||
Cow::Borrowed(name) => self.inner.get(name),
|
Cow::Borrowed(name) => self.inner.get(name),
|
||||||
Cow::Owned(name) => self.inner.get(&name),
|
Cow::Owned(name) => self.inner.get(&name),
|
||||||
}
|
}
|
||||||
|
@ -279,7 +279,7 @@ impl HeaderMap {
|
||||||
/// assert!(map.get("INVALID HEADER NAME").is_none());
|
/// assert!(map.get("INVALID HEADER NAME").is_none());
|
||||||
/// ```
|
/// ```
|
||||||
pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> {
|
pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> {
|
||||||
match key.try_as_name().ok()? {
|
match key.try_as_name(super::as_name::Seal).ok()? {
|
||||||
Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()),
|
Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()),
|
||||||
Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()),
|
Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()),
|
||||||
}
|
}
|
||||||
|
@ -327,7 +327,7 @@ impl HeaderMap {
|
||||||
/// assert!(map.contains_key(header::ACCEPT));
|
/// assert!(map.contains_key(header::ACCEPT));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn contains_key(&self, key: impl AsHeaderName) -> bool {
|
pub fn contains_key(&self, key: impl AsHeaderName) -> bool {
|
||||||
match key.try_as_name() {
|
match key.try_as_name(super::as_name::Seal) {
|
||||||
Ok(Cow::Borrowed(name)) => self.inner.contains_key(name),
|
Ok(Cow::Borrowed(name)) => self.inner.contains_key(name),
|
||||||
Ok(Cow::Owned(name)) => self.inner.contains_key(&name),
|
Ok(Cow::Owned(name)) => self.inner.contains_key(&name),
|
||||||
Err(_) => false,
|
Err(_) => false,
|
||||||
|
@ -410,7 +410,7 @@ impl HeaderMap {
|
||||||
///
|
///
|
||||||
/// assert!(map.is_empty());
|
/// assert!(map.is_empty());
|
||||||
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
|
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
|
||||||
let value = match key.try_as_name() {
|
let value = match key.try_as_name(super::as_name::Seal) {
|
||||||
Ok(Cow::Borrowed(name)) => self.inner.remove(name),
|
Ok(Cow::Borrowed(name)) => self.inner.remove(name),
|
||||||
Ok(Cow::Owned(name)) => self.inner.remove(&name),
|
Ok(Cow::Owned(name)) => self.inner.remove(&name),
|
||||||
Err(_) => None,
|
Err(_) => None,
|
||||||
|
|
|
@ -23,6 +23,9 @@ pub enum ContentEncoding {
|
||||||
/// Gzip algorithm.
|
/// Gzip algorithm.
|
||||||
Gzip,
|
Gzip,
|
||||||
|
|
||||||
|
// Zstd algorithm.
|
||||||
|
Zstd,
|
||||||
|
|
||||||
/// Indicates the identity function (i.e. no compression, nor modification).
|
/// Indicates the identity function (i.e. no compression, nor modification).
|
||||||
Identity,
|
Identity,
|
||||||
}
|
}
|
||||||
|
@ -41,6 +44,7 @@ impl ContentEncoding {
|
||||||
ContentEncoding::Br => "br",
|
ContentEncoding::Br => "br",
|
||||||
ContentEncoding::Gzip => "gzip",
|
ContentEncoding::Gzip => "gzip",
|
||||||
ContentEncoding::Deflate => "deflate",
|
ContentEncoding::Deflate => "deflate",
|
||||||
|
ContentEncoding::Zstd => "zstd",
|
||||||
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -53,6 +57,7 @@ impl ContentEncoding {
|
||||||
ContentEncoding::Gzip => 1.0,
|
ContentEncoding::Gzip => 1.0,
|
||||||
ContentEncoding::Deflate => 0.9,
|
ContentEncoding::Deflate => 0.9,
|
||||||
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
|
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
|
||||||
|
ContentEncoding::Zstd => 0.0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -81,6 +86,8 @@ impl From<&str> for ContentEncoding {
|
||||||
ContentEncoding::Gzip
|
ContentEncoding::Gzip
|
||||||
} else if val.eq_ignore_ascii_case("deflate") {
|
} else if val.eq_ignore_ascii_case("deflate") {
|
||||||
ContentEncoding::Deflate
|
ContentEncoding::Deflate
|
||||||
|
} else if val.eq_ignore_ascii_case("zstd") {
|
||||||
|
ContentEncoding::Zstd
|
||||||
} else {
|
} else {
|
||||||
ContentEncoding::default()
|
ContentEncoding::default()
|
||||||
}
|
}
|
||||||
|
|
|
@ -305,7 +305,8 @@ mod rustls {
|
||||||
Error = TlsError<io::Error, DispatchError>,
|
Error = TlsError<io::Error, DispatchError>,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()];
|
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
|
||||||
|
protos.extend_from_slice(&config.alpn_protocols);
|
||||||
config.set_protocols(&protos);
|
config.set_protocols(&protos);
|
||||||
|
|
||||||
Acceptor::new(config)
|
Acceptor::new(config)
|
||||||
|
|
|
@ -20,10 +20,15 @@ use futures_core::Stream;
|
||||||
use futures_util::stream::{once, StreamExt as _};
|
use futures_util::stream::{once, StreamExt as _};
|
||||||
use rustls::{
|
use rustls::{
|
||||||
internal::pemfile::{certs, pkcs8_private_keys},
|
internal::pemfile::{certs, pkcs8_private_keys},
|
||||||
NoClientAuth, ServerConfig as RustlsServerConfig,
|
NoClientAuth, ServerConfig as RustlsServerConfig, Session,
|
||||||
};
|
};
|
||||||
|
use webpki::DNSNameRef;
|
||||||
|
|
||||||
use std::io::{self, BufReader};
|
use std::{
|
||||||
|
io::{self, BufReader, Write},
|
||||||
|
net::{SocketAddr, TcpStream as StdTcpStream},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
|
||||||
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
|
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
|
||||||
where
|
where
|
||||||
|
@ -52,6 +57,25 @@ fn tls_config() -> RustlsServerConfig {
|
||||||
config
|
config
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_negotiated_alpn_protocol(
|
||||||
|
addr: SocketAddr,
|
||||||
|
client_alpn_protocol: &[u8],
|
||||||
|
) -> Option<Vec<u8>> {
|
||||||
|
let mut config = rustls::ClientConfig::new();
|
||||||
|
config.alpn_protocols.push(client_alpn_protocol.to_vec());
|
||||||
|
let mut sess = rustls::ClientSession::new(
|
||||||
|
&Arc::new(config),
|
||||||
|
DNSNameRef::try_from_ascii_str("localhost").unwrap(),
|
||||||
|
);
|
||||||
|
let mut sock = StdTcpStream::connect(addr).unwrap();
|
||||||
|
let mut stream = rustls::Stream::new(&mut sess, &mut sock);
|
||||||
|
// The handshake will fails because the client will not be able to verify the server
|
||||||
|
// certificate, but it doesn't matter here as we are just interested in the negotiated ALPN
|
||||||
|
// protocol
|
||||||
|
let _ = stream.flush();
|
||||||
|
sess.get_alpn_protocol().map(|proto| proto.to_vec())
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_h1() -> io::Result<()> {
|
async fn test_h1() -> io::Result<()> {
|
||||||
let srv = test_server(move || {
|
let srv = test_server(move || {
|
||||||
|
@ -460,3 +484,85 @@ async fn test_h1_service_error() {
|
||||||
let bytes = srv.load_body(response).await.unwrap();
|
let bytes = srv.load_body(response).await.unwrap();
|
||||||
assert_eq!(bytes, Bytes::from_static(b"error"));
|
assert_eq!(bytes, Bytes::from_static(b"error"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const H2_ALPN_PROTOCOL: &[u8] = b"h2";
|
||||||
|
const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1";
|
||||||
|
const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_alpn_h1() -> io::Result<()> {
|
||||||
|
let srv = test_server(move || {
|
||||||
|
let mut config = tls_config();
|
||||||
|
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||||
|
HttpService::build()
|
||||||
|
.h1(|_| ok::<_, Error>(Response::ok()))
|
||||||
|
.rustls(config)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
|
||||||
|
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = srv.sget("/").send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_alpn_h2() -> io::Result<()> {
|
||||||
|
let srv = test_server(move || {
|
||||||
|
let mut config = tls_config();
|
||||||
|
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||||
|
HttpService::build()
|
||||||
|
.h2(|_| ok::<_, Error>(Response::ok()))
|
||||||
|
.rustls(config)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
|
||||||
|
Some(H2_ALPN_PROTOCOL.to_vec())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
|
||||||
|
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = srv.sget("/").send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_alpn_h2_1() -> io::Result<()> {
|
||||||
|
let srv = test_server(move || {
|
||||||
|
let mut config = tls_config();
|
||||||
|
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
|
||||||
|
HttpService::build()
|
||||||
|
.finish(|_| ok::<_, Error>(Response::ok()))
|
||||||
|
.rustls(config)
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
|
||||||
|
Some(H2_ALPN_PROTOCOL.to_vec())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_negotiated_alpn_protocol(srv.addr(), HTTP1_1_ALPN_PROTOCOL),
|
||||||
|
Some(HTTP1_1_ALPN_PROTOCOL.to_vec())
|
||||||
|
);
|
||||||
|
assert_eq!(
|
||||||
|
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
|
||||||
|
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
|
||||||
|
);
|
||||||
|
|
||||||
|
let response = srv.sget("/").send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
|
@ -171,27 +171,10 @@ method_macro! {
|
||||||
#[proc_macro_attribute]
|
#[proc_macro_attribute]
|
||||||
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
pub fn main(_: TokenStream, item: TokenStream) -> TokenStream {
|
||||||
use quote::quote;
|
use quote::quote;
|
||||||
|
let input = syn::parse_macro_input!(item as syn::ItemFn);
|
||||||
let mut input = syn::parse_macro_input!(item as syn::ItemFn);
|
|
||||||
let attrs = &input.attrs;
|
|
||||||
let vis = &input.vis;
|
|
||||||
let sig = &mut input.sig;
|
|
||||||
let body = &input.block;
|
|
||||||
|
|
||||||
if sig.asyncness.is_none() {
|
|
||||||
return syn::Error::new_spanned(sig.fn_token, "only async fn is supported")
|
|
||||||
.to_compile_error()
|
|
||||||
.into();
|
|
||||||
}
|
|
||||||
|
|
||||||
sig.asyncness = None;
|
|
||||||
|
|
||||||
(quote! {
|
(quote! {
|
||||||
#(#attrs)*
|
#[actix_web::rt::main(system = "::actix_web::rt::System")]
|
||||||
#vis #sig {
|
#input
|
||||||
actix_web::rt::System::new()
|
|
||||||
.block_on(async move { #body })
|
|
||||||
}
|
|
||||||
})
|
})
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
|
|
|
@ -93,9 +93,17 @@ impl ResponseError for UrlencodedError {
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum JsonPayloadError {
|
pub enum JsonPayloadError {
|
||||||
/// Payload size is bigger than allowed. (default: 32kB)
|
/// Payload size is bigger than allowed & content length header set. (default: 2MB)
|
||||||
#[display(fmt = "Json payload size is bigger than allowed")]
|
#[display(
|
||||||
Overflow,
|
fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).",
|
||||||
|
length,
|
||||||
|
limit
|
||||||
|
)]
|
||||||
|
OverflowKnownLength { length: usize, limit: usize },
|
||||||
|
|
||||||
|
/// Payload size is bigger than allowed but no content length header set. (default: 2MB)
|
||||||
|
#[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)]
|
||||||
|
Overflow { limit: usize },
|
||||||
|
|
||||||
/// Content type error
|
/// Content type error
|
||||||
#[display(fmt = "Content type error")]
|
#[display(fmt = "Content type error")]
|
||||||
|
@ -123,7 +131,11 @@ impl From<PayloadError> for JsonPayloadError {
|
||||||
impl ResponseError for JsonPayloadError {
|
impl ResponseError for JsonPayloadError {
|
||||||
fn status_code(&self) -> StatusCode {
|
fn status_code(&self) -> StatusCode {
|
||||||
match self {
|
match self {
|
||||||
Self::Overflow => StatusCode::PAYLOAD_TOO_LARGE,
|
Self::OverflowKnownLength {
|
||||||
|
length: _,
|
||||||
|
limit: _,
|
||||||
|
} => StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
|
Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE,
|
||||||
Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
Self::Payload(err) => err.status_code(),
|
Self::Payload(err) => err.status_code(),
|
||||||
_ => StatusCode::BAD_REQUEST,
|
_ => StatusCode::BAD_REQUEST,
|
||||||
|
@ -208,7 +220,13 @@ mod tests {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_json_payload_error() {
|
fn test_json_payload_error() {
|
||||||
let resp = JsonPayloadError::Overflow.error_response();
|
let resp = JsonPayloadError::OverflowKnownLength {
|
||||||
|
length: 0,
|
||||||
|
limit: 0,
|
||||||
|
}
|
||||||
|
.error_response();
|
||||||
|
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||||
|
let resp = JsonPayloadError::Overflow { limit: 0 }.error_response();
|
||||||
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
|
||||||
let resp = JsonPayloadError::ContentType.error_response();
|
let resp = JsonPayloadError::ContentType.error_response();
|
||||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
|
|
@ -26,6 +26,7 @@
|
||||||
//!
|
//!
|
||||||
//! * [Website & User Guide](https://actix.rs/)
|
//! * [Website & User Guide](https://actix.rs/)
|
||||||
//! * [Examples Repository](https://github.com/actix/examples)
|
//! * [Examples Repository](https://github.com/actix/examples)
|
||||||
|
//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x)
|
||||||
//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web)
|
//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||||
//!
|
//!
|
||||||
//! To get started navigating the API docs, you may consider looking at the following pages first:
|
//! To get started navigating the API docs, you may consider looking at the following pages first:
|
||||||
|
@ -168,6 +169,8 @@ pub mod dev {
|
||||||
fn get_encoding(&self) -> Option<ContentEncoding>;
|
fn get_encoding(&self) -> Option<ContentEncoding>;
|
||||||
|
|
||||||
/// Set content encoding
|
/// Set content encoding
|
||||||
|
///
|
||||||
|
/// Must be used with [`crate::middleware::Compress`] to take effect.
|
||||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
|
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -137,53 +137,57 @@ where
|
||||||
|
|
||||||
let original_path = head.uri.path();
|
let original_path = head.uri.path();
|
||||||
|
|
||||||
// Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end
|
// An empty path here means that the URI has no valid path. We skip normalization in this
|
||||||
let path = match self.trailing_slash_behavior {
|
// case, because adding a path can make the URI invalid
|
||||||
TrailingSlash::Always => original_path.to_string() + "/",
|
if !original_path.is_empty() {
|
||||||
TrailingSlash::MergeOnly => original_path.to_string(),
|
// Either adds a string to the end (duplicates will be removed anyways) or trims all
|
||||||
TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
|
// slashes from the end
|
||||||
};
|
let path = match self.trailing_slash_behavior {
|
||||||
|
TrailingSlash::Always => format!("{}/", original_path),
|
||||||
// normalize multiple /'s to one /
|
TrailingSlash::MergeOnly => original_path.to_string(),
|
||||||
let path = self.merge_slash.replace_all(&path, "/");
|
TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(),
|
||||||
|
|
||||||
// Ensure root paths are still resolvable. If resulting path is blank after previous step
|
|
||||||
// it means the path was one or more slashes. Reduce to single slash.
|
|
||||||
let path = if path.is_empty() { "/" } else { path.as_ref() };
|
|
||||||
|
|
||||||
// Check whether the path has been changed
|
|
||||||
//
|
|
||||||
// This check was previously implemented as string length comparison
|
|
||||||
//
|
|
||||||
// That approach fails when a trailing slash is added,
|
|
||||||
// and a duplicate slash is removed,
|
|
||||||
// since the length of the strings remains the same
|
|
||||||
//
|
|
||||||
// For example, the path "/v1//s" will be normalized to "/v1/s/"
|
|
||||||
// Both of the paths have the same length,
|
|
||||||
// so the change can not be deduced from the length comparison
|
|
||||||
if path != original_path {
|
|
||||||
let mut parts = head.uri.clone().into_parts();
|
|
||||||
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
|
|
||||||
|
|
||||||
let path = if let Some(q) = query {
|
|
||||||
Bytes::from(format!("{}?{}", path, q))
|
|
||||||
} else {
|
|
||||||
Bytes::copy_from_slice(path.as_bytes())
|
|
||||||
};
|
};
|
||||||
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
|
|
||||||
|
|
||||||
let uri = Uri::from_parts(parts).unwrap();
|
// normalize multiple /'s to one /
|
||||||
req.match_info_mut().get_mut().update(&uri);
|
let path = self.merge_slash.replace_all(&path, "/");
|
||||||
req.head_mut().uri = uri;
|
|
||||||
|
// Ensure root paths are still resolvable. If resulting path is blank after previous
|
||||||
|
// step it means the path was one or more slashes. Reduce to single slash.
|
||||||
|
let path = if path.is_empty() { "/" } else { path.as_ref() };
|
||||||
|
|
||||||
|
// Check whether the path has been changed
|
||||||
|
//
|
||||||
|
// This check was previously implemented as string length comparison
|
||||||
|
//
|
||||||
|
// That approach fails when a trailing slash is added,
|
||||||
|
// and a duplicate slash is removed,
|
||||||
|
// since the length of the strings remains the same
|
||||||
|
//
|
||||||
|
// For example, the path "/v1//s" will be normalized to "/v1/s/"
|
||||||
|
// Both of the paths have the same length,
|
||||||
|
// so the change can not be deduced from the length comparison
|
||||||
|
if path != original_path {
|
||||||
|
let mut parts = head.uri.clone().into_parts();
|
||||||
|
let query = parts.path_and_query.as_ref().and_then(|pq| pq.query());
|
||||||
|
|
||||||
|
let path = match query {
|
||||||
|
Some(q) => Bytes::from(format!("{}?{}", path, q)),
|
||||||
|
None => Bytes::copy_from_slice(path.as_bytes()),
|
||||||
|
};
|
||||||
|
parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap());
|
||||||
|
|
||||||
|
let uri = Uri::from_parts(parts).unwrap();
|
||||||
|
req.match_info_mut().get_mut().update(&uri);
|
||||||
|
req.head_mut().uri = uri;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.service.call(req)
|
self.service.call(req)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use actix_http::StatusCode;
|
||||||
use actix_service::IntoService;
|
use actix_service::IntoService;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
@ -387,6 +391,22 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn no_path() {
|
||||||
|
let app = init_service(
|
||||||
|
App::new()
|
||||||
|
.wrap(NormalizePath::default())
|
||||||
|
.service(web::resource("/").to(HttpResponse::Ok)),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// This URI will be interpreted as an authority form, i.e. there is no path nor scheme
|
||||||
|
// (https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3)
|
||||||
|
let req = TestRequest::with_uri("eh").to_request();
|
||||||
|
let res = call_service(&app, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_in_place_normalization() {
|
async fn test_in_place_normalization() {
|
||||||
let srv = |req: ServiceRequest| {
|
let srv = |req: ServiceRequest| {
|
||||||
|
|
|
@ -368,7 +368,7 @@ where
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
/// Use listener for accepting incoming tls connection requests
|
/// Use listener for accepting incoming tls connection requests
|
||||||
///
|
///
|
||||||
/// This method sets alpn protocols to "h2" and "http/1.1"
|
/// This method prepends alpn protocols "h2" and "http/1.1" to configured ones
|
||||||
pub fn listen_rustls(
|
pub fn listen_rustls(
|
||||||
self,
|
self,
|
||||||
lst: net::TcpListener,
|
lst: net::TcpListener,
|
||||||
|
@ -482,7 +482,7 @@ where
|
||||||
#[cfg(feature = "rustls")]
|
#[cfg(feature = "rustls")]
|
||||||
/// Start listening for incoming tls connections.
|
/// Start listening for incoming tls connections.
|
||||||
///
|
///
|
||||||
/// This method sets alpn protocols to "h2" and "http/1.1"
|
/// This method prepends alpn protocols "h2" and "http/1.1" to configured ones
|
||||||
pub fn bind_rustls<A: net::ToSocketAddrs>(
|
pub fn bind_rustls<A: net::ToSocketAddrs>(
|
||||||
mut self,
|
mut self,
|
||||||
addr: A,
|
addr: A,
|
||||||
|
|
|
@ -240,7 +240,7 @@ pub struct JsonConfig {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl JsonConfig {
|
impl JsonConfig {
|
||||||
/// Set maximum accepted payload size. By default this limit is 32kB.
|
/// Set maximum accepted payload size. By default this limit is 2MB.
|
||||||
pub fn limit(mut self, limit: usize) -> Self {
|
pub fn limit(mut self, limit: usize) -> Self {
|
||||||
self.limit = limit;
|
self.limit = limit;
|
||||||
self
|
self
|
||||||
|
@ -273,9 +273,11 @@ impl JsonConfig {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb
|
||||||
|
|
||||||
/// Allow shared refs used as default.
|
/// Allow shared refs used as default.
|
||||||
const DEFAULT_CONFIG: JsonConfig = JsonConfig {
|
const DEFAULT_CONFIG: JsonConfig = JsonConfig {
|
||||||
limit: 32_768, // 2^15 bytes, (~32kB)
|
limit: DEFAULT_LIMIT,
|
||||||
err_handler: None,
|
err_handler: None,
|
||||||
content_type: None,
|
content_type: None,
|
||||||
};
|
};
|
||||||
|
@ -349,7 +351,7 @@ where
|
||||||
let payload = payload.take();
|
let payload = payload.take();
|
||||||
|
|
||||||
JsonBody::Body {
|
JsonBody::Body {
|
||||||
limit: 32_768,
|
limit: DEFAULT_LIMIT,
|
||||||
length,
|
length,
|
||||||
payload,
|
payload,
|
||||||
buf: BytesMut::with_capacity(8192),
|
buf: BytesMut::with_capacity(8192),
|
||||||
|
@ -357,7 +359,7 @@ where
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set maximum accepted payload size. The default limit is 32kB.
|
/// Set maximum accepted payload size. The default limit is 2MB.
|
||||||
pub fn limit(self, limit: usize) -> Self {
|
pub fn limit(self, limit: usize) -> Self {
|
||||||
match self {
|
match self {
|
||||||
JsonBody::Body {
|
JsonBody::Body {
|
||||||
|
@ -368,7 +370,10 @@ where
|
||||||
} => {
|
} => {
|
||||||
if let Some(len) = length {
|
if let Some(len) = length {
|
||||||
if len > limit {
|
if len > limit {
|
||||||
return JsonBody::Error(Some(JsonPayloadError::Overflow));
|
return JsonBody::Error(Some(JsonPayloadError::OverflowKnownLength {
|
||||||
|
length: len,
|
||||||
|
limit,
|
||||||
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -405,8 +410,11 @@ where
|
||||||
match res {
|
match res {
|
||||||
Some(chunk) => {
|
Some(chunk) => {
|
||||||
let chunk = chunk?;
|
let chunk = chunk?;
|
||||||
if (buf.len() + chunk.len()) > *limit {
|
let buf_len = buf.len() + chunk.len();
|
||||||
return Poll::Ready(Err(JsonPayloadError::Overflow));
|
if buf_len > *limit {
|
||||||
|
return Poll::Ready(Err(JsonPayloadError::Overflow {
|
||||||
|
limit: *limit,
|
||||||
|
}));
|
||||||
} else {
|
} else {
|
||||||
buf.extend_from_slice(&chunk);
|
buf.extend_from_slice(&chunk);
|
||||||
}
|
}
|
||||||
|
@ -445,7 +453,12 @@ mod tests {
|
||||||
|
|
||||||
fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool {
|
fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool {
|
||||||
match err {
|
match err {
|
||||||
JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow),
|
JsonPayloadError::Overflow { .. } => {
|
||||||
|
matches!(other, JsonPayloadError::Overflow { .. })
|
||||||
|
}
|
||||||
|
JsonPayloadError::OverflowKnownLength { .. } => {
|
||||||
|
matches!(other, JsonPayloadError::OverflowKnownLength { .. })
|
||||||
|
}
|
||||||
JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType),
|
JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType),
|
||||||
_ => false,
|
_ => false,
|
||||||
}
|
}
|
||||||
|
@ -538,7 +551,7 @@ mod tests {
|
||||||
|
|
||||||
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
|
let s = Json::<MyObject>::from_request(&req, &mut pl).await;
|
||||||
assert!(format!("{}", s.err().unwrap())
|
assert!(format!("{}", s.err().unwrap())
|
||||||
.contains("Json payload size is bigger than allowed"));
|
.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes)."));
|
||||||
|
|
||||||
let (req, mut pl) = TestRequest::default()
|
let (req, mut pl) = TestRequest::default()
|
||||||
.insert_header((
|
.insert_header((
|
||||||
|
@ -589,7 +602,30 @@ mod tests {
|
||||||
let json = JsonBody::<MyObject>::new(&req, &mut pl, None)
|
let json = JsonBody::<MyObject>::new(&req, &mut pl, None)
|
||||||
.limit(100)
|
.limit(100)
|
||||||
.await;
|
.await;
|
||||||
assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow));
|
assert!(json_eq(
|
||||||
|
json.err().unwrap(),
|
||||||
|
JsonPayloadError::OverflowKnownLength {
|
||||||
|
length: 10000,
|
||||||
|
limit: 100
|
||||||
|
}
|
||||||
|
));
|
||||||
|
|
||||||
|
let (req, mut pl) = TestRequest::default()
|
||||||
|
.insert_header((
|
||||||
|
header::CONTENT_TYPE,
|
||||||
|
header::HeaderValue::from_static("application/json"),
|
||||||
|
))
|
||||||
|
.set_payload(Bytes::from_static(&[0u8; 1000]))
|
||||||
|
.to_http_parts();
|
||||||
|
|
||||||
|
let json = JsonBody::<MyObject>::new(&req, &mut pl, None)
|
||||||
|
.limit(100)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
assert!(json_eq(
|
||||||
|
json.err().unwrap(),
|
||||||
|
JsonPayloadError::Overflow { limit: 100 }
|
||||||
|
));
|
||||||
|
|
||||||
let (req, mut pl) = TestRequest::default()
|
let (req, mut pl) = TestRequest::default()
|
||||||
.insert_header((
|
.insert_header((
|
||||||
|
@ -686,6 +722,7 @@ mod tests {
|
||||||
assert!(s.is_err());
|
assert!(s.is_err());
|
||||||
|
|
||||||
let err_str = s.err().unwrap().to_string();
|
let err_str = s.err().unwrap().to_string();
|
||||||
assert!(err_str.contains("Json payload size is bigger than allowed"));
|
assert!(err_str
|
||||||
|
.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes)."));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -29,6 +29,7 @@ use openssl::{
|
||||||
x509::X509,
|
x509::X509,
|
||||||
};
|
};
|
||||||
use rand::{distributions::Alphanumeric, Rng};
|
use rand::{distributions::Alphanumeric, Rng};
|
||||||
|
use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder};
|
||||||
|
|
||||||
use actix_web::dev::BodyEncoding;
|
use actix_web::dev::BodyEncoding;
|
||||||
use actix_web::middleware::{Compress, NormalizePath, TrailingSlash};
|
use actix_web::middleware::{Compress, NormalizePath, TrailingSlash};
|
||||||
|
@ -476,6 +477,125 @@ async fn test_body_brotli() {
|
||||||
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
|
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_zstd() {
|
||||||
|
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Compress::new(ContentEncoding::Zstd))
|
||||||
|
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))))
|
||||||
|
});
|
||||||
|
|
||||||
|
// client request
|
||||||
|
let mut response = srv
|
||||||
|
.get("/")
|
||||||
|
.append_header((ACCEPT_ENCODING, "zstd"))
|
||||||
|
.no_decompress()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
// read response
|
||||||
|
let bytes = response.body().await.unwrap();
|
||||||
|
|
||||||
|
// decode
|
||||||
|
let mut e = ZstdDecoder::new(&bytes[..]).unwrap();
|
||||||
|
let mut dec = Vec::new();
|
||||||
|
e.read_to_end(&mut dec).unwrap();
|
||||||
|
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_body_zstd_streaming() {
|
||||||
|
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
||||||
|
App::new()
|
||||||
|
.wrap(Compress::new(ContentEncoding::Zstd))
|
||||||
|
.service(web::resource("/").route(web::to(move || {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
.streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24))
|
||||||
|
})))
|
||||||
|
});
|
||||||
|
|
||||||
|
// client request
|
||||||
|
let mut response = srv
|
||||||
|
.get("/")
|
||||||
|
.append_header((ACCEPT_ENCODING, "zstd"))
|
||||||
|
.no_decompress()
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
// read response
|
||||||
|
let bytes = response.body().await.unwrap();
|
||||||
|
|
||||||
|
// decode
|
||||||
|
let mut e = ZstdDecoder::new(&bytes[..]).unwrap();
|
||||||
|
let mut dec = Vec::new();
|
||||||
|
e.read_to_end(&mut dec).unwrap();
|
||||||
|
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_zstd_encoding() {
|
||||||
|
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
||||||
|
App::new().service(
|
||||||
|
web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap();
|
||||||
|
e.write_all(STR.as_ref()).unwrap();
|
||||||
|
let enc = e.finish().unwrap();
|
||||||
|
|
||||||
|
// client request
|
||||||
|
let request = srv
|
||||||
|
.post("/")
|
||||||
|
.append_header((CONTENT_ENCODING, "zstd"))
|
||||||
|
.send_body(enc.clone());
|
||||||
|
let mut response = request.await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
// read response
|
||||||
|
let bytes = response.body().await.unwrap();
|
||||||
|
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_zstd_encoding_large() {
|
||||||
|
let data = rand::thread_rng()
|
||||||
|
.sample_iter(&Alphanumeric)
|
||||||
|
.take(320_000)
|
||||||
|
.map(char::from)
|
||||||
|
.collect::<String>();
|
||||||
|
|
||||||
|
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
||||||
|
App::new().service(
|
||||||
|
web::resource("/")
|
||||||
|
.app_data(web::PayloadConfig::new(320_000))
|
||||||
|
.route(web::to(move |body: Bytes| {
|
||||||
|
HttpResponse::Ok().streaming(TestBody::new(body, 10240))
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap();
|
||||||
|
e.write_all(data.as_ref()).unwrap();
|
||||||
|
let enc = e.finish().unwrap();
|
||||||
|
|
||||||
|
// client request
|
||||||
|
let request = srv
|
||||||
|
.post("/")
|
||||||
|
.append_header((CONTENT_ENCODING, "zstd"))
|
||||||
|
.send_body(enc.clone());
|
||||||
|
let mut response = request.await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
// read response
|
||||||
|
let bytes = response.body().limit(320_000).await.unwrap();
|
||||||
|
assert_eq!(bytes, Bytes::from(data));
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_encoding() {
|
async fn test_encoding() {
|
||||||
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
let srv = actix_test::start_with(actix_test::config().h1(), || {
|
||||||
|
|
Loading…
Reference in New Issue