diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7d0520d52..dc0ff0c19 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,6 +26,21 @@ jobs: steps: - uses: actions/checkout@v2 + # install OpenSSL on Windows + - name: Set vcpkg root + if: matrix.target.triple == 'x86_64-pc-windows-msvc' + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Install OpenSSL + if: matrix.target.triple == 'x86_64-pc-windows-msvc' + run: vcpkg install openssl:x64-windows + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} + profile: minimal + override: true + - name: Install ${{ matrix.version }} uses: actions-rs/toolchain@v1 with: diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml index c080dd8c3..94a2ddfbe 100644 --- a/.github/workflows/upload-doc.yml +++ b/.github/workflows/upload-doc.yml @@ -1,14 +1,12 @@ -name: Upload documentation +name: Upload Documentation on: push: - branches: - - master + branches: [master] jobs: build: runs-on: ubuntu-latest - if: github.repository == 'actix/actix-web' steps: - uses: actions/checkout@v2 @@ -20,14 +18,14 @@ jobs: profile: minimal override: true - - name: check build + - name: Build Docs uses: actions-rs/cargo@v1 with: command: doc args: --workspace --all-features --no-deps - name: Tweak HTML - run: echo "" > target/doc/index.html + run: echo '' > target/doc/index.html - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@3.7.1 diff --git a/CHANGES.md b/CHANGES.md index 7cb03c30c..5df0b6d6d 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,26 @@ # Changes ## Unreleased - 2021-xx-xx +### Fixed +* Double ampersand in Logger format is escaped correctly. [#2067] + +### Changed +* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed instead of skipping. + (Only the first error is kept when multiple error occur) [#2093] + +### Removed +* The `client` mod was removed. Clients should now use `awc` directly. + [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) + +[#2067]: https://github.com/actix/actix-web/pull/2067 +[#2093]: https://github.com/actix/actix-web/pull/2093 + + +## 4.0.0-beta.4 - 2021-03-09 ### Changed * Feature `cookies` is now optional and enabled by default. [#1981] -* `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the - default behaviour of the `web::Json` extractor. [#2010] +* `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default + behaviour of the `web::Json` extractor. [#2010] [#1981]: https://github.com/actix/actix-web/pull/1981 [#2010]: https://github.com/actix/actix-web/pull/2010 @@ -168,7 +184,7 @@ ## 3.0.0-beta.4 - 2020-09-09 ### Added -* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing +* `middleware::NormalizePath` now has configurable behavior for either always having a trailing slash, or as the new addition, always trimming trailing slashes. [#1639] ### Changed @@ -496,7 +512,7 @@ ## [1.0.0-rc] - 2019-05-18 -### Add +### Added * Add `Query::from_query()` to extract parameters from a query string. #846 * `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. @@ -512,7 +528,7 @@ ## [1.0.0-beta.4] - 2019-05-12 -### Add +### Added * Allow to set/override app data on scope level @@ -538,7 +554,7 @@ * CORS handling without headers #702 -* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types. +* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. ### Fixed @@ -602,7 +618,7 @@ ### Changed -* Allow to use any service as default service. +* Allow using any service as default service. * Remove generic type for request payload, always use default. @@ -665,13 +681,13 @@ ### Added -* rustls support +* Rustls support ### Changed -* use forked cookie +* Use forked cookie -* multipart::Field renamed to MultipartField +* Multipart::Field renamed to MultipartField ## [1.0.0-alpha.1] - 2019-03-28 diff --git a/Cargo.toml b/Cargo.toml index 1a1b8645c..f3a6271ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web" -version = "4.0.0-beta.3" +version = "4.0.0-beta.4" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" readme = "README.md" @@ -66,7 +66,7 @@ required-features = ["compress"] [[test]] name = "test_server" -required-features = ["compress"] +required-features = ["compress", "cookies"] [[example]] name = "on_connect" @@ -80,15 +80,15 @@ required-features = ["rustls"] actix-codec = "0.4.0-beta.1" actix-macros = "0.2.0" actix-router = "0.2.7" -actix-rt = "2" +actix-rt = "2.1" actix-server = "2.0.0-beta.3" actix-service = "2.0.0-beta.4" actix-utils = "3.0.0-beta.2" -actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true } +actix-tls = { version = "3.0.0-beta.4", default-features = false, optional = true } -actix-web-codegen = "0.5.0-beta.1" -actix-http = "3.0.0-beta.3" -awc = { version = "3.0.0-beta.2", default-features = false } +actix-web-codegen = "0.5.0-beta.2" +actix-http = "3.0.0-beta.4" +awc = { version = "3.0.0-beta.3", default-features = false } ahash = "0.7" bytes = "1" @@ -105,18 +105,12 @@ serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.7" smallvec = "1.6" -socket2 = "0.3.16" +socket2 = "0.4.0" time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { package = "openssl", version = "0.10.9", optional = true } tls-rustls = { package = "rustls", version = "0.19.0", optional = true } url = "2.1" -[target.'cfg(windows)'.dependencies.tls-openssl] -version = "0.10.9" -package = "openssl" -features = ["vendored"] -optional = true - [dev-dependencies] brotli2 = "0.3.2" criterion = "0.3" diff --git a/README.md b/README.md index bf68e7961..64fd7d08d 100644 --- a/README.md +++ b/README.md @@ -6,10 +6,10 @@

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.2)](https://docs.rs/actix-web/4.0.0-beta.2) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.4)](https://docs.rs/actix-web/4.0.0-beta.4) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.2/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.2) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.4)
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) @@ -71,18 +71,18 @@ async fn main() -> std::io::Result<()> { ### More examples -* [Basic Setup](https://github.com/actix/examples/tree/master/basics/) -* [Application State](https://github.com/actix/examples/tree/master/state/) -* [JSON Handling](https://github.com/actix/examples/tree/master/json/) -* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/) -* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/) -* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/) -* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/) -* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/) -* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/) -* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/) -* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/) -* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/) +* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/) +* [Application State](https://github.com/actix/examples/tree/master/basics/state/) +* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/) +* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/) +* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/) +* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/) +* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/) +* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/) +* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/) +* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/) +* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/) +* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/) You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples. diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6e2a241ac..c035d5afe 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.6.0-beta.3 - 2021-03-09 +* No notable changes. + + ## 0.6.0-beta.2 - 2021-02-10 * Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] * Replace `v_htmlescape` with `askama_escape`. [#1953] diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 08b7b36fc..49cd6966c 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.0-beta.2" +version = "0.6.0-beta.3" authors = ["Nikolay Kim "] description = "Static file serving for Actix Web" readme = "README.md" @@ -17,7 +17,7 @@ name = "actix_files" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.3", default-features = false } +actix-web = { version = "4.0.0-beta.4", default-features = false } actix-service = "2.0.0-beta.4" askama_escape = "0.10" @@ -33,5 +33,5 @@ mime_guess = "2.0.1" percent-encoding = "2.1" [dev-dependencies] -actix-rt = "2" -actix-web = "4.0.0-beta.3" +actix-rt = "2.1" +actix-web = "4.0.0-beta.4" diff --git a/actix-files/README.md b/actix-files/README.md index 463f20224..c7b7424ec 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,17 +3,17 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.5.0)](https://docs.rs/actix-files/0.5.0) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.3)](https://docs.rs/actix-files/0.6.0-beta.3) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.5.0/status.svg)](https://deps.rs/crate/actix-files/0.5.0) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.3/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.3) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-files/) -- [Example Project](https://github.com/actix/examples/tree/master/static_index) +- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) - [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum supported Rust version: 1.46 or later diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 6f8b28bbf..292e3fdf3 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -19,7 +19,7 @@ use crate::{ /// /// `Files` service must be registered with `App::service()` method. /// -/// ```rust +/// ``` /// use actix_web::App; /// use actix_files::Files; /// diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 04dd9f07f..018079b21 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -3,7 +3,7 @@ //! Provides a non-blocking service for serving static files from disk. //! //! # Example -//! ```rust +//! ``` //! use actix_web::App; //! use actix_files::Files; //! @@ -662,8 +662,12 @@ mod tests { #[actix_rt::test] async fn test_static_files_bad_directory() { - let _st: Files = Files::new("/", "missing"); - let _st: Files = Files::new("/", "Cargo.toml"); + let service = Files::new("/", "./missing").new_service(()).await.unwrap(); + + let req = TestRequest::with_uri("/").to_srv_request(); + let resp = test::call_service(&service, req).await; + + assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] @@ -676,75 +680,34 @@ mod tests { .await .unwrap(); let req = TestRequest::with_uri("/missing").to_srv_request(); - let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; assert_eq!(bytes, web::Bytes::from_static(b"default content")); } - // #[actix_rt::test] - // async fn test_serve_index() { - // let st = Files::new(".").index_file("test.binary"); - // let req = TestRequest::default().uri("/tests").finish(); + #[actix_rt::test] + async fn test_serve_index_nested() { + let service = Files::new(".", ".") + .index_file("lib.rs") + .new_service(()) + .await + .unwrap(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers() - // .get(header::CONTENT_TYPE) - // .expect("content type"), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers() - // .get(header::CONTENT_DISPOSITION) - // .expect("content disposition"), - // "attachment; filename=\"test.binary\"" - // ); + let req = TestRequest::default().uri("/src").to_srv_request(); + let resp = test::call_service(&service, req).await; - // let req = TestRequest::default().uri("/tests/").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "attachment; filename=\"test.binary\"" - // ); - - // // nonexistent index file - // let req = TestRequest::default().uri("/tests/unknown").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - - // let req = TestRequest::default().uri("/tests/unknown/").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // } - - // #[actix_rt::test] - // async fn test_serve_index_nested() { - // let st = Files::new(".").index_file("mod.rs"); - // let req = TestRequest::default().uri("/src/client").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/x-rust" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"mod.rs\"" - // ); - // } + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-rust" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"lib.rs\"" + ); + } #[actix_rt::test] async fn integration_serve_index() { diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index a688b2e6c..2846646a2 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -60,7 +60,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ``` /// use actix_files::NamedFile; /// use std::io::{self, Write}; /// use std::env; @@ -137,7 +137,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ``` /// use actix_files::NamedFile; /// /// let file = NamedFile::open("foo.txt"); @@ -156,7 +156,7 @@ impl NamedFile { /// /// # Examples /// - /// ```rust + /// ``` /// # use std::io; /// use actix_files::NamedFile; /// diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 14eea6ebc..3214963ed 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,4 +1,4 @@ -use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll}; +use std::{fmt, io, path::PathBuf, rc::Rc}; use actix_service::Service; use actix_web::{ diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 2f47d700d..d6a2cdd9b 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.3 - 2021-03-09 +* No notable changes. + + ## 3.0.0-beta.2 - 2021-02-10 * No notable changes. diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 6dcf73637..a7efc5310 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.2" +version = "3.0.0-beta.3" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" readme = "README.md" @@ -31,18 +31,18 @@ openssl = ["tls-openssl", "awc/openssl"] [dependencies] actix-service = "2.0.0-beta.4" actix-codec = "0.4.0-beta.1" -actix-tls = "3.0.0-beta.3" +actix-tls = "3.0.0-beta.4" actix-utils = "3.0.0-beta.2" -actix-rt = "2" +actix-rt = "2.1" actix-server = "2.0.0-beta.3" -awc = "3.0.0-beta.2" +awc = { version = "3.0.0-beta.3", default-features = false } base64 = "0.13" bytes = "1" futures-core = { version = "0.3.7", default-features = false } http = "0.2.2" log = "0.4" -socket2 = "0.3" +socket2 = "0.4" serde = "1.0" serde_json = "1.0" slab = "0.4" @@ -50,12 +50,6 @@ serde_urlencoded = "0.7" time = { version = "0.2.23", default-features = false, features = ["std"] } tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -[target.'cfg(windows)'.dependencies.tls-openssl] -version = "0.10.9" -package = "openssl" -features = ["vendored"] -optional = true - [dev-dependencies] -actix-web = "4.0.0-beta.3" -actix-http = "3.0.0-beta.3" +actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.4" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 66f15979d..8cec94808 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,9 +3,9 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=2.1.0)](https://docs.rs/actix-http-test/2.1.0) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.3)](https://docs.rs/actix-http-test/3.0.0-beta.3) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) -[![Dependency Status](https://deps.rs/crate/actix-http-test/2.1.0/status.svg)](https://deps.rs/crate/actix-http-test/2.1.0) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.3) [![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation & Resources diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 8de07c8d3..3749b78ca 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -26,7 +26,7 @@ use socket2::{Domain, Protocol, Socket, Type}; /// /// # Examples /// -/// ```rust +/// ``` /// use actix_http::HttpService; /// use actix_http_test::TestServer; /// use actix_web::{web, App, HttpResponse, Error}; @@ -118,10 +118,10 @@ pub async fn test_server_with_addr>( /// Get first available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); + let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap(); socket.bind(&addr.into()).unwrap(); socket.set_reuse_address(true).unwrap(); - let tcp = socket.into_tcp_listener(); + let tcp = net::TcpListener::from(socket); tcp.local_addr().unwrap() } diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 6ba111eb3..c4e0aec89 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,15 +1,32 @@ # Changes ## Unreleased - 2021-xx-xx +### Added +* `client::Connector::handshake_timeout` method for customize tls connection handshake timeout. [#2081] +* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] +* `client::ConnectionIo` trait alias [#2081] + +### Chaged +* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] + +[#2063]: https://github.com/actix/actix-web/pull/2063 +[#2081]: https://github.com/actix/actix-web/pull/2081 + + +## 3.0.0-beta.4 - 2021-03-08 ### Changed * Feature `cookies` is now optional and disabled by default. [#1981] +* `ws::hash_key` now returns array. [#2035] +* `ResponseBuilder::json` now takes `impl Serialize`. [#2052] ### Removed -* re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] +* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] * `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994] [#1981]: https://github.com/actix/actix-web/pull/1981 [#1994]: https://github.com/actix/actix-web/pull/1994 +[#2035]: https://github.com/actix/actix-web/pull/2035 +[#2052]: https://github.com/actix/actix-web/pull/2052 ## 3.0.0-beta.3 - 2021-02-10 diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 78fb55079..e1aebb76b 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "3.0.0-beta.3" +version = "3.0.0-beta.4" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" readme = "README.md" @@ -47,8 +47,8 @@ trust-dns = ["trust-dns-resolver"] actix-service = "2.0.0-beta.4" actix-codec = "0.4.0-beta.1" actix-utils = "3.0.0-beta.2" -actix-rt = "2" -actix-tls = "3.0.0-beta.2" +actix-rt = "2.1" +actix-tls = "3.0.0-beta.4" ahash = "0.7" base64 = "0.13" @@ -61,12 +61,12 @@ derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } -h2 = "0.3.0" +h2 = "0.3.1" http = "0.2.2" httparse = "1.3" itoa = "0.4" language-tags = "0.2" -lazy_static = "1.4" +once_cell = "1.5" log = "0.4" mime = "0.3" percent-encoding = "2.1" @@ -89,8 +89,8 @@ trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] actix-server = "2.0.0-beta.3" -actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] } +actix-tls = { version = "3.0.0-beta.4", features = ["openssl"] } criterion = "0.3" env_logger = "0.8" rcgen = "0.8" @@ -98,10 +98,9 @@ serde_derive = "1.0" tls-openssl = { version = "0.10", package = "openssl" } tls-rustls = { version = "0.19", package = "rustls" } -[target.'cfg(windows)'.dev-dependencies.tls-openssl] -version = "0.10.9" -package = "openssl" -features = ["vendored"] +[[example]] +name = "ws" +required-features = ["rustls"] [[bench]] name = "write-camel-case" diff --git a/actix-http/README.md b/actix-http/README.md index 881fbc8c5..53fedd40e 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,11 +3,11 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.3)](https://docs.rs/actix-http/3.0.0-beta.3) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http/3.0.0-beta.4) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.3) +[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.4) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 90d768cbe..176ac5c2b 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -3,7 +3,7 @@ use std::{env, io}; use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; -use futures_util::StreamExt; +use futures_util::StreamExt as _; use http::header::HeaderValue; use log::info; diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index bc932ce8f..408a40114 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -4,7 +4,7 @@ use actix_http::http::HeaderValue; use actix_http::{Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; -use futures_util::StreamExt; +use futures_util::StreamExt as _; use log::info; async fn handle_request(mut req: Request) -> Result { diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs new file mode 100644 index 000000000..4e03aa8ab --- /dev/null +++ b/actix-http/examples/ws.rs @@ -0,0 +1,107 @@ +//! Sets up a WebSocket server over TCP and TLS. +//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames. + +extern crate tls_rustls as rustls; + +use std::{ + env, io, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +use actix_codec::Encoder; +use actix_http::{error::Error, ws, HttpService, Request, Response}; +use actix_rt::time::{interval, Interval}; +use actix_server::Server; +use bytes::{Bytes, BytesMut}; +use bytestring::ByteString; +use futures_core::{ready, Stream}; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env::set_var("RUST_LOG", "actix=info,h2_ws=info"); + env_logger::init(); + + Server::build() + .bind("tcp", ("127.0.0.1", 8080), || { + HttpService::build().h1(handler).tcp() + })? + .bind("tls", ("127.0.0.1", 8443), || { + HttpService::build().finish(handler).rustls(tls_config()) + })? + .run() + .await +} + +async fn handler(req: Request) -> Result { + log::info!("handshaking"); + let mut res = ws::handshake(req.head())?; + + // handshake will always fail under HTTP/2 + + log::info!("responding"); + Ok(res.streaming(Heartbeat::new(ws::Codec::new()))) +} + +struct Heartbeat { + codec: ws::Codec, + interval: Interval, +} + +impl Heartbeat { + fn new(codec: ws::Codec) -> Self { + Self { + codec, + interval: interval(Duration::from_secs(4)), + } + } +} + +impl Stream for Heartbeat { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + log::trace!("poll"); + + ready!(self.as_mut().interval.poll_tick(cx)); + + let mut buffer = BytesMut::new(); + + self.as_mut() + .codec + .encode( + ws::Message::Text(ByteString::from_static("hello world")), + &mut buffer, + ) + .unwrap(); + + Poll::Ready(Some(Ok(buffer.freeze()))) + } +} + +fn tls_config() -> rustls::ServerConfig { + use std::io::BufReader; + + use rustls::{ + internal::pemfile::{certs, pkcs8_private_keys}, + NoClientAuth, ServerConfig, + }; + + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + + let mut config = ServerConfig::new(NoClientAuth::new()); + let cert_file = &mut BufReader::new(cert_file.as_bytes()); + let key_file = &mut BufReader::new(key_file.as_bytes()); + + let cert_chain = certs(cert_file).unwrap(); + let mut keys = pkcs8_private_keys(key_file).unwrap(); + config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + + config +} diff --git a/actix-http/src/client/config.rs b/actix-http/src/client/config.rs index fad902d04..1c0405cbc 100644 --- a/actix-http/src/client/config.rs +++ b/actix-http/src/client/config.rs @@ -1,3 +1,4 @@ +use std::net::IpAddr; use std::time::Duration; const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB @@ -7,24 +8,28 @@ const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB #[derive(Clone)] pub(crate) struct ConnectorConfig { pub(crate) timeout: Duration, + pub(crate) handshake_timeout: Duration, pub(crate) conn_lifetime: Duration, pub(crate) conn_keep_alive: Duration, pub(crate) disconnect_timeout: Option, pub(crate) limit: usize, pub(crate) conn_window_size: u32, pub(crate) stream_window_size: u32, + pub(crate) local_address: Option, } impl Default for ConnectorConfig { fn default() -> Self { Self { timeout: Duration::from_secs(5), + handshake_timeout: Duration::from_secs(5), conn_lifetime: Duration::from_secs(75), conn_keep_alive: Duration::from_secs(15), disconnect_timeout: Some(Duration::from_millis(3000)), limit: 100, conn_window_size: DEFAULT_H2_CONN_WINDOW, stream_window_size: DEFAULT_H2_STREAM_WINDOW, + local_address: None, } } } diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs index 778083a1c..78101397d 100644 --- a/actix-http/src/client/connection.rs +++ b/actix-http/src/client/connection.rs @@ -1,14 +1,16 @@ -use std::ops::{Deref, DerefMut}; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{fmt, io, time}; +use std::{ + io, + ops::{Deref, DerefMut}, + pin::Pin, + task::{Context, Poll}, + time, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_rt::task::JoinHandle; use bytes::Bytes; use futures_core::future::LocalBoxFuture; use h2::client::SendRequest; -use pin_project::pin_project; use crate::body::MessageBody; use crate::h1::ClientCodec; @@ -19,28 +21,148 @@ use super::error::SendRequestError; use super::pool::Acquired; use super::{h1proto, h2proto}; -pub(crate) enum ConnectionType { - H1(Io), - H2(H2Connection), +/// Trait alias for types impl [tokio::io::AsyncRead] and [tokio::io::AsyncWrite]. +pub trait ConnectionIo: AsyncRead + AsyncWrite + Unpin + 'static {} + +impl ConnectionIo for T {} + +/// HTTP client connection +pub struct H1Connection { + io: Option, + created: time::Instant, + acquired: Acquired, } -/// `H2Connection` has two parts: `SendRequest` and `Connection`. +impl H1Connection { + /// close or release the connection to pool based on flag input + pub(super) fn on_release(&mut self, keep_alive: bool) { + if keep_alive { + self.release(); + } else { + self.close(); + } + } + + /// Close connection + fn close(&mut self) { + let io = self.io.take().unwrap(); + self.acquired.close(ConnectionInnerType::H1(io)); + } + + /// Release this connection to the connection pool + fn release(&mut self) { + let io = self.io.take().unwrap(); + self.acquired + .release(ConnectionInnerType::H1(io), self.created); + } + + fn io_pin_mut(self: Pin<&mut Self>) -> Pin<&mut Io> { + Pin::new(self.get_mut().io.as_mut().unwrap()) + } +} + +impl AsyncRead for H1Connection { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.io_pin_mut().poll_read(cx, buf) + } +} + +impl AsyncWrite for H1Connection { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.io_pin_mut().poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.io_pin_mut().poll_flush(cx) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.io_pin_mut().poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + self.io_pin_mut().poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.io.as_ref().unwrap().is_write_vectored() + } +} + +/// HTTP2 client connection +pub struct H2Connection { + io: Option, + created: time::Instant, + acquired: Acquired, +} + +impl Deref for H2Connection { + type Target = SendRequest; + + fn deref(&self) -> &Self::Target { + &self.io.as_ref().unwrap().sender + } +} + +impl DerefMut for H2Connection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.io.as_mut().unwrap().sender + } +} + +impl H2Connection { + /// close or release the connection to pool based on flag input + pub(super) fn on_release(&mut self, close: bool) { + if close { + self.close(); + } else { + self.release(); + } + } + + /// Close connection + fn close(&mut self) { + let io = self.io.take().unwrap(); + self.acquired.close(ConnectionInnerType::H2(io)); + } + + /// Release this connection to the connection pool + fn release(&mut self) { + let io = self.io.take().unwrap(); + self.acquired + .release(ConnectionInnerType::H2(io), self.created); + } +} + +/// `H2ConnectionInner` has two parts: `SendRequest` and `Connection`. /// -/// `Connection` is spawned as an async task on runtime and `H2Connection` holds a handle for -/// this task. Therefore, it can wake up and quit the task when SendRequest is dropped. -pub(crate) struct H2Connection { +/// `Connection` is spawned as an async task on runtime and `H2ConnectionInner` holds a handle +/// for this task. Therefore, it can wake up and quit the task when SendRequest is dropped. +pub(super) struct H2ConnectionInner { handle: JoinHandle<()>, sender: SendRequest, } -impl H2Connection { - pub(crate) fn new( +impl H2ConnectionInner { + pub(super) fn new( sender: SendRequest, connection: h2::client::Connection, - ) -> Self - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - { + ) -> Self { let handle = actix_rt::spawn(async move { let _ = connection.await; }); @@ -49,248 +171,188 @@ impl H2Connection { } } -// cancel spawned connection task on drop. -impl Drop for H2Connection { +/// Cancel spawned connection task on drop. +impl Drop for H2ConnectionInner { fn drop(&mut self) { self.handle.abort(); } } -// only expose sender type to public. -impl Deref for H2Connection { - type Target = SendRequest; - - fn deref(&self) -> &Self::Target { - &self.sender - } -} - -impl DerefMut for H2Connection { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.sender - } -} - -pub trait Connection { - type Io: AsyncRead + AsyncWrite + Unpin; - - /// Send request and body - fn send_request>( - self, - head: H, - body: B, - ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>; - - /// Send request, returns Response and Framed - fn open_tunnel + 'static>( - self, - head: H, - ) -> LocalBoxFuture< - 'static, - Result<(ResponseHead, Framed), SendRequestError>, - >; -} - -pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { - /// Close connection - fn close(self: Pin<&mut Self>); - - /// Release connection to the connection pool - fn release(self: Pin<&mut Self>); -} - -#[doc(hidden)] -/// HTTP client connection -pub struct IoConnection -where - T: AsyncWrite + Unpin + 'static, -{ - io: Option>, - created: time::Instant, - pool: Option>, -} - -impl fmt::Debug for IoConnection -where - T: AsyncWrite + Unpin + fmt::Debug + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.io { - Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io), - Some(ConnectionType::H2(_)) => write!(f, "H2Connection"), - None => write!(f, "Connection(Empty)"), - } - } -} - -impl IoConnection { - pub(crate) fn new( - io: ConnectionType, - created: time::Instant, - pool: Option>, - ) -> Self { - IoConnection { - pool, - created, - io: Some(io), - } - } - - pub(crate) fn into_inner(self) -> (ConnectionType, time::Instant) { - (self.io.unwrap(), self.created) - } - - #[cfg(test)] - pub(crate) fn into_parts(self) -> (ConnectionType, time::Instant, Acquired) { - (self.io.unwrap(), self.created, self.pool.unwrap()) - } -} - -impl Connection for IoConnection -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - type Io = T; - - fn send_request>( - mut self, - head: H, - body: B, - ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> { - match self.io.take().unwrap() { - ConnectionType::H1(io) => Box::pin(h1proto::send_request( - io, - head.into(), - body, - self.created, - self.pool, - )), - ConnectionType::H2(io) => Box::pin(h2proto::send_request( - io, - head.into(), - body, - self.created, - self.pool, - )), - } - } - - /// Send request, returns Response and Framed - fn open_tunnel>( - mut self, - head: H, - ) -> LocalBoxFuture< - 'static, - Result<(ResponseHead, Framed), SendRequestError>, - > { - match self.io.take().unwrap() { - ConnectionType::H1(io) => Box::pin(h1proto::open_tunnel(io, head.into())), - ConnectionType::H2(io) => { - if let Some(mut pool) = self.pool.take() { - pool.release(IoConnection::new( - ConnectionType::H2(io), - self.created, - None, - )); - } - Box::pin(async { Err(SendRequestError::TunnelNotSupported) }) - } - } - } -} - #[allow(dead_code)] -pub(crate) enum EitherIoConnection +/// Unified connection type cover Http1 Plain/Tls and Http2 protocols +pub enum Connection> where - A: AsyncRead + AsyncWrite + Unpin + 'static, - B: AsyncRead + AsyncWrite + Unpin + 'static, + A: ConnectionIo, + B: ConnectionIo, { - A(IoConnection), - B(IoConnection), + Tcp(ConnectionType), + Tls(ConnectionType), } -impl Connection for EitherIoConnection -where - A: AsyncRead + AsyncWrite + Unpin + 'static, - B: AsyncRead + AsyncWrite + Unpin + 'static, -{ - type Io = EitherIo; +/// Unified connection type cover Http1/2 protocols +pub enum ConnectionType { + H1(H1Connection), + H2(H2Connection), +} - fn send_request>( +/// Helper type for storing connection types in pool. +pub(super) enum ConnectionInnerType { + H1(Io), + H2(H2ConnectionInner), +} + +impl ConnectionType { + pub(super) fn from_pool( + inner: ConnectionInnerType, + created: time::Instant, + acquired: Acquired, + ) -> Self { + match inner { + ConnectionInnerType::H1(io) => Self::from_h1(io, created, acquired), + ConnectionInnerType::H2(io) => Self::from_h2(io, created, acquired), + } + } + + pub(super) fn from_h1( + io: Io, + created: time::Instant, + acquired: Acquired, + ) -> Self { + Self::H1(H1Connection { + io: Some(io), + created, + acquired, + }) + } + + pub(super) fn from_h2( + io: H2ConnectionInner, + created: time::Instant, + acquired: Acquired, + ) -> Self { + Self::H2(H2Connection { + io: Some(io), + created, + acquired, + }) + } +} + +impl Connection +where + A: ConnectionIo, + B: ConnectionIo, +{ + /// Send a request through connection. + pub fn send_request( self, head: H, body: RB, - ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> { - match self { - EitherIoConnection::A(con) => con.send_request(head, body), - EitherIoConnection::B(con) => con.send_request(head, body), - } + ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> + where + RB: MessageBody + 'static, + H: Into + 'static, + { + Box::pin(async move { + match self { + Connection::Tcp(ConnectionType::H1(conn)) => { + h1proto::send_request(conn, head.into(), body).await + } + Connection::Tls(ConnectionType::H1(conn)) => { + h1proto::send_request(conn, head.into(), body).await + } + Connection::Tls(ConnectionType::H2(conn)) => { + h2proto::send_request(conn, head.into(), body).await + } + _ => unreachable!( + "Plain Tcp connection can be used only in Http1 protocol" + ), + } + }) } - /// Send request, returns Response and Framed - fn open_tunnel + 'static>( + /// Send request, returns Response and Framed tunnel. + pub fn open_tunnel + 'static>( self, head: H, ) -> LocalBoxFuture< 'static, - Result<(ResponseHead, Framed), SendRequestError>, + Result<(ResponseHead, Framed, ClientCodec>), SendRequestError>, > { - match self { - EitherIoConnection::A(con) => Box::pin(async { - let (head, framed) = con.open_tunnel(head).await?; - Ok((head, framed.into_map_io(EitherIo::A))) - }), - EitherIoConnection::B(con) => Box::pin(async { - let (head, framed) = con.open_tunnel(head).await?; - Ok((head, framed.into_map_io(EitherIo::B))) - }), - } + Box::pin(async move { + match self { + Connection::Tcp(ConnectionType::H1(ref _conn)) => { + let (head, framed) = h1proto::open_tunnel(self, head.into()).await?; + Ok((head, framed)) + } + Connection::Tls(ConnectionType::H1(ref _conn)) => { + let (head, framed) = h1proto::open_tunnel(self, head.into()).await?; + Ok((head, framed)) + } + Connection::Tls(ConnectionType::H2(mut conn)) => { + conn.release(); + Err(SendRequestError::TunnelNotSupported) + } + Connection::Tcp(ConnectionType::H2(_)) => { + unreachable!( + "Plain Tcp connection can be used only in Http1 protocol" + ) + } + } + }) } } -#[pin_project(project = EitherIoProj)] -pub enum EitherIo { - A(#[pin] A), - B(#[pin] B), -} - -impl AsyncRead for EitherIo +impl AsyncRead for Connection where - A: AsyncRead, - B: AsyncRead, + A: ConnectionIo, + B: ConnectionIo, { fn poll_read( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &mut ReadBuf<'_>, ) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_read(cx, buf), - EitherIoProj::B(val) => val.poll_read(cx, buf), + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_read(cx, buf) + } + Connection::Tls(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_read(cx, buf) + } + _ => unreachable!("H2Connection can not impl AsyncRead trait"), } } } -impl AsyncWrite for EitherIo +const H2_UNREACHABLE_WRITE: &str = "H2Connection can not impl AsyncWrite trait"; + +impl AsyncWrite for Connection where - A: AsyncWrite, - B: AsyncWrite, + A: ConnectionIo, + B: ConnectionIo, { fn poll_write( self: Pin<&mut Self>, cx: &mut Context<'_>, buf: &[u8], ) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_write(cx, buf), - EitherIoProj::B(val) => val.poll_write(cx, buf), + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_write(cx, buf) + } + Connection::Tls(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_write(cx, buf) + } + _ => unreachable!(H2_UNREACHABLE_WRITE), } } fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_flush(cx), - EitherIoProj::B(val) => val.poll_flush(cx), + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), + _ => unreachable!(H2_UNREACHABLE_WRITE), } } @@ -298,9 +360,38 @@ where self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_shutdown(cx), - EitherIoProj::B(val) => val.poll_shutdown(cx), + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_shutdown(cx) + } + Connection::Tls(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_shutdown(cx) + } + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_write_vectored(cx, bufs) + } + Connection::Tls(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_write_vectored(cx, bufs) + } + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } + + fn is_write_vectored(&self) -> bool { + match *self { + Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), + Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), + _ => unreachable!(H2_UNREACHABLE_WRITE), } } } @@ -323,10 +414,13 @@ mod test { let tcp = TcpStream::connect(local).await.unwrap(); let (sender, connection) = h2::client::handshake(tcp).await.unwrap(); - let conn = H2Connection::new(sender.clone(), connection); + let conn = H2ConnectionInner::new(sender.clone(), connection); assert!(sender.clone().ready().await.is_ok()); - assert!(h2::client::SendRequest::clone(&*conn).ready().await.is_ok()); + assert!(h2::client::SendRequest::clone(&conn.sender) + .ready() + .await + .is_ok()); drop(conn); diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 65536f257..6996677d2 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -1,45 +1,53 @@ -use std::fmt; -use std::marker::PhantomData; -use std::time::Duration; - -use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::net::TcpStream; -use actix_service::{apply_fn, Service, ServiceExt}; -use actix_tls::connect::{ - new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver, +use std::{ + fmt, + future::Future, + net::IpAddr, + pin::Pin, + rc::Rc, + task::{Context, Poll}, + time::Duration, }; -use actix_utils::timeout::{TimeoutError, TimeoutService}; + +use actix_rt::{ + net::TcpStream, + time::{sleep, Sleep}, +}; +use actix_service::Service; +use actix_tls::connect::{ + new_connector, Connect as TcpConnect, ConnectError as TcpConnectError, + Connection as TcpConnection, Resolver, +}; +use futures_core::{future::LocalBoxFuture, ready}; use http::Uri; +use pin_project::pin_project; use super::config::ConnectorConfig; -use super::connection::Connection; +use super::connection::{Connection, ConnectionIo}; use super::error::ConnectError; -use super::pool::{ConnectionPool, Protocol}; +use super::pool::ConnectionPool; use super::Connect; +use super::Protocol; #[cfg(feature = "openssl")] use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; #[cfg(feature = "rustls")] use actix_tls::connect::ssl::rustls::ClientConfig; -#[cfg(feature = "rustls")] -use std::sync::Arc; -#[cfg(any(feature = "openssl", feature = "rustls"))] enum SslConnector { + #[allow(dead_code)] + None, #[cfg(feature = "openssl")] Openssl(OpensslConnector), #[cfg(feature = "rustls")] - Rustls(Arc), + Rustls(std::sync::Arc), } -#[cfg(not(any(feature = "openssl", feature = "rustls")))] -type SslConnector = (); /// Manages HTTP client network connectivity. /// /// The `Connector` type uses a builder-like combinator pattern for service /// construction that finishes by calling the `.finish()` method. /// -/// ```rust,ignore +/// ```ignore /// use std::time::Duration; /// use actix_http::client::Connector; /// @@ -47,18 +55,14 @@ type SslConnector = (); /// .timeout(Duration::from_secs(5)) /// .finish(); /// ``` -pub struct Connector { +pub struct Connector { connector: T, config: ConnectorConfig, #[allow(dead_code)] ssl: SslConnector, - _phantom: PhantomData, } -trait Io: AsyncRead + AsyncWrite + Unpin {} -impl Io for T {} - -impl Connector<(), ()> { +impl Connector<()> { #[allow(clippy::new_ret_no_self, clippy::let_unit_value)] pub fn new() -> Connector< impl Service< @@ -66,13 +70,11 @@ impl Connector<(), ()> { Response = TcpConnection, Error = actix_tls::connect::ConnectError, > + Clone, - TcpStream, > { Connector { ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), connector: new_connector(resolver::resolver()), config: ConnectorConfig::default(), - _phantom: PhantomData, } } @@ -103,51 +105,59 @@ impl Connector<(), ()> { config.root_store.add_server_trust_anchors( &actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS, ); - SslConnector::Rustls(Arc::new(config)) + SslConnector::Rustls(std::sync::Arc::new(config)) } // ssl turned off, provides empty ssl connector #[cfg(not(any(feature = "openssl", feature = "rustls")))] - fn build_ssl(_: Vec>) -> SslConnector {} + fn build_ssl(_: Vec>) -> SslConnector { + SslConnector::None + } } -impl Connector { +impl Connector { /// Use custom connector. - pub fn connector(self, connector: T1) -> Connector + pub fn connector(self, connector: S1) -> Connector where - U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug, - T1: Service< + Io1: ConnectionIo + fmt::Debug, + S1: Service< TcpConnect, - Response = TcpConnection, - Error = actix_tls::connect::ConnectError, + Response = TcpConnection, + Error = TcpConnectError, > + Clone, { Connector { connector, config: self.config, ssl: self.ssl, - _phantom: PhantomData, } } } -impl Connector +impl Connector where - U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, - T: Service< + Io: ConnectionIo + fmt::Debug, + S: Service< TcpConnect, - Response = TcpConnection, - Error = actix_tls::connect::ConnectError, + Response = TcpConnection, + Error = TcpConnectError, > + Clone + 'static, { - /// Connection timeout, i.e. max time to connect to remote host including dns name resolution. - /// Set to 1 second by default. + /// Tcp connection timeout, i.e. max time to connect to remote host including dns name + /// resolution. Set to 5 second by default. pub fn timeout(mut self, timeout: Duration) -> Self { self.config.timeout = timeout; self } + /// Tls handshake timeout, i.e. max time to do tls handshake with remote host after tcp + /// connection established. Set to 5 second by default. + pub fn handshake_timeout(mut self, timeout: Duration) -> Self { + self.config.handshake_timeout = timeout; + self + } + #[cfg(feature = "openssl")] /// Use custom `SslConnector` instance. pub fn ssl(mut self, connector: OpensslConnector) -> Self { @@ -156,7 +166,8 @@ where } #[cfg(feature = "rustls")] - pub fn rustls(mut self, connector: Arc) -> Self { + /// Use custom `SslConnector` instance. + pub fn rustls(mut self, connector: std::sync::Arc) -> Self { self.ssl = SslConnector::Rustls(connector); self } @@ -237,263 +248,434 @@ where self } + /// Set local IP Address the connector would use for establishing connection. + pub fn local_address(mut self, addr: IpAddr) -> Self { + self.config.local_address = Some(addr); + self + } + /// Finish configuration process and create connector service. /// The Connector builder always concludes by calling `finish()` last in /// its combinator chain. - pub fn finish( - self, - ) -> impl Service + Clone - { - #[cfg(not(any(feature = "openssl", feature = "rustls")))] - { - let connector = TimeoutService::new( - self.config.timeout, - apply_fn(self.connector, |msg: Connect, srv| { - srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) - }) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectError::Timeout, - }); + pub fn finish(self) -> ConnectorService { + let local_address = self.config.local_address; + let timeout = self.config.timeout; - connect_impl::InnerConnector { - tcp_pool: ConnectionPool::new( - connector, - self.config.no_disconnect_timeout(), - ), - } - } - #[cfg(any(feature = "openssl", feature = "rustls"))] - { - const H2: &[u8] = b"h2"; - use actix_service::{boxed::service, pipeline}; + let tcp_service_inner = + TcpConnectorInnerService::new(self.connector, timeout, local_address); + + #[allow(clippy::redundant_clone)] + let tcp_service = TcpConnectorService { + service: tcp_service_inner.clone(), + }; + + let tls_service = match self.ssl { + SslConnector::None => None, #[cfg(feature = "openssl")] - use actix_tls::connect::ssl::openssl::OpensslConnector; + SslConnector::Openssl(tls) => { + const H2: &[u8] = b"h2"; + + use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream}; + + impl IntoConnectionIo for TcpConnection> { + fn into_connection_io(self) -> (Box, Protocol) { + let sock = self.into_parts().0; + let h2 = sock + .ssl() + .selected_alpn_protocol() + .map(|protos| protos.windows(2).any(|w| w == H2)) + .unwrap_or(false); + if h2 { + (Box::new(sock), Protocol::Http2) + } else { + (Box::new(sock), Protocol::Http1) + } + } + } + + let handshake_timeout = self.config.handshake_timeout; + + let tls_service = TlsConnectorService { + tcp_service: tcp_service_inner, + tls_service: OpensslConnector::service(tls), + timeout: handshake_timeout, + }; + + Some(actix_service::boxed::rc_service(tls_service)) + } #[cfg(feature = "rustls")] - use actix_tls::connect::ssl::rustls::{RustlsConnector, Session}; + SslConnector::Rustls(tls) => { + const H2: &[u8] = b"h2"; - let ssl_service = TimeoutService::new( - self.config.timeout, - pipeline( - apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) - }) - .map_err(ConnectError::from), - ) - .and_then(match self.ssl { - #[cfg(feature = "openssl")] - SslConnector::Openssl(ssl) => service( - OpensslConnector::service(ssl) - .map(|stream| { - let sock = stream.into_parts().0; - let h2 = sock - .ssl() - .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (Box::new(sock) as Box, Protocol::Http2) - } else { - (Box::new(sock) as Box, Protocol::Http1) - } - }) - .map_err(ConnectError::from), - ), - #[cfg(feature = "rustls")] - SslConnector::Rustls(ssl) => service( - RustlsConnector::service(ssl) - .map_err(ConnectError::from) - .map(|stream| { - let sock = stream.into_parts().0; - let h2 = sock - .get_ref() - .1 - .get_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (Box::new(sock) as Box, Protocol::Http2) - } else { - (Box::new(sock) as Box, Protocol::Http1) - } - }), - ), - }), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectError::Timeout, - }); + use actix_tls::connect::ssl::rustls::{ + RustlsConnector, Session, TlsStream, + }; - let tcp_service = TimeoutService::new( - self.config.timeout, - apply_fn(self.connector, |msg: Connect, srv| { - srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) - }) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectError::Timeout, - }); + impl IntoConnectionIo for TcpConnection> { + fn into_connection_io(self) -> (Box, Protocol) { + let sock = self.into_parts().0; + let h2 = sock + .get_ref() + .1 + .get_alpn_protocol() + .map(|protos| protos.windows(2).any(|w| w == H2)) + .unwrap_or(false); + if h2 { + (Box::new(sock), Protocol::Http2) + } else { + (Box::new(sock), Protocol::Http1) + } + } + } - connect_impl::InnerConnector { - tcp_pool: ConnectionPool::new( - tcp_service, - self.config.no_disconnect_timeout(), - ), - ssl_pool: ConnectionPool::new(ssl_service, self.config), + let handshake_timeout = self.config.handshake_timeout; + + let tls_service = TlsConnectorService { + tcp_service: tcp_service_inner, + tls_service: RustlsConnector::service(tls), + timeout: handshake_timeout, + }; + + Some(actix_service::boxed::rc_service(tls_service)) } + }; + + let tcp_config = self.config.no_disconnect_timeout(); + + let tcp_pool = ConnectionPool::new(tcp_service, tcp_config); + + let tls_config = self.config; + let tls_pool = tls_service + .map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); + + ConnectorServicePriv { tcp_pool, tls_pool } + } +} + +/// tcp service for map `TcpConnection` type to `(Io, Protocol)` +#[derive(Clone)] +pub struct TcpConnectorService { + service: S, +} + +impl Service for TcpConnectorService +where + S: Service, Error = ConnectError> + + Clone + + 'static, +{ + type Response = (Io, Protocol); + type Error = ConnectError; + type Future = TcpConnectorFuture; + + actix_service::forward_ready!(service); + + fn call(&self, req: Connect) -> Self::Future { + TcpConnectorFuture { + fut: self.service.call(req), } } } -#[cfg(not(any(feature = "openssl", feature = "rustls")))] -mod connect_impl { - use std::task::{Context, Poll}; +#[pin_project] +pub struct TcpConnectorFuture { + #[pin] + fut: Fut, +} - use futures_core::future::LocalBoxFuture; +impl Future for TcpConnectorFuture +where + Fut: Future, ConnectError>>, +{ + type Output = Result<(Io, Protocol), ConnectError>; - use super::*; - use crate::client::connection::IoConnection; + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project() + .fut + .poll(cx) + .map_ok(|res| (res.into_parts().0, Protocol::Http1)) + } +} - pub(crate) struct InnerConnector - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - pub(crate) tcp_pool: ConnectionPool, +/// service for establish tcp connection and do client tls handshake. +/// operation is canceled when timeout limit reached. +struct TlsConnectorService { + /// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting. + tcp_service: S, + /// tls connection is canceled on `TlsConnectorService`'s timeout setting. + tls_service: St, + timeout: Duration, +} + +impl Service for TlsConnectorService +where + S: Service, Error = ConnectError> + + Clone + + 'static, + St: Service, Response = Res, Error = std::io::Error> + + Clone + + 'static, + Io: ConnectionIo, + Res: IntoConnectionIo, +{ + type Response = (Box, Protocol); + type Error = ConnectError; + type Future = TlsConnectorFuture; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + ready!(self.tcp_service.poll_ready(cx))?; + ready!(self.tls_service.poll_ready(cx))?; + Poll::Ready(Ok(())) } - impl Clone for InnerConnector - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - fn clone(&self) -> Self { - InnerConnector { - tcp_pool: self.tcp_pool.clone(), - } - } - } + fn call(&self, req: Connect) -> Self::Future { + let fut = self.tcp_service.call(req); + let tls_service = self.tls_service.clone(); + let timeout = self.timeout; - impl Service for InnerConnector - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - type Response = IoConnection; - type Error = ConnectError; - type Future = LocalBoxFuture<'static, Result, ConnectError>>; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.tcp_pool.poll_ready(cx) - } - - fn call(&self, req: Connect) -> Self::Future { - match req.uri.scheme_str() { - Some("https") | Some("wss") => { - Box::pin(async { Err(ConnectError::SslIsNotSupported) }) - } - _ => self.tcp_pool.call(req), - } + TlsConnectorFuture::TcpConnect { + fut, + tls_service: Some(tls_service), + timeout, } } } -#[cfg(any(feature = "openssl", feature = "rustls"))] -mod connect_impl { - use std::future::Future; - use std::pin::Pin; - use std::task::{Context, Poll}; +#[pin_project(project = TlsConnectorProj)] +#[allow(clippy::large_enum_variant)] +enum TlsConnectorFuture { + TcpConnect { + #[pin] + fut: Fut1, + tls_service: Option, + timeout: Duration, + }, + TlsConnect { + #[pin] + fut: Fut2, + #[pin] + timeout: Sleep, + }, +} - use super::*; - use crate::client::connection::EitherIoConnection; +/// helper trait for generic over different TlsStream types between tls crates. +trait IntoConnectionIo { + fn into_connection_io(self) -> (Box, Protocol); +} - pub(crate) struct InnerConnector - where - S1: Service + 'static, - S2: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - pub(crate) tcp_pool: ConnectionPool, - pub(crate) ssl_pool: ConnectionPool, - } +impl Future for TlsConnectorFuture +where + S: Service< + TcpConnection, + Response = Res, + Error = std::io::Error, + Future = Fut2, + >, + Fut1: Future, ConnectError>>, + Fut2: Future>, + Io: ConnectionIo, + Res: IntoConnectionIo, +{ + type Output = Result<(Box, Protocol), ConnectError>; - impl Clone for InnerConnector - where - S1: Service + 'static, - S2: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - fn clone(&self) -> Self { - InnerConnector { - tcp_pool: self.tcp_pool.clone(), - ssl_pool: self.ssl_pool.clone(), + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.as_mut().project() { + TlsConnectorProj::TcpConnect { + fut, + tls_service, + timeout, + } => { + let res = ready!(fut.poll(cx))?; + let fut = tls_service + .take() + .expect("TlsConnectorFuture polled after complete") + .call(res); + let timeout = sleep(*timeout); + self.set(TlsConnectorFuture::TlsConnect { fut, timeout }); + self.poll(cx) } + TlsConnectorProj::TlsConnect { fut, timeout } => match fut.poll(cx)? { + Poll::Ready(res) => Poll::Ready(Ok(res.into_connection_io())), + Poll::Pending => timeout.poll(cx).map(|_| Err(ConnectError::Timeout)), + }, } } +} - impl Service for InnerConnector - where - S1: Service + 'static, - S2: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - type Response = EitherIoConnection; - type Error = ConnectError; - type Future = InnerConnectorResponse; +/// service for establish tcp connection. +/// operation is canceled when timeout limit reached. +#[derive(Clone)] +pub struct TcpConnectorInnerService { + service: S, + timeout: Duration, + local_address: Option, +} - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.tcp_pool.poll_ready(cx) - } - - fn call(&self, req: Connect) -> Self::Future { - match req.uri.scheme_str() { - Some("https") | Some("wss") => { - InnerConnectorResponse::Io2(self.ssl_pool.call(req)) - } - _ => InnerConnectorResponse::Io1(self.tcp_pool.call(req)), - } +impl TcpConnectorInnerService { + fn new( + service: S, + timeout: Duration, + local_address: Option, + ) -> Self { + Self { + service, + timeout, + local_address, } } +} - #[pin_project::pin_project(project = InnerConnectorProj)] - pub(crate) enum InnerConnectorResponse - where - S1: Service + 'static, - S2: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - Io1(#[pin] as Service>::Future), - Io2(#[pin] as Service>::Future), +impl Service for TcpConnectorInnerService +where + S: Service< + TcpConnect, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone + + 'static, +{ + type Response = S::Response; + type Error = ConnectError; + type Future = TcpConnectorInnerFuture; + + actix_service::forward_ready!(service); + + fn call(&self, req: Connect) -> Self::Future { + let mut req = TcpConnect::new(req.uri).set_addr(req.addr); + + if let Some(local_addr) = self.local_address { + req = req.set_local_addr(local_addr); + } + + TcpConnectorInnerFuture { + fut: self.service.call(req), + timeout: sleep(self.timeout), + } + } +} + +#[pin_project] +pub struct TcpConnectorInnerFuture { + #[pin] + fut: Fut, + #[pin] + timeout: Sleep, +} + +impl Future for TcpConnectorInnerFuture +where + Fut: Future, TcpConnectError>>, +{ + type Output = Result, ConnectError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match this.fut.poll(cx) { + Poll::Ready(res) => Poll::Ready(res.map_err(ConnectError::from)), + Poll::Pending => this.timeout.poll(cx).map(|_| Err(ConnectError::Timeout)), + } + } +} + +/// Connector service for pooled Plain/Tls Tcp connections. +pub type ConnectorService = ConnectorServicePriv< + TcpConnectorService>, + Rc< + dyn Service< + Connect, + Response = (Box, Protocol), + Error = ConnectError, + Future = LocalBoxFuture< + 'static, + Result<(Box, Protocol), ConnectError>, + >, + >, + >, + Io, + Box, +>; + +pub struct ConnectorServicePriv +where + S1: Service, + S2: Service, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + tcp_pool: ConnectionPool, + tls_pool: Option>, +} + +impl Service for ConnectorServicePriv +where + S1: Service + + Clone + + 'static, + S2: Service + + Clone + + 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + type Response = Connection; + type Error = ConnectError; + type Future = ConnectorServiceFuture; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + ready!(self.tcp_pool.poll_ready(cx))?; + if let Some(ref tls_pool) = self.tls_pool { + ready!(tls_pool.poll_ready(cx))?; + } + Poll::Ready(Ok(())) } - impl Future for InnerConnectorResponse - where - S1: Service + 'static, - S2: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - type Output = Result, ConnectError>; + fn call(&self, req: Connect) -> Self::Future { + match req.uri.scheme_str() { + Some("https") | Some("wss") => match self.tls_pool { + None => ConnectorServiceFuture::SslIsNotSupported, + Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)), + }, + _ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)), + } + } +} - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.project() { - InnerConnectorProj::Io1(fut) => { - fut.poll(cx).map_ok(EitherIoConnection::A) - } - InnerConnectorProj::Io2(fut) => { - fut.poll(cx).map_ok(EitherIoConnection::B) - } +#[pin_project(project = ConnectorServiceProj)] +pub enum ConnectorServiceFuture +where + S1: Service + + Clone + + 'static, + S2: Service + + Clone + + 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + Tcp(#[pin] as Service>::Future), + Tls(#[pin] as Service>::Future), + SslIsNotSupported, +} + +impl Future for ConnectorServiceFuture +where + S1: Service + + Clone + + 'static, + S2: Service + + Clone + + 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + type Output = Result, ConnectError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.project() { + ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp), + ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls), + ConnectorServiceProj::SslIsNotSupported => { + Poll::Ready(Err(ConnectError::SslIsNotSupported)) } } } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index 082c4b8e2..01a6e1edf 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -1,36 +1,35 @@ -use std::io::Write; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{io, time}; +use std::{ + io::Write, + pin::Pin, + task::{Context, Poll}, +}; -use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; +use actix_codec::Framed; use bytes::buf::BufMut; use bytes::{Bytes, BytesMut}; use futures_core::Stream; -use futures_util::future::poll_fn; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{future::poll_fn, SinkExt as _}; use crate::error::PayloadError; use crate::h1; -use crate::header::HeaderMap; -use crate::http::header::{IntoHeaderValue, HOST}; +use crate::http::{ + header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, + StatusCode, +}; use crate::message::{RequestHeadType, ResponseHead}; use crate::payload::{Payload, PayloadStream}; -use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; +use super::connection::{ConnectionIo, H1Connection}; use super::error::{ConnectError, SendRequestError}; -use super::pool::Acquired; use crate::body::{BodySize, MessageBody}; -pub(crate) async fn send_request( - io: T, +pub(crate) async fn send_request( + io: H1Connection, mut head: RequestHeadType, body: B, - created: time::Instant, - pool: Option>, ) -> Result<(ResponseHead, Payload), SendRequestError> where - T: AsyncRead + AsyncWrite + Unpin + 'static, + Io: ConnectionIo, B: MessageBody, { // set request host header @@ -40,9 +39,9 @@ where if let Some(host) = head.as_ref().uri.host() { let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - let _ = match head.as_ref().uri.port_u16() { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), + match head.as_ref().uri.port_u16() { + None | Some(80) | Some(443) => write!(wrt, "{}", host)?, + Some(port) => write!(wrt, "{}:{}", host, port)?, }; match wrt.get_mut().split().freeze().try_into_value() { @@ -60,71 +59,102 @@ where } } - let io = H1Connection { - created, - pool, - io: Some(io), - }; + // create Framed and prepare sending request + let mut framed = Framed::new(io, h1::ClientCodec::default()); - // create Framed and send request - let mut framed_inner = Framed::new(io, h1::ClientCodec::default()); - framed_inner.send((head, body.size()).into()).await?; + // Check EXPECT header and enable expect handle flag accordingly. + // + // RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1 + let is_expect = if head.as_ref().headers.contains_key(EXPECT) { + match body.size() { + BodySize::None | BodySize::Empty | BodySize::Sized(0) => { + let keep_alive = framed.codec_ref().keepalive(); + framed.io_mut().on_release(keep_alive); - // send request body - match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} - _ => send_body(body, Pin::new(&mut framed_inner)).await?, - }; - - // read response and init read body - let res = Pin::new(&mut framed_inner).into_future().await; - let (head, framed) = if let (Some(result), framed) = res { - let item = result.map_err(SendRequestError::from)?; - (item, framed) + // TODO: use a new variant or a new type better describing error violate + // `Requirements for clients` session of above RFC + return Err(SendRequestError::Connect(ConnectError::Disconnected)); + } + _ => true, + } } else { - return Err(SendRequestError::from(ConnectError::Disconnected)); + false }; - match framed.codec_ref().message_type() { + framed.send((head, body.size()).into()).await?; + + let mut pin_framed = Pin::new(&mut framed); + + // special handle for EXPECT request. + let (do_send, mut res_head) = if is_expect { + let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) + .await + .ok_or(ConnectError::Disconnected)??; + + // return response head in case status code is not continue + // and current head would be used as final response head. + (head.status == StatusCode::CONTINUE, Some(head)) + } else { + (true, None) + }; + + if do_send { + // send request body + match body.size() { + BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} + _ => send_body(body, pin_framed.as_mut()).await?, + }; + + // read response and init read body + let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) + .await + .ok_or(ConnectError::Disconnected)??; + + res_head = Some(head); + } + + let head = res_head.unwrap(); + + match pin_framed.codec_ref().message_type() { h1::MessageType::None => { - let force_close = !framed.codec_ref().keepalive(); - release_connection(framed, force_close); + let keep_alive = pin_framed.codec_ref().keepalive(); + pin_framed.io_mut().on_release(keep_alive); + Ok((head, Payload::None)) } _ => { - let pl: PayloadStream = PlStream::new(framed_inner).boxed_local(); + let pl: PayloadStream = Box::pin(PlStream::new(framed)); Ok((head, pl.into())) } } } -pub(crate) async fn open_tunnel( - io: T, +pub(crate) async fn open_tunnel( + io: Io, head: RequestHeadType, -) -> Result<(ResponseHead, Framed), SendRequestError> +) -> Result<(ResponseHead, Framed), SendRequestError> where - T: AsyncRead + AsyncWrite + Unpin + 'static, + Io: ConnectionIo, { - // create Framed and send request + // create Framed and send request. let mut framed = Framed::new(io, h1::ClientCodec::default()); framed.send((head, BodySize::None).into()).await?; - // read response - if let (Some(result), framed) = framed.into_future().await { - let head = result.map_err(SendRequestError::from)?; - Ok((head, framed)) - } else { - Err(SendRequestError::from(ConnectError::Disconnected)) - } + // read response head. + let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx)) + .await + .ok_or(ConnectError::Disconnected)??; + + Ok((head, framed)) } /// send request body to the peer -pub(crate) async fn send_body( +pub(crate) async fn send_body( body: B, - mut framed: Pin<&mut Framed>, + mut framed: Pin<&mut Framed>, ) -> Result<(), SendRequestError> where - T: ConnectionLifetime + Unpin, + Io: ConnectionIo, B: MessageBody, { actix_rt::pin!(body); @@ -159,95 +189,21 @@ where } } - SinkExt::flush(Pin::into_inner(framed)).await?; + framed.get_mut().flush().await?; Ok(()) } -#[doc(hidden)] -/// HTTP client connection -pub struct H1Connection -where - T: AsyncWrite + Unpin + 'static, -{ - /// T should be `Unpin` - io: Option, - created: time::Instant, - pool: Option>, -} - -impl ConnectionLifetime for H1Connection -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - /// Close connection - fn close(mut self: Pin<&mut Self>) { - if let Some(mut pool) = self.pool.take() { - if let Some(io) = self.io.take() { - pool.close(IoConnection::new( - ConnectionType::H1(io), - self.created, - None, - )); - } - } - } - - /// Release this connection to the connection pool - fn release(mut self: Pin<&mut Self>) { - if let Some(mut pool) = self.pool.take() { - if let Some(io) = self.io.take() { - pool.release(IoConnection::new( - ConnectionType::H1(io), - self.created, - None, - )); - } - } - } -} - -impl AsyncRead for H1Connection { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf) - } -} - -impl AsyncWrite for H1Connection { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf) - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(self.io.as_mut().unwrap()).poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx) - } -} - #[pin_project::pin_project] -pub(crate) struct PlStream { +pub(crate) struct PlStream +where + Io: ConnectionIo, +{ #[pin] - framed: Option>, + framed: Option, h1::ClientPayloadCodec>>, } -impl PlStream { - fn new(framed: Framed) -> Self { +impl PlStream { + fn new(framed: Framed, h1::ClientCodec>) -> Self { let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); PlStream { @@ -256,24 +212,23 @@ impl PlStream { } } -impl Stream for PlStream { +impl Stream for PlStream { type Item = Result; fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Poll> { - let mut this = self.project(); + let mut framed = self.project().framed.as_pin_mut().unwrap(); - match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { + match framed.as_mut().next_item(cx)? { Poll::Pending => Poll::Pending, Poll::Ready(Some(chunk)) => { if let Some(chunk) = chunk { Poll::Ready(Some(Ok(chunk))) } else { - let framed = this.framed.as_mut().as_pin_mut().unwrap(); - let force_close = !framed.codec_ref().keepalive(); - release_connection(framed, force_close); + let keep_alive = framed.codec_ref().keepalive(); + framed.io_mut().on_release(keep_alive); Poll::Ready(None) } } @@ -281,14 +236,3 @@ impl Stream for PlStream { } } } - -fn release_connection(framed: Pin<&mut Framed>, force_close: bool) -where - T: ConnectionLifetime, -{ - if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() { - framed.io_pin().release() - } else { - framed.io_pin().close() - } -} diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index 0deb5c014..437b9ae76 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -1,8 +1,5 @@ -use std::convert::TryFrom; use std::future::Future; -use std::time; -use actix_codec::{AsyncRead, AsyncWrite}; use bytes::Bytes; use futures_util::future::poll_fn; use h2::{ @@ -18,20 +15,16 @@ use crate::message::{RequestHeadType, ResponseHead}; use crate::payload::Payload; use super::config::ConnectorConfig; -use super::connection::{ConnectionType, IoConnection}; +use super::connection::{ConnectionIo, H2Connection}; use super::error::SendRequestError; -use super::pool::Acquired; -use crate::client::connection::H2Connection; -pub(crate) async fn send_request( - mut io: H2Connection, +pub(crate) async fn send_request( + mut io: H2Connection, head: RequestHeadType, body: B, - created: time::Instant, - pool: Option>, ) -> Result<(ResponseHead, Payload), SendRequestError> where - T: AsyncRead + AsyncWrite + Unpin + 'static, + Io: ConnectionIo, B: MessageBody, { trace!("Sending client request: {:?} {:?}", head, body.size()); @@ -61,10 +54,14 @@ where BodySize::Empty => req .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), - BodySize::Sized(len) => req.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(format!("{}", len)).unwrap(), - ), + BodySize::Sized(len) => { + let mut buf = itoa::Buffer::new(); + + req.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::from_str(buf.format(len)).unwrap(), + ) + } }; // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. @@ -87,7 +84,10 @@ where // copy headers for (key, value) in headers { match *key { - CONNECTION | TRANSFER_ENCODING => continue, // http2 specific + // TODO: consider skipping other headers according to: + // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 + // omit HTTP/1.x only headers + CONNECTION | TRANSFER_ENCODING => continue, CONTENT_LENGTH if skip_len => continue, // DATE => has_date = true, _ => {} @@ -97,13 +97,13 @@ where let res = poll_fn(|cx| io.poll_ready(cx)).await; if let Err(e) = res { - release(io, pool, created, e.is_io()); + io.on_release(e.is_io()); return Err(SendRequestError::from(e)); } let resp = match io.send_request(req, eof) { Ok((fut, send)) => { - release(io, pool, created, false); + io.on_release(false); if !eof { send_body(body, send).await?; @@ -111,7 +111,7 @@ where fut.await.map_err(SendRequestError::from)? } Err(e) => { - release(io, pool, created, e.is_io()); + io.on_release(e.is_io()); return Err(e.into()); } }; @@ -172,28 +172,10 @@ async fn send_body( } } -/// release SendRequest object -fn release( - io: H2Connection, - pool: Option>, - created: time::Instant, - close: bool, -) { - if let Some(mut pool) = pool { - if close { - pool.close(IoConnection::new(ConnectionType::H2(io), created, None)); - } else { - pool.release(IoConnection::new(ConnectionType::H2(io), created, None)); - } - } -} - -pub(crate) fn handshake( +pub(crate) fn handshake( io: Io, config: &ConnectorConfig, ) -> impl Future, Connection), h2::Error>> -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, { let mut builder = Builder::new(); builder diff --git a/actix-http/src/client/mod.rs b/actix-http/src/client/mod.rs index 5f5e57edb..41d5fef2a 100644 --- a/actix-http/src/client/mod.rs +++ b/actix-http/src/client/mod.rs @@ -14,10 +14,10 @@ pub use actix_tls::connect::{ Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection, }; -pub use self::connection::Connection; -pub use self::connector::Connector; +pub use self::connection::{Connection, ConnectionIo}; +pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; -pub use self::pool::Protocol; +pub use crate::Protocol; #[derive(Clone)] pub struct Connect { diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index 3800696fa..88188038f 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -1,40 +1,38 @@ //! Client connection pooling keyed on the authority part of the connection URI. -use std::collections::VecDeque; -use std::future::Future; -use std::ops::Deref; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::Arc; -use std::task::{Context, Poll}; -use std::time::{Duration, Instant}; -use std::{cell::RefCell, io}; +use std::{ + cell::RefCell, + collections::VecDeque, + future::Future, + io, + ops::Deref, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + time::{Duration, Instant}, +}; -use actix_codec::{AsyncRead, AsyncWrite}; +use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_rt::time::{sleep, Sleep}; use actix_service::Service; use ahash::AHashMap; use futures_core::future::LocalBoxFuture; use http::uri::Authority; use pin_project::pin_project; -use tokio::io::ReadBuf; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use super::config::ConnectorConfig; -use super::connection::{ConnectionType, H2Connection, IoConnection}; +use super::connection::{ + ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner, +}; use super::error::ConnectError; use super::h2proto::handshake; use super::Connect; - -#[derive(Clone, Copy, PartialEq)] -/// Protocol version -pub enum Protocol { - Http1, - Http2, -} +use super::Protocol; #[derive(Hash, Eq, PartialEq, Clone, Debug)] -pub(crate) struct Key { +pub struct Key { authority: Authority, } @@ -44,17 +42,18 @@ impl From for Key { } } +#[doc(hidden)] /// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key. -pub(crate) struct ConnectionPool +pub struct ConnectionPool where Io: AsyncWrite + Unpin + 'static, { - connector: Rc, + connector: S, inner: ConnectionPoolInner, } /// wrapper type for check the ref count of Rc. -struct ConnectionPoolInner(Rc>) +pub struct ConnectionPoolInner(Rc>) where Io: AsyncWrite + Unpin + 'static; @@ -62,10 +61,21 @@ impl ConnectionPoolInner where Io: AsyncWrite + Unpin + 'static, { + fn new(config: ConnectorConfig) -> Self { + let permits = Arc::new(Semaphore::new(config.limit)); + let available = RefCell::new(AHashMap::default()); + + Self(Rc::new(ConnectionPoolInnerPriv { + config, + available, + permits, + })) + } + /// spawn a async for graceful shutdown h1 Io type with a timeout. - fn close(&self, conn: ConnectionType) { + fn close(&self, conn: ConnectionInnerType) { if let Some(timeout) = self.config.disconnect_timeout { - if let ConnectionType::H1(io) = conn { + if let ConnectionInnerType::H1(io) = conn { actix_rt::spawn(CloseConnection::new(io, timeout)); } } @@ -110,7 +120,7 @@ where } } -struct ConnectionPoolInnerPriv +pub struct ConnectionPoolInnerPriv where Io: AsyncWrite + Unpin + 'static, { @@ -134,40 +144,22 @@ where /// Any requests beyond limit would be wait in fifo order and get notified in async manner /// by [`tokio::sync::Semaphore`] pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self { - let permits = Arc::new(Semaphore::new(config.limit)); - let available = RefCell::new(AHashMap::default()); - let connector = Rc::new(connector); - - let inner = ConnectionPoolInner(Rc::new(ConnectionPoolInnerPriv { - config, - available, - permits, - })); + let inner = ConnectionPoolInner::new(config); Self { connector, inner } } } -impl Clone for ConnectionPool -where - Io: AsyncWrite + Unpin + 'static, -{ - fn clone(&self) -> Self { - Self { - connector: self.connector.clone(), - inner: self.inner.clone(), - } - } -} - impl Service for ConnectionPool where - S: Service + 'static, - Io: AsyncRead + AsyncWrite + Unpin + 'static, + S: Service + + Clone + + 'static, + Io: ConnectionIo, { - type Response = IoConnection; + type Response = ConnectionType; type Error = ConnectError; - type Future = LocalBoxFuture<'static, Result, ConnectError>>; + type Future = LocalBoxFuture<'static, Result>; actix_service::forward_ready!(connector); @@ -211,7 +203,7 @@ where inner.close(c.conn); } else { // check if the connection is still usable - if let ConnectionType::H1(ref mut io) = c.conn { + if let ConnectionInnerType::H1(ref mut io) = c.conn { let check = ConnectionCheckFuture { io }; match check.await { ConnectionState::Tainted => { @@ -235,28 +227,26 @@ where // construct acquired. It's used to put Io type back to pool/ close the Io type. // permit is carried with the whole lifecycle of Acquired. - let acquired = Some(Acquired { key, inner, permit }); + let acquired = Acquired { key, inner, permit }; // match the connection and spawn new one if did not get anything. match conn { - Some(conn) => Ok(IoConnection::new(conn.conn, conn.created, acquired)), + Some(conn) => { + Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)) + } None => { let (io, proto) = connector.call(req).await?; + // TODO: remove when http3 is added in support. + assert!(proto != Protocol::Http3); + if proto == Protocol::Http1 { - Ok(IoConnection::new( - ConnectionType::H1(io), - Instant::now(), - acquired, - )) + Ok(ConnectionType::from_h1(io, Instant::now(), acquired)) } else { - let config = &acquired.as_ref().unwrap().inner.config; + let config = &acquired.inner.config; let (sender, connection) = handshake(io, config).await?; - Ok(IoConnection::new( - ConnectionType::H2(H2Connection::new(sender, connection)), - Instant::now(), - acquired, - )) + let inner = H2ConnectionInner::new(sender, connection); + Ok(ConnectionType::from_h2(inner, Instant::now(), acquired)) } } } @@ -307,7 +297,7 @@ where } struct PooledConnection { - conn: ConnectionType, + conn: ConnectionInnerType, used: Instant, created: Instant, } @@ -347,28 +337,26 @@ where } } -pub(crate) struct Acquired +pub struct Acquired where Io: AsyncWrite + Unpin + 'static, { + /// authority key for identify connection. key: Key, + /// handle to connection pool. inner: ConnectionPoolInner, + /// permit for limit concurrent in-flight connection for a Client object. permit: OwnedSemaphorePermit, } -impl Acquired -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ +impl Acquired { /// Close the IO. - pub(crate) fn close(&mut self, conn: IoConnection) { - let (conn, _) = conn.into_inner(); + pub(super) fn close(&self, conn: ConnectionInnerType) { self.inner.close(conn); } /// Release IO back into pool. - pub(crate) fn release(&mut self, conn: IoConnection) { - let (io, created) = conn.into_inner(); + pub(super) fn release(&self, conn: ConnectionInnerType, created: Instant) { let Acquired { key, inner, .. } = self; inner @@ -377,12 +365,12 @@ where .entry(key.clone()) .or_insert_with(VecDeque::new) .push_back(PooledConnection { - conn: io, + conn, created, used: Instant::now(), }); - let _ = &mut self.permit; + let _ = &self.permit; } } @@ -393,7 +381,7 @@ mod test { use http::Uri; use super::*; - use crate::client::connection::IoConnection; + use crate::client::connection::ConnectionType; /// A stream type that always returns pending on async read. /// @@ -440,6 +428,7 @@ mod test { } } + #[derive(Clone)] struct TestPoolConnector { generated: Rc>, } @@ -458,12 +447,14 @@ mod test { } } - fn release(conn: IoConnection) + fn release(conn: ConnectionType) where T: AsyncRead + AsyncWrite + Unpin + 'static, { - let (conn, created, mut acquired) = conn.into_parts(); - acquired.release(IoConnection::new(conn, created, None)); + match conn { + ConnectionType::H1(mut conn) => conn.on_release(true), + ConnectionType::H2(mut conn) => conn.on_release(false), + } } #[actix_rt::test] diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index 9f84b8694..9a2293e92 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -126,9 +126,7 @@ impl ServiceConfig { pub fn client_timer(&self) -> Option { let delay_time = self.0.client_timeout; if delay_time != 0 { - Some(sleep_until( - self.0.date_service.now() + Duration::from_millis(delay_time), - )) + Some(sleep_until(self.now() + Duration::from_millis(delay_time))) } else { None } @@ -138,7 +136,7 @@ impl ServiceConfig { pub fn client_timer_expire(&self) -> Option { let delay = self.0.client_timeout; if delay != 0 { - Some(self.0.date_service.now() + Duration::from_millis(delay)) + Some(self.now() + Duration::from_millis(delay)) } else { None } @@ -148,7 +146,7 @@ impl ServiceConfig { pub fn client_disconnect_timer(&self) -> Option { let delay = self.0.client_disconnect; if delay != 0 { - Some(self.0.date_service.now() + Duration::from_millis(delay)) + Some(self.now() + Duration::from_millis(delay)) } else { None } @@ -157,20 +155,12 @@ impl ServiceConfig { #[inline] /// Return keep-alive timer delay is configured. pub fn keep_alive_timer(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(sleep_until(self.0.date_service.now() + ka)) - } else { - None - } + self.keep_alive().map(|ka| sleep_until(self.now() + ka)) } /// Keep-alive expire time pub fn keep_alive_expire(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(self.0.date_service.now() + ka) - } else { - None - } + self.keep_alive().map(|ka| self.now() + ka) } #[inline] diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 97f2b3eff..1354e998e 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -54,7 +54,7 @@ impl Error { /// Similar to `as_response_error` but downcasts. pub fn as_error(&self) -> Option<&T> { - ResponseError::downcast_ref(self.cause.as_ref()) + ::downcast_ref(self.cause.as_ref()) } } @@ -483,7 +483,7 @@ where /// response as opposite to *INTERNAL SERVER ERROR* which is defined by /// default. /// -/// ```rust +/// ``` /// # use std::io; /// # use actix_http::*; /// @@ -983,6 +983,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + #[cfg(feature = "cookies")] #[test] fn test_cookie_parse() { let resp: Response = CookieParseError::EmptyName.error_response(); diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 6df579c0a..e5989e5ee 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -662,7 +662,7 @@ where // got timeout during shutdown, drop connection if this.flags.contains(Flags::SHUTDOWN) { return Err(DispatchError::DisconnectTimeout); - // exceed deadline. check for any outstanding tasks + // exceed deadline. check for any outstanding tasks } else if timer.deadline() >= *this.ka_expire { // have no task at hand. if this.state.is_empty() && this.write_buf.is_empty() { @@ -695,15 +695,15 @@ where this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.state.set(State::None); } - // still have unfinished task. try to reset and register keep-alive. + // still have unfinished task. try to reset and register keep-alive. } else if let Some(deadline) = this.codec.config().keep_alive_expire() { timer.as_mut().reset(deadline); let _ = timer.poll(cx); } - // timer resolved but still have not met the keep-alive expire deadline. - // reset and register for later wakeup. + // timer resolved but still have not met the keep-alive expire deadline. + // reset and register for later wakeup. } else { timer.as_mut().reset(*this.ka_expire); let _ = timer.poll(cx); @@ -951,14 +951,15 @@ mod tests { use std::str; use actix_service::fn_service; - use futures_util::future::{lazy, ready}; + use futures_util::future::{lazy, ready, Ready}; use super::*; - use crate::test::TestBuffer; - use crate::{error::Error, KeepAlive}; use crate::{ + error::Error, h1::{ExpectHandler, UpgradeHandler}, - test::TestSeqBuffer, + http::Method, + test::{TestBuffer, TestSeqBuffer}, + HttpMessage, KeepAlive, }; fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option { @@ -1282,14 +1283,30 @@ mod tests { #[actix_rt::test] async fn test_upgrade() { + struct TestUpgrade; + + impl Service<(Request, Framed)> for TestUpgrade { + type Response = (); + type Error = Error; + type Future = Ready>; + + actix_service::always_ready!(); + + fn call(&self, (req, _framed): (Request, Framed)) -> Self::Future { + assert_eq!(req.method(), Method::GET); + assert!(req.upgrade()); + assert_eq!(req.headers().get("upgrade").unwrap(), "websocket"); + ready(Ok(())) + } + } + lazy(|cx| { let mut buf = TestSeqBuffer::empty(); let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); - let services = - HttpFlow::new(ok_service(), ExpectHandler, Some(UpgradeHandler)); + let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade)); - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new( buf.clone(), cfg, services, diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index 97916e7db..4e9903284 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -632,10 +632,9 @@ mod tests { let mut res: Response<()> = Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>(); + res.headers_mut().insert(DATE, HeaderValue::from_static("")); res.headers_mut() - .insert(DATE, HeaderValue::from_static(&"")); - res.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static(&"0")); + .insert(CONTENT_LENGTH, HeaderValue::from_static("0")); let _ = res.encode_headers( &mut bytes, diff --git a/actix-http/src/h1/expect.rs b/actix-http/src/h1/expect.rs index 65856edf6..5015069bb 100644 --- a/actix-http/src/h1/expect.rs +++ b/actix-http/src/h1/expect.rs @@ -1,5 +1,3 @@ -use std::task::Poll; - use actix_service::{Service, ServiceFactory}; use futures_util::future::{ready, Ready}; diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index d4cfee146..32275ac6b 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -3,9 +3,8 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::pin::Pin; use std::rc::{Rc, Weak}; -use std::task::{Context, Poll}; +use std::task::{Context, Poll, Waker}; -use actix_utils::task::LocalWaker; use bytes::Bytes; use futures_core::Stream; @@ -134,7 +133,7 @@ impl PayloadSender { if shared.borrow().need_read { PayloadStatus::Read } else { - shared.borrow_mut().io_task.register(cx.waker()); + shared.borrow_mut().register_io(cx); PayloadStatus::Pause } } else { @@ -150,8 +149,8 @@ struct Inner { err: Option, need_read: bool, items: VecDeque, - task: LocalWaker, - io_task: LocalWaker, + task: Option, + io_task: Option, } impl Inner { @@ -162,8 +161,48 @@ impl Inner { err: None, items: VecDeque::new(), need_read: true, - task: LocalWaker::new(), - io_task: LocalWaker::new(), + task: None, + io_task: None, + } + } + + /// Wake up future waiting for payload data to be available. + fn wake(&mut self) { + if let Some(waker) = self.task.take() { + waker.wake(); + } + } + + /// Wake up future feeding data to Payload. + fn wake_io(&mut self) { + if let Some(waker) = self.io_task.take() { + waker.wake(); + } + } + + /// Register future waiting data from payload. + /// Waker would be used in `Inner::wake` + fn register(&mut self, cx: &mut Context<'_>) { + if self + .task + .as_ref() + .map(|w| !cx.waker().will_wake(w)) + .unwrap_or(true) + { + self.task = Some(cx.waker().clone()); + } + } + + // Register future feeding data to payload. + /// Waker would be used in `Inner::wake_io` + fn register_io(&mut self, cx: &mut Context<'_>) { + if self + .io_task + .as_ref() + .map(|w| !cx.waker().will_wake(w)) + .unwrap_or(true) + { + self.io_task = Some(cx.waker().clone()); } } @@ -182,7 +221,7 @@ impl Inner { self.len += data.len(); self.items.push_back(data); self.need_read = self.len < MAX_BUFFER_SIZE; - self.task.wake(); + self.wake(); } #[cfg(test)] @@ -199,9 +238,9 @@ impl Inner { self.need_read = self.len < MAX_BUFFER_SIZE; if self.need_read && !self.eof { - self.task.register(cx.waker()); + self.register(cx); } - self.io_task.wake(); + self.wake_io(); Poll::Ready(Some(Ok(data))) } else if let Some(err) = self.err.take() { Poll::Ready(Some(Err(err))) @@ -209,8 +248,8 @@ impl Inner { Poll::Ready(None) } else { self.need_read = true; - self.task.register(cx.waker()); - self.io_task.wake(); + self.register(cx); + self.wake_io(); Poll::Pending } } diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index b79453ebd..51303886b 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -94,10 +94,10 @@ mod openssl { use super::*; use actix_service::ServiceFactoryExt; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; + use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::TlsError; - impl H1Service, S, B, X, U> + impl H1Service, S, B, X, U> where S: ServiceFactory, S::Error: Into, @@ -108,7 +108,7 @@ mod openssl { X::Error: Into, X::InitError: fmt::Debug, U: ServiceFactory< - (Request, Framed, Codec>), + (Request, Framed, Codec>), Config = (), Response = (), >, @@ -131,7 +131,7 @@ mod openssl { .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) - .and_then(|io: SslStream| { + .and_then(|io: TlsStream| { let peer_addr = io.get_ref().peer_addr().ok(); ready(Ok((io, peer_addr))) }) diff --git a/actix-http/src/h1/upgrade.rs b/actix-http/src/h1/upgrade.rs index 5e24d84e3..e57ea8ae9 100644 --- a/actix-http/src/h1/upgrade.rs +++ b/actix-http/src/h1/upgrade.rs @@ -1,8 +1,6 @@ -use std::task::Poll; - use actix_codec::Framed; use actix_service::{Service, ServiceFactory}; -use futures_util::future::{ready, Ready}; +use futures_core::future::LocalBoxFuture; use crate::error::Error; use crate::h1::Codec; @@ -16,7 +14,7 @@ impl ServiceFactory<(Request, Framed)> for UpgradeHandler { type Config = (); type Service = UpgradeHandler; type InitError = Error; - type Future = Ready>; + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { unimplemented!() @@ -26,11 +24,11 @@ impl ServiceFactory<(Request, Framed)> for UpgradeHandler { impl Service<(Request, Framed)> for UpgradeHandler { type Response = (); type Error = Error; - type Future = Ready>; + type Future = LocalBoxFuture<'static, Result>; actix_service::always_ready!(); fn call(&self, _: (Request, Framed)) -> Self::Future { - ready(Ok(())) + unimplemented!() } } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 5ccd2a9d1..6e6cd5a2f 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -1,18 +1,14 @@ -use std::future::Future; -use std::marker::PhantomData; -use std::net; -use std::pin::Pin; -use std::rc::Rc; use std::task::{Context, Poll}; -use std::{cmp, convert::TryFrom}; +use std::{cmp, future::Future, marker::PhantomData, net, pin::Pin, rc::Rc}; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::time::{Instant, Sleep}; use actix_service::Service; use bytes::{Bytes, BytesMut}; use futures_core::ready; -use h2::server::{Connection, SendResponse}; -use h2::SendStream; +use h2::{ + server::{Connection, SendResponse}, + SendStream, +}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use log::{error, trace}; @@ -41,8 +37,6 @@ where on_connect_data: OnConnectData, config: ServiceConfig, peer_addr: Option, - ka_expire: Instant, - ka_timer: Option, _phantom: PhantomData, } @@ -59,33 +53,14 @@ where connection: Connection, on_connect_data: OnConnectData, config: ServiceConfig, - timeout: Option, peer_addr: Option, ) -> Self { - // let keepalive = config.keep_alive_enabled(); - // let flags = if keepalive { - // Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED - // } else { - // Flags::empty() - // }; - - // keep-alive timer - let (ka_expire, ka_timer) = if let Some(delay) = timeout { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = config.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (config.now(), None) - }; - Dispatcher { flow, config, peer_addr, connection, on_connect_data, - ka_expire, - ka_timer, _phantom: PhantomData, } } @@ -113,19 +88,12 @@ where Some(Err(err)) => return Poll::Ready(Err(err.into())), Some(Ok((req, res))) => { - // update keep-alive expire - if this.ka_timer.is_some() { - if let Some(expire) = this.config.keep_alive_expire() { - this.ka_expire = expire; - } - } - let (parts, body) = req.into_parts(); let pl = crate::h2::Payload::new(body); let pl = Payload::::H2(pl); let mut req = Request::with_payload(pl); - let head = &mut req.head_mut(); + let head = req.head_mut(); head.uri = parts.uri; head.method = parts.method; head.version = parts.version; @@ -135,7 +103,7 @@ where // merge on_connect_ext data into request extensions this.on_connect_data.merge_into(&mut req); - let svc = ServiceResponse:: { + let svc = ServiceResponse { state: ServiceResponseState::ServiceCall( this.flow.service.call(req), Some(res), @@ -203,16 +171,22 @@ where BodySize::Empty => res .headers_mut() .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), - BodySize::Sized(len) => res.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(format!("{}", len)).unwrap(), - ), + BodySize::Sized(len) => { + let mut buf = itoa::Buffer::new(); + + res.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::from_str(buf.format(*len)).unwrap(), + ) + } }; // copy headers for (key, value) in head.headers.iter() { match *key { - // omit HTTP/1 only headers + // TODO: consider skipping other headers according to: + // https://tools.ietf.org/html/rfc7540#section-8.1.2.2 + // omit HTTP/1.x only headers CONNECTION | TRANSFER_ENCODING => continue, CONTENT_LENGTH if skip_len => continue, DATE => has_date = true, @@ -311,57 +285,50 @@ where ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => { loop { - loop { - match this.buffer { - Some(ref mut buffer) => { - match ready!(stream.poll_capacity(cx)) { - None => return Poll::Ready(()), + match this.buffer { + Some(ref mut buffer) => match ready!(stream.poll_capacity(cx)) { + None => return Poll::Ready(()), - Some(Ok(cap)) => { - let len = buffer.len(); - let bytes = buffer.split_to(cmp::min(cap, len)); + Some(Ok(cap)) => { + let len = buffer.len(); + let bytes = buffer.split_to(cmp::min(cap, len)); - if let Err(e) = stream.send_data(bytes, false) { - warn!("{:?}", e); - return Poll::Ready(()); - } else if !buffer.is_empty() { - let cap = cmp::min(buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - this.buffer.take(); - } - } - - Some(Err(e)) => { - warn!("{:?}", e); - return Poll::Ready(()); - } + if let Err(e) = stream.send_data(bytes, false) { + warn!("{:?}", e); + return Poll::Ready(()); + } else if !buffer.is_empty() { + let cap = cmp::min(buffer.len(), CHUNK_SIZE); + stream.reserve_capacity(cap); + } else { + this.buffer.take(); } } - None => match ready!(body.as_mut().poll_next(cx)) { - None => { - if let Err(e) = stream.send_data(Bytes::new(), true) - { - warn!("{:?}", e); - } - return Poll::Ready(()); - } + Some(Err(e)) => { + warn!("{:?}", e); + return Poll::Ready(()); + } + }, - Some(Ok(chunk)) => { - stream.reserve_capacity(cmp::min( - chunk.len(), - CHUNK_SIZE, - )); - *this.buffer = Some(chunk); + None => match ready!(body.as_mut().poll_next(cx)) { + None => { + if let Err(e) = stream.send_data(Bytes::new(), true) { + warn!("{:?}", e); } + return Poll::Ready(()); + } - Some(Err(e)) => { - error!("Response payload stream error: {:?}", e); - return Poll::Ready(()); - } - }, - } + Some(Ok(chunk)) => { + stream + .reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE)); + *this.buffer = Some(chunk); + } + + Some(Err(e)) => { + error!("Response payload stream error: {:?}", e); + return Poll::Ready(()); + } + }, } } } diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index e00c8d968..c64139564 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -13,7 +13,7 @@ use actix_service::{ use bytes::Bytes; use futures_core::ready; use futures_util::future::ok; -use h2::server::{self, Handshake}; +use h2::server::{handshake, Handshake}; use log::error; use crate::body::MessageBody; @@ -93,12 +93,12 @@ where #[cfg(feature = "openssl")] mod openssl { use actix_service::{fn_factory, fn_service, ServiceFactoryExt}; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; + use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::TlsError; use super::*; - impl H2Service, S, B> + impl H2Service, S, B> where S: ServiceFactory, S::Error: Into + 'static, @@ -123,7 +123,7 @@ mod openssl { .map_init_err(|_| panic!()), ) .and_then(fn_factory(|| { - ok::<_, S::InitError>(fn_service(|io: SslStream| { + ok::<_, S::InitError>(fn_service(|io: TlsStream| { let peer_addr = io.get_ref().peer_addr().ok(); ok((io, peer_addr)) })) @@ -307,7 +307,7 @@ where Some(self.cfg.clone()), addr, on_connect_data, - server::handshake(io), + handshake(io), ), } } @@ -368,7 +368,6 @@ where conn, on_connect_data, config.take().unwrap(), - None, *peer_addr, )); self.poll(cx) diff --git a/actix-http/src/header/common/cache_control.rs b/actix-http/src/header/common/cache_control.rs index 94ce9a750..b19823d22 100644 --- a/actix-http/src/header/common/cache_control.rs +++ b/actix-http/src/header/common/cache_control.rs @@ -36,7 +36,7 @@ use crate::header::{ /// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); /// ``` /// -/// ```rust +/// ``` /// use actix_http::Response; /// use actix_http::http::header::{CacheControl, CacheDirective}; /// diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index ecc59aba3..6076d033c 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -6,7 +6,7 @@ //! Browser conformance tests at: http://greenbytes.de/tech/tc2231/ //! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; use std::fmt::{self, Write}; @@ -520,9 +520,7 @@ impl fmt::Display for DispositionParam { // // // See also comments in test_from_raw_unnecessary_percent_decode. - lazy_static! { - static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap(); - } + static RE: Lazy = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); match self { DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Filename(ref value) => { diff --git a/actix-http/src/header/shared/entity.rs b/actix-http/src/header/shared/entity.rs index eb383cd6f..2505216f2 100644 --- a/actix-http/src/header/shared/entity.rs +++ b/actix-http/src/header/shared/entity.rs @@ -127,9 +127,8 @@ impl Display for EntityTag { impl FromStr for EntityTag { type Err = crate::error::ParseError; - fn from_str(s: &str) -> Result { - let length: usize = s.len(); - let slice = &s[..]; + fn from_str(slice: &str) -> Result { + let length = slice.len(); // Early exits if it doesn't terminate in a DQUOTE. if !slice.ends_with('"') || slice.len() < 2 { return Err(crate::error::ParseError::Header); diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 6bdcb7922..9fd4cdfb0 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -88,9 +88,9 @@ pub fn parse_extended_value( }; Ok(ExtendedValue { - value, charset, language_tag, + value, }) } diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index 13195f7db..74188717d 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -1,15 +1,15 @@ use std::io; -use bytes::{BufMut, BytesMut}; +use bytes::BufMut; use http::Version; const DIGITS_START: u8 = b'0'; -pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { +pub(crate) fn write_status_line(version: Version, n: u16, buf: &mut B) { match version { - Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), - Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), - Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), + Version::HTTP_11 => buf.put_slice(b"HTTP/1.1 "), + Version::HTTP_10 => buf.put_slice(b"HTTP/1.0 "), + Version::HTTP_09 => buf.put_slice(b"HTTP/0.9 "), _ => { // other HTTP version handlers do not use this method } @@ -19,33 +19,36 @@ pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) let d10 = ((n / 10) % 10) as u8; let d1 = (n % 10) as u8; - bytes.put_u8(DIGITS_START + d100); - bytes.put_u8(DIGITS_START + d10); - bytes.put_u8(DIGITS_START + d1); + buf.put_u8(DIGITS_START + d100); + buf.put_u8(DIGITS_START + d10); + buf.put_u8(DIGITS_START + d1); // trailing space before reason - bytes.put_u8(b' '); + buf.put_u8(b' '); } /// NOTE: bytes object has to contain enough space -pub fn write_content_length(n: u64, bytes: &mut BytesMut) { +pub fn write_content_length(n: u64, buf: &mut B) { if n == 0 { - bytes.put_slice(b"\r\ncontent-length: 0\r\n"); + buf.put_slice(b"\r\ncontent-length: 0\r\n"); return; } - let mut buf = itoa::Buffer::new(); + let mut buffer = itoa::Buffer::new(); - bytes.put_slice(b"\r\ncontent-length: "); - bytes.put_slice(buf.format(n).as_bytes()); - bytes.put_slice(b"\r\n"); + buf.put_slice(b"\r\ncontent-length: "); + buf.put_slice(buffer.format(n).as_bytes()); + buf.put_slice(b"\r\n"); } -pub(crate) struct Writer<'a>(pub &'a mut BytesMut); +pub(crate) struct Writer<'a, B>(pub &'a mut B); -impl<'a> io::Write for Writer<'a> { +impl<'a, B> io::Write for Writer<'a, B> +where + B: BufMut, +{ fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); + self.0.put_slice(buf); Ok(buf.len()) } @@ -58,6 +61,8 @@ impl<'a> io::Write for Writer<'a> { mod tests { use std::str::from_utf8; + use bytes::BytesMut; + use super::*; #[test] diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 471dacd28..94f12bcc3 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -5,7 +5,6 @@ use std::{ convert::TryInto, fmt, future::Future, - ops, pin::Pin, str, task::{Context, Poll}, @@ -358,7 +357,7 @@ impl ResponseBuilder { /// Insert a header, replacing any that were set with an equivalent field name. /// - /// ```rust + /// ``` /// # use actix_http::Response; /// use actix_http::http::header::ContentType; /// @@ -385,7 +384,7 @@ impl ResponseBuilder { /// Append a header, keeping any that were set with an equivalent field name. /// - /// ```rust + /// ``` /// # use actix_http::Response; /// use actix_http::http::header::ContentType; /// @@ -498,7 +497,8 @@ impl ResponseBuilder { /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. #[inline] pub fn no_chunking(&mut self, len: u64) -> &mut Self { - self.insert_header((header::CONTENT_LENGTH, len)); + let mut buf = itoa::Buffer::new(); + self.insert_header((header::CONTENT_LENGTH, buf.format(len))); if let Some(parts) = parts(&mut self.head, &self.err) { parts.no_chunking(true); @@ -525,7 +525,7 @@ impl ResponseBuilder { /// Set a cookie /// - /// ```rust + /// ``` /// use actix_http::{http, Request, Response}; /// /// fn index(req: Request) -> Response { @@ -555,7 +555,7 @@ impl ResponseBuilder { /// Remove cookie /// - /// ```rust + /// ``` /// use actix_http::{http, Request, Response, HttpMessage}; /// /// fn index(req: Request) -> Response { @@ -672,12 +672,8 @@ impl ResponseBuilder { /// Set a json body and generate `Response` /// /// `ResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Response - where - T: ops::Deref, - T::Target: Serialize, - { - match serde_json::to_string(&*value) { + pub fn json(&mut self, value: impl Serialize) -> Response { + match serde_json::to_string(&value) { Ok(body) => { let contains = if let Some(parts) = parts(&mut self.head, &self.err) { parts.headers.contains_key(header::CONTENT_TYPE) @@ -896,8 +892,9 @@ mod tests { use super::*; use crate::body::Body; - use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE}; - use crate::HttpMessage; + use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; + #[cfg(feature = "cookies")] + use crate::{http::header::SET_COOKIE, HttpMessage}; #[test] fn test_debug() { @@ -909,6 +906,7 @@ mod tests { assert!(dbg.contains("Response")); } + #[cfg(feature = "cookies")] #[test] fn test_response_cookies() { let req = crate::test::TestRequest::default() @@ -946,6 +944,7 @@ mod tests { ); } + #[cfg(feature = "cookies")] #[test] fn test_update_response_cookies() { let mut r = Response::Ok() @@ -1003,7 +1002,12 @@ mod tests { #[test] fn test_json() { - let resp = Response::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); + let resp = Response::Ok().json(vec!["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("application/json")); + assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); + + let resp = Response::Ok().json(&["v1", "v2", "v3"]); let ct = resp.headers().get(CONTENT_TYPE).unwrap(); assert_eq!(ct, HeaderValue::from_static("application/json")); assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); @@ -1097,6 +1101,7 @@ mod tests { assert_eq!(resp.body().get_ref(), b"test"); } + #[cfg(feature = "cookies")] #[test] fn test_into_builder() { let mut resp: Response = "test".into(); diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index fee26dcc3..1a06cec3d 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -1,14 +1,19 @@ -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{fmt, net, rc::Rc}; +use std::{ + fmt, + future::Future, + marker::PhantomData, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use bytes::Bytes; -use futures_core::{ready, Future}; -use h2::server::{self, Handshake}; +use futures_core::ready; +use h2::server::{handshake, Handshake}; use pin_project::pin_project; use crate::body::MessageBody; @@ -185,10 +190,10 @@ where mod openssl { use super::*; use actix_service::ServiceFactoryExt; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; + use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::TlsError; - impl HttpService, S, B, X, U> + impl HttpService, S, B, X, U> where S: ServiceFactory, S::Error: Into + 'static, @@ -201,13 +206,13 @@ mod openssl { X::InitError: fmt::Debug, >::Future: 'static, U: ServiceFactory< - (Request, Framed, h1::Codec>), + (Request, Framed, h1::Codec>), Config = (), Response = (), >, U::Error: fmt::Display + Into, U::InitError: fmt::Debug, - , h1::Codec>)>>::Future: 'static, + , h1::Codec>)>>::Future: 'static, { /// Create openssl based service pub fn openssl( @@ -225,7 +230,7 @@ mod openssl { .map_err(TlsError::Tls) .map_init_err(|_| panic!()), ) - .and_then(|io: SslStream| async { + .and_then(|io: TlsStream| async { let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { if protos.windows(2).any(|window| window == b"h2") { Protocol::Http2 @@ -562,7 +567,7 @@ where match proto { Protocol::Http2 => HttpServiceHandlerResponse { state: State::H2Handshake(Some(( - server::handshake(io), + handshake(io), self.cfg.clone(), self.flow.clone(), on_connect_data, @@ -658,7 +663,6 @@ where conn, on_connect_data, cfg, - None, peer_addr, ))); self.poll(cx) diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 870a656df..ad3dc74b2 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -26,7 +26,7 @@ use crate::{ /// Test `Request` builder /// -/// ```rust,ignore +/// ```ignore /// # use http::{header, StatusCode}; /// # use actix_web::*; /// use actix_web::test::TestRequest; diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs index 46bf73037..fd82fd42e 100644 --- a/actix-http/src/time_parser.rs +++ b/actix-http/src/time_parser.rs @@ -1,7 +1,7 @@ use time::{Date, OffsetDateTime, PrimitiveDateTime}; /// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime. -pub fn parse_http_date(time: &str) -> Option { +pub(crate) fn parse_http_date(time: &str) -> Option { try_parse_rfc_1123(time) .or_else(|| try_parse_rfc_850(time)) .or_else(|| try_parse_asctime(time)) diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 54d850854..8655216fa 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -80,7 +80,7 @@ bitflags! { impl Codec { /// Create new WebSocket frames decoder. - pub fn new() -> Codec { + pub const fn new() -> Codec { Codec { max_size: 65_536, flags: Flags::SERVER, diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 0490163d5..cec73db96 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -1,4 +1,4 @@ -//! WebSocket protocol. +//! WebSocket protocol implementation. //! //! To setup a WebSocket, first perform the WebSocket handshake then on success convert `Payload` into a //! `WsStream` stream and then use `WsWriter` to communicate with the peer. @@ -8,9 +8,12 @@ use std::io; use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; -use crate::error::ResponseError; -use crate::message::RequestHead; -use crate::response::{Response, ResponseBuilder}; +use crate::{ + error::ResponseError, + header::HeaderValue, + message::RequestHead, + response::{Response, ResponseBuilder}, +}; mod codec; mod dispatcher; @@ -89,7 +92,7 @@ pub enum HandshakeError { NoVersionHeader, /// Unsupported WebSocket version. - #[display(fmt = "Unsupported version.")] + #[display(fmt = "Unsupported WebSocket version.")] UnsupportedVersion, /// WebSocket key is not set or wrong. @@ -105,19 +108,19 @@ impl ResponseError for HandshakeError { .finish(), HandshakeError::NoWebsocketUpgrade => Response::BadRequest() - .reason("No WebSocket UPGRADE header found") + .reason("No WebSocket Upgrade header found") .finish(), HandshakeError::NoConnectionUpgrade => Response::BadRequest() - .reason("No CONNECTION upgrade") + .reason("No Connection upgrade") .finish(), HandshakeError::NoVersionHeader => Response::BadRequest() - .reason("Websocket version header is required") + .reason("WebSocket version header is required") .finish(), HandshakeError::UnsupportedVersion => Response::BadRequest() - .reason("Unsupported version") + .reason("Unsupported WebSocket version") .finish(), HandshakeError::BadWebsocketKey => { @@ -193,7 +196,11 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { Response::build(StatusCode::SWITCHING_PROTOCOLS) .upgrade("websocket") .insert_header((header::TRANSFER_ENCODING, "chunked")) - .insert_header((header::SEC_WEBSOCKET_ACCEPT, key)) + .insert_header(( + header::SEC_WEBSOCKET_ACCEPT, + // key is known to be header value safe ascii + HeaderValue::from_bytes(&key).unwrap(), + )) .take() } diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 1e8bf7af3..fdcde5eac 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -1,5 +1,7 @@ -use std::convert::{From, Into}; -use std::fmt; +use std::{ + convert::{From, Into}, + fmt, +}; /// Operation codes as part of RFC6455. #[derive(Debug, Eq, PartialEq, Clone, Copy)] @@ -28,8 +30,9 @@ pub enum OpCode { impl fmt::Display for OpCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use self::OpCode::*; - match *self { + use OpCode::*; + + match self { Continue => write!(f, "CONTINUE"), Text => write!(f, "TEXT"), Binary => write!(f, "BINARY"), @@ -44,6 +47,7 @@ impl fmt::Display for OpCode { impl From for u8 { fn from(op: OpCode) -> u8 { use self::OpCode::*; + match op { Continue => 0, Text => 1, @@ -62,6 +66,7 @@ impl From for u8 { impl From for OpCode { fn from(byte: u8) -> OpCode { use self::OpCode::*; + match byte { 0 => Continue, 1 => Text, @@ -77,63 +82,66 @@ impl From for OpCode { /// Status code used to indicate why an endpoint is closing the WebSocket connection. #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum CloseCode { - /// Indicates a normal closure, meaning that the purpose for - /// which the connection was established has been fulfilled. + /// Indicates a normal closure, meaning that the purpose for which the connection was + /// established has been fulfilled. Normal, - /// Indicates that an endpoint is "going away", such as a server - /// going down or a browser having navigated away from a page. + + /// Indicates that an endpoint is "going away", such as a server going down or a browser having + /// navigated away from a page. Away, - /// Indicates that an endpoint is terminating the connection due - /// to a protocol error. + + /// Indicates that an endpoint is terminating the connection due to a protocol error. Protocol, - /// Indicates that an endpoint is terminating the connection - /// because it has received a type of data it cannot accept (e.g., an - /// endpoint that understands only text data MAY send this if it + + /// Indicates that an endpoint is terminating the connection because it has received a type of + /// data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it /// receives a binary message). Unsupported, - /// Indicates an abnormal closure. If the abnormal closure was due to an - /// error, this close code will not be used. Instead, the `on_error` method - /// of the handler will be called with the error. However, if the connection - /// is simply dropped, without an error, this close code will be sent to the - /// handler. + + /// Indicates an abnormal closure. If the abnormal closure was due to an error, this close code + /// will not be used. Instead, the `on_error` method of the handler will be called with + /// the error. However, if the connection is simply dropped, without an error, this close code + /// will be sent to the handler. Abnormal, - /// Indicates that an endpoint is terminating the connection - /// because it has received data within a message that was not - /// consistent with the type of the message (e.g., non-UTF-8 \[RFC3629\] + + /// Indicates that an endpoint is terminating the connection because it has received data within + /// a message that was not consistent with the type of the message (e.g., non-UTF-8 \[RFC3629\] /// data within a text message). Invalid, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that violates its policy. This - /// is a generic status code that can be returned when there is no - /// other more suitable status code (e.g., Unsupported or Size) or if there - /// is a need to hide specific details about the policy. + + /// Indicates that an endpoint is terminating the connection because it has received a message + /// that violates its policy. This is a generic status code that can be returned when there is + /// no other more suitable status code (e.g., Unsupported or Size) or if there is a need to hide + /// specific details about the policy. Policy, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that is too big for it to - /// process. + + /// Indicates that an endpoint is terminating the connection because it has received a message + /// that is too big for it to process. Size, - /// Indicates that an endpoint (client) is terminating the - /// connection because it has expected the server to negotiate one or - /// more extension, but the server didn't return them in the response - /// message of the WebSocket handshake. The list of extensions that - /// are needed should be given as the reason for closing. - /// Note that this status code is not used by the server, because it - /// can fail the WebSocket handshake instead. + + /// Indicates that an endpoint (client) is terminating the connection because it has expected + /// the server to negotiate one or more extension, but the server didn't return them in the + /// response message of the WebSocket handshake. The list of extensions that are needed should + /// be given as the reason for closing. Note that this status code is not used by the server, + /// because it can fail the WebSocket handshake instead. Extension, - /// Indicates that a server is terminating the connection because - /// it encountered an unexpected condition that prevented it from - /// fulfilling the request. + + /// Indicates that a server is terminating the connection because it encountered an unexpected + /// condition that prevented it from fulfilling the request. Error, - /// Indicates that the server is restarting. A client may choose to - /// reconnect, and if it does, it should use a randomized delay of 5-30 - /// seconds between attempts. + + /// Indicates that the server is restarting. A client may choose to reconnect, and if it does, + /// it should use a randomized delay of 5-30 seconds between attempts. Restart, - /// Indicates that the server is overloaded and the client should either - /// connect to a different IP (when multiple targets exist), or - /// reconnect to the same IP when a user has performed an action. + + /// Indicates that the server is overloaded and the client should either connect to a different + /// IP (when multiple targets exist), or reconnect to the same IP when a user has performed + /// an action. Again, + #[doc(hidden)] Tls, + #[doc(hidden)] Other(u16), } @@ -141,6 +149,7 @@ pub enum CloseCode { impl From for u16 { fn from(code: CloseCode) -> u16 { use self::CloseCode::*; + match code { Normal => 1000, Away => 1001, @@ -163,6 +172,7 @@ impl From for u16 { impl From for CloseCode { fn from(code: u16) -> CloseCode { use self::CloseCode::*; + match code { 1000 => Normal, 1001 => Away, @@ -210,17 +220,29 @@ impl> From<(CloseCode, T)> for CloseReason { } } -static WS_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +/// The WebSocket GUID as stated in the spec. See https://tools.ietf.org/html/rfc6455#section-1.3. +static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; -// TODO: hash is always same size, we don't need String -pub fn hash_key(key: &[u8]) -> String { - use sha1::Digest; - let mut hasher = sha1::Sha1::new(); +/// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec. +/// +/// Result is a Base64 encoded byte array. `base64(sha1(input))` is always 28 bytes. +pub fn hash_key(key: &[u8]) -> [u8; 28] { + let hash = { + use sha1::Digest as _; - hasher.update(key); - hasher.update(WS_GUID.as_bytes()); + let mut hasher = sha1::Sha1::new(); - base64::encode(&hasher.finalize()) + hasher.update(key); + hasher.update(WS_GUID); + + hasher.finalize() + }; + + let mut hash_b64 = [0; 28]; + let n = base64::encode_config_slice(&hash, base64::STANDARD, &mut hash_b64); + assert_eq!(n, 28); + + hash_b64 } #[cfg(test)] @@ -288,11 +310,11 @@ mod test { #[test] fn test_hash_key() { let hash = hash_key(b"hello actix-web"); - assert_eq!(&hash, "cR1dlyUUJKp0s/Bel25u5TgvC3E="); + assert_eq!(&hash, b"cR1dlyUUJKp0s/Bel25u5TgvC3E="); } #[test] - fn closecode_from_u16() { + fn close_code_from_u16() { assert_eq!(CloseCode::from(1000u16), CloseCode::Normal); assert_eq!(CloseCode::from(1001u16), CloseCode::Away); assert_eq!(CloseCode::from(1002u16), CloseCode::Protocol); @@ -310,7 +332,7 @@ mod test { } #[test] - fn closecode_into_u16() { + fn close_code_into_u16() { assert_eq!(1000u16, Into::::into(CloseCode::Normal)); assert_eq!(1001u16, Into::::into(CloseCode::Away)); assert_eq!(1002u16, Into::::into(CloseCode::Protocol)); diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 91b2412f4..758e39745 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,8 +1,13 @@ -use actix_http::{http, HttpService, Request, Response}; +use actix_http::{ + error, http, http::StatusCode, HttpMessage, HttpService, Request, Response, +}; use actix_http_test::test_server; use actix_service::ServiceFactoryExt; use bytes::Bytes; -use futures_util::future::{self, ok}; +use futures_util::{ + future::{self, ok}, + StreamExt as _, +}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -88,3 +93,55 @@ async fn test_with_query_parameter() { let response = request.send().await.unwrap(); assert!(response.status().is_success()); } + +#[actix_rt::test] +async fn test_h1_expect() { + let srv = test_server(move || { + HttpService::build() + .expect(|req: Request| async { + if req.headers().contains_key("AUTH") { + Ok(req) + } else { + Err(error::ErrorExpectationFailed("expect failed")) + } + }) + .h1(|req: Request| async move { + let (_, mut body) = req.into_parts(); + let mut buf = Vec::new(); + while let Some(Ok(chunk)) = body.next().await { + buf.extend_from_slice(&chunk); + } + let str = std::str::from_utf8(&buf).unwrap(); + assert_eq!(str, "expect body"); + + Ok::<_, ()>(Response::Ok().finish()) + }) + .tcp() + }) + .await; + + // test expect without payload. + let request = srv + .request(http::Method::GET, srv.url("/")) + .insert_header(("Expect", "100-continue")); + + let response = request.send().await; + assert!(response.is_err()); + + // test expect would fail to continue + let request = srv + .request(http::Method::GET, srv.url("/")) + .insert_header(("Expect", "100-continue")); + + let response = request.send_body("expect body").await.unwrap(); + assert_eq!(response.status(), StatusCode::EXPECTATION_FAILED); + + // test exepct would continue + let request = srv + .request(http::Method::GET, srv.url("/")) + .insert_header(("Expect", "100-continue")) + .insert_header(("AUTH", "996")); + + let response = request.send_body("expect body").await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index f44968baa..49a68a60d 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -12,8 +12,11 @@ use actix_http::{body, Error, HttpService, Request, Response}; use actix_http_test::test_server; use actix_service::{fn_service, ServiceFactoryExt}; use bytes::{Bytes, BytesMut}; -use futures_util::future::{err, ok, ready}; -use futures_util::stream::{once, Stream, StreamExt}; +use futures_core::Stream; +use futures_util::{ + future::{err, ok, ready}, + stream::{once, StreamExt as _}, +}; use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslMethod}, @@ -123,16 +126,14 @@ async fn test_h2_content_length() { let srv = test_server(move || { HttpService::build() .h2(|req: Request| { - let indx: usize = req.uri().path()[1..].parse().unwrap(); + let idx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ - StatusCode::NO_CONTENT, StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, + StatusCode::NO_CONTENT, StatusCode::OK, StatusCode::NOT_FOUND, ]; - ok::<_, ()>(Response::new(statuses[indx])) + ok::<_, ()>(Response::new(statuses[idx])) }) .openssl(tls_config()) .map_err(|_| ()) @@ -143,21 +144,29 @@ async fn test_h2_content_length() { let value = HeaderValue::from_static("0"); { - for i in 0..4 { + for &i in &[0] { + let req = srv + .request(Method::HEAD, srv.surl(&format!("/{}", i))) + .send(); + let _response = req.await.expect_err("should timeout on recv 1xx frame"); + // assert_eq!(response.headers().get(&header), None); + + let req = srv + .request(Method::GET, srv.surl(&format!("/{}", i))) + .send(); + let _response = req.await.expect_err("should timeout on recv 1xx frame"); + // assert_eq!(response.headers().get(&header), None); + } + + for &i in &[1] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); let response = req.await.unwrap(); assert_eq!(response.headers().get(&header), None); - - let req = srv - .request(Method::HEAD, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); } - for i in 4..6 { + for &i in &[2, 3] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index a36400910..7a3cb1473 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -10,14 +10,14 @@ use actix_http_test::test_server; use actix_service::{fn_factory_with_config, fn_service}; use bytes::{Bytes, BytesMut}; +use futures_core::Stream; use futures_util::future::{self, err, ok}; -use futures_util::stream::{once, Stream, StreamExt}; +use futures_util::stream::{once, StreamExt as _}; use rustls::{ internal::pemfile::{certs, pkcs8_private_keys}, NoClientAuth, ServerConfig as RustlsServerConfig, }; -use std::fs::File; use std::io::{self, BufReader}; async fn load_body(mut stream: S) -> Result @@ -139,10 +139,8 @@ async fn test_h2_content_length() { .h2(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ - StatusCode::NO_CONTENT, StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, + StatusCode::NO_CONTENT, StatusCode::OK, StatusCode::NOT_FOUND, ]; @@ -154,22 +152,31 @@ async fn test_h2_content_length() { let header = HeaderName::from_static("content-length"); let value = HeaderValue::from_static("0"); + { - for i in 0..4 { + for &i in &[0] { + let req = srv + .request(Method::HEAD, srv.surl(&format!("/{}", i))) + .send(); + let _response = req.await.expect_err("should timeout on recv 1xx frame"); + // assert_eq!(response.headers().get(&header), None); + + let req = srv + .request(Method::GET, srv.surl(&format!("/{}", i))) + .send(); + let _response = req.await.expect_err("should timeout on recv 1xx frame"); + // assert_eq!(response.headers().get(&header), None); + } + + for &i in &[1] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); let response = req.await.unwrap(); assert_eq!(response.headers().get(&header), None); - - let req = srv - .request(Method::HEAD, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); } - for i in 4..6 { + for &i in &[2, 3] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index 910fa81f2..6d145400c 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -7,7 +7,7 @@ use actix_rt::time::sleep; use actix_service::fn_service; use bytes::Bytes; use futures_util::future::{self, err, ok, ready, FutureExt}; -use futures_util::stream::{once, StreamExt}; +use futures_util::stream::{once, StreamExt as _}; use regex::Regex; use actix_http::HttpMessage; @@ -126,7 +126,7 @@ async fn test_chunked_payload() { .take_payload() .map(|res| match res { Ok(pl) => pl, - Err(e) => panic!(format!("Error reading payload: {}", e)), + Err(e) => panic!("Error reading payload: {}", e), }) .fold(0usize, |acc, chunk| ready(acc + chunk.len())) .map(|req_size| { @@ -162,7 +162,7 @@ async fn test_chunked_payload() { let re = Regex::new(r"size=(\d+)").unwrap(); let size: usize = match re.captures(&data) { Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(), - None => panic!(format!("Failed to find size in HTTP Response: {}", data)), + None => panic!("Failed to find size in HTTP Response: {}", data), }; size }; diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 7ed9b0df1..3b90b4e54 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -12,7 +12,7 @@ use actix_utils::dispatcher::Dispatcher; use bytes::Bytes; use futures_util::future; use futures_util::task::{Context, Poll}; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{SinkExt as _, StreamExt as _}; struct WsService(Arc, Cell)>>); diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 2142ebf4b..ce5bd57d7 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.3 - 2021-03-09 +* No notable changes. + + ## 0.4.0-beta.2 - 2021-02-10 * No notable changes. diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 67b2698bd..9a3ea7bb5 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.2" +version = "0.4.0-beta.3" authors = ["Nikolay Kim "] description = "Multipart form support for Actix Web" readme = "README.md" @@ -16,17 +16,19 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.3", default-features = false } +actix-web = { version = "4.0.0-beta.4", default-features = false } actix-utils = "3.0.0-beta.2" bytes = "1" derive_more = "0.99.5" httparse = "1.3" -futures-util = { version = "0.3.7", default-features = false } +futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } log = "0.4" mime = "0.3" twoway = "0.2" [dev-dependencies] -actix-rt = "2" -actix-http = "3.0.0-beta.3" +actix-rt = "2.1" +actix-http = "3.0.0-beta.4" +tokio = { version = "1", features = ["sync"] } +tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index defcf7828..e4a54ac47 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -3,13 +3,12 @@ > Multipart form support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.2)](https://docs.rs/actix-multipart/0.4.0-beta.2) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.3)](https://docs.rs/actix-multipart/0.4.0-beta.3) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.2/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.2) +[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.3/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.3) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation & Resources diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index 6aaa415c4..bffbe8a1b 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -10,7 +10,7 @@ use crate::server::Multipart; /// /// ## Server example /// -/// ```rust +/// ``` /// use futures_util::stream::{Stream, StreamExt}; /// use actix_web::{web, HttpResponse, Error}; /// use actix_multipart as mp; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 8cd1c8e0c..d9ff3d574 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -804,12 +804,13 @@ mod tests { use super::*; use actix_http::h1::Payload; - use actix_utils::mpsc; use actix_web::http::header::{DispositionParam, DispositionType}; use actix_web::test::TestRequest; use actix_web::FromRequest; use bytes::Bytes; use futures_util::future::lazy; + use tokio::sync::mpsc; + use tokio_stream::wrappers::UnboundedReceiverStream; #[actix_rt::test] async fn test_boundary() { @@ -855,13 +856,17 @@ mod tests { } fn create_stream() -> ( - mpsc::Sender>, + mpsc::UnboundedSender>, impl Stream>, ) { - let (tx, rx) = mpsc::channel(); + let (tx, rx) = mpsc::unbounded_channel(); - (tx, rx.map(|res| res.map_err(|_| panic!()))) + ( + tx, + UnboundedReceiverStream::new(rx).map(|res| res.map_err(|_| panic!())), + ) } + // Stream that returns from a Bytes, one char at a time and Pending every other poll() struct SlowStream { bytes: Bytes, @@ -889,9 +894,11 @@ mod tests { cx.waker().wake_by_ref(); return Poll::Pending; } + if this.pos == this.bytes.len() { return Poll::Ready(None); } + let res = Poll::Ready(Some(Ok(this.bytes.slice(this.pos..(this.pos + 1))))); this.pos += 1; this.ready = false; diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index acd9ceada..80eef08c1 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -3,6 +3,10 @@ ## Unreleased - 2021-xx-xx +## 4.0.0-beta.3 - 2021-03-09 +* No notable changes. + + ## 4.0.0-beta.2 - 2021-02-10 * No notable changes. diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 698ff9420..77663540c 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.2" +version = "4.0.0-beta.3" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" readme = "README.md" @@ -16,10 +16,10 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = { version = "0.11.0-beta.2", default-features = false } +actix = { version = "0.11.0-beta.3", default-features = false } actix-codec = "0.4.0-beta.1" -actix-http = "3.0.0-beta.3" -actix-web = { version = "4.0.0-beta.3", default-features = false } +actix-http = "3.0.0-beta.4" +actix-web = { version = "4.0.0-beta.4", default-features = false } bytes = "1" bytestring = "1" @@ -28,6 +28,6 @@ pin-project = "1.0.0" tokio = { version = "1", features = ["sync"] } [dev-dependencies] -actix-rt = "2" +actix-rt = "2.1" env_logger = "0.8" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index c9b588153..6d9d573d7 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -3,11 +3,11 @@ > Actix actors support for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=0.5.0)](https://docs.rs/actix-web-actors/0.5.0) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.3)](https://docs.rs/actix-web-actors/4.0.0-beta.3) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/0.5.0/status.svg)](https://deps.rs/crate/actix-web-actors/0.5.0) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.3) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index bd6635cba..d7459aea4 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -44,7 +44,7 @@ where #[inline] fn spawn(&mut self, fut: F) -> SpawnHandle where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.spawn(fut) } @@ -52,7 +52,7 @@ where #[inline] fn wait(&mut self, fut: F) where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.wait(fut) } diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 1ab4cfce5..77d7041f0 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -15,10 +15,13 @@ use actix::{ SpawnHandle, }; use actix_codec::{Decoder, Encoder}; -use actix_http::ws::{hash_key, Codec}; pub use actix_http::ws::{ CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, }; +use actix_http::{ + http::HeaderValue, + ws::{hash_key, Codec}, +}; use actix_web::dev::HttpResponseBuilder; use actix_web::error::{Error, PayloadError}; use actix_web::http::{header, Method, StatusCode}; @@ -162,7 +165,11 @@ pub fn handshake_with_protocols( let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) .upgrade("websocket") - .insert_header((header::SEC_WEBSOCKET_ACCEPT, key)) + .insert_header(( + header::SEC_WEBSOCKET_ACCEPT, + // key is known to be header value safe ascii + HeaderValue::from_bytes(&key).unwrap(), + )) .take(); if let Some(protocol) = protocol { @@ -204,14 +211,14 @@ where { fn spawn(&mut self, fut: F) -> SpawnHandle where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.spawn(fut) } fn wait(&mut self, fut: F) where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.wait(fut) } @@ -488,7 +495,6 @@ where if !*this.closed { loop { - this = self.as_mut().project(); match Pin::new(&mut this.stream).poll_next(cx) { Poll::Ready(Some(Ok(chunk))) => { this.buf.extend_from_slice(&chunk[..]); diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 2ce728aad..f2c9b44b5 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -3,6 +3,14 @@ ## Unreleased - 2021-xx-xx +## 0.5.0-beta.2 - 2021-03-09 +* Preserve doc comments when using route macros. [#2022] +* Add `name` attribute to `route` macro. [#1934] + +[#2022]: https://github.com/actix/actix-web/pull/2022 +[#1934]: https://github.com/actix/actix-web/pull/1934 + + ## 0.5.0-beta.1 - 2021-02-10 * Use new call signature for `System::new`. diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 886d9ac3e..d8a189565 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.5.0-beta.1" +version = "0.5.0-beta.2" description = "Routing and runtime macros for Actix Web" readme = "README.md" homepage = "https://actix.rs" @@ -19,8 +19,8 @@ syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" [dev-dependencies] -actix-rt = "2" -actix-web = "4.0.0-beta.3" +actix-rt = "2.1" +actix-web = "4.0.0-beta.4" futures-util = { version = "0.3.7", default-features = false } trybuild = "1" rustversion = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 5820bb443..9552d4b56 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -3,11 +3,11 @@ > Routing and runtime macros for Actix Web. [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.1)](https://docs.rs/actix-web-codegen/0.5.0-beta.1) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.2)](https://docs.rs/actix-web-codegen/0.5.0-beta.2) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.1/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.1) +[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.2/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.2) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 670d82ce9..336345014 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -71,6 +71,7 @@ mod route; /// /// # Attributes /// - `"path"` - Raw literal string with path for which to register handler. +/// - `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used. /// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. @@ -81,7 +82,7 @@ mod route; /// /// # Example /// -/// ```rust +/// ``` /// # use actix_web::HttpResponse; /// # use actix_web_codegen::route; /// #[route("/test", method="GET", method="HEAD")] @@ -116,6 +117,7 @@ Creates route handler with `actix_web::guard::", stringify!($variant), "`. # Attributes - `"path"` - Raw literal string with path for which to register handler. +- `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used. - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. - `wrap="Middleware"` - Registers a resource middleware. @@ -125,7 +127,7 @@ code, e.g `my_guard` or `my_module::my_guard`. # Example -```rust +``` # use actix_web::HttpResponse; # use actix_web_codegen::"#, stringify!($method), "; #[", stringify!($method), r#"("/")] @@ -160,7 +162,7 @@ method_macro! { /// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. /// /// # Examples -/// ```rust +/// ``` /// #[actix_web_codegen::main] /// async fn main() { /// async { println!("Hello world"); }.await diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index ddbd42454..ac0b7cea1 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -78,6 +78,7 @@ impl TryFrom<&syn::LitStr> for MethodType { struct Args { path: syn::LitStr, + resource_name: Option, guards: Vec, wrappers: Vec, methods: HashSet, @@ -86,6 +87,7 @@ struct Args { impl Args { fn new(args: AttributeArgs, method: Option) -> syn::Result { let mut path = None; + let mut resource_name = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); let mut methods = HashSet::new(); @@ -109,7 +111,16 @@ impl Args { } }, NestedMeta::Meta(syn::Meta::NameValue(nv)) => { - if nv.path.is_ident("guard") { + if nv.path.is_ident("name") { + if let syn::Lit::Str(lit) = nv.lit { + resource_name = Some(lit); + } else { + return Err(syn::Error::new_spanned( + nv.lit, + "Attribute name expects literal string!", + )); + } + } else if nv.path.is_ident("guard") { if let syn::Lit::Str(lit) = nv.lit { guards.push(Ident::new(&lit.value(), Span::call_site())); } else { @@ -164,6 +175,7 @@ impl Args { } Ok(Args { path: path.unwrap(), + resource_name, guards, wrappers, methods, @@ -176,6 +188,9 @@ pub struct Route { args: Args, ast: syn::ItemFn, resource_type: ResourceType, + + /// The doc comment attributes to copy to generated struct, if any. + doc_attributes: Vec, } fn guess_resource_type(typ: &syn::Type) -> ResourceType { @@ -221,6 +236,18 @@ impl Route { let ast: syn::ItemFn = syn::parse(input)?; let name = ast.sig.ident.clone(); + // Try and pull out the doc comments so that we can reapply them to the + // generated struct. + // + // Note that multi line doc comments are converted to multiple doc + // attributes. + let doc_attributes = ast + .attrs + .iter() + .filter(|attr| attr.path.is_ident("doc")) + .cloned() + .collect(); + let args = Args::new(args, method)?; if args.methods.is_empty() { return Err(syn::Error::new( @@ -248,6 +275,7 @@ impl Route { args, ast, resource_type, + doc_attributes, }) } } @@ -260,13 +288,17 @@ impl ToTokens for Route { args: Args { path, + resource_name, guards, wrappers, methods, }, resource_type, + doc_attributes, } = self; - let resource_name = name.to_string(); + let resource_name = resource_name + .as_ref() + .map_or_else(|| name.to_string(), |n| n.value()); let method_guards = { let mut others = methods.iter(); // unwrapping since length is checked to be at least one @@ -287,6 +319,7 @@ impl ToTokens for Route { }; let stream = quote! { + #(#doc_attributes)* #[allow(non_camel_case_types, missing_docs)] pub struct #name; diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 34cdad649..0cbb64ba5 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -83,6 +83,13 @@ async fn route_test() -> impl Responder { HttpResponse::Ok() } +#[get("/custom_resource_name", name = "custom")] +async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder { + assert!(req.url_for_static("custom").is_ok()); + assert!(req.url_for_static("custom_resource_name_test").is_err()); + HttpResponse::Ok() +} + pub struct ChangeStatusCode; impl Transform for ChangeStatusCode @@ -174,6 +181,7 @@ async fn test_body() { .service(patch_test) .service(test_handler) .service(route_test) + .service(custom_resource_name_test) }); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); @@ -228,6 +236,10 @@ async fn test_body() { let request = srv.request(http::Method::PATCH, srv.url("/multi")); let response = request.send().await.unwrap(); assert!(!response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/custom_resource_name")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); } #[actix_rt::test] diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index d2d8a38f5..afbe7b728 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -9,6 +9,8 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); + + t.pass("tests/trybuild/docstring-ok.rs"); } // #[rustversion::not(nightly)] diff --git a/actix-web-codegen/tests/trybuild/docstring-ok.rs b/actix-web-codegen/tests/trybuild/docstring-ok.rs new file mode 100644 index 000000000..2910976c7 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/docstring-ok.rs @@ -0,0 +1,17 @@ +use actix_web::{Responder, HttpResponse, App, test}; +use actix_web_codegen::*; + +/// Docstrings shouldn't break anything. +#[get("/")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/awc/CHANGES.md b/awc/CHANGES.md index e6ead2cc8..d2cb7c009 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,20 +1,31 @@ # Changes ## Unreleased - 2021-xx-xx +### Changed +* `ConnectorService` type is renamed to `BoxConnectorService` [#2081] + +[#2081]: https://github.com/actix/actix-web/pull/2081 + + +## 3.0.0-beta.3 - 2021-03-08 ### Added * `ClientResponse::timeout` for set the timeout of collecting response body. [#1931] +* `ClientBuilder::local_address` for bind to a local ip address for this client. [#2024] ### Changed * Feature `cookies` is now optional and enabled by default. [#1981] * `ClientBuilder::connector` method would take `actix_http::client::Connector` type. [#2008] +* Basic auth password now takes blank passwords as an empty string instead of Option. [#2050] ### Removed * `ClientBuilder::default` function [#2008] -* `ClientBuilder::disable_redirects` and `ClientBuilder::max_redirects` method [#2008] [#1931]: https://github.com/actix/actix-web/pull/1931 [#1981]: https://github.com/actix/actix-web/pull/1981 [#2008]: https://github.com/actix/actix-web/pull/2008 +[#2024]: https://github.com/actix/actix-web/pull/2024 +[#2050]: https://github.com/actix/actix-web/pull/2050 + ## 3.0.0-beta.2 - 2021-02-10 ### Added diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 9beecc6d4..ec2e03a96 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.0-beta.2" +version = "3.0.0-beta.3" authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library built on the Actix ecosystem" readme = "README.md" @@ -46,17 +46,19 @@ trust-dns = ["actix-http/trust-dns"] [dependencies] actix-codec = "0.4.0-beta.1" actix-service = "2.0.0-beta.4" -actix-http = "3.0.0-beta.3" -actix-rt = "2" +actix-http = "3.0.0-beta.4" +actix-rt = { version = "2.1", default-features = false } base64 = "0.13" bytes = "1" cfg-if = "1.0" derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } +itoa = "0.4" log =" 0.4" mime = "0.3" percent-encoding = "2.1" +pin-project-lite = "0.2" rand = "0.8" serde = "1.0" serde_json = "1.0" @@ -64,23 +66,17 @@ serde_urlencoded = "0.7" tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } -[target.'cfg(windows)'.dependencies.tls-openssl] -version = "0.10.9" -package = "openssl" -features = ["vendored"] -optional = true - [dev-dependencies] -actix-web = { version = "4.0.0-beta.3", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.3", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] } +actix-web = { version = "4.0.0-beta.4", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.4", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] } actix-utils = "3.0.0-beta.1" actix-server = "2.0.0-beta.3" -actix-tls = { version = "3.0.0-beta.3", features = ["openssl", "rustls"] } +actix-tls = { version = "3.0.0-beta.4", features = ["openssl", "rustls"] } brotli2 = "0.3.2" +env_logger = "0.8" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } -env_logger = "0.8" rcgen = "0.8" webpki = "0.21" diff --git a/awc/README.md b/awc/README.md index 043ae6a41..2e7dad320 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,19 +3,20 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.2)](https://docs.rs/awc/3.0.0-beta.2) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.3)](https://docs.rs/awc/3.0.0-beta.3) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.2/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.2) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.3/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.3) [![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) ## Documentation & Resources - [API Documentation](https://docs.rs/awc) -- [Example Project](https://github.com/actix/examples/tree/HEAD/awc_https) +- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) - [Chat on Gitter](https://gitter.im/actix/actix-web) - Minimum Supported Rust Version (MSRV): 1.46.0 ## Example + ```rust use actix_rt::System; use awc::Client; @@ -26,7 +27,7 @@ fn main() { let res = client .get("http://www.rust-lang.org") // <- Create request builder - .header("User-Agent", "Actix-web") + .insert_header(("User-Agent", "Actix-web")) .send() // <- Send http request .await; diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 4495b39fd..925d9ae2a 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -1,31 +1,37 @@ use std::convert::TryFrom; use std::fmt; +use std::net::IpAddr; use std::rc::Rc; use std::time::Duration; use actix_codec::{AsyncRead, AsyncWrite}; use actix_http::{ - client::{Connector, TcpConnect, TcpConnectError, TcpConnection}, + client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection}, http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}, }; use actix_rt::net::TcpStream; -use actix_service::Service; +use actix_service::{boxed, Service}; -use crate::connect::ConnectorWrapper; -use crate::{Client, ClientConfig}; +use crate::connect::DefaultConnector; +use crate::error::SendRequestError; +use crate::middleware::{NestTransform, Redirect, Transform}; +use crate::{Client, ClientConfig, ConnectRequest, ConnectResponse}; /// An HTTP Client builder /// /// This type can be used to construct an instance of `Client` through a /// builder-like pattern. -pub struct ClientBuilder { +pub struct ClientBuilder { default_headers: bool, max_http_version: Option, stream_window_size: Option, conn_window_size: Option, headers: HeaderMap, timeout: Option, - connector: Connector, + connector: Connector, + middleware: M, + local_address: Option, + max_redirects: u8, } impl ClientBuilder { @@ -36,21 +42,24 @@ impl ClientBuilder { Response = TcpConnection, Error = TcpConnectError, > + Clone, - TcpStream, + (), > { ClientBuilder { + middleware: (), default_headers: true, headers: HeaderMap::new(), timeout: Some(Duration::from_secs(5)), + local_address: None, connector: Connector::new(), max_http_version: None, stream_window_size: None, conn_window_size: None, + max_redirects: 10, } } } -impl ClientBuilder +impl ClientBuilder where S: Service, Response = TcpConnection, Error = TcpConnectError> + Clone @@ -58,7 +67,7 @@ where Io: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, { /// Use custom connector service. - pub fn connector(self, connector: Connector) -> ClientBuilder + pub fn connector(self, connector: Connector) -> ClientBuilder where S1: Service< TcpConnect, @@ -69,13 +78,16 @@ where Io1: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, { ClientBuilder { + middleware: self.middleware, default_headers: self.default_headers, headers: self.headers, timeout: self.timeout, + local_address: self.local_address, connector, max_http_version: self.max_http_version, stream_window_size: self.stream_window_size, conn_window_size: self.conn_window_size, + max_redirects: self.max_redirects, } } @@ -94,6 +106,12 @@ where self } + /// Set local IP Address the connector would use for establishing connection. + pub fn local_address(mut self, addr: IpAddr) -> Self { + self.local_address = Some(addr); + self + } + /// Maximum supported HTTP major version. /// /// Supported versions are HTTP/1.1 and HTTP/2. @@ -102,6 +120,22 @@ where self } + /// Do not follow redirects. + /// + /// Redirects are allowed by default. + pub fn disable_redirects(mut self) -> Self { + self.max_redirects = 0; + self + } + + /// Set max number of redirects. + /// + /// Max redirects is set to 10 by default. + pub fn max_redirects(mut self, num: u8) -> Self { + self.max_redirects = num; + self + } + /// Indicates the initial window size (in octets) for /// HTTP2 stream-level flow control for received data. /// @@ -171,8 +205,55 @@ where self.header(header::AUTHORIZATION, format!("Bearer {}", token)) } + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// life-cycle (request -> response), modifying request/response as + /// necessary, across all requests managed by the Client. + pub fn wrap( + self, + mw: M1, + ) -> ClientBuilder> + where + M: Transform, + M1: Transform, + { + ClientBuilder { + middleware: NestTransform::new(self.middleware, mw), + default_headers: self.default_headers, + max_http_version: self.max_http_version, + stream_window_size: self.stream_window_size, + conn_window_size: self.conn_window_size, + headers: self.headers, + timeout: self.timeout, + connector: self.connector, + local_address: self.local_address, + max_redirects: self.max_redirects, + } + } + /// Finish build process and create `Client` instance. - pub fn finish(self) -> Client { + pub fn finish(self) -> Client + where + M: Transform>, ConnectRequest> + 'static, + M::Transform: + Service, + { + let redirect_time = self.max_redirects; + + if redirect_time > 0 { + self.wrap(Redirect::new().max_redirect_times(redirect_time)) + ._finish() + } else { + self._finish() + } + } + + fn _finish(self) -> Client + where + M: Transform>, ConnectRequest> + 'static, + M::Transform: + Service, + { let mut connector = self.connector; if let Some(val) = self.max_http_version { @@ -184,14 +265,18 @@ where if let Some(val) = self.stream_window_size { connector = connector.initial_window_size(val) }; + if let Some(val) = self.local_address { + connector = connector.local_address(val); + } - let config = ClientConfig { - headers: self.headers, + let connector = DefaultConnector::new(connector.finish()); + let connector = boxed::rc_service(self.middleware.new_transform(connector)); + + Client(ClientConfig { + headers: Rc::new(self.headers), timeout: self.timeout, - connector: Box::new(ConnectorWrapper::new(connector.finish())) as _, - }; - - Client(Rc::new(config)) + connector, + }) } } diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 97af2d1cc..6a9fc4630 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -1,32 +1,26 @@ use std::{ - fmt, io, net, + future::Future, + net, pin::Pin, + rc::Rc, task::{Context, Poll}, }; -use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; +use actix_codec::Framed; use actix_http::{ body::Body, - client::{Connect as ClientConnect, ConnectError, Connection, SendRequestError}, + client::{ + Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, + }, h1::ClientCodec, - RequestHead, RequestHeadType, ResponseHead, + Payload, RequestHead, RequestHeadType, ResponseHead, }; use actix_service::Service; -use futures_core::future::LocalBoxFuture; +use futures_core::{future::LocalBoxFuture, ready}; use crate::response::ClientResponse; -pub(crate) struct ConnectorWrapper { - connector: T, -} - -impl ConnectorWrapper { - pub(crate) fn new(connector: T) -> Self { - Self { connector } - } -} - -pub type ConnectService = Box< +pub type BoxConnectorService = Rc< dyn Service< ConnectRequest, Response = ConnectResponse, @@ -35,6 +29,8 @@ pub type ConnectService = Box< >, >; +pub type BoxedSocket = Box; + pub enum ConnectRequest { Client(RequestHeadType, Body, Option), Tunnel(RequestHead, Option), @@ -65,16 +61,24 @@ impl ConnectResponse { } } -impl Service for ConnectorWrapper +pub struct DefaultConnector { + connector: S, +} + +impl DefaultConnector { + pub(crate) fn new(connector: S) -> Self { + Self { connector } + } +} + +impl Service for DefaultConnector where - T: Service, - T::Response: Connection, - ::Io: 'static, - T::Future: 'static, + S: Service>, + Io: ConnectionIo, { type Response = ConnectResponse; type Error = SendRequestError; - type Future = LocalBoxFuture<'static, Result>; + type Future = ConnectRequestFuture; actix_service::forward_ready!(connector); @@ -91,81 +95,77 @@ where }), }; - Box::pin(async move { - let connection = fut.await?; + ConnectRequestFuture::Connection { + fut, + req: Some(req), + } + } +} - match req { - ConnectRequest::Client(head, body, ..) => { - // send request - let (head, payload) = connection.send_request(head, body).await?; +pin_project_lite::pin_project! { + #[project = ConnectRequestProj] + pub enum ConnectRequestFuture + where + Io: ConnectionIo + { + Connection { + #[pin] + fut: Fut, + req: Option + }, + Client { + fut: LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> + }, + Tunnel { + fut: LocalBoxFuture< + 'static, + Result<(ResponseHead, Framed, ClientCodec>), SendRequestError>, + >, + } + } +} - Ok(ConnectResponse::Client(ClientResponse::new(head, payload))) - } - ConnectRequest::Tunnel(head, ..) => { - // send request - let (head, framed) = - connection.open_tunnel(RequestHeadType::from(head)).await?; - - let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); - Ok(ConnectResponse::Tunnel(head, framed)) +impl Future for ConnectRequestFuture +where + Fut: Future, ConnectError>>, + Io: ConnectionIo, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.as_mut().project() { + ConnectRequestProj::Connection { fut, req } => { + let connection = ready!(fut.poll(cx))?; + let req = req.take().unwrap(); + match req { + ConnectRequest::Client(head, body, ..) => { + // send request + let fut = ConnectRequestFuture::Client { + fut: connection.send_request(head, body), + }; + self.set(fut); + } + ConnectRequest::Tunnel(head, ..) => { + // send request + let fut = ConnectRequestFuture::Tunnel { + fut: connection.open_tunnel(RequestHeadType::from(head)), + }; + self.set(fut); + } } + self.poll(cx) } - }) - } -} - -trait AsyncSocket { - fn as_read(&self) -> &(dyn AsyncRead + Unpin); - fn as_read_mut(&mut self) -> &mut (dyn AsyncRead + Unpin); - fn as_write(&mut self) -> &mut (dyn AsyncWrite + Unpin); -} - -struct Socket(T); - -impl AsyncSocket for Socket { - fn as_read(&self) -> &(dyn AsyncRead + Unpin) { - &self.0 - } - fn as_read_mut(&mut self) -> &mut (dyn AsyncRead + Unpin) { - &mut self.0 - } - fn as_write(&mut self) -> &mut (dyn AsyncWrite + Unpin) { - &mut self.0 - } -} - -pub struct BoxedSocket(Box); - -impl fmt::Debug for BoxedSocket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BoxedSocket") - } -} - -impl AsyncRead for BoxedSocket { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(self.get_mut().0.as_read_mut()).poll_read(cx, buf) - } -} - -impl AsyncWrite for BoxedSocket { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(self.get_mut().0.as_write()).poll_write(cx, buf) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(self.get_mut().0.as_write()).poll_flush(cx) - } - - fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(self.get_mut().0.as_write()).poll_shutdown(cx) + ConnectRequestProj::Client { fut } => { + let (head, payload) = ready!(fut.as_mut().poll(cx))?; + Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new( + head, payload, + )))) + } + ConnectRequestProj::Tunnel { fut } => { + let (head, framed) = ready!(fut.as_mut().poll(cx))?; + let framed = framed.into_map_io(|io| Box::new(io) as _); + Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed))) + } + } } } diff --git a/awc/src/error.rs b/awc/src/error.rs index f86224e62..b715f6213 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -18,24 +18,31 @@ pub enum WsClientError { /// Invalid response status #[display(fmt = "Invalid response status")] InvalidResponseStatus(StatusCode), + /// Invalid upgrade header #[display(fmt = "Invalid upgrade header")] InvalidUpgradeHeader, + /// Invalid connection header #[display(fmt = "Invalid connection header")] InvalidConnectionHeader(HeaderValue), - /// Missing CONNECTION header - #[display(fmt = "Missing CONNECTION header")] + + /// Missing Connection header + #[display(fmt = "Missing Connection header")] MissingConnectionHeader, - /// Missing SEC-WEBSOCKET-ACCEPT header - #[display(fmt = "Missing SEC-WEBSOCKET-ACCEPT header")] + + /// Missing Sec-Websocket-Accept header + #[display(fmt = "Missing Sec-Websocket-Accept header")] MissingWebSocketAcceptHeader, + /// Invalid challenge response #[display(fmt = "Invalid challenge response")] - InvalidChallengeResponse(String, HeaderValue), + InvalidChallengeResponse([u8; 28], HeaderValue), + /// Protocol error #[display(fmt = "{}", _0)] Protocol(WsProtocolError), + /// Send request error #[display(fmt = "{}", _0)] SendRequest(SendRequestError), diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 46b4063a0..5fe8edb19 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -23,7 +23,7 @@ pub struct FrozenClientRequest { pub(crate) addr: Option, pub(crate) response_decompress: bool, pub(crate) timeout: Option, - pub(crate) config: Rc, + pub(crate) config: ClientConfig, } impl FrozenClientRequest { @@ -51,7 +51,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, body, ) } @@ -62,7 +62,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, value, ) } @@ -73,7 +73,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, value, ) } @@ -88,7 +88,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, stream, ) } @@ -99,7 +99,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, ) } @@ -168,7 +168,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, body, ) } @@ -183,7 +183,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, value, ) } @@ -198,7 +198,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, value, ) } @@ -217,7 +217,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, stream, ) } @@ -232,7 +232,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, ) } } diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 66ff55402..c7bb68a8f 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -113,6 +113,7 @@ mod builder; mod connect; pub mod error; mod frozen; +pub mod middleware; mod request; mod response; mod sender; @@ -120,19 +121,17 @@ pub mod test; pub mod ws; pub use self::builder::ClientBuilder; -pub use self::connect::{BoxedSocket, ConnectRequest, ConnectResponse, ConnectService}; +pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; pub use self::response::{ClientResponse, JsonBody, MessageBody}; pub use self::sender::SendClientRequest; -use self::connect::ConnectorWrapper; - /// An asynchronous HTTP and WebSocket client. /// /// ## Examples /// -/// ```rust +/// ``` /// use awc::Client; /// /// #[actix_rt::main] @@ -148,21 +147,18 @@ use self::connect::ConnectorWrapper; /// } /// ``` #[derive(Clone)] -pub struct Client(Rc); +pub struct Client(ClientConfig); +#[derive(Clone)] pub(crate) struct ClientConfig { - pub(crate) connector: ConnectService, - pub(crate) headers: HeaderMap, + pub(crate) connector: BoxConnectorService, + pub(crate) headers: Rc, pub(crate) timeout: Option, } impl Default for Client { fn default() -> Self { - Client(Rc::new(ClientConfig { - connector: Box::new(ConnectorWrapper::new(Connector::new().finish())), - headers: HeaderMap::new(), - timeout: Some(Duration::from_secs(5)), - })) + ClientBuilder::new().finish() } } @@ -180,7 +176,6 @@ impl Client { Response = TcpConnection, Error = TcpConnectError, > + Clone, - TcpStream, > { ClientBuilder::new() } diff --git a/awc/src/middleware/mod.rs b/awc/src/middleware/mod.rs new file mode 100644 index 000000000..330e3b7fe --- /dev/null +++ b/awc/src/middleware/mod.rs @@ -0,0 +1,71 @@ +mod redirect; + +pub use self::redirect::Redirect; + +use std::marker::PhantomData; + +use actix_service::Service; + +/// Trait for transform a type to another one. +/// Both the input and output type should impl [actix_service::Service] trait. +pub trait Transform { + type Transform: Service; + + /// Creates and returns a new Transform component. + fn new_transform(self, service: S) -> Self::Transform; +} + +#[doc(hidden)] +/// Helper struct for constructing Nested types that would call `Transform::new_transform` +/// in a chain. +/// +/// The child field would be called first and the output `Service` type is +/// passed to parent as input type. +pub struct NestTransform +where + T1: Transform, + T2: Transform, +{ + child: T1, + parent: T2, + _service: PhantomData<(S, Req)>, +} + +impl NestTransform +where + T1: Transform, + T2: Transform, +{ + pub(crate) fn new(child: T1, parent: T2) -> Self { + NestTransform { + child, + parent, + _service: PhantomData, + } + } +} + +impl Transform for NestTransform +where + T1: Transform, + T2: Transform, +{ + type Transform = T2::Transform; + + fn new_transform(self, service: S) -> Self::Transform { + let service = self.child.new_transform(service); + self.parent.new_transform(service) + } +} + +/// Dummy impl for kick start `NestTransform` type in `ClientBuilder` type +impl Transform for () +where + S: Service, +{ + type Transform = S; + + fn new_transform(self, service: S) -> Self::Transform { + service + } +} diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs new file mode 100644 index 000000000..62ea1d0ac --- /dev/null +++ b/awc/src/middleware/redirect.rs @@ -0,0 +1,351 @@ +use std::{ + convert::TryFrom, + future::Future, + net::SocketAddr, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_http::{ + body::Body, + client::{InvalidUrl, SendRequestError}, + http::{header, Method, StatusCode, Uri}, + RequestHead, RequestHeadType, +}; +use actix_service::Service; +use bytes::Bytes; +use futures_core::ready; + +use super::Transform; + +use crate::connect::{ConnectRequest, ConnectResponse}; +use crate::ClientResponse; + +pub struct Redirect { + max_redirect_times: u8, +} + +impl Default for Redirect { + fn default() -> Self { + Self::new() + } +} + +impl Redirect { + pub fn new() -> Self { + Self { + max_redirect_times: 10, + } + } + + pub fn max_redirect_times(mut self, times: u8) -> Self { + self.max_redirect_times = times; + self + } +} + +impl Transform for Redirect +where + S: Service + 'static, +{ + type Transform = RedirectService; + + fn new_transform(self, service: S) -> Self::Transform { + RedirectService { + max_redirect_times: self.max_redirect_times, + connector: Rc::new(service), + } + } +} + +pub struct RedirectService { + max_redirect_times: u8, + connector: Rc, +} + +impl Service for RedirectService +where + S: Service + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = RedirectServiceFuture; + + actix_service::forward_ready!(connector); + + fn call(&self, req: ConnectRequest) -> Self::Future { + match req { + ConnectRequest::Tunnel(head, addr) => { + let fut = self.connector.call(ConnectRequest::Tunnel(head, addr)); + RedirectServiceFuture::Tunnel { fut } + } + ConnectRequest::Client(head, body, addr) => { + let connector = self.connector.clone(); + let max_redirect_times = self.max_redirect_times; + + // backup the uri and method for reuse schema and authority. + let (uri, method) = match head { + RequestHeadType::Owned(ref head) => (head.uri.clone(), head.method.clone()), + RequestHeadType::Rc(ref head, ..) => { + (head.uri.clone(), head.method.clone()) + } + }; + + let body_opt = match body { + Body::Bytes(ref b) => Some(b.clone()), + _ => None, + }; + + let fut = connector.call(ConnectRequest::Client(head, body, addr)); + + RedirectServiceFuture::Client { + fut, + max_redirect_times, + uri: Some(uri), + method: Some(method), + body: body_opt, + addr, + connector: Some(connector), + } + } + } + } +} + +pin_project_lite::pin_project! { + #[project = RedirectServiceProj] + pub enum RedirectServiceFuture + where + S: Service, + S: 'static + { + Tunnel { #[pin] fut: S::Future }, + Client { + #[pin] + fut: S::Future, + max_redirect_times: u8, + uri: Option, + method: Option, + body: Option, + addr: Option, + connector: Option> + } + } +} + +impl Future for RedirectServiceFuture +where + S: Service + 'static, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.as_mut().project() { + RedirectServiceProj::Tunnel { fut } => fut.poll(cx), + RedirectServiceProj::Client { + fut, + max_redirect_times, + uri, + method, + body, + addr, + connector, + } => match ready!(fut.poll(cx))? { + ConnectResponse::Client(res) => match res.head().status { + StatusCode::MOVED_PERMANENTLY + | StatusCode::FOUND + | StatusCode::SEE_OTHER + if *max_redirect_times > 0 => + { + let org_uri = uri.take().unwrap(); + // rebuild uri from the location header value. + let uri = rebuild_uri(&res, org_uri)?; + + // reset method + let method = method.take().unwrap(); + let method = match method { + Method::GET | Method::HEAD => method, + _ => Method::GET, + }; + + // take ownership of states that could be reused + let addr = addr.take(); + let connector = connector.take(); + let mut max_redirect_times = *max_redirect_times; + + // use a new request head. + let mut head = RequestHead::default(); + head.uri = uri.clone(); + head.method = method.clone(); + + let head = RequestHeadType::Owned(head); + + max_redirect_times -= 1; + + let fut = connector + .as_ref() + .unwrap() + // remove body + .call(ConnectRequest::Client(head, Body::None, addr)); + + self.set(RedirectServiceFuture::Client { + fut, + max_redirect_times, + uri: Some(uri), + method: Some(method), + // body is dropped on 301,302,303 + body: None, + addr, + connector, + }); + + self.poll(cx) + } + StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT + if *max_redirect_times > 0 => + { + let org_uri = uri.take().unwrap(); + // rebuild uri from the location header value. + let uri = rebuild_uri(&res, org_uri)?; + + // try to reuse body + let body = body.take(); + let body_new = match body { + Some(ref bytes) => Body::Bytes(bytes.clone()), + // TODO: should this be Body::Empty or Body::None. + _ => Body::Empty, + }; + + let addr = addr.take(); + let method = method.take().unwrap(); + let connector = connector.take(); + let mut max_redirect_times = *max_redirect_times; + + // use a new request head. + let mut head = RequestHead::default(); + head.uri = uri.clone(); + head.method = method.clone(); + + let head = RequestHeadType::Owned(head); + + max_redirect_times -= 1; + + let fut = connector + .as_ref() + .unwrap() + .call(ConnectRequest::Client(head, body_new, addr)); + + self.set(RedirectServiceFuture::Client { + fut, + max_redirect_times, + uri: Some(uri), + method: Some(method), + body, + addr, + connector, + }); + + self.poll(cx) + } + _ => Poll::Ready(Ok(ConnectResponse::Client(res))), + }, + _ => unreachable!("ConnectRequest::Tunnel is not handled by Redirect"), + }, + } + } +} + +fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result { + let uri = res + .headers() + .get(header::LOCATION) + .map(|value| { + // try to parse the location to a full uri + let uri = Uri::try_from(value.as_bytes()) + .map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; + if uri.scheme().is_none() || uri.authority().is_none() { + let uri = Uri::builder() + .scheme(org_uri.scheme().cloned().unwrap()) + .authority(org_uri.authority().cloned().unwrap()) + .path_and_query(value.as_bytes()) + .build()?; + Ok::<_, SendRequestError>(uri) + } else { + Ok(uri) + } + }) + // TODO: this error type is wrong. + .ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??; + + Ok(uri) +} + +#[cfg(test)] +mod tests { + use actix_web::{test::start, web, App, Error, HttpResponse}; + + use super::*; + + use crate::ClientBuilder; + + #[actix_rt::test] + async fn test_basic_redirect() { + let client = ClientBuilder::new() + .disable_redirects() + .wrap(Redirect::new().max_redirect_times(10)) + .finish(); + + let srv = start(|| { + App::new() + .service(web::resource("/test").route(web::to(|| async { + Ok::<_, Error>(HttpResponse::BadRequest()) + }))) + .service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Found() + .append_header(("location", "/test")) + .finish(), + ) + }))) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status().as_u16(), 400); + } + + #[actix_rt::test] + async fn test_redirect_limit() { + let client = ClientBuilder::new() + .disable_redirects() + .wrap(Redirect::new().max_redirect_times(1)) + .connector(crate::Connector::new()) + .finish(); + + let srv = start(|| { + App::new() + .service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Found() + .append_header(("location", "/test")) + .finish(), + ) + }))) + .service(web::resource("/test").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Found() + .append_header(("location", "/test2")) + .finish(), + ) + }))) + .service(web::resource("/test2").route(web::to(|| async { + Ok::<_, Error>(HttpResponse::BadRequest()) + }))) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status().as_u16(), 302); + } +} diff --git a/awc/src/request.rs b/awc/src/request.rs index 812c0e805..a847b09a3 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -36,7 +36,7 @@ cfg_if::cfg_if! { /// This type can be used to construct an instance of `ClientRequest` through a /// builder-like pattern. /// -/// ```rust +/// ``` /// #[actix_rt::main] /// async fn main() { /// let response = awc::Client::new() @@ -57,7 +57,7 @@ pub struct ClientRequest { addr: Option, response_decompress: bool, timeout: Option, - config: Rc, + config: ClientConfig, #[cfg(feature = "cookies")] cookies: Option, @@ -65,7 +65,7 @@ pub struct ClientRequest { impl ClientRequest { /// Create new client request builder. - pub(crate) fn new(method: Method, uri: U, config: Rc) -> Self + pub(crate) fn new(method: Method, uri: U, config: ClientConfig) -> Self where Uri: TryFrom, >::Error: Into, @@ -190,7 +190,7 @@ impl ClientRequest { /// Append a header, keeping any that were set with an equivalent field name. /// - /// ```rust + /// ``` /// # #[actix_rt::main] /// # async fn main() { /// # use awc::Client; @@ -248,35 +248,30 @@ impl ClientRequest { /// Set content length #[inline] pub fn content_length(self, len: u64) -> Self { - self.append_header((header::CONTENT_LENGTH, len)) + let mut buf = itoa::Buffer::new(); + self.insert_header((header::CONTENT_LENGTH, buf.format(len))) } - /// Set HTTP basic authorization header - pub fn basic_auth(self, username: U, password: Option<&str>) -> Self - where - U: fmt::Display, - { - let auth = match password { - Some(password) => format!("{}:{}", username, password), - None => format!("{}:", username), - }; - self.append_header(( + /// Set HTTP basic authorization header. + /// + /// If no password is needed, just provide an empty string. + pub fn basic_auth(self, username: impl fmt::Display, password: impl fmt::Display) -> Self { + let auth = format!("{}:{}", username, password); + + self.insert_header(( header::AUTHORIZATION, format!("Basic {}", base64::encode(&auth)), )) } /// Set HTTP bearer authentication header - pub fn bearer_auth(self, token: T) -> Self - where - T: fmt::Display, - { - self.append_header((header::AUTHORIZATION, format!("Bearer {}", token))) + pub fn bearer_auth(self, token: impl fmt::Display) -> Self { + self.insert_header((header::AUTHORIZATION, format!("Bearer {}", token))) } /// Set a cookie /// - /// ```rust + /// ``` /// #[actix_rt::main] /// async fn main() { /// let resp = awc::Client::new().get("https://www.rust-lang.org") @@ -403,7 +398,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, body, ) } @@ -419,7 +414,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, value, ) } @@ -437,7 +432,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, value, ) } @@ -457,7 +452,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, stream, ) } @@ -473,7 +468,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, ) } @@ -643,9 +638,7 @@ mod tests { #[actix_rt::test] async fn client_basic_auth() { - let req = Client::new() - .get("/") - .basic_auth("username", Some("password")); + let req = Client::new().get("/").basic_auth("username", "password"); assert_eq!( req.head .headers @@ -656,7 +649,7 @@ mod tests { "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" ); - let req = Client::new().get("/").basic_auth("username", None); + let req = Client::new().get("/").basic_auth("username", ""); assert_eq!( req.head .headers diff --git a/awc/src/response.rs b/awc/src/response.rs index 514b8a90b..994ddb761 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -228,12 +228,13 @@ impl fmt::Debug for ClientResponse { } } +const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; + /// Future that resolves to a complete HTTP message body. pub struct MessageBody { length: Option, - err: Option, timeout: ResponseTimeout, - fut: Option>, + body: Result, Option>, } impl MessageBody @@ -242,41 +243,38 @@ where { /// Create `MessageBody` for request. pub fn new(res: &mut ClientResponse) -> MessageBody { - let mut len = None; - if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } else { - return Self::err(PayloadError::UnknownLength); + let length = match res.headers().get(&header::CONTENT_LENGTH) { + Some(value) => { + let len = value.to_str().ok().and_then(|s| s.parse::().ok()); + + match len { + None => return Self::err(PayloadError::UnknownLength), + len => len, } - } else { - return Self::err(PayloadError::UnknownLength); } - } + None => None, + }; MessageBody { - length: len, - err: None, + length, timeout: std::mem::take(&mut res.timeout), - fut: Some(ReadBody::new(res.take_payload(), 262_144)), + body: Ok(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), } } - /// Change max size of payload. By default max size is 256kB + /// Change max size of payload. By default max size is 2048kB pub fn limit(mut self, limit: usize) -> Self { - if let Some(ref mut fut) = self.fut { - fut.limit = limit; + if let Ok(ref mut body) = self.body { + body.limit = limit; } self } fn err(e: PayloadError) -> Self { MessageBody { - fut: None, - err: Some(e), length: None, timeout: ResponseTimeout::default(), + body: Err(Some(e)), } } } @@ -290,19 +288,20 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - if let Some(err) = this.err.take() { - return Poll::Ready(Err(err)); - } + match this.body { + Err(ref mut err) => Poll::Ready(Err(err.take().unwrap())), + Ok(ref mut body) => { + if let Some(len) = this.length.take() { + if len > body.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + } - if let Some(len) = this.length.take() { - if len > this.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(PayloadError::Overflow)); + this.timeout.poll_timeout(cx)?; + + Pin::new(body).poll(cx) } } - - this.timeout.poll_timeout(cx)?; - - Pin::new(&mut this.fut.as_mut().unwrap()).poll(cx) } } @@ -415,7 +414,7 @@ impl ReadBody { fn new(stream: Payload, limit: usize) -> Self { Self { stream, - buf: BytesMut::with_capacity(std::cmp::min(limit, 32768)), + buf: BytesMut::new(), limit, } } @@ -430,20 +429,14 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - loop { - return match Pin::new(&mut this.stream).poll_next(cx)? { - Poll::Ready(Some(chunk)) => { - if (this.buf.len() + chunk.len()) > this.limit { - Poll::Ready(Err(PayloadError::Overflow)) - } else { - this.buf.extend_from_slice(&chunk); - continue; - } - } - Poll::Ready(None) => Poll::Ready(Ok(this.buf.split().freeze())), - Poll::Pending => Poll::Pending, - }; + while let Some(chunk) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) { + if (this.buf.len() + chunk.len()) > this.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + this.buf.extend_from_slice(&chunk); } + + Poll::Ready(Ok(this.buf.split().freeze())) } } @@ -462,7 +455,7 @@ mod tests { _ => unreachable!("error"), } - let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "10000000").finish(); match req.body().await.err().unwrap() { PayloadError::Overflow => {} _ => unreachable!("error"), @@ -492,9 +485,7 @@ mod tests { JsonPayloadError::Payload(PayloadError::Overflow) => { matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) } - JsonPayloadError::ContentType => { - matches!(other, JsonPayloadError::ContentType) - } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 5f4570963..df25b7289 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -28,7 +28,6 @@ use std::convert::TryFrom; use std::net::SocketAddr; -use std::rc::Rc; use std::{fmt, str}; use actix_codec::Framed; @@ -56,7 +55,7 @@ pub struct WebsocketsRequest { addr: Option, max_size: usize, server_mode: bool, - config: Rc, + config: ClientConfig, #[cfg(feature = "cookies")] cookies: Option, @@ -64,7 +63,7 @@ pub struct WebsocketsRequest { impl WebsocketsRequest { /// Create new WebSocket connection - pub(crate) fn new(uri: U, config: Rc) -> Self + pub(crate) fn new(uri: U, config: ClientConfig) -> Self where Uri: TryFrom, >::Error: Into, @@ -381,12 +380,14 @@ impl WebsocketsRequest { if let Some(hdr_key) = head.headers.get(&header::SEC_WEBSOCKET_ACCEPT) { let encoded = ws::hash_key(key.as_ref()); - if hdr_key.as_bytes() != encoded.as_bytes() { + + if hdr_key.as_bytes() != encoded { log::trace!( - "Invalid challenge response: expected: {} received: {:?}", - encoded, + "Invalid challenge response: expected: {:?} received: {:?}", + &encoded, key ); + return Err(WsClientError::InvalidChallengeResponse( encoded, hdr_key.clone(), diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index a41a8dac3..50d2b5eac 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,5 +1,6 @@ use std::collections::HashMap; use std::io::{Read, Write}; +use std::net::{IpAddr, Ipv4Addr}; use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use std::time::Duration; @@ -828,7 +829,7 @@ async fn client_basic_auth() { .unwrap() .to_str() .unwrap() - == "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + == format!("Basic {}", base64::encode("username:password")) { HttpResponse::Ok() } else { @@ -839,7 +840,7 @@ async fn client_basic_auth() { }); // set authorization header to Basic - let request = srv.get("/").basic_auth("username", Some("password")); + let request = srv.get("/").basic_auth("username", "password"); let response = request.send().await.unwrap(); assert!(response.status().is_success()); } @@ -871,3 +872,34 @@ async fn client_bearer_auth() { let response = request.send().await.unwrap(); assert!(response.status().is_success()); } + +#[actix_rt::test] +async fn test_local_address() { + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + + let srv = test::start(move || { + App::new().service(web::resource("/").route(web::to( + move |req: HttpRequest| async move { + assert_eq!(req.peer_addr().unwrap().ip(), ip); + Ok::<_, Error>(HttpResponse::Ok()) + }, + ))) + }); + let client = awc::Client::builder().local_address(ip).finish(); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status(), 200); + + let client = awc::Client::builder() + .connector( + // connector local address setting should always be override by client builder. + awc::Connector::new().local_address(IpAddr::V4(Ipv4Addr::new(128, 0, 0, 1))), + ) + .local_address(ip) + .finish(); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status(), 200); +} diff --git a/benches/service.rs b/benches/service.rs index 0d3264857..30708477d 100644 --- a/benches/service.rs +++ b/benches/service.rs @@ -9,7 +9,7 @@ use actix_web::test::{init_service, ok_service, TestRequest}; /// Criterion Benchmark for async Service /// Should be used from within criterion group: -/// ```rust,ignore +/// ```ignore /// let mut criterion: ::criterion::Criterion<_> = /// ::criterion::Criterion::default().configure_from_args(); /// bench_async_service(&mut criterion, ok_service(), "async_service_direct"); diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 000000000..eb66960ac --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.46" diff --git a/codecov.yml b/codecov.yml index e6bc40203..e45672bfc 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,10 +4,10 @@ coverage: status: project: default: - threshold: 10% # make CI green + threshold: 100% # make CI green patch: default: - threshold: 10% # make CI green + threshold: 100% # make CI green ignore: # ignore code coverage on following paths - "**/tests" diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index ec0f7a946..2c6e2779b 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -1,35 +1,44 @@ digraph { subgraph cluster_web { - label="actix/actix-web" + label="actix/web" "awc" - "actix-web" - "actix-files" - "actix-http" - "actix-multipart" - "actix-web-actors" - "actix-web-codegen" - "actix-http-test" + "web" + "files" + "http" + "multipart" + "web-actors" + "web-codegen" + "http-test" + + { rank=same; "multipart" "web-actors" "http-test" }; + { rank=same; "files" "awc" "web" }; + { rank=same; "web-codegen" "http" }; } - "actix-web" -> { "actix-codec" "actix-service" "actix-utils" "actix-router" "actix-rt" "actix-server" "macros" "threadpool" "actix-tls" "actix-web-codegen" "actix-http" "awc" } - "awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" } - "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } - "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } - "actix-http" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "threadpool" } - "actix-http" -> { "actix-tls" }[color=blue] // optional - "actix-files" -> { "actix-web" } - "actix-http-test" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "actix-server" "awc" } + "web" -> { "codec" "service" "utils" "router" "rt" "server" "macros" "web-codegen" "http" "awc" } + "web" -> { "tls" }[color=blue] // optional + "awc" -> { "codec" "service" "http" "rt" } + "web-actors" -> { "actix" "web" "http" "codec" } + "multipart" -> { "web" "service" "utils" } + "http" -> { "service" "codec" "utils" "rt" } + "http" -> { "tls" }[color=blue] // optional + "files" -> { "web" } + "http-test" -> { "service" "codec" "utils" "rt" "server" "awc" } + "http-test" -> { "tls" }[color=blue] // optional // net - "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } - "actix-tracing" -> { "actix-service" } - "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } - "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } - "actix-rt" -> { "macros" "threadpool" } + "utils" -> { "service" "rt" "codec" } + "tracing" -> { "service" } + "tls" -> { "service" "codec" "utils" } + "server" -> { "service" "rt" "codec" "utils" } + "rt" -> { "macros" } + + { rank=same; "utils" "codec" }; + { rank=same; "rt" "macros" "service" "router" }; // actix - "actix" -> { "actix-rt" } + "actix" -> { "rt" } } diff --git a/examples/basic.rs b/examples/basic.rs index 99eef3ee1..796f002e8 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -30,7 +30,7 @@ async fn main() -> std::io::Result<()> { .service( web::resource("/resource2/index.html") .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) - .default_service(web::route().to(|| HttpResponse::MethodNotAllowed())) + .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) diff --git a/examples/on_connect.rs b/examples/on_connect.rs index ba5a18f3f..24ac86c6b 100644 --- a/examples/on_connect.rs +++ b/examples/on_connect.rs @@ -2,7 +2,7 @@ //! properties and pass them to a handler through request-local data. //! //! For an example of extracting a client TLS certificate, see: -//! +//! use std::{any::Any, io, net::SocketAddr}; diff --git a/examples/uds.rs b/examples/uds.rs index 096781984..1db252fef 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -34,7 +34,7 @@ async fn main() -> std::io::Result<()> { .service( web::resource("/resource2/index.html") .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) - .default_service(web::route().to(|| HttpResponse::MethodNotAllowed())) + .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) diff --git a/src/app.rs b/src/app.rs index 7a26a3a89..f2c6bce8a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -79,7 +79,7 @@ where /// uses `Arc` so data could be created outside of app factory and clones could /// be stored via `App::app_data()` method. /// - /// ```rust + /// ``` /// use std::cell::Cell; /// use actix_web::{web, App, HttpResponse, Responder}; /// @@ -152,7 +152,7 @@ where /// different module or even library. For example, /// some of the resource's configuration could be moved to different module. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// // this function could be located in different module @@ -185,7 +185,7 @@ where /// This method can be used multiple times with same path, in that case /// multiple resources with one route would be registered for same resource path. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -228,7 +228,7 @@ where /// /// It is possible to use services like `Resource`, `Route`. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index() -> &'static str { @@ -246,7 +246,7 @@ where /// /// It is also possible to use static files as default service. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { @@ -283,7 +283,7 @@ where /// and are never considered for matching at request time. Calls to /// `HttpRequest::url_for()` will work as expected. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpRequest, HttpResponse, Result}; /// /// async fn index(req: HttpRequest) -> Result { @@ -325,7 +325,7 @@ where /// the builder chain. Consequently, the *first* middleware registered /// in the builder chain is the *last* to execute during request processing. /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{middleware, web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; @@ -382,7 +382,7 @@ where /// /// Use middleware when you need to read or modify *every* request or response in some way. /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; diff --git a/src/app_service.rs b/src/app_service.rs index 9b4ae3354..be4ccf22f 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,6 +1,5 @@ use std::cell::RefCell; use std::rc::Rc; -use std::task::Poll; use actix_http::{Extensions, Request, Response}; use actix_router::{Path, ResourceDef, Router, Url}; diff --git a/src/config.rs b/src/config.rs index bd9a25c6f..cd14eb4cc 100644 --- a/src/config.rs +++ b/src/config.rs @@ -113,7 +113,7 @@ pub struct AppConfig { impl AppConfig { pub(crate) fn new(secure: bool, addr: SocketAddr, host: String) -> Self { - AppConfig { secure, addr, host } + AppConfig { secure, host, addr } } /// Server host name. diff --git a/src/data.rs b/src/data.rs index 133248212..56ecdb8ae 100644 --- a/src/data.rs +++ b/src/data.rs @@ -5,6 +5,7 @@ use std::sync::Arc; use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; use futures_util::future::{err, ok, LocalBoxFuture, Ready}; +use serde::Serialize; use crate::dev::Payload; use crate::extract::FromRequest; @@ -36,7 +37,7 @@ pub(crate) type FnDataFactory = /// If route data is not set for a handler, using `Data` extractor would cause *Internal /// Server Error* response. /// -/// ```rust +/// ``` /// use std::sync::Mutex; /// use actix_web::{web, App, HttpResponse, Responder}; /// @@ -102,6 +103,18 @@ impl From> for Data { } } +impl Serialize for Data +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + impl FromRequest for Data { type Config = (); type Error = Error; diff --git a/src/extract.rs b/src/extract.rs index 7a677bca4..8851481e3 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -51,7 +51,7 @@ pub trait FromRequest: Sized { /// /// ## Example /// -/// ```rust +/// ``` /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; @@ -143,7 +143,7 @@ where /// /// ## Example /// -/// ```rust +/// ``` /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; @@ -302,13 +302,13 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { } )+ - if ready { - Poll::Ready(Ok( - ($(this.items.$n.take().unwrap(),)+) - )) - } else { - Poll::Pending - } + if ready { + Poll::Ready(Ok( + ($(this.items.$n.take().unwrap(),)+) + )) + } else { + Poll::Pending + } } } } @@ -318,16 +318,16 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { mod m { use super::*; -tuple_from_req!(TupleFromRequest1, (0, A)); -tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); -tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); -tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); -tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); -tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); + tuple_from_req!(TupleFromRequest1, (0, A)); + tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); + tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); + tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); + tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); + tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); + tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); + tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); + tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); + tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); } #[cfg(test)] diff --git a/src/guard.rs b/src/guard.rs index 5d0de58c2..3a1f5bb14 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -12,7 +12,7 @@ //! to store extra attributes on a request by using the `Extensions` container. //! Extensions containers are available via the `RequestHead::extensions()` method. //! -//! ```rust +//! ``` //! use actix_web::{web, http, dev, guard, App, HttpResponse}; //! //! fn main() { @@ -42,7 +42,7 @@ pub trait Guard { /// Create guard object for supplied function. /// -/// ```rust +/// ``` /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { @@ -85,7 +85,7 @@ where /// Return guard that matches if any of supplied guards. /// -/// ```rust +/// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { @@ -124,7 +124,7 @@ impl Guard for AnyGuard { /// Return guard that matches if all of the supplied guards. /// -/// ```rust +/// ``` /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { @@ -259,7 +259,7 @@ impl Guard for HeaderGuard { /// Return predicate that matches if request contains specified Host name. /// -/// ```rust +/// ``` /// use actix_web::{web, guard::Host, App, HttpResponse}; /// /// fn main() { diff --git a/src/handler.rs b/src/handler.rs index 0016b741e..7e3c5f47e 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -28,17 +28,6 @@ where fn call(&self, param: T) -> R; } -impl Handler<(), R> for F -where - F: Fn() -> R + Clone + 'static, - R: Future, - R::Output: Responder, -{ - fn call(&self, _: ()) -> R { - (self)() - } -} - #[doc(hidden)] /// Extract arguments from request, run factory function and make response. pub struct HandlerService @@ -177,30 +166,29 @@ where } /// FromRequest trait impl for tuples -macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { - impl Handler<($($T,)+), Res> for Func - where Func: Fn($($T,)+) -> Res + Clone + 'static, +macro_rules! factory_tuple ({ $($param:ident)* } => { + impl Handler<($($param,)*), Res> for Func + where Func: Fn($($param),*) -> Res + Clone + 'static, Res: Future, Res::Output: Responder, { - fn call(&self, param: ($($T,)+)) -> Res { - (self)($(param.$n,)+) + #[allow(non_snake_case)] + fn call(&self, ($($param,)*): ($($param,)*)) -> Res { + (self)($($param,)*) } } }); -#[rustfmt::skip] -mod m { - use super::*; - - factory_tuple!((0, A)); - factory_tuple!((0, A), (1, B)); - factory_tuple!((0, A), (1, B), (2, C)); - factory_tuple!((0, A), (1, B), (2, C), (3, D)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -} +factory_tuple! {} +factory_tuple! { A } +factory_tuple! { A B } +factory_tuple! { A B C } +factory_tuple! { A B C D } +factory_tuple! { A B C D E } +factory_tuple! { A B C D E F } +factory_tuple! { A B C D E F G } +factory_tuple! { A B C D E F G H } +factory_tuple! { A B C D E F G H I } +factory_tuple! { A B C D E F G H I J } +factory_tuple! { A B C D E F G H I J K } +factory_tuple! { A B C D E F G H I J K L } diff --git a/src/lib.rs b/src/lib.rs index 16b2ab186..7a6498546 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,7 +2,7 @@ //! //! ## Example //! -//! ```rust,no_run +//! ```no_run //! use actix_web::{get, web, App, HttpServer, Responder}; //! //! #[get("/{id}/{name}/index.html")] @@ -173,11 +173,7 @@ pub mod dev { impl BodyEncoding for ResponseBuilder { fn get_encoding(&self) -> Option { - if let Some(ref enc) = self.extensions().get::() { - Some(enc.0) - } else { - None - } + self.extensions().get::().map(|enc| enc.0) } fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { @@ -188,11 +184,7 @@ pub mod dev { impl BodyEncoding for Response { fn get_encoding(&self) -> Option { - if let Some(ref enc) = self.extensions().get::() { - Some(enc.0) - } else { - None - } + self.extensions().get::().map(|enc| enc.0) } fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index 6f60264b1..71193a5c5 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -16,7 +16,7 @@ use crate::{error::Error, service::ServiceResponse}; /// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). /// /// # Examples -/// ```rust +/// ``` /// use actix_web::middleware::{Logger, Compat}; /// use actix_web::{App, web}; /// diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 698ba768e..a397bccd6 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -31,7 +31,7 @@ use crate::{ /// encoding to `ContentEncoding::Identity`. /// /// # Examples -/// ```rust +/// ``` /// use actix_web::{web, middleware, App, HttpResponse}; /// /// let app = App::new() @@ -197,22 +197,23 @@ impl AcceptEncoding { /// Parse a raw Accept-Encoding header value into an ordered list. pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { - let mut encodings: Vec<_> = raw + let mut encodings = raw .replace(' ', "") .split(',') .map(|l| AcceptEncoding::new(l)) - .collect(); + .flatten() + .collect::>(); + encodings.sort(); for enc in encodings { - if let Some(enc) = enc { - if encoding == ContentEncoding::Auto { - return enc.encoding; - } else if encoding == enc.encoding { - return encoding; - } + if encoding == ContentEncoding::Auto { + return enc.encoding; + } else if encoding == enc.encoding { + return encoding; } } + ContentEncoding::Identity } } diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index f0c344062..dd599a0cb 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -12,7 +12,7 @@ use futures_util::future::{Either, FutureExt, LocalBoxFuture}; /// middleware for a workaround. /// /// # Examples -/// ```rust +/// ``` /// use actix_web::middleware::{Condition, NormalizePath}; /// use actix_web::App; /// @@ -106,6 +106,7 @@ mod tests { HttpResponse, }; + #[allow(clippy::unnecessary_wraps)] fn render_500(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index a36cc2f29..12d70ab2c 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -29,7 +29,7 @@ use crate::{ /// Headers with the same key that are already set in a response will *not* be overwritten. /// /// # Examples -/// ```rust +/// ``` /// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// fn main() { diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 4673ed4ce..fddd87a99 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -34,7 +34,7 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() @@ -205,6 +206,7 @@ mod tests { assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } + #[allow(clippy::unnecessary_wraps)] fn render_500_async( mut res: ServiceResponse, ) -> Result> { diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 5b5b5577c..8f5391757 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -43,7 +43,7 @@ use crate::{ /// ``` /// /// # Examples -/// ```rust +/// ``` /// use actix_web::{middleware::Logger, App}; /// /// // access logs are printed with the INFO level so ensure it is enabled by default @@ -124,7 +124,7 @@ impl Logger { /// It is convention to print "-" to indicate no output instead of an empty string. /// /// # Example - /// ```rust + /// ``` /// # use actix_web::{http::HeaderValue, middleware::Logger}; /// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() } /// Logger::new("example %{JWT_ID}xi") @@ -363,7 +363,7 @@ impl Format { /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { log::trace!("Access log format: {}", s); - let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap(); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[%atPrUsbTD]?)").unwrap(); let mut idx = 0; let mut results = Vec::new(); @@ -639,6 +639,38 @@ mod tests { let _res = srv.call(req).await.unwrap(); } + #[actix_rt::test] + async fn test_escape_percent() { + let mut format = Format::new("%%{r}a"); + + let req = TestRequest::default() + .insert_header(( + header::FORWARDED, + header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), + )) + .to_srv_request(); + + let now = OffsetDateTime::now_utc(); + for unit in &mut format.0 { + unit.render_request(now, &req); + } + + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + for unit in &mut format.0 { + unit.render_response(&resp); + } + + let entry_time = OffsetDateTime::now_utc(); + let render = |fmt: &mut fmt::Formatter<'_>| { + for unit in &format.0 { + unit.render(fmt, 1024, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert_eq!(s, "%{r}a"); + } + #[actix_rt::test] async fn test_url_path() { let mut format = Format::new("%T %U"); diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index ea21a7215..2a97a047b 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -54,7 +54,7 @@ impl Default for TrailingSlash { /// `TrailingSlash::Always` behavior), as shown in the example tests below. /// /// # Examples -/// ```rust +/// ``` /// use actix_web::{web, middleware, App}; /// /// # actix_web::rt::System::new().block_on(async { diff --git a/src/request.rs b/src/request.rs index 514b7466e..15c97345c 100644 --- a/src/request.rs +++ b/src/request.rs @@ -159,7 +159,7 @@ impl HttpRequest { /// Generate url for named resource /// - /// ```rust + /// ``` /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { @@ -231,7 +231,7 @@ impl HttpRequest { /// /// If `App::data` was used to store object, use `Data`: /// - /// ```rust,ignore + /// ```ignore /// let opt_t = req.app_data::>(); /// ``` pub fn app_data(&self) -> Option<&T> { @@ -302,7 +302,7 @@ impl Drop for HttpRequest { /// /// ## Example /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpRequest}; /// use serde_derive::Deserialize; /// diff --git a/src/request_data.rs b/src/request_data.rs index beee8ac12..fc711d011 100644 --- a/src/request_data.rs +++ b/src/request_data.rs @@ -23,7 +23,7 @@ use crate::{dev::Payload, FromRequest, HttpRequest}; /// provided to make this potential foot-gun more obvious. /// /// # Example -/// ```rust,no_run +/// ```no_run /// # use actix_web::{web, HttpResponse, HttpRequest, Responder}; /// /// #[derive(Debug, Clone, PartialEq)] diff --git a/src/resource.rs b/src/resource.rs index 944beeefa..8f356c76d 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -2,7 +2,6 @@ use std::cell::RefCell; use std::fmt; use std::future::Future; use std::rc::Rc; -use std::task::Poll; use actix_http::{Error, Extensions, Response}; use actix_router::IntoPattern; @@ -36,7 +35,7 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// and check guards for specific route, if request matches all /// guards, route considered matched and route handler get called. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { @@ -98,7 +97,7 @@ where /// Add match guard to a resource. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -131,7 +130,7 @@ where /// Register a new route. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { @@ -148,7 +147,7 @@ where /// Multiple routes could be added to a resource. Resource object uses /// match guards for route selection. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App}; /// /// fn main() { @@ -173,7 +172,7 @@ where /// Provided data is available for all routes registered for the current resource. /// Resource data overrides data registered by `App::data()` method. /// - /// ```rust + /// ``` /// use actix_web::{web, App, FromRequest}; /// /// /// extract text data from request @@ -212,7 +211,7 @@ where /// Register a new route and add handler. This route matches all requests. /// - /// ```rust + /// ``` /// use actix_web::*; /// /// fn index(req: HttpRequest) -> HttpResponse { @@ -224,7 +223,7 @@ where /// /// This is shortcut for: /// - /// ```rust + /// ``` /// # extern crate actix_web; /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } @@ -290,7 +289,7 @@ where /// Resource level middlewares are not allowed to change response /// type (i.e modify response's body). /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; @@ -450,9 +449,9 @@ impl ServiceFactory for ResourceFactory { .collect::, _>>()?; Ok(ResourceService { + routes, app_data, default, - routes, }) }) } diff --git a/src/responder.rs b/src/responder.rs index 92945cdaa..b75c95083 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -18,7 +18,7 @@ pub trait Responder { /// Override a status code for a Responder. /// - /// ```rust + /// ``` /// use actix_web::{http::StatusCode, HttpRequest, Responder}; /// /// fn index(req: HttpRequest) -> impl Responder { @@ -36,7 +36,7 @@ pub trait Responder { /// /// Overrides other headers with the same name. /// - /// ```rust + /// ``` /// use actix_web::{web, HttpRequest, Responder}; /// use serde::Serialize; /// @@ -155,8 +155,7 @@ impl Responder for BytesMut { pub struct CustomResponder { responder: T, status: Option, - headers: Option, - error: Option, + headers: Result, } impl CustomResponder { @@ -164,14 +163,13 @@ impl CustomResponder { CustomResponder { responder, status: None, - headers: None, - error: None, + headers: Ok(HeaderMap::new()), } } /// Override a status code for the Responder's response. /// - /// ```rust + /// ``` /// use actix_web::{HttpRequest, Responder, http::StatusCode}; /// /// fn index(req: HttpRequest) -> impl Responder { @@ -187,7 +185,7 @@ impl CustomResponder { /// /// Overrides other headers with the same name. /// - /// ```rust + /// ``` /// use actix_web::{web, HttpRequest, Responder}; /// use serde::Serialize; /// @@ -206,32 +204,33 @@ impl CustomResponder { where H: IntoHeaderPair, { - if self.headers.is_none() { - self.headers = Some(HeaderMap::new()); + if let Ok(ref mut headers) = self.headers { + match header.try_into_header_pair() { + Ok((key, value)) => headers.append(key, value), + Err(e) => self.headers = Err(e.into()), + }; } - match header.try_into_header_pair() { - Ok((key, value)) => self.headers.as_mut().unwrap().append(key, value), - Err(e) => self.error = Some(e.into()), - }; - self } } impl Responder for CustomResponder { fn respond_to(self, req: &HttpRequest) -> HttpResponse { + let headers = match self.headers { + Ok(headers) => headers, + Err(err) => return HttpResponse::from_error(Error::from(err)), + }; + let mut res = self.responder.respond_to(req); if let Some(status) = self.status { *res.status_mut() = status; } - if let Some(ref headers) = self.headers { - for (k, v) in headers { - // TODO: before v4, decide if this should be append instead - res.headers_mut().insert(k.clone(), v.clone()); - } + for (k, v) in headers { + // TODO: before v4, decide if this should be append instead + res.headers_mut().insert(k, v); } res diff --git a/src/route.rs b/src/route.rs index b6b2482cd..0a297b456 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,13 +1,13 @@ #![allow(clippy::rc_buffer)] // inner value is mutated before being shared (`Rc::get_mut`) -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; +use std::{future::Future, rc::Rc}; use actix_http::{http::Method, Error}; -use actix_service::{Service, ServiceFactory}; -use futures_util::future::{ready, FutureExt, LocalBoxFuture}; +use actix_service::{ + boxed::{self, BoxService, BoxServiceFactory}, + Service, ServiceFactory, +}; +use futures_core::future::LocalBoxFuture; use crate::extract::FromRequest; use crate::guard::{self, Guard}; @@ -16,33 +16,12 @@ use crate::responder::Responder; use crate::service::{ServiceRequest, ServiceResponse}; use crate::HttpResponse; -type BoxedRouteService = Box< - dyn Service< - ServiceRequest, - Response = ServiceResponse, - Error = Error, - Future = LocalBoxFuture<'static, Result>, - >, ->; - -type BoxedRouteNewService = Box< - dyn ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - Service = BoxedRouteService, - Future = LocalBoxFuture<'static, Result>, - >, ->; - /// Resource route definition /// /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. pub struct Route { - service: BoxedRouteNewService, + service: BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>, guards: Rc>>, } @@ -51,9 +30,7 @@ impl Route { #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { - service: Box::new(RouteNewService::new(HandlerService::new(|| { - ready(HttpResponse::NotFound()) - }))), + service: boxed::factory(HandlerService::new(HttpResponse::NotFound)), guards: Rc::new(Vec::new()), } } @@ -64,44 +41,26 @@ impl Route { } impl ServiceFactory for Route { - type Config = (); type Response = ServiceResponse; type Error = Error; - type InitError = (); + type Config = (); type Service = RouteService; - type Future = CreateRouteService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - CreateRouteService { - fut: self.service.new_service(()), - guards: self.guards.clone(), - } - } -} + let fut = self.service.new_service(()); + let guards = self.guards.clone(); -pub struct CreateRouteService { - fut: LocalBoxFuture<'static, Result>, - guards: Rc>>, -} - -impl Future for CreateRouteService { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - match this.fut.as_mut().poll(cx)? { - Poll::Ready(service) => Poll::Ready(Ok(RouteService { - service, - guards: this.guards.clone(), - })), - Poll::Pending => Poll::Pending, - } + Box::pin(async move { + let service = fut.await?; + Ok(RouteService { service, guards }) + }) } } pub struct RouteService { - service: BoxedRouteService, + service: BoxService, guards: Rc>>, } @@ -121,9 +80,7 @@ impl Service for RouteService { type Error = Error; type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); fn call(&self, req: ServiceRequest) -> Self::Future { self.service.call(req) @@ -133,7 +90,7 @@ impl Service for RouteService { impl Route { /// Add method guard to the route. /// - /// ```rust + /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( @@ -153,7 +110,7 @@ impl Route { /// Add guard to the route. /// - /// ```rust + /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( @@ -171,7 +128,7 @@ impl Route { /// Set handler function, use request extractors for parameters. /// - /// ```rust + /// ``` /// use actix_web::{web, http, App}; /// use serde_derive::Deserialize; /// @@ -195,7 +152,7 @@ impl Route { /// /// It is possible to use multiple extractors for one handler function. /// - /// ```rust + /// ``` /// # use std::collections::HashMap; /// # use serde_derive::Deserialize; /// use actix_web::{web, App}; @@ -224,80 +181,11 @@ impl Route { R: Future + 'static, R::Output: Responder + 'static, { - self.service = Box::new(RouteNewService::new(HandlerService::new(handler))); + self.service = boxed::factory(HandlerService::new(handler)); self } } -struct RouteNewService -where - T: ServiceFactory, -{ - service: T, -} - -impl RouteNewService -where - T: ServiceFactory, - T::Future: 'static, - T::Service: 'static, - >::Future: 'static, -{ - pub fn new(service: T) -> Self { - RouteNewService { service } - } -} - -impl ServiceFactory for RouteNewService -where - T: ServiceFactory, - T::Future: 'static, - T::Service: 'static, - >::Future: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = BoxedRouteService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - self.service - .new_service(()) - .map(|result| match result { - Ok(service) => { - let service = Box::new(RouteServiceWrapper { service }) as _; - Ok(service) - } - Err(_) => Err(()), - }) - .boxed_local() - } -} - -struct RouteServiceWrapper> { - service: T, -} - -impl Service for RouteServiceWrapper -where - T::Future: 'static, - T: Service, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&self, req: ServiceRequest) -> Self::Future { - Box::pin(self.service.call(req)) - } -} - #[cfg(test)] mod tests { use std::time::Duration; diff --git a/src/scope.rs b/src/scope.rs index dd02501b0..693d6860f 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -2,7 +2,6 @@ use std::cell::RefCell; use std::fmt; use std::future::Future; use std::rc::Rc; -use std::task::Poll; use actix_http::Extensions; use actix_router::{ResourceDef, Router}; @@ -41,7 +40,7 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// You can get variable path segments from `HttpRequest::match_info()`. /// `Path` extractor also is able to extract scope level variable segments. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { @@ -98,7 +97,7 @@ where { /// Add match guard to a scope. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App, HttpRequest, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -124,7 +123,7 @@ where /// Set or override application data. Application data could be accessed /// by using `Data` extractor where `T` is data type. /// - /// ```rust + /// ``` /// use std::cell::Cell; /// use actix_web::{web, App, HttpResponse, Responder}; /// @@ -169,7 +168,7 @@ where /// different module or even library. For example, /// some of the resource's configuration could be moved to different module. /// - /// ```rust + /// ``` /// # extern crate actix_web; /// use actix_web::{web, middleware, App, HttpResponse}; /// @@ -216,7 +215,7 @@ where /// * *Scope* is a set of resources with common root path. /// * "StaticFiles" is a service for static files support /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpRequest}; /// /// struct AppState; @@ -248,7 +247,7 @@ where /// This method can be called multiple times, in that case /// multiple resources with one route would be registered for same resource path. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -342,7 +341,7 @@ where /// to Route or Application level middleware, in that Scope-level middleware /// can not modify ServiceResponse. /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; diff --git a/src/server.rs b/src/server.rs index d69d6570d..97c4fdaa2 100644 --- a/src/server.rs +++ b/src/server.rs @@ -12,13 +12,6 @@ use actix_http::{ use actix_server::{Server, ServerBuilder}; use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory}; -#[cfg(unix)] -use actix_http::Protocol; -#[cfg(unix)] -use actix_service::pipeline_factory; -#[cfg(unix)] -use futures_util::future::ok; - #[cfg(feature = "openssl")] use actix_tls::accept::openssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; #[cfg(feature = "rustls")] @@ -42,7 +35,7 @@ struct Config { /// /// Create new HTTP server with application factory. /// -/// ```rust,no_run +/// ```no_run /// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// #[actix_rt::main] @@ -489,7 +482,9 @@ where #[cfg(unix)] /// Start listening for unix domain (UDS) connections on existing listener. pub fn listen_uds(mut self, lst: std::os::unix::net::UnixListener) -> io::Result { + use actix_http::Protocol; use actix_rt::net::UnixStream; + use actix_service::pipeline_factory; let cfg = self.config.clone(); let factory = self.factory.clone(); @@ -511,19 +506,22 @@ where c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), ); - pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then({ - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout); + pipeline_factory(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }) + .and_then({ + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| { + (&*handler)(io as &dyn Any, ext) + }) + } else { + svc + }; - svc.finish(map_config(factory(), move |_| config.clone())) - }) + svc.finish(map_config(factory(), move |_| config.clone())) + }) })?; Ok(self) } @@ -534,7 +532,9 @@ where where A: AsRef, { + use actix_http::Protocol; use actix_rt::net::UnixStream; + use actix_service::pipeline_factory; let cfg = self.config.clone(); let factory = self.factory.clone(); @@ -555,12 +555,13 @@ where socket_addr, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), ); - pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then( - HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .finish(map_config(factory(), move |_| config.clone())), - ) + pipeline_factory(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }) + .and_then( + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .finish(map_config(factory(), move |_| config.clone())), + ) }, )?; Ok(self) @@ -587,7 +588,7 @@ where /// This methods panics if no socket address can be bound or an `Actix` system is not yet /// configured. /// - /// ```rust,no_run + /// ```no_run /// use std::io; /// use actix_web::{web, App, HttpResponse, HttpServer}; /// @@ -606,17 +607,14 @@ where fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result { use socket2::{Domain, Protocol, Socket, Type}; - let domain = match addr { - net::SocketAddr::V4(_) => Domain::ipv4(), - net::SocketAddr::V6(_) => Domain::ipv6(), - }; - let socket = Socket::new(domain, Type::stream(), Some(Protocol::tcp()))?; + let domain = Domain::for_address(addr); + let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; socket.set_reuse_address(true)?; socket.bind(&addr.into())?; // clamp backlog to max u32 that fits in i32 range let backlog = cmp::min(backlog, i32::MAX as u32) as i32; socket.listen(backlog)?; - Ok(socket.into_tcp_listener()) + Ok(net::TcpListener::from(socket)) } #[cfg(feature = "openssl")] diff --git a/src/service.rs b/src/service.rs index fcbe61a02..32f152f7d 100644 --- a/src/service.rs +++ b/src/service.rs @@ -462,7 +462,7 @@ impl WebService { /// Add match guard to a web service. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, dev, App, Error, HttpResponse}; /// /// async fn index(req: dev::ServiceRequest) -> Result { @@ -573,31 +573,28 @@ macro_rules! services { } /// HttpServiceFactory trait impl for tuples -macro_rules! service_tuple ({ $(($n:tt, $T:ident)),+} => { +macro_rules! service_tuple ({ $($T:ident)+ } => { impl<$($T: HttpServiceFactory),+> HttpServiceFactory for ($($T,)+) { + #[allow(non_snake_case)] fn register(self, config: &mut AppService) { - $(self.$n.register(config);)+ + let ($($T,)*) = self; + $($T.register(config);)+ } } }); -#[rustfmt::skip] -mod m { - use super::*; - - service_tuple!((0, A)); - service_tuple!((0, A), (1, B)); - service_tuple!((0, A), (1, B), (2, C)); - service_tuple!((0, A), (1, B), (2, C), (3, D)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J), (10, K)); - service_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J), (10, K), (11, L)); -} +service_tuple! { A } +service_tuple! { A B } +service_tuple! { A B C } +service_tuple! { A B C D } +service_tuple! { A B C D E } +service_tuple! { A B C D E F } +service_tuple! { A B C D E F G } +service_tuple! { A B C D E F G H } +service_tuple! { A B C D E F G H I } +service_tuple! { A B C D E F G H I J } +service_tuple! { A B C D E F G H I J K } +service_tuple! { A B C D E F G H I J K L } #[cfg(test)] mod tests { diff --git a/src/test.rs b/src/test.rs index dd2426fec..7e8bf1069 100644 --- a/src/test.rs +++ b/src/test.rs @@ -54,7 +54,7 @@ pub fn default_service( /// This method accepts application builder instance, and constructs /// service. /// -/// ```rust +/// ``` /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// @@ -101,7 +101,7 @@ where /// Calls service and waits for response future completion. /// -/// ```rust +/// ``` /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// /// #[actix_rt::test] @@ -131,7 +131,7 @@ where /// Helper function that returns a response body of a TestRequest /// -/// ```rust +/// ``` /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// @@ -174,7 +174,7 @@ where /// Helper function that returns a response body of a ServiceResponse. /// -/// ```rust +/// ``` /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// @@ -212,7 +212,7 @@ where /// Helper function that returns a deserialized response body of a ServiceResponse. /// -/// ```rust +/// ``` /// use actix_web::{App, test, web, HttpResponse, http::header}; /// use serde::{Serialize, Deserialize}; /// @@ -271,7 +271,7 @@ where /// Helper function that returns a deserialized response body of a TestRequest /// -/// ```rust +/// ``` /// use actix_web::{App, test, web, HttpResponse, http::header}; /// use serde::{Serialize, Deserialize}; /// @@ -324,7 +324,7 @@ where /// * `TestRequest::to_srv_response` creates `ServiceResponse` instance. /// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// -/// ```rust +/// ``` /// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; /// use actix_web::http::{header, StatusCode}; /// @@ -572,7 +572,7 @@ impl TestRequest { /// /// # Examples /// -/// ```rust +/// ``` /// use actix_web::{web, test, App, HttpResponse, Error}; /// /// async fn my_handler() -> Result { @@ -612,7 +612,7 @@ where /// /// # Examples /// -/// ```rust +/// ``` /// use actix_web::{web, test, App, HttpResponse, Error}; /// /// async fn my_handler() -> Result { @@ -774,10 +774,10 @@ where }; TestServer { - ssl, addr, client, system, + ssl, server, } } @@ -862,10 +862,10 @@ impl TestServerConfig { /// Get first available unused address pub fn unused_addr() -> net::SocketAddr { let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); + let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap(); socket.bind(&addr.into()).unwrap(); socket.set_reuse_address(true).unwrap(); - let tcp = socket.into_tcp_listener(); + let tcp = net::TcpListener::from(socket); tcp.local_addr().unwrap() } diff --git a/src/types/form.rs b/src/types/form.rs index 0b5c3c1b4..57a742e38 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -106,6 +106,18 @@ impl ops::DerefMut for Form { } } +impl Serialize for Form +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + /// See [here](#extractor) for example of usage as an extractor. impl FromRequest for Form where @@ -400,12 +412,8 @@ mod tests { UrlencodedError::Overflow { .. } => { matches!(other, UrlencodedError::Overflow { .. }) } - UrlencodedError::UnknownLength => { - matches!(other, UrlencodedError::UnknownLength) - } - UrlencodedError::ContentType => { - matches!(other, UrlencodedError::ContentType) - } + UrlencodedError::UnknownLength => matches!(other, UrlencodedError::UnknownLength), + UrlencodedError::ContentType => matches!(other, UrlencodedError::ContentType), _ => false, } } diff --git a/src/types/json.rs b/src/types/json.rs index 31ff680f4..d8ce3cb71 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -114,6 +114,18 @@ where } } +impl Serialize for Json +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + /// Creates response with OK status code, correct content type header, and serialized JSON payload. /// /// If serialization failed @@ -441,9 +453,7 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), - JsonPayloadError::ContentType => { - matches!(other, JsonPayloadError::ContentType) - } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } diff --git a/src/types/path.rs b/src/types/path.rs index 4ab124d53..294df6cf2 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -20,7 +20,7 @@ use crate::{dev::Payload, error::PathError, FromRequest, HttpRequest}; /// // extract path info from "/{name}/{count}/index.html" into tuple /// // {name} - deserialize a String /// // {count} - deserialize a u32 -/// #[get("/")] +/// #[get("/{name}/{count}/index.html")] /// async fn index(path: web::Path<(String, u32)>) -> String { /// let (name, count) = path.into_inner(); /// format!("Welcome {}! {}", name, count) @@ -40,7 +40,7 @@ use crate::{dev::Payload, error::PathError, FromRequest, HttpRequest}; /// } /// /// // extract `Info` from a path using serde -/// #[get("/")] +/// #[get("/{name}")] /// async fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.name) /// } diff --git a/src/types/query.rs b/src/types/query.rs index 691a4792b..79af32581 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -29,7 +29,7 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// Code /// } /// -/// #[derive(Deserialize)] +/// #[derive(Debug, Deserialize)] /// pub struct AuthRequest { /// id: u64, /// response_type: ResponseType, @@ -42,9 +42,23 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// async fn index(info: web::Query) -> String { /// format!("Authorization request for id={} and type={:?}!", info.id, info.response_type) /// } +/// +/// // To access the entire underlying query struct, use `.into_inner()`. +/// #[get("/debug1")] +/// async fn debug1(info: web::Query) -> String { +/// dbg!("Authorization object={:?}", info.into_inner()); +/// "OK".to_string() +/// } +/// +/// // Or use `.0`, which is equivalent to `.into_inner()`. +/// #[get("/debug2")] +/// async fn debug2(info: web::Query) -> String { +/// dbg!("Authorization object={:?}", info.0); +/// "OK".to_string() +/// } /// ``` #[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Query(T); +pub struct Query(pub T); impl Query { /// Unwrap into inner `T` value. diff --git a/src/web.rs b/src/web.rs index 1cef37109..8662848a4 100644 --- a/src/web.rs +++ b/src/web.rs @@ -42,7 +42,7 @@ pub use crate::types::*; /// `/users/{userid}/{friend}` and store `userid` and `friend` in /// the exposed `Params` object: /// -/// ```rust +/// ``` /// # extern crate actix_web; /// use actix_web::{web, App, HttpResponse}; /// @@ -61,7 +61,7 @@ pub fn resource(path: T) -> Resource { /// Scopes collect multiple paths under a common path prefix. /// Scope path can contain variable path segments as resources. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -88,7 +88,7 @@ pub fn route() -> Route { /// Create *route* with `GET` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -106,7 +106,7 @@ pub fn get() -> Route { /// Create *route* with `POST` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -124,7 +124,7 @@ pub fn post() -> Route { /// Create *route* with `PUT` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -142,7 +142,7 @@ pub fn put() -> Route { /// Create *route* with `PATCH` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -160,7 +160,7 @@ pub fn patch() -> Route { /// Create *route* with `DELETE` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -178,7 +178,7 @@ pub fn delete() -> Route { /// Create *route* with `HEAD` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -196,7 +196,7 @@ pub fn head() -> Route { /// Create *route* with `TRACE` method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -214,7 +214,7 @@ pub fn trace() -> Route { /// Create *route* and add method guard. /// -/// ```rust +/// ``` /// use actix_web::{web, http, App, HttpResponse}; /// /// let app = App::new().service( @@ -232,7 +232,7 @@ pub fn method(method: Method) -> Route { /// Create a new route and add handler. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse, Responder}; /// /// async fn index() -> impl Responder { @@ -256,7 +256,7 @@ where /// Create raw service for a specific path. /// -/// ```rust +/// ``` /// use actix_web::{dev, web, guard, App, Error, HttpResponse}; /// /// async fn my_service(req: dev::ServiceRequest) -> Result { diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 043159376..12225b7e5 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -1,15 +1,11 @@ -use std::sync::mpsc; -use std::{thread, time::Duration}; - #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -#[cfg(feature = "rustls")] -extern crate tls_rustls as rustls; -#[cfg(feature = "openssl")] -use openssl::ssl::SslAcceptorBuilder; - -use actix_web::{test, web, App, HttpResponse, HttpServer}; +#[cfg(any(unix, feature = "openssl"))] +use { + actix_web::{test, web, App, HttpResponse, HttpServer}, + std::{sync::mpsc, thread, time::Duration}, +}; #[cfg(unix)] #[actix_rt::test] @@ -72,7 +68,7 @@ async fn test_start() { } #[cfg(feature = "openssl")] -fn ssl_acceptor() -> std::io::Result { +fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslMethod}, @@ -89,7 +85,7 @@ fn ssl_acceptor() -> std::io::Result { builder.set_certificate(&cert).unwrap(); builder.set_private_key(&key).unwrap(); - Ok(builder) + builder } #[actix_rt::test] @@ -102,7 +98,7 @@ async fn test_start_ssl() { thread::spawn(move || { let sys = actix_rt::System::new(); - let builder = ssl_acceptor().unwrap(); + let builder = ssl_acceptor(); let srv = HttpServer::new(|| { App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { diff --git a/tests/test_server.rs b/tests/test_server.rs index 2466730f9..b35af657d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -21,6 +21,7 @@ use flate2::{ Compression, }; use futures_util::ready; +#[cfg(feature = "openssl")] use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslMethod}, @@ -54,6 +55,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; +#[cfg(feature = "openssl")] fn openssl_config() -> SslAcceptor { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); @@ -786,11 +788,6 @@ mod plus_rustls { #[actix_rt::test] async fn test_reading_deflate_encoding_large_random_rustls() { - use rustls::internal::pemfile::{certs, pkcs8_private_keys}; - use rustls::{NoClientAuth, ServerConfig}; - use std::fs::File; - use std::io::BufReader; - let data = rand::thread_rng() .sample_iter(&Alphanumeric) .take(160_000)