diff --git a/CHANGES.md b/CHANGES.md index 162f9f61b..ae1d435cc 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -5,17 +5,23 @@ * `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] ### 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::checked_expr` now returns a `Result`. [#2201] * Update `language-tags` to `0.3`. * `ServiceResponse::take_body`. [#2201] * `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody` 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 * `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] [#2200]: https://github.com/actix/actix-web/pull/2200 [#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 diff --git a/Cargo.toml b/Cargo.toml index 5aa302333..bd4cdd91f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,7 +58,7 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] [dependencies] actix-codec = "0.4.0" -actix-macros = "0.2.0" +actix-macros = "0.2.1" actix-router = "0.2.7" actix-rt = "2.2" actix-server = "2.0.0-beta.3" @@ -101,6 +101,7 @@ brotli2 = "0.3.2" criterion = "0.3" env_logger = "0.8" flate2 = "1.0.13" +zstd = "0.7" rand = "0.8" rcgen = "0.8" serde_derive = "1.0" diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 519234f0d..2183eab5f 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -235,6 +235,8 @@ impl NamedFile { } /// Set content encoding for serving this file + /// + /// Must be used with [`actix_web::middleware::Compress`] to take effect. #[inline] pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { self.encoding = Some(enc); diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index ec7d8ee3b..99953ff26 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -9,6 +9,7 @@ * Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] * `Response::into_body` that consumes response and returns body type. [#2201] * `impl Default` for `Response`. [#2201] +* Add zstd support for `ContentEncoding`. [#2244] ### Changed * The `MessageBody` trait now has an associated `Error` type. [#2183] @@ -18,6 +19,7 @@ * 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] * `ResponseBuilder::message_body` now returns a `Result`. [#2201] +* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] ### Removed * 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 [#2205]: https://github.com/actix/actix-web/pull/2205 [#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 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 1f7df39a6..1a0a32599 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -32,7 +32,7 @@ openssl = ["actix-tls/openssl"] rustls = ["actix-tls/rustls"] # enable compression support -compress = ["flate2", "brotli2"] +compress = ["flate2", "brotli2", "zstd"] # trust-dns as client dns resolver trust-dns = ["trust-dns-resolver"] @@ -76,6 +76,7 @@ tokio = { version = "1.2", features = ["sync"] } # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } +zstd = { version = "0.7", 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" tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } +webpki = { version = "0.21.0" } [[example]] name = "ws" diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index f0abae865..58981e82e 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -12,6 +12,7 @@ use brotli2::write::BrotliDecoder; use bytes::Bytes; use flate2::write::{GzDecoder, ZlibDecoder}; use futures_core::{ready, Stream}; +use zstd::stream::write::Decoder as ZstdDecoder; use crate::{ encoding::Writer, @@ -45,6 +46,12 @@ where ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::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, }; @@ -144,6 +151,9 @@ enum ContentDecoder { Deflate(Box>), Gzip(Box>), Br(Box>), + // 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>), } impl ContentDecoder { @@ -186,6 +196,18 @@ impl ContentDecoder { } 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), }, + + 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), + }, } } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index b8bc8b68d..6adde9be2 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -15,6 +15,7 @@ use derive_more::Display; use flate2::write::{GzEncoder, ZlibEncoder}; use futures_core::ready; use pin_project::pin_project; +use zstd::stream::write::Encoder as ZstdEncoder; use crate::{ body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody}, @@ -237,6 +238,9 @@ enum ContentEncoder { Deflate(ZlibEncoder), Gzip(GzEncoder), Br(BrotliEncoder), + // 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 { @@ -253,6 +257,10 @@ impl ContentEncoder { ContentEncoding::Br => { Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) } + ContentEncoding::Zstd => { + let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; + Some(ContentEncoder::Zstd(encoder)) + } _ => None, } } @@ -263,6 +271,7 @@ impl ContentEncoder { ContentEncoder::Br(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::Zstd(ref mut encoder) => encoder.get_mut().take(), } } @@ -280,6 +289,10 @@ impl ContentEncoder { Ok(writer) => Ok(writer.buf.freeze()), 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) } }, + ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(()), + Err(err) => { + trace!("Error decoding ztsd encoding: {}", err); + Err(err) + } + }, } } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index a75abef7d..3a6d535d9 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -171,7 +171,8 @@ mod rustls { Error = TlsError, 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); Acceptor::new(config) diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs index af81ff7f2..5ce321566 100644 --- a/actix-http/src/header/as_name.rs +++ b/actix-http/src/header/as_name.rs @@ -8,40 +8,42 @@ use http::header::{HeaderName, InvalidHeaderName}; pub trait AsHeaderName: Sealed {} +pub struct Seal; + pub trait Sealed { - fn try_as_name(&self) -> Result, InvalidHeaderName>; + fn try_as_name(&self, seal: Seal) -> Result, InvalidHeaderName>; } impl Sealed for HeaderName { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { Ok(Cow::Borrowed(self)) } } impl AsHeaderName for HeaderName {} impl Sealed for &HeaderName { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { Ok(Cow::Borrowed(*self)) } } impl AsHeaderName for &HeaderName {} impl Sealed for &str { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } } impl AsHeaderName for &str {} impl Sealed for String { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } } impl AsHeaderName for String {} impl Sealed for &String { - fn try_as_name(&self) -> Result, InvalidHeaderName> { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { HeaderName::from_str(self).map(Cow::Owned) } } diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 106e44edb..be33ec02a 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -213,7 +213,7 @@ impl HeaderMap { } 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::Owned(name) => self.inner.get(&name), } @@ -279,7 +279,7 @@ impl HeaderMap { /// assert!(map.get("INVALID HEADER NAME").is_none()); /// ``` 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::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()), } @@ -327,7 +327,7 @@ impl HeaderMap { /// assert!(map.contains_key(header::ACCEPT)); /// ``` 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::Owned(name)) => self.inner.contains_key(&name), Err(_) => false, @@ -410,7 +410,7 @@ impl HeaderMap { /// /// assert!(map.is_empty()); 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::Owned(name)) => self.inner.remove(&name), Err(_) => None, diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index b93d66101..b9c1d2795 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -23,6 +23,9 @@ pub enum ContentEncoding { /// Gzip algorithm. Gzip, + // Zstd algorithm. + Zstd, + /// Indicates the identity function (i.e. no compression, nor modification). Identity, } @@ -41,6 +44,7 @@ impl ContentEncoding { ContentEncoding::Br => "br", ContentEncoding::Gzip => "gzip", ContentEncoding::Deflate => "deflate", + ContentEncoding::Zstd => "zstd", ContentEncoding::Identity | ContentEncoding::Auto => "identity", } } @@ -53,6 +57,7 @@ impl ContentEncoding { ContentEncoding::Gzip => 1.0, ContentEncoding::Deflate => 0.9, ContentEncoding::Identity | ContentEncoding::Auto => 0.1, + ContentEncoding::Zstd => 0.0, } } } @@ -81,6 +86,8 @@ impl From<&str> for ContentEncoding { ContentEncoding::Gzip } else if val.eq_ignore_ascii_case("deflate") { ContentEncoding::Deflate + } else if val.eq_ignore_ascii_case("zstd") { + ContentEncoding::Zstd } else { ContentEncoding::default() } diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index d25a67a19..1c81e7568 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -305,7 +305,8 @@ mod rustls { Error = TlsError, 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); Acceptor::new(config) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 2382d1ad3..eec417541 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -20,10 +20,15 @@ use futures_core::Stream; use futures_util::stream::{once, StreamExt as _}; use rustls::{ 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(mut stream: S) -> Result where @@ -52,6 +57,25 @@ fn tls_config() -> RustlsServerConfig { config } +pub fn get_negotiated_alpn_protocol( + addr: SocketAddr, + client_alpn_protocol: &[u8], +) -> Option> { + 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] async fn test_h1() -> io::Result<()> { let srv = test_server(move || { @@ -460,3 +484,85 @@ async fn test_h1_service_error() { let bytes = srv.load_body(response).await.unwrap(); 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(()) +} diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 336345014..2237f422c 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -171,27 +171,10 @@ method_macro! { #[proc_macro_attribute] pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { use quote::quote; - - 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; - + let input = syn::parse_macro_input!(item as syn::ItemFn); (quote! { - #(#attrs)* - #vis #sig { - actix_web::rt::System::new() - .block_on(async move { #body }) - } + #[actix_web::rt::main(system = "::actix_web::rt::System")] + #input }) .into() } diff --git a/src/error/mod.rs b/src/error/mod.rs index 7be9f501b..146146c71 100644 --- a/src/error/mod.rs +++ b/src/error/mod.rs @@ -93,9 +93,17 @@ impl ResponseError for UrlencodedError { #[derive(Debug, Display, Error)] #[non_exhaustive] pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 32kB) - #[display(fmt = "Json payload size is bigger than allowed")] - Overflow, + /// Payload size is bigger than allowed & content length header set. (default: 2MB) + #[display( + 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 #[display(fmt = "Content type error")] @@ -123,7 +131,11 @@ impl From for JsonPayloadError { impl ResponseError for JsonPayloadError { fn status_code(&self) -> StatusCode { 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::Payload(err) => err.status_code(), _ => StatusCode::BAD_REQUEST, @@ -208,7 +220,13 @@ mod tests { #[test] 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); let resp = JsonPayloadError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); diff --git a/src/lib.rs b/src/lib.rs index 96e6ecbf8..4e8093a2a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -26,6 +26,7 @@ //! //! * [Website & User Guide](https://actix.rs/) //! * [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) //! //! 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; /// Set content encoding + /// + /// Must be used with [`crate::middleware::Compress`] to take effect. fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; } diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index cbed78714..219af1c6a 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -137,53 +137,57 @@ where 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 - let path = match self.trailing_slash_behavior { - TrailingSlash::Always => original_path.to_string() + "/", - TrailingSlash::MergeOnly => original_path.to_string(), - TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), - }; - - // normalize multiple /'s to one / - let path = self.merge_slash.replace_all(&path, "/"); - - // 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()) + // An empty path here means that the URI has no valid path. We skip normalization in this + // case, because adding a path can make the URI invalid + if !original_path.is_empty() { + // Either adds a string to the end (duplicates will be removed anyways) or trims all + // slashes from the end + let path = match self.trailing_slash_behavior { + TrailingSlash::Always => format!("{}/", original_path), + TrailingSlash::MergeOnly => original_path.to_string(), + TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), }; - 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; + // normalize multiple /'s to one / + let path = self.merge_slash.replace_all(&path, "/"); + + // 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) } } #[cfg(test)] mod tests { + use actix_http::StatusCode; use actix_service::IntoService; 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] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { diff --git a/src/server.rs b/src/server.rs index 44ae6f880..80e300b9a 100644 --- a/src/server.rs +++ b/src/server.rs @@ -368,7 +368,7 @@ where #[cfg(feature = "rustls")] /// 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( self, lst: net::TcpListener, @@ -482,7 +482,7 @@ where #[cfg(feature = "rustls")] /// 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( mut self, addr: A, diff --git a/src/types/json.rs b/src/types/json.rs index 5762c6428..24abcecea 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -240,7 +240,7 @@ pub struct 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 { self.limit = limit; self @@ -273,9 +273,11 @@ impl JsonConfig { } } +const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb + /// Allow shared refs used as default. const DEFAULT_CONFIG: JsonConfig = JsonConfig { - limit: 32_768, // 2^15 bytes, (~32kB) + limit: DEFAULT_LIMIT, err_handler: None, content_type: None, }; @@ -349,7 +351,7 @@ where let payload = payload.take(); JsonBody::Body { - limit: 32_768, + limit: DEFAULT_LIMIT, length, payload, 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 { match self { JsonBody::Body { @@ -368,7 +370,10 @@ where } => { if let Some(len) = length { 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 { Some(chunk) => { let chunk = chunk?; - if (buf.len() + chunk.len()) > *limit { - return Poll::Ready(Err(JsonPayloadError::Overflow)); + let buf_len = buf.len() + chunk.len(); + if buf_len > *limit { + return Poll::Ready(Err(JsonPayloadError::Overflow { + limit: *limit, + })); } else { buf.extend_from_slice(&chunk); } @@ -445,7 +453,12 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { 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), _ => false, } @@ -538,7 +551,7 @@ mod tests { let s = Json::::from_request(&req, &mut pl).await; 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() .insert_header(( @@ -589,7 +602,30 @@ mod tests { let json = JsonBody::::new(&req, &mut pl, None) .limit(100) .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::::new(&req, &mut pl, None) + .limit(100) + .await; + + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Overflow { limit: 100 } + )); let (req, mut pl) = TestRequest::default() .insert_header(( @@ -686,6 +722,7 @@ mod tests { assert!(s.is_err()); 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).")); } } diff --git a/tests/test_server.rs b/tests/test_server.rs index c341aa0ce..520eb5ce2 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -29,6 +29,7 @@ use openssl::{ x509::X509, }; use rand::{distributions::Alphanumeric, Rng}; +use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder}; use actix_web::dev::BodyEncoding; 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())); } +#[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::(); + + 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] async fn test_encoding() { let srv = actix_test::start_with(actix_test::config().h1(), || {