diff --git a/.cargo/config.toml b/.cargo/config.toml index 0cf09f710..72f445d8a 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -3,6 +3,7 @@ chk = "check --workspace --all-features --tests --examples --bins" lint = "clippy --workspace --tests --examples" ci-min = "hack check --workspace --no-default-features" ci-min-test = "hack check --workspace --no-default-features --tests --examples" -ci-default = "hack check --workspace" -ci-full = "check --workspace --bins --examples --tests" -ci-test = "test --workspace --all-features --no-fail-fast" +ci-default = "check --workspace --bins --tests --examples" +ci-full = "check --workspace --all-features --bins --tests --examples" +ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" +ci-doctest = "hack test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c57db463a..be595e35c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,43 +66,33 @@ jobs: - name: check minimal uses: actions-rs/cargo@v1 - with: - command: hack - args: check --workspace --no-default-features + with: { command: ci-min } - name: check minimal + tests uses: actions-rs/cargo@v1 - with: - command: hack - args: check --workspace --no-default-features --tests --examples + with: { command: ci-min-test } + - name: check default + uses: actions-rs/cargo@v1 + with: { command: ci-default } + - name: check full uses: actions-rs/cargo@v1 - with: - command: check - args: --workspace --bins --examples --tests + with: { command: ci-full } - name: tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --workspace --all-features --no-fail-fast -- --nocapture - --skip=test_h2_content_length - --skip=test_reading_deflate_encoding_large_random_rustls - - - name: tests (actix-http) uses: actions-rs/cargo@v1 timeout-minutes: 40 with: - command: test - args: --package=actix-http --no-default-features --features=rustls -- --nocapture + command: ci-test + args: --skip=test_reading_deflate_encoding_large_random_rustls - - name: tests (awc) + - name: doc tests + # due to unknown issue with running doc tests on macOS + if: matrix.target.os == 'ubuntu-latest' uses: actions-rs/cargo@v1 timeout-minutes: 40 - with: - command: test - args: --package=awc --no-default-features --features=rustls -- --nocapture + with: { command: ci-doctest } - name: Generate coverage file if: > diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index e966fa4ab..957256d32 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -36,4 +36,4 @@ jobs: uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --workspace --tests --all-features + args: --workspace --all-features --tests diff --git a/CHANGES.md b/CHANGES.md index 0032a347e..f3d041d57 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,9 +1,20 @@ # Changes ## Unreleased - 2021-xx-xx -### Changed -* App data set on `Scope` instance can now be accessed in middlewares defined on the same scope. [#2267] +### Added +* Add `ServiceRequest::parts_mut`. [#2177] +* Add extractors for `Uri` and `Method`. [#2263] +* Add extractor for `ConnectionInfo` and `PeerAddr`. [#2263] +### Changed +* Change compression algorithm features flags. [#2250] +* Deprecate `App::data` and `App::data_factory`. [#2271] +* `app_data` set on a `Scope` instance can now be accessed in middlewares defined on the same scope. This applies to `Resource` too. [#2267] + +[#2177]: https://github.com/actix/actix-web/pull/2177 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2271]: https://github.com/actix/actix-web/pull/2271 +[#2263]: https://github.com/actix/actix-web/pull/2263 [#2267]: (https://github.com/actix/actix-web/pull/2267) @@ -12,7 +23,6 @@ * `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] diff --git a/Cargo.toml b/Cargo.toml index 11a173714..320751c66 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -17,7 +17,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"] +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] [lib] name = "actix_web" @@ -39,10 +39,14 @@ members = [ # resolver = "2" [features] -default = ["compress", "cookies"] +default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] -# content-encoding support -compress = ["actix-http/compress"] +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] # support for cookies cookies = ["cookie"] @@ -56,6 +60,10 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # rustls rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + [dependencies] actix-codec = "0.4.0" actix-macros = "0.2.1" @@ -71,6 +79,7 @@ actix-http = "3.0.0-beta.7" ahash = "0.7" bytes = "1" +cfg-if = "1" cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" either = "1.5.3" @@ -94,7 +103,7 @@ time = { version = "0.2.23", default-features = false, features = ["std"] } url = "2.1" [dev-dependencies] -actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } awc = { version = "3.0.0-beta.6", features = ["openssl"] } brotli2 = "0.3.2" @@ -126,15 +135,15 @@ awc = { path = "awc" } [[test]] name = "test_server" -required-features = ["compress", "cookies"] +required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] [[example]] name = "basic" -required-features = ["compress"] +required-features = ["compress-gzip"] [[example]] name = "uds" -required-features = ["compress"] +required-features = ["compress-gzip"] [[example]] name = "on_connect" diff --git a/MIGRATION.md b/MIGRATION.md index e01702868..9c29b8db9 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -10,6 +10,18 @@ Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. +* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). + By default all compression algorithms are enabled. + To select algorithm you want to include with `middleware::Compress` use following flags: + - `compress-brotli` + - `compress-gzip` + - `compress-zstd` + If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want + to have compression enabled. Please change features selection like bellow: + + Before: `"compress"` + After: `"compress-brotli", "compress-gzip", "compress-zstd"` + ## 3.0.0 diff --git a/README.md b/README.md index c6ef6d868..d9048a06b 100644 --- a/README.md +++ b/README.md @@ -25,7 +25,7 @@ * Streaming and pipelining * Keep-alive and slow requests handling * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -* Transparent content compression/decompression (br, gzip, deflate) +* Transparent content compression/decompression (br, gzip, deflate, zstd) * Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 9a643f127..bec67dd4e 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -8,11 +8,13 @@ * For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] * `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] * `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] +* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2135]: https://github.com/actix/actix-web/pull/2135 [#2156]: https://github.com/actix/actix-web/pull/2156 [#2225]: https://github.com/actix/actix-web/pull/2225 [#2257]: https://github.com/actix/actix-web/pull/2257 +[#2228]: https://github.com/actix/actix-web/pull/2228 ## 0.6.0-beta.4 - 2021-04-02 diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 44c29dc92..65dce628b 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -36,4 +36,4 @@ percent-encoding = "2.1" [dev-dependencies] actix-rt = "2.2" actix-web = "4.0.0-beta.7" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 1e0aec454..09786ca67 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -114,6 +114,9 @@ impl Files { /// Show files listing for directories. /// /// By default show files listing is disabled. + /// + /// When used with [`Files::index_file()`], files listing is shown as a fallback + /// when the index file is not found. pub fn show_files_listing(mut self) -> Self { self.show_index = true; self @@ -148,8 +151,11 @@ impl Files { /// Set index file /// - /// Shows specific index file for directory "/" instead of + /// Shows specific index file for directories instead of /// showing files listing. + /// + /// If the index file is not found, files listing is shown as a fallback if + /// [`Files::show_files_listing()`] is set. pub fn index_file>(mut self, index: T) -> Self { self.index = Some(index.into()); self diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 48d3c49f4..c9cc79193 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -872,4 +872,33 @@ mod tests { "inline; filename=\"symlink-test.png\"" ); } + + #[actix_rt::test] + async fn test_index_with_show_files_listing() { + let service = Files::new(".", ".") + .index_file("lib.rs") + .show_files_listing() + .new_service(()) + .await + .unwrap(); + + // Serve the index if exists + let req = TestRequest::default().uri("/src").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-rust" + ); + + // Show files listing, otherwise. + let req = TestRequest::default().uri("/tests").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + let bytes = test::read_body(resp).await; + assert!(format!("{:?}", bytes).contains("/tests/test.png")); + } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 831115dc6..64938e5ef 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -102,26 +102,20 @@ impl Service for FilesService { ))); } - if let Some(ref redir_index) = self.index { - let path = path.join(redir_index); - - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - named_file.flags = self.file_flags; - - let (req, _) = req.into_parts(); - let res = named_file.into_response(&req); - Box::pin(ok(ServiceResponse::new(req, res))) - } - Err(err) => self.handle_err(err, req), + let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + let res = named_file.into_response(&req); + Box::pin(ok(ServiceResponse::new(req, res))) + }; + + let show_index = |req: ServiceRequest| { + let dir = Directory::new(self.directory.clone(), path.clone()); let (req, _) = req.into_parts(); let x = (self.renderer)(&dir, &req); @@ -130,11 +124,19 @@ impl Service for FilesService { Ok(resp) => ok(resp), Err(err) => ok(ServiceResponse::from_err(err, req)), }) - } else { - Box::pin(ok(ServiceResponse::from_err( + }; + + match self.index { + Some(ref index) => match NamedFile::open(path.join(index)) { + Ok(named_file) => serve_named_file(req, named_file), + Err(_) if self.show_index => show_index(req), + Err(err) => self.handle_err(err, req), + }, + None if self.show_index => show_index(req), + _ => Box::pin(ok(ServiceResponse::from_err( FilesError::IsDirectory, req.into_parts().0, - ))) + ))), } } else { match NamedFile::open(path) { diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 16a650d90..c8d65e393 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Change compression algorithm features flags. [#2250] + +[#2250]: https://github.com/actix/actix-web/pull/2250 ## 3.0.0-beta.7 - 2021-06-17 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index bfb51885f..35ea89862 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -16,7 +16,7 @@ edition = "2018" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress"] +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] [lib] name = "actix_http" @@ -32,11 +32,17 @@ openssl = ["actix-tls/openssl"] rustls = ["actix-tls/rustls"] # enable compression support -compress = ["flate2", "brotli2", "zstd"] +compress-brotli = ["brotli2", "__compress"] +compress-gzip = ["flate2", "__compress"] +compress-zstd = ["zstd", "__compress"] # trust-dns as client dns resolver trust-dns = ["trust-dns-resolver"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + [dependencies] actix-service = "2.0.0" actix-codec = "0.4.0" diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index 58981e82e..d3e304836 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -8,10 +8,16 @@ use std::{ }; use actix_rt::task::{spawn_blocking, JoinHandle}; -use brotli2::write::BrotliDecoder; use bytes::Bytes; -use flate2::write::{GzDecoder, ZlibDecoder}; use futures_core::{ready, Stream}; + +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliDecoder; + +#[cfg(feature = "compress-gzip")] +use flate2::write::{GzDecoder, ZlibDecoder}; + +#[cfg(feature = "compress-zstd")] use zstd::stream::write::Decoder as ZstdDecoder; use crate::{ @@ -37,15 +43,19 @@ where #[inline] pub fn new(stream: S, encoding: ContentEncoding) -> Decoder { let decoder = match encoding { + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( BrotliDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( GzDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-zstd")] ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new( ZstdDecoder::new(Writer::new()).expect( "Failed to create zstd decoder. This is a bug. \ @@ -148,17 +158,22 @@ where } enum ContentDecoder { + #[cfg(feature = "compress-gzip")] Deflate(Box>), + #[cfg(feature = "compress-gzip")] Gzip(Box>), + #[cfg(feature = "compress-brotli")] 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` + #[cfg(feature = "compress-zstd")] Zstd(Box>), } impl ContentDecoder { fn feed_eof(&mut self) -> io::Result> { match self { + #[cfg(feature = "compress-brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.flush() { Ok(()) => { let b = decoder.get_mut().take(); @@ -172,6 +187,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -185,6 +201,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); @@ -197,6 +214,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-zstd")] ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() { Ok(_) => { let b = decoder.get_mut().take(); @@ -213,6 +231,7 @@ impl ContentDecoder { fn feed_data(&mut self, data: Bytes) -> io::Result> { match self { + #[cfg(feature = "compress-brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -227,6 +246,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -241,6 +261,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-gzip")] ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; @@ -255,6 +276,7 @@ impl ContentDecoder { Err(e) => Err(e), }, + #[cfg(feature = "compress-zstd")] ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 36509b371..1e69990a0 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -9,12 +9,18 @@ use std::{ }; use actix_rt::task::{spawn_blocking, JoinHandle}; -use brotli2::write::BrotliEncoder; use bytes::Bytes; use derive_more::Display; -use flate2::write::{GzEncoder, ZlibEncoder}; use futures_core::ready; use pin_project::pin_project; + +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliEncoder; + +#[cfg(feature = "compress-gzip")] +use flate2::write::{GzEncoder, ZlibEncoder}; + +#[cfg(feature = "compress-zstd")] use zstd::stream::write::Encoder as ZstdEncoder; use crate::{ @@ -233,28 +239,36 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { } enum ContentEncoder { + #[cfg(feature = "compress-gzip")] Deflate(ZlibEncoder), + #[cfg(feature = "compress-gzip")] Gzip(GzEncoder), + #[cfg(feature = "compress-brotli")] 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` + #[cfg(feature = "compress-zstd")] Zstd(ZstdEncoder<'static, Writer>), } impl ContentEncoder { fn encoder(encoding: ContentEncoding) -> Option { match encoding { + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => { Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) } + #[cfg(feature = "compress-zstd")] ContentEncoding::Zstd => { let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; Some(ContentEncoder::Zstd(encoder)) @@ -266,27 +280,35 @@ impl ContentEncoder { #[inline] pub(crate) fn take(&mut self) -> Bytes { match *self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), } } fn finish(self) -> Result { match self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), @@ -296,6 +318,7 @@ impl ContentEncoder { fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { match *self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -303,6 +326,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -310,6 +334,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -317,6 +342,7 @@ impl ContentEncoder { Err(err) } }, + #[cfg(feature = "compress-zstd")] ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 9f94faaa5..924d5441f 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,12 +1,14 @@ //! HTTP primitives for the Actix ecosystem. //! //! ## Crate Features -//! | Feature | Functionality | -//! | ---------------- | ----------------------------------------------------- | -//! | `openssl` | TLS support via [OpenSSL]. | -//! | `rustls` | TLS support via [rustls]. | -//! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) | -//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | +//! | Feature | Functionality | +//! | ------------------- | ------------------------------------------- | +//! | `openssl` | TLS support via [OpenSSL]. | +//! | `rustls` | TLS support via [rustls]. | +//! | `compress-brotli` | Payload compression support: Brotli. | +//! | `compress-gzip` | Payload compression support: Deflate, Gzip. | +//! | `compress-zstd` | Payload compression support: Zstd. | +//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! //! [OpenSSL]: https://crates.io/crates/openssl //! [rustls]: https://crates.io/crates/rustls @@ -32,7 +34,8 @@ pub mod body; mod builder; pub mod client; mod config; -#[cfg(feature = "compress")] + +#[cfg(feature = "__compress")] pub mod encoding; mod extensions; pub mod header; diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index 2276fe745..fa554ba2e 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.1.0-beta.3 - 2021-06-20 +* No significant changes from `0.1.0-beta.2`. + + ## 0.1.0-beta.2 - 2021-04-17 * No significant changes from `0.1.0-beta.1`. diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 607038377..ca814e0e5 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0-beta.2" +version = "0.1.0-beta.3" authors = [ "Nikolay Kim ", "Rob Ede ", diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index a7ee7a9e1..decbe2219 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,6 +1,9 @@ # Changes ## Unreleased - 2021-xx-xx +* Update `actix` to `0.12`. [#2277] + +[#2277]: https://github.com/actix/actix-web/pull/2277 ## 4.0.0-beta.5 - 2021-06-17 diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 159b10d58..669cd4001 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,7 +16,7 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = { version = "0.11.0-beta.3", default-features = false } +actix = { version = "0.12.0", default-features = false } actix-codec = "0.4.0" actix-http = "3.0.0-beta.7" actix-web = { version = "4.0.0-beta.7", default-features = false } @@ -29,7 +29,7 @@ tokio = { version = "1", features = ["sync"] } [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" awc = { version = "3.0.0-beta.6", default-features = false } env_logger = "0.8" diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 29565f74a..327f16bc5 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -20,7 +20,7 @@ proc-macro2 = "1" [dev-dependencies] actix-rt = "2.2" -actix-test = "0.1.0-beta.2" +actix-test = "0.1.0-beta.3" actix-utils = "3.0.0" actix-web = "4.0.0-beta.7" diff --git a/awc/CHANGES.md b/awc/CHANGES.md index c66c6cda8..2e56eb958 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,6 +1,10 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* Change compression algorithm features flags. [#2250] + +[#2250]: https://github.com/actix/actix-web/pull/2250 ## 3.0.0-beta.6 - 2021-06-17 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 645f70101..26c625a05 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -24,10 +24,10 @@ path = "src/lib.rs" [package.metadata.docs.rs] # features that docs.rs will build with -features = ["openssl", "rustls", "compress", "cookies"] +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] [features] -default = ["compress", "cookies"] +default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] # openssl openssl = ["tls-openssl", "actix-http/openssl"] @@ -35,8 +35,12 @@ openssl = ["tls-openssl", "actix-http/openssl"] # rustls rustls = ["tls-rustls", "actix-http/rustls"] -# content-encoding support -compress = ["actix-http/compress"] +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] # cookie parsing and cookie jar cookies = ["cookie"] @@ -44,6 +48,10 @@ cookies = ["cookie"] # trust-dns as dns resolver trust-dns = ["actix-http/trust-dns"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + [dependencies] actix-codec = "0.4.0" actix-service = "2.0.0" @@ -52,6 +60,7 @@ actix-rt = { version = "2.1", default-features = false } base64 = "0.13" bytes = "1" +cfg-if = "1" cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } @@ -74,7 +83,7 @@ actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-utils = "3.0.0" actix-server = "2.0.0-beta.3" actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } -actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } brotli2 = "0.3.2" env_logger = "0.8" diff --git a/awc/examples/client.rs b/awc/examples/client.rs index 234ee3ae4..653cb226f 100644 --- a/awc/examples/client.rs +++ b/awc/examples/client.rs @@ -2,17 +2,19 @@ use std::error::Error as StdError; #[actix_web::main] async fn main() -> Result<(), Box> { - std::env::set_var("RUST_LOG", "actix_http=trace"); + std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace"); env_logger::init(); let client = awc::Client::new(); // Create request builder, configure request and send - let mut response = client + let request = client .get("https://www.rust-lang.org/") - .append_header(("User-Agent", "Actix-web")) - .send() - .await?; + .append_header(("User-Agent", "Actix-web")); + + println!("Request: {:?}", request); + + let mut response = request.send().await?; // server http response println!("Response: {:?}", response); diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 122f3845c..c0290ddcf 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,7 +1,6 @@ //! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem. //! -//! ## Making a GET request -//! +//! # Making a GET request //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -16,10 +15,8 @@ //! # } //! ``` //! -//! ## Making POST requests -//! -//! ### Raw body contents -//! +//! # Making POST requests +//! ## Raw body contents //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -31,8 +28,7 @@ //! # } //! ``` //! -//! ### Forms -//! +//! ## Forms //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -46,8 +42,7 @@ //! # } //! ``` //! -//! ### JSON -//! +//! ## JSON //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -64,8 +59,24 @@ //! # } //! ``` //! -//! ## WebSocket support +//! # Response Compression +//! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! +//! The `Accept-Encoding` header will automatically be populated with enabled codecs and added to +//! outgoing requests, allowing servers to select their `Content-Encoding` accordingly. +//! +//! Feature flags enable these codecs according to the table below. By default, all `compress-*` +//! features are enabled. +//! +//! | Feature | Codecs | +//! | ----------------- | ------------- | +//! | `compress-brotli` | brotli | +//! | `compress-gzip` | gzip, deflate | +//! | `compress-zstd` | zstd | +//! +//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding +//! +//! # WebSocket support //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), Box> { @@ -128,6 +139,9 @@ pub use self::sender::SendClientRequest; /// An asynchronous HTTP and WebSocket client. /// +/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU +/// and memory usage. +/// /// # Examples /// ``` /// use awc::Client; @@ -136,10 +150,10 @@ pub use self::sender::SendClientRequest; /// async fn main() { /// let mut client = Client::default(); /// -/// let res = client.get("http://www.rust-lang.org") // <- Create request builder -/// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send HTTP request -/// .await; // <- send request and wait for response +/// let res = client.get("http://www.rust-lang.org") +/// .insert_header(("User-Agent", "my-app/1.2")) +/// .send() +/// .await; /// /// println!("Response: {:?}", res); /// } diff --git a/awc/src/request.rs b/awc/src/request.rs index c95cee839..46dae7fa3 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -8,7 +8,7 @@ use actix_http::{ body::Body, http::{ header::{self, IntoHeaderPair}, - uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, + ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, }, RequestHead, }; @@ -22,11 +22,6 @@ use crate::{ ClientConfig, }; -#[cfg(feature = "compress")] -const HTTPS_ENCODING: &str = "br, gzip, deflate"; -#[cfg(not(feature = "compress"))] -const HTTPS_ENCODING: &str = "br"; - /// An HTTP Client request builder /// /// This type can be used to construct an instance of `ClientRequest` through a @@ -480,22 +475,37 @@ impl ClientRequest { let mut slf = self; + // Set Accept-Encoding HTTP header depending on enabled feature. + // If decompress is not ask, then we are not able to find which encoding is + // supported, so we cannot guess Accept-Encoding HTTP header. if slf.response_decompress { - let https = slf - .head - .uri - .scheme() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true); + // Set Accept-Encoding with compression algorithm awc is built with. + #[cfg(feature = "__compress")] + let accept_encoding = { + let mut encoding = vec![]; - if https { - slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING)); - } else { - #[cfg(feature = "compress")] + #[cfg(feature = "compress-brotli")] + encoding.push("br"); + + #[cfg(feature = "compress-gzip")] { - slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, "gzip, deflate")); + encoding.push("gzip"); + encoding.push("deflate"); } + + #[cfg(feature = "compress-zstd")] + encoding.push("zstd"); + + assert!(!encoding.is_empty(), "encoding cannot be empty unless __compress feature has been explictily enabled."); + encoding.join(", ") }; + + // Otherwise tell the server, we do not support any compression algorithm. + // So we clearly indicate that we do want identity encoding. + #[cfg(not(feature = "__compress"))] + let accept_encoding = "identity"; + + slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, accept_encoding)); } Ok(slf) diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 7ac9c8ce9..c0639606e 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -22,7 +22,7 @@ use derive_more::From; use futures_core::Stream; use serde::Serialize; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream}; use crate::{ @@ -91,7 +91,7 @@ impl SendClientRequest { } } -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] impl Future for SendClientRequest { type Output = Result>>, SendRequestError>; @@ -131,7 +131,7 @@ impl Future for SendClientRequest { } } -#[cfg(not(feature = "compress"))] +#[cfg(not(feature = "__compress"))] impl Future for SendClientRequest { type Output = Result; diff --git a/src/app.rs b/src/app.rs index 357d45eeb..8c622dd36 100644 --- a/src/app.rs +++ b/src/app.rs @@ -98,13 +98,18 @@ where /// web::resource("/index.html").route( /// web::get().to(index))); /// ``` + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } - /// Set application data factory. This function is - /// similar to `.data()` but it accepts data factory. Data object get - /// constructed asynchronously during application initialization. + /// Add application data factory. This function is similar to `.data()` but it accepts a + /// "data factory". Data values are constructed asynchronously during application + /// initialization, before the server starts accepting requests. + #[deprecated( + since = "4.0.0", + note = "Construct data value before starting server and use `.app_data(Data::new(val))` instead." + )] pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -518,6 +523,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory() { let srv = init_service( @@ -541,6 +548,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory_errors() { let srv = try_init_service( diff --git a/src/app_service.rs b/src/app_service.rs index 2139ebd7c..01e1b5a2e 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -365,6 +365,8 @@ mod tests { } } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_drop_data() { let data = Arc::new(AtomicBool::new(false)); diff --git a/src/config.rs b/src/config.rs index a40dd94f2..642ffc57c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -124,6 +124,7 @@ impl AppConfig { AppConfig { secure, host, addr } } + /// Needed in actix-test crate. #[doc(hidden)] pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { AppConfig::new(secure, host, addr) diff --git a/src/data.rs b/src/data.rs index f09a88891..51db6ce4c 100644 --- a/src/data.rs +++ b/src/data.rs @@ -154,6 +154,8 @@ mod tests { web, App, HttpResponse, }; + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_extractor() { let srv = init_service(App::new().data("TEST".to_string()).service( @@ -221,6 +223,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_route_data_extractor() { let srv = init_service( @@ -250,6 +254,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { let srv = diff --git a/src/extract.rs b/src/extract.rs index 45cb330a3..d7b67cd90 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,12 +1,14 @@ //! Request extractors use std::{ + convert::Infallible, future::Future, pin::Pin, task::{Context, Poll}, }; -use actix_utils::future::{ready, Ready}; +use actix_http::http::{Method, Uri}; +use actix_utils::future::{ok, Ready}; use futures_core::ready; use crate::{dev::Payload, Error, HttpRequest}; @@ -216,14 +218,58 @@ where } } +/// Extract the request's URI. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Uri, web, App, Responder}; +/// +/// async fn handler(uri: Uri) -> impl Responder { +/// format!("Requested path: {}", uri.path()) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Uri { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.uri().clone()) + } +} + +/// Extract the request's method. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Method, web, App, Responder}; +/// +/// async fn handler(method: Method) -> impl Responder { +/// format!("Request method: {}", method) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Method { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.method().clone()) + } +} + #[doc(hidden)] impl FromRequest for () { - type Error = Error; - type Future = Ready>; + type Error = Infallible; + type Future = Ready>; type Config = (); fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(Ok(())) + ok(()) } } @@ -411,4 +457,18 @@ mod tests { .unwrap(); assert!(r.is_err()); } + + #[actix_rt::test] + async fn test_uri() { + let req = TestRequest::default().uri("/foo/bar").to_http_request(); + let uri = Uri::extract(&req).await.unwrap(); + assert_eq!(uri.path(), "/foo/bar"); + } + + #[actix_rt::test] + async fn test_method() { + let req = TestRequest::default().method(Method::GET).to_http_request(); + let method = Method::extract(&req).await.unwrap(); + assert_eq!(method, Method::GET); + } } diff --git a/src/http/header/encoding.rs b/src/http/header/encoding.rs index aa49dea45..ce31c100f 100644 --- a/src/http/header/encoding.rs +++ b/src/http/header/encoding.rs @@ -1,7 +1,7 @@ use std::{fmt, str}; pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, + Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd, }; /// A value to represent an encoding used in `Transfer-Encoding` @@ -22,6 +22,8 @@ pub enum Encoding { Identity, /// The `trailers` encoding. Trailers, + /// The `zstd` encoding. + Zstd, /// Some other encoding that is less common, can be any String. EncodingExt(String), } @@ -36,6 +38,7 @@ impl fmt::Display for Encoding { Compress => "compress", Identity => "identity", Trailers => "trailers", + Zstd => "zstd", EncodingExt(ref s) => s.as_ref(), }) } @@ -52,6 +55,7 @@ impl str::FromStr for Encoding { "compress" => Ok(Compress), "identity" => Ok(Identity), "trailers" => Ok(Trailers), + "zstd" => Ok(Zstd), _ => Ok(EncodingExt(s.to_owned())), } } diff --git a/src/info.rs b/src/info.rs index c9ddf6ec4..c6ff54efe 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,13 +1,36 @@ -use std::cell::Ref; +use std::{cell::Ref, convert::Infallible, net::SocketAddr}; -use crate::dev::{AppConfig, RequestHead}; -use crate::http::header::{self, HeaderName}; +use actix_utils::future::{err, ok, Ready}; +use derive_more::{Display, Error}; + +use crate::{ + dev::{AppConfig, Payload, RequestHead}, + http::header::{self, HeaderName}, + FromRequest, HttpRequest, ResponseError, +}; const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; -/// `HttpRequest` connection information +/// HTTP connection information. +/// +/// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers. +/// +/// # Examples +/// ``` +/// # use actix_web::{HttpResponse, Responder}; +/// use actix_web::dev::ConnectionInfo; +/// +/// async fn handler(conn: ConnectionInfo) -> impl Responder { +/// match conn.host() { +/// "actix.rs" => HttpResponse::Ok().body("Welcome!"), +/// "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."), +/// _ => HttpResponse::NotFound().finish() +/// } +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { scheme: String, @@ -187,6 +210,65 @@ impl ConnectionInfo { } } +impl FromRequest for ConnectionInfo { + type Error = Infallible; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.connection_info().clone()) + } +} + +/// Extractor for peer's socket address. +/// +/// Also see [`HttpRequest::peer_addr`]. +/// +/// # Examples +/// ``` +/// # use actix_web::Responder; +/// use actix_web::dev::PeerAddr; +/// +/// async fn handler(peer_addr: PeerAddr) -> impl Responder { +/// let socket_addr = peer_addr.0; +/// socket_addr.to_string() +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] +#[display(fmt = "{}", _0)] +pub struct PeerAddr(pub SocketAddr); + +impl PeerAddr { + /// Unwrap into inner `SocketAddr` value. + pub fn into_inner(self) -> SocketAddr { + self.0 + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +#[display(fmt = "Missing peer address")] +pub struct MissingPeerAddr; + +impl ResponseError for MissingPeerAddr {} + +impl FromRequest for PeerAddr { + type Error = MissingPeerAddr; + type Future = Ready>; + type Config = (); + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + match req.peer_addr() { + Some(addr) => ok(PeerAddr(addr)), + None => { + log::error!("Missing peer address."); + err(MissingPeerAddr) + } + } + } +} + #[cfg(test)] mod tests { use super::*; @@ -239,4 +321,25 @@ mod tests { let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } + + #[actix_rt::test] + async fn test_conn_info() { + let req = TestRequest::default() + .uri("http://actix.rs/") + .to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.scheme(), "http"); + } + + #[actix_rt::test] + async fn test_peer_addr() { + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let peer_addr = PeerAddr::extract(&req).await.unwrap(); + assert_eq!(peer_addr, PeerAddr(addr)); + + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + } } diff --git a/src/lib.rs b/src/lib.rs index b488b962b..9d8bf62e7 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -47,7 +47,7 @@ //! * Streaming and pipelining //! * Keep-alive and slow requests handling //! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -//! * Transparent content compression/decompression (br, gzip, deflate) +//! * Transparent content compression/decompression (br, gzip, deflate, zstd) //! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) //! * Multipart streams //! * Static assets @@ -57,8 +57,10 @@ //! * Runs on stable Rust 1.46+ //! //! # Crate Features -//! * `compress` - content encoding compression support (enabled by default) //! * `cookies` - cookies support (enabled by default) +//! * `compress-brotli` - brotli content encoding compression support (enabled by default) +//! * `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) +//! * `compress-zstd` - zstd content encoding compression support (enabled by default) //! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` //! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` //! * `secure-cookies` - secure cookies support @@ -129,7 +131,7 @@ pub mod dev { pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] pub use crate::handler::Handler; - pub use crate::info::ConnectionInfo; + pub use crate::info::{ConnectionInfo, PeerAddr}; pub use crate::rmap::ResourceMap; pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; @@ -140,7 +142,8 @@ pub mod dev { pub use actix_http::body::{ AnyBody, Body, BodySize, MessageBody, ResponseBody, SizedStream, }; - #[cfg(feature = "compress")] + + #[cfg(feature = "__compress")] pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::ResponseBuilder as BaseHttpResponseBuilder; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 95f5f4b52..0a6256fe2 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -144,7 +144,7 @@ mod tests { use crate::{web, App, HttpResponse}; #[actix_rt::test] - #[cfg(all(feature = "cookies", feature = "compress"))] + #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_scope_middleware() { use crate::middleware::Compress; @@ -167,7 +167,7 @@ mod tests { } #[actix_rt::test] - #[cfg(all(feature = "cookies", feature = "compress"))] + #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_resource_scope_middleware() { use crate::middleware::Compress; diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index e24782f07..96a361fcf 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -14,7 +14,8 @@ pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; pub use self::normalize::{NormalizePath, TrailingSlash}; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] mod compress; -#[cfg(feature = "compress")] + +#[cfg(feature = "__compress")] pub use self::compress::Compress; diff --git a/src/request.rs b/src/request.rs index 42c722c46..bff66f08e 100644 --- a/src/request.rs +++ b/src/request.rs @@ -60,18 +60,6 @@ impl HttpRequest { }), } } - - #[doc(hidden)] - pub fn __priv_test_new( - path: Path, - head: Message, - rmap: Rc, - config: AppConfig, - app_data: Rc, - ) -> HttpRequest { - let app_state = AppInitServiceState::new(rmap, config); - Self::new(path, head, app_state, app_data) - } } impl HttpRequest { @@ -723,6 +711,8 @@ mod tests { assert_eq!(body, Bytes::from_static(b"1")); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_extensions_dropped() { struct Tracker { diff --git a/src/resource.rs b/src/resource.rs index e1299edd2..d9f15e329 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -196,6 +196,7 @@ where /// )); /// } /// ``` + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } @@ -679,6 +680,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::NO_CONTENT); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let srv = init_service( @@ -711,6 +714,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { let srv = init_service( diff --git a/src/response/response.rs b/src/response/response.rs index 9dd804be0..9a3bb2874 100644 --- a/src/response/response.rs +++ b/src/response/response.rs @@ -49,7 +49,10 @@ impl HttpResponse { /// Create an error response. #[inline] pub fn from_error(error: impl Into) -> Self { - error.into().as_response_error().error_response() + let error = error.into(); + let mut response = error.as_response_error().error_response(); + response.error = Some(error); + response } } diff --git a/src/scope.rs b/src/scope.rs index 20dd3e215..7567737c4 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -146,6 +146,7 @@ where /// ); /// } /// ``` + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } @@ -994,6 +995,8 @@ mod tests { ); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_middleware_app_data() { let srv = init_service( @@ -1032,6 +1035,8 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { let srv = init_service(App::new().data(1usize).service( diff --git a/src/service.rs b/src/service.rs index 308b00ad3..fbfa4b0a8 100644 --- a/src/service.rs +++ b/src/service.rs @@ -74,18 +74,18 @@ impl ServiceRequest { Self { req, payload } } - /// Construct service request. - #[doc(hidden)] - pub fn __priv_test_new(req: HttpRequest, payload: Payload) -> Self { - Self::new(req, payload) - } - /// Deconstruct request into parts #[inline] pub fn into_parts(self) -> (HttpRequest, Payload) { (self.req, self.payload) } + /// Get mutable access to inner `HttpRequest` and `Payload` + #[inline] + pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { + (&mut self.req, &mut self.payload) + } + /// Construct request from parts. pub fn from_parts(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } @@ -655,6 +655,8 @@ mod tests { assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_service_data() { let srv = diff --git a/src/test.rs b/src/test.rs index de97dc8aa..05a4ba7f2 100644 --- a/src/test.rs +++ b/src/test.rs @@ -839,6 +839,8 @@ mod tests { assert!(res.status().is_success()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_server_data() { async fn handler(data: web::Data) -> impl Responder { diff --git a/src/types/form.rs b/src/types/form.rs index ce85983d1..4ce075d99 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -16,7 +16,7 @@ use futures_core::{future::LocalBoxFuture, ready}; use futures_util::{FutureExt as _, StreamExt as _}; use serde::{de::DeserializeOwned, Serialize}; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, Error, @@ -80,6 +80,10 @@ use crate::{ /// }) /// } /// ``` +/// +/// # Panics +/// URL encoded forms consist of unordered `key=value` pairs, therefore they cannot be decoded into +/// any type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. #[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Form(pub T); @@ -255,9 +259,9 @@ impl Default for FormConfig { /// - content type is not `application/x-www-form-urlencoded` /// - content length is greater than [limit](UrlEncoded::limit()) pub struct UrlEncoded { - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] stream: Option>, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] stream: Option, limit: usize, @@ -293,10 +297,15 @@ impl UrlEncoded { } }; - #[cfg(feature = "compress")] - let payload = Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let payload = payload.take(); + let payload = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; UrlEncoded { encoding, diff --git a/src/types/json.rs b/src/types/json.rs index 44b548355..fc02c8854 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -16,7 +16,7 @@ use serde::{de::DeserializeOwned, Serialize}; use actix_http::Payload; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ error::{Error, JsonPayloadError}, @@ -300,9 +300,9 @@ pub enum JsonBody { Body { limit: usize, length: Option, - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] payload: Decompress, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] payload: Payload, buf: BytesMut, _res: PhantomData, @@ -345,10 +345,15 @@ where // As the internal usage always call JsonBody::limit after JsonBody::new. // And limit check to return an error variant of JsonBody happens there. - #[cfg(feature = "compress")] - let payload = Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let payload = payload.take(); + let payload = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; JsonBody::Body { limit: DEFAULT_LIMIT, diff --git a/src/types/payload.rs b/src/types/payload.rs index d69e0a126..87378701b 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -282,9 +282,9 @@ impl Default for PayloadConfig { pub struct HttpMessageBody { limit: usize, length: Option, - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] stream: dev::Decompress, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] stream: dev::Payload, buf: BytesMut, err: Option, @@ -312,10 +312,15 @@ impl HttpMessageBody { } } - #[cfg(feature = "compress")] - let stream = dev::Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let stream = payload.take(); + let stream = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + dev::Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; HttpMessageBody { stream, @@ -393,6 +398,8 @@ mod tests { assert!(cfg.check_mimetype(&req).is_ok()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_config_recall_locations() { async fn bytes_handler(_: Bytes) -> impl Responder { diff --git a/tests/test_error_propagation.rs b/tests/test_error_propagation.rs new file mode 100644 index 000000000..1b56615a0 --- /dev/null +++ b/tests/test_error_propagation.rs @@ -0,0 +1,100 @@ +use actix_utils::future::{ok, Ready}; +use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; +use actix_web::test::{call_service, init_service, TestRequest}; +use actix_web::{HttpResponse, ResponseError}; +use futures_util::lock::Mutex; +use std::future::Future; +use std::pin::Pin; +use std::sync::Arc; +use std::task::{Context, Poll}; + +#[derive(Debug, Clone)] +pub struct MyError; + +impl ResponseError for MyError {} + +impl std::fmt::Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "A custom error") + } +} + +#[actix_web::get("/test")] +async fn test() -> Result { + Err(MyError)?; + Ok(HttpResponse::NoContent().finish()) +} + +#[derive(Clone)] +pub struct SpyMiddleware(Arc>>); + +impl Transform for SpyMiddleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = Middleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(Middleware { + was_error: self.0.clone(), + service, + }) + } +} + +#[doc(hidden)] +pub struct Middleware { + was_error: Arc>>, + service: S, +} + +impl Service for Middleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = Pin>>>; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + self.service.poll_ready(cx) + } + + fn call(&self, req: ServiceRequest) -> Self::Future { + let lock = self.was_error.clone(); + let response_future = self.service.call(req); + Box::pin(async move { + let response = response_future.await; + if let Ok(success) = &response { + *lock.lock().await = Some(success.response().error().is_some()); + } + response + }) + } +} + +#[actix_rt::test] +async fn error_cause_should_be_propagated_to_middlewares() { + let lock = Arc::new(Mutex::new(None)); + let spy_middleware = SpyMiddleware(lock.clone()); + + let app = init_service( + actix_web::App::new() + .wrap(spy_middleware.clone()) + .service(test), + ) + .await; + + call_service(&app, TestRequest::with_uri("/test").to_request()).await; + + let was_error_captured = lock.lock().await.unwrap(); + assert!(was_error_captured); +} diff --git a/tests/test_server.rs b/tests/test_server.rs index 520eb5ce2..afea39dd9 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -879,7 +879,7 @@ async fn test_brotli_encoding_large_openssl() { assert_eq!(bytes, Bytes::from(data)); } -#[cfg(all(feature = "rustls", feature = "openssl"))] +#[cfg(feature = "rustls")] mod plus_rustls { use std::io::BufReader; @@ -1028,6 +1028,8 @@ async fn test_normalize() { assert!(response.status().is_success()); } +// allow deprecated App::data +#[allow(deprecated)] #[actix_rt::test] async fn test_data_drop() { use std::sync::{