diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 000000000..128f51ffd --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,37 @@ +--- +name: bug report +about: create a bug report +--- + +Your issue may already be reported! +Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one. + +## Expected Behavior + + + +## Current Behavior + + + +## Possible Solution + + + +## Steps to Reproduce (for bugs) + + +1. +2. +3. +4. + +## Context + + + +## Your Environment + + +* Rust Version (I.e, output of `rustc -V`): +* Actix Web Version: diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml new file mode 100644 index 000000000..08bb81d48 --- /dev/null +++ b/.github/workflows/bench.yml @@ -0,0 +1,47 @@ +name: Benchmark (Linux) + +on: [push, pull_request] + +jobs: + check_benchmark: + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@master + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: + command: generate-lockfile + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo index + uses: actions/cache@v1 + with: + path: ~/.cargo/git + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo build + uses: actions/cache@v1 + with: + path: target + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }} + + - name: Check benchmark + uses: actions-rs/cargo@v1 + with: + command: bench + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --no-default-features --features ci-autoclean + cargo-cache diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml new file mode 100644 index 000000000..7cabb8020 --- /dev/null +++ b/.github/workflows/linux.yml @@ -0,0 +1,90 @@ +name: CI (Linux) + +on: [push, pull_request] + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + version: + - 1.39.0 # MSRV + - stable + - nightly + + name: ${{ matrix.version }} - x86_64-unknown-linux-gnu + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@master + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: + command: generate-lockfile + - name: Cache cargo registry + uses: actions/cache@v1 + with: + path: ~/.cargo/registry + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo index + uses: actions/cache@v1 + with: + path: ~/.cargo/git + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} + - name: Cache cargo build + uses: actions/cache@v1 + with: + path: target + key: ${{ matrix.version }}-x86_64-unknown-linux-gnu-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }} + + - name: check build + uses: actions-rs/cargo@v1 + with: + command: check + args: --all --bins --examples --tests + + - name: tests + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --all --all-features --no-fail-fast -- --nocapture + + - name: tests (actix-http) + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --package=actix-http --no-default-features --features=rustls -- --nocapture + + - name: tests (awc) + uses: actions-rs/cargo@v1 + timeout-minutes: 40 + with: + command: test + args: --package=awc --no-default-features --features=rustls -- --nocapture + + - name: Generate coverage file + if: matrix.version == 'stable' && github.ref == 'refs/heads/master' + run: | + cargo install cargo-tarpaulin + cargo tarpaulin --out Xml + - name: Upload to Codecov + if: matrix.version == 'stable' && github.ref == 'refs/heads/master' + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} + file: cobertura.xml + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --no-default-features --features ci-autoclean + cargo-cache diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index f50ae2f05..397236a29 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -27,22 +27,22 @@ jobs: - name: Generate Cargo.lock uses: actions-rs/cargo@v1 with: - command: update + command: generate-lockfile - name: Cache cargo registry uses: actions/cache@v1 with: path: ~/.cargo/registry - key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-registry-${{ hashFiles('**/Cargo.lock') }} + key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-registry-trimmed-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo index uses: actions/cache@v1 with: path: ~/.cargo/git - key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-index-${{ hashFiles('**/Cargo.lock') }} + key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-index-trimmed-${{ hashFiles('**/Cargo.lock') }} - name: Cache cargo build uses: actions/cache@v1 with: path: target - key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-build-${{ hashFiles('**/Cargo.lock') }} + key: ${{ matrix.version }}-x86_64-apple-darwin-cargo-build-trimmed-${{ hashFiles('**/Cargo.lock') }} - name: check build uses: actions-rs/cargo@v1 @@ -57,3 +57,8 @@ jobs: args: --all --all-features --no-fail-fast -- --nocapture --skip=test_h2_content_length --skip=test_reading_deflate_encoding_large_random_rustls + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --no-default-features --features ci-autoclean + cargo-cache diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml new file mode 100644 index 000000000..388ae3704 --- /dev/null +++ b/.github/workflows/upload-doc.yml @@ -0,0 +1,35 @@ +name: Upload documentation + +on: + push: + branches: + - master + +jobs: + build: + runs-on: ubuntu-latest + if: github.repository == 'actix/actix-web' + + steps: + - uses: actions/checkout@master + + - name: Install Rust + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: check build + uses: actions-rs/cargo@v1 + with: + command: doc + args: --no-deps --all-features + + - name: Tweak HTML + run: echo "" > target/doc/index.html + + - name: Upload documentation + run: | + git clone https://github.com/davisp/ghp-import.git + ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://${{ secrets.GITHUB_TOKEN }}@github.com/"${{ github.repository }}.git" target/doc \ No newline at end of file diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml index 9aa3d3ba4..5fd785fad 100644 --- a/.github/workflows/windows.yml +++ b/.github/workflows/windows.yml @@ -27,37 +27,14 @@ jobs: profile: minimal override: true - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: - command: update - - name: Cache cargo registry - uses: actions/cache@v1 - with: - path: ~/.cargo/registry - key: ${{ matrix.version }}-x86_64-pc-windows-msvc-cargo-registry-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo index - uses: actions/cache@v1 - with: - path: ~/.cargo/git - key: ${{ matrix.version }}-x86_64-pc-windows-msvc-cargo-index-${{ hashFiles('**/Cargo.lock') }} - - name: Cache cargo build - uses: actions/cache@v1 - with: - path: target - key: ${{ matrix.version }}-x86_64-pc-windows-msvc-cargo-build-${{ hashFiles('**/Cargo.lock') }} - - name: Cache vcpkg package - uses: actions/cache@v1 - id: cache-vcpkg - with: - path: C:\vcpkg - key: windows_x64-${{ matrix.version }}-vcpkg - - name: Install OpenSSL - if: steps.cache-vcpkg.outputs.cache-hit != 'true' run: | vcpkg integrate install vcpkg install openssl:x64-windows + Copy-Item C:\vcpkg\installed\x64-windows\bin\libcrypto-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libcrypto.dll + Copy-Item C:\vcpkg\installed\x64-windows\bin\libssl-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libssl.dll + Get-ChildItem C:\vcpkg\installed\x64-windows\bin + Get-ChildItem C:\vcpkg\installed\x64-windows\lib - name: check build uses: actions-rs/cargo@v1 @@ -76,4 +53,7 @@ jobs: --skip=test_simple --skip=test_expect_continue --skip=test_http10_keepalive - --skip=test_slow_request + --skip=test_slow_request + --skip=test_connection_force_close + --skip=test_connection_server_close + --skip=test_connection_wait_queue_force_close diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index f10f82a48..000000000 --- a/.travis.yml +++ /dev/null @@ -1,61 +0,0 @@ -language: rust -sudo: required -dist: trusty - -cache: - # cargo: true - apt: true - -matrix: - include: - - rust: stable - - rust: beta - - rust: nightly-2019-11-20 - allow_failures: - - rust: nightly-2019-11-20 - -env: - global: - # - RUSTFLAGS="-C link-dead-code" - - OPENSSL_VERSION=openssl-1.0.2 - -before_install: - - sudo add-apt-repository -y ppa:0k53d-karl-f830m/openssl - - sudo apt-get update -qq - - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev - -before_cache: | - if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-11-20" ]]; then - RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --version 0.6.11 cargo-tarpaulin - fi - -# Add clippy -before_script: - - export PATH=$PATH:~/.cargo/bin - -script: - - cargo update - - cargo check --all --no-default-features - - | - if [[ "$TRAVIS_RUST_VERSION" == "stable" || "$TRAVIS_RUST_VERSION" == "beta" ]]; then - cargo test --all-features --all -- --nocapture - cd actix-http; cargo test --no-default-features --features="rustls" -- --nocapture; cd .. - cd awc; cargo test --no-default-features --features="rustls" -- --nocapture; cd .. - fi - -# Upload docs -after_success: - - | - if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then - cargo doc --no-deps --all-features && - echo "" > target/doc/index.html && - git clone https://github.com/davisp/ghp-import.git && - ./ghp-import/ghp_import.py -n -p -f -m "Documentation upload" -r https://"$GH_TOKEN"@github.com/"$TRAVIS_REPO_SLUG.git" target/doc && - echo "Uploaded documentation" - fi - - | - if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-11-20" ]]; then - taskset -c 0 cargo tarpaulin --out Xml --all --all-features - bash <(curl -s https://codecov.io/bash) - echo "Uploaded code coverage" - fi diff --git a/CHANGES.md b/CHANGES.md index 29f78e0b1..da4a77a80 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -1,10 +1,19 @@ # Changes + ## [2.0.NEXT] - 2020-01-xx +### Added + +* Add helper function for creating routes with `TRACE` method guard `web::trace()` + ### Changed -* Use `sha-1` crate instead of unmaintained `sha1` crate +* Use `sha-1` crate instead of unmaintained `sha1` crate +* Skip empty chunks when returning response from a `Stream` #1308 +* Update the `time` dependency to 0.2.7 +* Update `actix-tls` dependency to 2.0.0-alpha.1 +* Update `rustls` dependency to 0.17 ## [2.0.0] - 2019-12-25 diff --git a/Cargo.toml b/Cargo.toml index 9e1b559eb..0cb0506ca 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -33,8 +33,8 @@ members = [ "actix-cors", "actix-files", "actix-framed", - "actix-session", - "actix-identity", +# "actix-session", +# "actix-identity", "actix-multipart", "actix-web-actors", "actix-web-codegen", @@ -68,10 +68,10 @@ actix-server = "1.0.0" actix-testing = "1.0.0" actix-macros = "0.1.0" actix-threadpool = "0.3.1" -actix-tls = "1.0.0" +actix-tls = "2.0.0-alpha.1" actix-web-codegen = "0.2.0" -actix-http = "1.0.1" +actix-http = "2.0.0-alpha.2" awc = { version = "1.0.1", default-features = false } bytes = "0.5.3" @@ -87,18 +87,19 @@ regex = "1.3" serde = { version = "1.0", features=["derive"] } serde_json = "1.0" serde_urlencoded = "0.6.1" -time = "0.1.42" +time = { version = "0.2.7", default-features = false, features = ["std"] } url = "2.1" open-ssl = { version="0.10", package = "openssl", optional = true } -rust-tls = { version = "0.16.0", package = "rustls", optional = true } +rust-tls = { version = "0.17.0", package = "rustls", optional = true } [dev-dependencies] -actix = "0.9.0" +actix = "0.10.0-alpha.1" rand = "0.7" env_logger = "0.6" serde_derive = "1.0" brotli2 = "0.3.2" flate2 = "1.0.13" +criterion = "0.3" [profile.release] lto = true @@ -116,3 +117,11 @@ actix-session = { path = "actix-session" } actix-files = { path = "actix-files" } actix-multipart = { path = "actix-multipart" } awc = { path = "awc" } + +[[bench]] +name = "server" +harness = false + +[[bench]] +name = "service" +harness = false diff --git a/MIGRATION.md b/MIGRATION.md index 529f9714d..86721e0eb 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -1,3 +1,11 @@ +## Unreleased + +* Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now + result in `SameSite=None` being sent with the response Set-Cookie header. + To create a cookie without a SameSite attribute, remove any calls setting same_site. +* actix-http support for Actors messages was moved to actix-http crate and is enabled + with feature `actors` + ## 2.0.0 * `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 104eb3dfa..c37d023f6 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -19,7 +19,7 @@ path = "src/lib.rs" [dependencies] actix-web = { version = "2.0.0-rc", default-features = false } -actix-http = "1.0.1" +actix-http = "2.0.0-alpha.2" actix-service = "1.0.1" bitflags = "1" bytes = "0.5.3" diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index 49a46e58d..9b30cbaa2 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -5,6 +5,7 @@ use derive_more::Display; #[derive(Display, Debug, PartialEq)] pub enum FilesError { /// Path is not a directory + #[allow(dead_code)] #[display(fmt = "Path is not a directory. Unable to serve static files")] IsNotDirectory, diff --git a/actix-framed/Cargo.toml b/actix-framed/Cargo.toml index 7e322e1d4..4389fe69b 100644 --- a/actix-framed/Cargo.toml +++ b/actix-framed/Cargo.toml @@ -23,7 +23,7 @@ actix-codec = "0.2.0" actix-service = "1.0.1" actix-router = "0.2.1" actix-rt = "1.0.0" -actix-http = "1.0.1" +actix-http = "2.0.0-alpha.2" bytes = "0.5.3" futures = "0.3.1" @@ -32,6 +32,6 @@ log = "0.4" [dev-dependencies] actix-server = "1.0.0" -actix-connect = { version = "1.0.0", features=["openssl"] } +actix-connect = { version = "2.0.0-alpha.1", features=["openssl"] } actix-http-test = { version = "1.0.0", features=["openssl"] } actix-utils = "1.0.3" diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 1c8e4f053..fb1c3a329 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,5 +1,34 @@ # Changes +## [2.0.0-alpha.2] - 2020-03-07 + +### Changed + +* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] + +* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively + to improve download speed for awc when downloading large objects. [#1394] + +* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394] + +* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] + +[#1394]: https://github.com/actix/actix-web/pull/1394 +[#1395]: https://github.com/actix/actix-web/pull/1395 + +## [2.0.0-alpha.1] - 2020-02-27 + +### Changed + +* Update the `time` dependency to 0.2.7. +* Moved actors messages support from actix crate, enabled with feature `actors`. +* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next(). +* MessageBody is not implemented for &'static [u8] anymore. + +### Fixed + +* Allow `SameSite=None` cookies to be sent in a response. + ## [1.0.1] - 2019-12-20 ### Fixed diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 93aaa756e..c8b6d7d7d 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-http" -version = "1.0.1" +version = "2.0.0-alpha.2" authors = ["Nikolay Kim "] description = "Actix http primitives" readme = "README.md" @@ -15,7 +15,7 @@ license = "MIT/Apache-2.0" edition = "2018" [package.metadata.docs.rs] -features = ["openssl", "rustls", "failure", "compress", "secure-cookies"] +features = ["openssl", "rustls", "failure", "compress", "secure-cookies","actors"] [lib] name = "actix_http" @@ -39,20 +39,23 @@ failure = ["fail-ure"] # support for secure cookies secure-cookies = ["ring"] +# support for actix Actor messages +actors = ["actix"] + [dependencies] -actix-service = "1.0.1" +actix-service = "1.0.5" actix-codec = "0.2.0" -actix-connect = "1.0.1" -actix-utils = "1.0.3" +actix-connect = "2.0.0-alpha.1" +actix-utils = "1.0.6" actix-rt = "1.0.0" actix-threadpool = "0.3.1" -actix-tls = { version = "1.0.0", optional = true } +actix-tls = { version = "2.0.0-alpha.1", optional = true } +actix = { version = "0.10.0-alpha.1", optional = true } base64 = "0.11" bitflags = "1.2" bytes = "0.5.3" copyless = "0.1.4" -chrono = "0.4.6" derive_more = "0.99.2" either = "1.5.3" encoding_rs = "0.8" @@ -77,7 +80,7 @@ serde_json = "1.0" sha-1 = "0.8" slab = "0.4" serde_urlencoded = "0.6.1" -time = "0.1.42" +time = { version = "0.2.7", default-features = false, features = ["std"] } # for secure cookie ring = { version = "0.16.9", optional = true } @@ -90,12 +93,17 @@ flate2 = { version = "1.0.13", optional = true } fail-ure = { version = "0.1.5", package="failure", optional = true } [dev-dependencies] -actix-server = "1.0.0" -actix-connect = { version = "1.0.0", features=["openssl"] } +actix-server = "1.0.1" +actix-connect = { version = "2.0.0-alpha.1", features=["openssl"] } actix-http-test = { version = "1.0.0", features=["openssl"] } -actix-tls = { version = "1.0.0", features=["openssl"] } +actix-tls = { version = "2.0.0-alpha.1", features=["openssl"] } +criterion = "0.3" futures = "0.3.1" -env_logger = "0.6" +env_logger = "0.7" serde_derive = "1.0" open-ssl = { version="0.10", package = "openssl" } -rust-tls = { version="0.16", package = "rustls" } +rust-tls = { version="0.17", package = "rustls" } + +[[bench]] +name = "content-length" +harness = false diff --git a/actix-http/README.md b/actix-http/README.md index d75e822ba..9acad3e6d 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -14,19 +14,34 @@ Actix http ```rust // see examples/framed_hello.rs for complete list of used crates. -extern crate actix_http; -use actix_http::{h1, Response, ServiceConfig}; +use std::{env, io}; -fn main() { - Server::new().bind("framed_hello", "127.0.0.1:8080", || { - IntoFramed::new(|| h1::Codec::new(ServiceConfig::default())) // <- create h1 codec - .and_then(TakeItem::new().map_err(|_| ())) // <- read one request - .and_then(|(_req, _framed): (_, Framed<_, _>)| { // <- send response and close conn - SendResponse::send(_framed, Response::Ok().body("Hello world!")) - .map_err(|_| ()) - .map(|_| ()) - }) - }).unwrap().run(); +use actix_http::{HttpService, Response}; +use actix_server::Server; +use futures::future; +use http::header::HeaderValue; +use log::info; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env::set_var("RUST_LOG", "hello_world=info"); + env_logger::init(); + + Server::build() + .bind("hello-world", "127.0.0.1:8080", || { + HttpService::build() + .client_timeout(1000) + .client_disconnect(1000) + .finish(|_req| { + info!("{:?}", _req); + let mut res = Response::Ok(); + res.header("x-head", HeaderValue::from_static("dummy value!")); + future::ok::<_, ()>(res.body("Hello world!")) + }) + .tcp() + })? + .run() + .await } ``` diff --git a/actix-http/benches/content-length.rs b/actix-http/benches/content-length.rs new file mode 100644 index 000000000..b001b3931 --- /dev/null +++ b/actix-http/benches/content-length.rs @@ -0,0 +1,267 @@ +use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; + +use bytes::BytesMut; + +// benchmark sending all requests at the same time +fn bench_write_content_length(c: &mut Criterion) { + let mut group = c.benchmark_group("write_content_length"); + + let sizes = [ + 0, 1, 11, 83, 101, 653, 1001, 6323, 10001, 56329, 100001, 123456, 98724245, + 4294967202, + ]; + + for i in sizes.iter() { + group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _original::write_content_length(i, &mut b) + }) + }); + + group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { + b.iter(|| { + let mut b = BytesMut::with_capacity(35); + _new::write_content_length(i, &mut b) + }) + }); + } + + group.finish(); +} + +criterion_group!(benches, bench_write_content_length); +criterion_main!(benches); + +mod _new { + use bytes::{BufMut, BytesMut}; + + const DIGITS_START: u8 = b'0'; + + /// NOTE: bytes object has to contain enough space + pub fn write_content_length(n: usize, bytes: &mut BytesMut) { + if n == 0 { + bytes.put_slice(b"\r\ncontent-length: 0\r\n"); + return; + } + + bytes.put_slice(b"\r\ncontent-length: "); + + if n < 10 { + bytes.put_u8(DIGITS_START + (n as u8)); + } else if n < 100 { + let n = n as u8; + + let d10 = n / 10; + let d1 = n % 10; + + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 1000 { + let n = n as u16; + + let d100 = (n / 100) as u8; + 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); + } else if n < 10_000 { + let n = n as u16; + + let d1000 = (n / 1000) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 100_000 { + let n = n as u32; + + let d10000 = (n / 10000) as u8; + let d1000 = ((n / 1000) % 10) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d10000); + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 1_000_000 { + let n = n as u32; + + let d100000 = (n / 100000) as u8; + let d10000 = ((n / 10000) % 10) as u8; + let d1000 = ((n / 1000) % 10) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d100000); + bytes.put_u8(DIGITS_START + d10000); + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else { + write_usize(n, bytes); + } + + bytes.put_slice(b"\r\n"); + } + + fn write_usize(n: usize, bytes: &mut BytesMut) { + let mut n = n; + + // 20 chars is max length of a usize (2^64) + // digits will be added to the buffer from lsd to msd + let mut buf = BytesMut::with_capacity(20); + + while n > 9 { + // "pop" the least-significant digit + let lsd = (n % 10) as u8; + + // remove the lsd from n + n = n / 10; + + buf.put_u8(DIGITS_START + lsd); + } + + // put msd to result buffer + bytes.put_u8(DIGITS_START + (n as u8)); + + // put, in reverse (msd to lsd), remaining digits to buffer + for i in (0..buf.len()).rev() { + bytes.put_u8(buf[i]); + } + } +} + +mod _original { + use std::{mem, ptr, slice}; + + use bytes::{BufMut, BytesMut}; + + const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ + 2021222324252627282930313233343536373839\ + 4041424344454647484950515253545556575859\ + 6061626364656667686970717273747576777879\ + 8081828384858687888990919293949596979899"; + + /// NOTE: bytes object has to contain enough space + pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) { + if n < 10 { + let mut buf: [u8; 21] = [ + b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', + b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n', + ]; + buf[18] = (n as u8) + b'0'; + bytes.put_slice(&buf); + } else if n < 100 { + let mut buf: [u8; 22] = [ + b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', + b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n', + ]; + let d1 = n << 1; + unsafe { + ptr::copy_nonoverlapping( + DEC_DIGITS_LUT.as_ptr().add(d1), + buf.as_mut_ptr().offset(18), + 2, + ); + } + bytes.put_slice(&buf); + } else if n < 1000 { + let mut buf: [u8; 23] = [ + b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', + b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r', + b'\n', + ]; + // decode 2 more chars, if > 2 chars + let d1 = (n % 100) << 1; + n /= 100; + unsafe { + ptr::copy_nonoverlapping( + DEC_DIGITS_LUT.as_ptr().add(d1), + buf.as_mut_ptr().offset(19), + 2, + ) + }; + + // decode last 1 + buf[18] = (n as u8) + b'0'; + + bytes.put_slice(&buf); + } else { + bytes.put_slice(b"\r\ncontent-length: "); + convert_usize(n, bytes); + } + } + + pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { + let mut curr: isize = 39; + let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() }; + buf[39] = b'\r'; + buf[40] = b'\n'; + let buf_ptr = buf.as_mut_ptr(); + let lut_ptr = DEC_DIGITS_LUT.as_ptr(); + + // eagerly decode 4 characters at a time + while n >= 10_000 { + let rem = (n % 10_000) as isize; + n /= 10_000; + + let d1 = (rem / 100) << 1; + let d2 = (rem % 100) << 1; + curr -= 4; + unsafe { + ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); + ptr::copy_nonoverlapping( + lut_ptr.offset(d2), + buf_ptr.offset(curr + 2), + 2, + ); + } + } + + // if we reach here numbers are <= 9999, so at most 4 chars long + let mut n = n as isize; // possibly reduce 64bit math + + // decode 2 more chars, if > 2 chars + if n >= 100 { + let d1 = (n % 100) << 1; + n /= 100; + curr -= 2; + unsafe { + ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); + } + } + + // decode last 1 or 2 chars + if n < 10 { + curr -= 1; + unsafe { + *buf_ptr.offset(curr) = (n as u8) + b'0'; + } + } else { + let d1 = n << 1; + curr -= 2; + unsafe { + ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); + } + } + + unsafe { + bytes.extend_from_slice(slice::from_raw_parts( + buf_ptr.offset(curr), + 41 - curr as usize, + )); + } + } +} diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 3d57a472a..b2b88a7ea 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -17,23 +17,18 @@ async fn main() -> io::Result<()> { HttpService::build() .client_timeout(1000) .client_disconnect(1000) - .finish(|mut req: Request| { - async move { - let mut body = BytesMut::new(); - while let Some(item) = req.payload().next().await { - body.extend_from_slice(&item?); - } - - info!("request body: {:?}", body); - Ok::<_, Error>( - Response::Ok() - .header( - "x-head", - HeaderValue::from_static("dummy value!"), - ) - .body(body), - ) + .finish(|mut req: Request| async move { + let mut body = BytesMut::new(); + while let Some(item) = req.payload().next().await { + body.extend_from_slice(&item?); } + + info!("request body: {:?}", body); + Ok::<_, Error>( + Response::Ok() + .header("x-head", HeaderValue::from_static("dummy value!")) + .body(body), + ) }) .tcp() })? diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs index 850f97ee4..c581db604 100644 --- a/actix-http/src/body.rs +++ b/actix-http/src/body.rs @@ -5,6 +5,7 @@ use std::{fmt, mem}; use bytes::{Bytes, BytesMut}; use futures_core::Stream; +use futures_util::ready; use pin_project::{pin_project, project}; use crate::error::Error; @@ -35,33 +36,46 @@ impl BodySize { pub trait MessageBody { fn size(&self) -> BodySize; - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>>; + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>>; + + downcast_get_type_id!(); } +downcast!(MessageBody); + impl MessageBody for () { fn size(&self) -> BodySize { BodySize::Empty } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { Poll::Ready(None) } } -impl MessageBody for Box { +impl MessageBody for Box { fn size(&self) -> BodySize { self.as_ref().size() } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - self.as_mut().poll_next(cx) + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(self.get_mut().as_mut()).poll_next(cx) } } #[pin_project] pub enum ResponseBody { - Body(B), - Other(Body), + Body(#[pin] B), + Other(#[pin] Body), } impl ResponseBody { @@ -97,10 +111,15 @@ impl MessageBody for ResponseBody { } } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - match self { - ResponseBody::Body(ref mut body) => body.poll_next(cx), - ResponseBody::Other(ref mut body) => body.poll_next(cx), + #[project] + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + #[project] + match self.project() { + ResponseBody::Body(body) => body.poll_next(cx), + ResponseBody::Other(body) => body.poll_next(cx), } } } @@ -115,12 +134,13 @@ impl Stream for ResponseBody { ) -> Poll> { #[project] match self.project() { - ResponseBody::Body(ref mut body) => body.poll_next(cx), - ResponseBody::Other(ref mut body) => body.poll_next(cx), + ResponseBody::Body(body) => body.poll_next(cx), + ResponseBody::Other(body) => body.poll_next(cx), } } } +#[pin_project] /// Represents various types of http message body. pub enum Body { /// Empty response. `Content-Length` header is not set. @@ -130,7 +150,7 @@ pub enum Body { /// Specific response body. Bytes(Bytes), /// Generic message body. - Message(Box), + Message(Box), } impl Body { @@ -140,7 +160,7 @@ impl Body { } /// Create body from generic message body. - pub fn from_message(body: B) -> Body { + pub fn from_message(body: B) -> Body { Body::Message(Box::new(body)) } } @@ -155,8 +175,13 @@ impl MessageBody for Body { } } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - match self { + #[project] + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + #[project] + match self.project() { Body::None => Poll::Ready(None), Body::Empty => Poll::Ready(None), Body::Bytes(ref mut bin) => { @@ -167,7 +192,7 @@ impl MessageBody for Body { Poll::Ready(Some(Ok(mem::replace(bin, Bytes::new())))) } } - Body::Message(ref mut body) => body.poll_next(cx), + Body::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx), } } } @@ -253,7 +278,7 @@ impl From for Body { impl From> for Body where - S: Stream> + 'static, + S: Stream> + Unpin + 'static, { fn from(s: SizedStream) -> Body { Body::from_message(s) @@ -262,7 +287,7 @@ where impl From> for Body where - S: Stream> + 'static, + S: Stream> + Unpin + 'static, E: Into + 'static, { fn from(s: BodyStream) -> Body { @@ -275,11 +300,14 @@ impl MessageBody for Bytes { BodySize::Sized(self.len()) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(mem::replace(self, Bytes::new())))) + Poll::Ready(Some(Ok(mem::replace(self.get_mut(), Bytes::new())))) } } } @@ -289,11 +317,16 @@ impl MessageBody for BytesMut { BodySize::Sized(self.len()) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(mem::replace(self, BytesMut::new()).freeze()))) + Poll::Ready(Some(Ok( + mem::replace(self.get_mut(), BytesMut::new()).freeze() + ))) } } } @@ -303,41 +336,36 @@ impl MessageBody for &'static str { BodySize::Sized(self.len()) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { Poll::Ready(Some(Ok(Bytes::from_static( - mem::replace(self, "").as_ref(), + mem::replace(self.get_mut(), "").as_ref(), )))) } } } -impl MessageBody for &'static [u8] { - fn size(&self) -> BodySize { - BodySize::Sized(self.len()) - } - - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(Bytes::from_static(mem::replace(self, b""))))) - } - } -} - impl MessageBody for Vec { fn size(&self) -> BodySize { BodySize::Sized(self.len()) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { - Poll::Ready(Some(Ok(Bytes::from(mem::replace(self, Vec::new()))))) + Poll::Ready(Some(Ok(Bytes::from(mem::replace( + self.get_mut(), + Vec::new(), + ))))) } } } @@ -347,12 +375,15 @@ impl MessageBody for String { BodySize::Sized(self.len()) } - fn poll_next(&mut self, _: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + _: &mut Context<'_>, + ) -> Poll>> { if self.is_empty() { Poll::Ready(None) } else { Poll::Ready(Some(Ok(Bytes::from( - mem::replace(self, String::new()).into_bytes(), + mem::replace(self.get_mut(), String::new()).into_bytes(), )))) } } @@ -361,7 +392,7 @@ impl MessageBody for String { /// Type represent streaming body. /// Response does not contain `content-length` header and appropriate transfer encoding is used. #[pin_project] -pub struct BodyStream { +pub struct BodyStream { #[pin] stream: S, _t: PhantomData, @@ -369,7 +400,7 @@ pub struct BodyStream { impl BodyStream where - S: Stream>, + S: Stream> + Unpin, E: Into, { pub fn new(stream: S) -> Self { @@ -382,26 +413,37 @@ where impl MessageBody for BodyStream where - S: Stream>, + S: Stream> + Unpin, E: Into, { fn size(&self) -> BodySize { BodySize::Stream } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - unsafe { Pin::new_unchecked(self) } - .project() - .stream - .poll_next(cx) - .map(|res| res.map(|res| res.map_err(std::convert::Into::into))) + /// Attempts to pull out the next value of the underlying [`Stream`]. + /// + /// Empty values are skipped to prevent [`BodyStream`]'s transmission being + /// ended on a zero-length chunk, but rather proceed until the underlying + /// [`Stream`] ends. + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut stream = self.project().stream; + loop { + let stream = stream.as_mut(); + return Poll::Ready(match ready!(stream.poll_next(cx)) { + Some(Ok(ref bytes)) if bytes.is_empty() => continue, + opt => opt.map(|res| res.map_err(Into::into)), + }); + } } } /// Type represent streaming body. This body implementation should be used /// if total size of stream is known. Data get sent as is without using transfer encoding. #[pin_project] -pub struct SizedStream { +pub struct SizedStream { size: u64, #[pin] stream: S, @@ -409,7 +451,7 @@ pub struct SizedStream { impl SizedStream where - S: Stream>, + S: Stream> + Unpin, { pub fn new(size: u64, stream: S) -> Self { SizedStream { size, stream } @@ -418,24 +460,38 @@ where impl MessageBody for SizedStream where - S: Stream>, + S: Stream> + Unpin, { fn size(&self) -> BodySize { BodySize::Sized64(self.size) } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - unsafe { Pin::new_unchecked(self) } - .project() - .stream - .poll_next(cx) + /// Attempts to pull out the next value of the underlying [`Stream`]. + /// + /// Empty values are skipped to prevent [`SizedStream`]'s transmission being + /// ended on a zero-length chunk, but rather proceed until the underlying + /// [`Stream`] ends. + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut stream: Pin<&mut S> = self.project().stream; + loop { + let stream = stream.as_mut(); + return Poll::Ready(match ready!(stream.poll_next(cx)) { + Some(Ok(ref bytes)) if bytes.is_empty() => continue, + val => val, + }); + } } } #[cfg(test)] mod tests { use super::*; + use futures::stream; use futures_util::future::poll_fn; + use futures_util::pin_mut; impl Body { pub(crate) fn get_ref(&self) -> &[u8] { @@ -463,7 +519,10 @@ mod tests { assert_eq!("test".size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| "test".poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| Pin::new(&mut "test").poll_next(cx)) + .await + .unwrap() + .ok(), Some(Bytes::from("test")) ); } @@ -477,13 +536,12 @@ mod tests { BodySize::Sized(4) ); assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test"); + let sb = Bytes::from(&b"test"[..]); + pin_mut!(sb); - assert_eq!((&b"test"[..]).size(), BodySize::Sized(4)); + assert_eq!(sb.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| (&b"test"[..]).poll_next(cx)) - .await - .unwrap() - .ok(), + poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } @@ -492,10 +550,12 @@ mod tests { async fn test_vec() { assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4)); assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test"); + let test_vec = Vec::from("test"); + pin_mut!(test_vec); - assert_eq!(Vec::from("test").size(), BodySize::Sized(4)); + assert_eq!(test_vec.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| Vec::from("test").poll_next(cx)) + poll_fn(|cx| test_vec.as_mut().poll_next(cx)) .await .unwrap() .ok(), @@ -505,41 +565,44 @@ mod tests { #[actix_rt::test] async fn test_bytes() { - let mut b = Bytes::from("test"); + let b = Bytes::from("test"); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + pin_mut!(b); assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } #[actix_rt::test] async fn test_bytes_mut() { - let mut b = BytesMut::from("test"); + let b = BytesMut::from("test"); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); + pin_mut!(b); assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } #[actix_rt::test] async fn test_string() { - let mut b = "test".to_owned(); + let b = "test".to_owned(); assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4)); assert_eq!(Body::from(b.clone()).get_ref(), b"test"); assert_eq!(Body::from(&b).size(), BodySize::Sized(4)); assert_eq!(Body::from(&b).get_ref(), b"test"); + pin_mut!(b); assert_eq!(b.size(), BodySize::Sized(4)); assert_eq!( - poll_fn(|cx| b.poll_next(cx)).await.unwrap().ok(), + poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(), Some(Bytes::from("test")) ); } @@ -547,14 +610,17 @@ mod tests { #[actix_rt::test] async fn test_unit() { assert_eq!(().size(), BodySize::Empty); - assert!(poll_fn(|cx| ().poll_next(cx)).await.is_none()); + assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) + .await + .is_none()); } #[actix_rt::test] async fn test_box() { - let mut val = Box::new(()); + let val = Box::new(()); + pin_mut!(val); assert_eq!(val.size(), BodySize::Empty); - assert!(poll_fn(|cx| val.poll_next(cx)).await.is_none()); + assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); } #[actix_rt::test] @@ -589,4 +655,97 @@ mod tests { BodySize::Sized(25) ); } + + mod body_stream { + use super::*; + //use futures::task::noop_waker; + //use futures::stream::once; + + #[actix_rt::test] + async fn skips_empty_chunks() { + let body = BodyStream::new(stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok(Bytes::from(v)) as Result), + )); + pin_mut!(body); + + assert_eq!( + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), + Some(Bytes::from("1")), + ); + assert_eq!( + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), + Some(Bytes::from("2")), + ); + } + + /* Now it does not compile as it should + #[actix_rt::test] + async fn move_pinned_pointer() { + let (sender, receiver) = futures::channel::oneshot::channel(); + let mut body_stream = Ok(BodyStream::new(once(async { + let x = Box::new(0i32); + let y = &x; + receiver.await.unwrap(); + let _z = **y; + Ok::<_, ()>(Bytes::new()) + }))); + + let waker = noop_waker(); + let mut context = Context::from_waker(&waker); + pin_mut!(body_stream); + + let _ = body_stream.as_mut().unwrap().poll_next(&mut context); + sender.send(()).unwrap(); + let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context); + }*/ + } + + mod sized_stream { + use super::*; + + #[actix_rt::test] + async fn skips_empty_chunks() { + let body = SizedStream::new( + 2, + stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))), + ); + pin_mut!(body); + assert_eq!( + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), + Some(Bytes::from("1")), + ); + assert_eq!( + poll_fn(|cx| body.as_mut().poll_next(cx)) + .await + .unwrap() + .ok(), + Some(Bytes::from("2")), + ); + } + } + + #[actix_rt::test] + async fn test_body_casting() { + let mut body = String::from("hello cast"); + let resp_body: &mut dyn MessageBody = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push_str("!"); + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast!"); + let not_body = resp_body.downcast_ref::<()>(); + assert!(not_body.is_none()); + } } diff --git a/actix-http/src/client/config.rs b/actix-http/src/client/config.rs new file mode 100644 index 000000000..c86c697a2 --- /dev/null +++ b/actix-http/src/client/config.rs @@ -0,0 +1,39 @@ +use std::time::Duration; + +// These values are taken from hyper/src/proto/h2/client.rs +const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb +const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb + +/// Connector configuration +#[derive(Clone)] +pub(crate) struct ConnectorConfig { + pub(crate) 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, +} + +impl Default for ConnectorConfig { + fn default() -> Self { + Self { + timeout: Duration::from_secs(1), + 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, + } + } +} + +impl ConnectorConfig { + pub(crate) fn no_disconnect_timeout(&self) -> Self { + let mut res = self.clone(); + res.disconnect_timeout = None; + res + } +} diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs index 055d4276d..e1aed6382 100644 --- a/actix-http/src/client/connector.rs +++ b/actix-http/src/client/connector.rs @@ -11,6 +11,7 @@ use actix_service::{apply_fn, Service}; use actix_utils::timeout::{TimeoutError, TimeoutService}; use http::Uri; +use super::config::ConnectorConfig; use super::connection::Connection; use super::error::ConnectError; use super::pool::{ConnectionPool, Protocol}; @@ -48,11 +49,7 @@ type SslConnector = (); /// ``` pub struct Connector { connector: T, - timeout: Duration, - conn_lifetime: Duration, - conn_keep_alive: Duration, - disconnect_timeout: Duration, - limit: usize, + config: ConnectorConfig, #[allow(dead_code)] ssl: SslConnector, _t: PhantomData, @@ -71,42 +68,47 @@ impl Connector<(), ()> { > + Clone, TcpStream, > { - let ssl = { - #[cfg(feature = "openssl")] - { - use actix_connect::ssl::openssl::SslMethod; - - let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); - let _ = ssl - .set_alpn_protos(b"\x02h2\x08http/1.1") - .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); - SslConnector::Openssl(ssl.build()) - } - #[cfg(all(not(feature = "openssl"), feature = "rustls"))] - { - let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - let mut config = ClientConfig::new(); - config.set_protocols(&protos); - config - .root_store - .add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS); - SslConnector::Rustls(Arc::new(config)) - } - #[cfg(not(any(feature = "openssl", feature = "rustls")))] - {} - }; - Connector { - ssl, + ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), connector: default_connector(), - timeout: Duration::from_secs(1), - conn_lifetime: Duration::from_secs(75), - conn_keep_alive: Duration::from_secs(15), - disconnect_timeout: Duration::from_millis(3000), - limit: 100, + config: ConnectorConfig::default(), _t: PhantomData, } } + + // Build Ssl connector with openssl, based on supplied alpn protocols + #[cfg(feature = "openssl")] + fn build_ssl(protocols: Vec>) -> SslConnector { + use actix_connect::ssl::openssl::SslMethod; + use bytes::{BufMut, BytesMut}; + + let mut alpn = BytesMut::with_capacity(20); + for proto in protocols.iter() { + alpn.put_u8(proto.len() as u8); + alpn.put(proto.as_slice()); + } + + let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap(); + let _ = ssl + .set_alpn_protos(&alpn) + .map_err(|e| error!("Can not set alpn protocol: {:?}", e)); + SslConnector::Openssl(ssl.build()) + } + + // Build Ssl connector with rustls, based on supplied alpn protocols + #[cfg(all(not(feature = "openssl"), feature = "rustls"))] + fn build_ssl(protocols: Vec>) -> SslConnector { + let mut config = ClientConfig::new(); + config.set_protocols(&protocols); + config + .root_store + .add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS); + SslConnector::Rustls(Arc::new(config)) + } + + // ssl turned off, provides empty ssl connector + #[cfg(not(any(feature = "openssl", feature = "rustls")))] + fn build_ssl(_: Vec>) -> SslConnector {} } impl Connector { @@ -122,11 +124,7 @@ impl Connector { { Connector { connector, - timeout: self.timeout, - conn_lifetime: self.conn_lifetime, - conn_keep_alive: self.conn_keep_alive, - disconnect_timeout: self.disconnect_timeout, - limit: self.limit, + config: self.config, ssl: self.ssl, _t: PhantomData, } @@ -146,7 +144,7 @@ where /// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Set to 1 second by default. pub fn timeout(mut self, timeout: Duration) -> Self { - self.timeout = timeout; + self.config.timeout = timeout; self } @@ -163,12 +161,44 @@ where self } + /// Maximum supported http major version + /// Supported versions http/1.1, http/2 + pub fn max_http_version(mut self, val: http::Version) -> Self { + let versions = match val { + http::Version::HTTP_11 => vec![b"http/1.1".to_vec()], + http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()], + _ => { + unimplemented!("actix-http:client: supported versions http/1.1, http/2") + } + }; + self.ssl = Connector::build_ssl(versions); + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 stream-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_window_size(mut self, size: u32) -> Self { + self.config.stream_window_size = size; + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 connection-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_connection_window_size(mut self, size: u32) -> Self { + self.config.conn_window_size = size; + self + } + /// Set total number of simultaneous connections per type of scheme. /// /// If limit is 0, the connector has no limit. /// The default limit size is 100. pub fn limit(mut self, limit: usize) -> Self { - self.limit = limit; + self.config.limit = limit; self } @@ -179,7 +209,7 @@ where /// exceeds this period, the connection is closed. /// Default keep-alive period is 15 seconds. pub fn conn_keep_alive(mut self, dur: Duration) -> Self { - self.conn_keep_alive = dur; + self.config.conn_keep_alive = dur; self } @@ -189,7 +219,7 @@ where /// until it is closed regardless of keep-alive period. /// Default lifetime period is 75 seconds. pub fn conn_lifetime(mut self, dur: Duration) -> Self { - self.conn_lifetime = dur; + self.config.conn_lifetime = dur; self } @@ -202,7 +232,7 @@ where /// /// By default disconnect timeout is set to 3000 milliseconds. pub fn disconnect_timeout(mut self, dur: Duration) -> Self { - self.disconnect_timeout = dur; + self.config.disconnect_timeout = Some(dur); self } @@ -216,7 +246,7 @@ where #[cfg(not(any(feature = "openssl", feature = "rustls")))] { let connector = TimeoutService::new( - self.timeout, + self.config.timeout, apply_fn(self.connector, |msg: Connect, srv| { srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) }) @@ -231,10 +261,7 @@ where connect_impl::InnerConnector { tcp_pool: ConnectionPool::new( connector, - self.conn_lifetime, - self.conn_keep_alive, - None, - self.limit, + self.config.no_disconnect_timeout(), ), } } @@ -248,7 +275,7 @@ where use actix_service::{boxed::service, pipeline}; let ssl_service = TimeoutService::new( - self.timeout, + self.config.timeout, pipeline( apply_fn(self.connector.clone(), |msg: Connect, srv| { srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) @@ -301,7 +328,7 @@ where }); let tcp_service = TimeoutService::new( - self.timeout, + self.config.timeout, apply_fn(self.connector, |msg: Connect, srv| { srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) }) @@ -316,18 +343,9 @@ where connect_impl::InnerConnector { tcp_pool: ConnectionPool::new( tcp_service, - self.conn_lifetime, - self.conn_keep_alive, - None, - self.limit, - ), - ssl_pool: ConnectionPool::new( - ssl_service, - self.conn_lifetime, - self.conn_keep_alive, - Some(self.disconnect_timeout), - self.limit, + self.config.no_disconnect_timeout(), ), + ssl_pool: ConnectionPool::new(ssl_service, self.config), } } } diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs index a0a20edf6..51e853b3d 100644 --- a/actix-http/src/client/h1proto.rs +++ b/actix-http/src/client/h1proto.rs @@ -8,7 +8,7 @@ use bytes::buf::BufMutExt; use bytes::{Bytes, BytesMut}; use futures_core::Stream; use futures_util::future::poll_fn; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{pin_mut, SinkExt, StreamExt}; use crate::error::PayloadError; use crate::h1; @@ -120,7 +120,7 @@ where /// send request body to the peer pub(crate) async fn send_body( - mut body: B, + body: B, framed: &mut Framed, ) -> Result<(), SendRequestError> where @@ -128,9 +128,10 @@ where B: MessageBody, { let mut eof = false; + pin_mut!(body); while !eof { while !eof && !framed.is_write_buf_full() { - match poll_fn(|cx| body.poll_next(cx)).await { + match poll_fn(|cx| body.as_mut().poll_next(cx)).await { Some(result) => { framed.write(h1::Message::Chunk(Some(result?)))?; } diff --git a/actix-http/src/client/h2proto.rs b/actix-http/src/client/h2proto.rs index eabf54e97..2afd2d80b 100644 --- a/actix-http/src/client/h2proto.rs +++ b/actix-http/src/client/h2proto.rs @@ -1,10 +1,15 @@ 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::{client::SendRequest, SendStream}; +use futures_util::pin_mut; +use h2::{ + client::{Builder, Connection, SendRequest}, + SendStream, +}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::{request::Request, Method, Version}; @@ -13,6 +18,7 @@ use crate::header::HeaderMap; use crate::message::{RequestHeadType, ResponseHead}; use crate::payload::Payload; +use super::config::ConnectorConfig; use super::connection::{ConnectionType, IoConnection}; use super::error::SendRequestError; use super::pool::Acquired; @@ -123,13 +129,14 @@ where } async fn send_body( - mut body: B, + body: B, mut send: SendStream, ) -> Result<(), SendRequestError> { let mut buf = None; + pin_mut!(body); loop { if buf.is_none() { - match poll_fn(|cx| body.poll_next(cx)).await { + match poll_fn(|cx| body.as_mut().poll_next(cx)).await { Some(Ok(b)) => { send.reserve_capacity(b.len()); buf = Some(b); @@ -183,3 +190,18 @@ fn release( } } } + +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 + .initial_window_size(config.stream_window_size) + .initial_connection_window_size(config.conn_window_size) + .enable_push(false); + builder.handshake(io) +} diff --git a/actix-http/src/client/mod.rs b/actix-http/src/client/mod.rs index a45aebcd5..dd1e9b25a 100644 --- a/actix-http/src/client/mod.rs +++ b/actix-http/src/client/mod.rs @@ -1,6 +1,7 @@ //! Http client api use http::Uri; +mod config; mod connection; mod connector; mod error; diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs index acf76559a..983396f92 100644 --- a/actix-http/src/client/pool.rs +++ b/actix-http/src/client/pool.rs @@ -13,13 +13,16 @@ use actix_utils::{oneshot, task::LocalWaker}; use bytes::Bytes; use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture}; use fxhash::FxHashMap; -use h2::client::{handshake, Connection, SendRequest}; +use h2::client::{Connection, SendRequest}; use http::uri::Authority; use indexmap::IndexSet; +use pin_project::pin_project; use slab::Slab; +use super::config::ConnectorConfig; use super::connection::{ConnectionType, IoConnection}; use super::error::ConnectError; +use super::h2proto::handshake; use super::Connect; #[derive(Clone, Copy, PartialEq)] @@ -49,20 +52,11 @@ where T: Service + 'static, { - pub(crate) fn new( - connector: T, - conn_lifetime: Duration, - conn_keep_alive: Duration, - disconnect_timeout: Option, - limit: usize, - ) -> Self { + pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self { ConnectionPool( Rc::new(RefCell::new(connector)), Rc::new(RefCell::new(Inner { - conn_lifetime, - conn_keep_alive, - disconnect_timeout, - limit, + config, acquired: 0, waiters: Slab::new(), waiters_queue: IndexSet::new(), @@ -128,6 +122,8 @@ where // open tcp connection let (io, proto) = connector.call(req).await?; + let config = inner.borrow().config.clone(); + let guard = OpenGuard::new(key, inner); if proto == Protocol::Http1 { @@ -137,7 +133,7 @@ where Some(guard.consume()), )) } else { - let (snd, connection) = handshake(io).await?; + let (snd, connection) = handshake(io, &config).await?; actix_rt::spawn(connection.map(|_| ())); Ok(IoConnection::new( ConnectionType::H2(snd), @@ -254,10 +250,7 @@ struct AvailableConnection { } pub(crate) struct Inner { - conn_lifetime: Duration, - conn_keep_alive: Duration, - disconnect_timeout: Option, - limit: usize, + config: ConnectorConfig, acquired: usize, available: FxHashMap>>, waiters: Slab< @@ -310,7 +303,7 @@ where fn acquire(&mut self, key: &Key, cx: &mut Context<'_>) -> Acquire { // check limits - if self.limit > 0 && self.acquired >= self.limit { + if self.config.limit > 0 && self.acquired >= self.config.limit { return Acquire::NotAvailable; } @@ -322,10 +315,10 @@ where let now = Instant::now(); while let Some(conn) = connections.pop_back() { // check if it still usable - if (now - conn.used) > self.conn_keep_alive - || (now - conn.created) > self.conn_lifetime + if (now - conn.used) > self.config.conn_keep_alive + || (now - conn.created) > self.config.conn_lifetime { - if let Some(timeout) = self.disconnect_timeout { + if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionType::H1(io) = conn.io { actix_rt::spawn(CloseConnection::new(io, timeout)) } @@ -337,7 +330,7 @@ where match Pin::new(s).poll_read(cx, &mut buf) { Poll::Pending => (), Poll::Ready(Ok(n)) if n > 0 => { - if let Some(timeout) = self.disconnect_timeout { + if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionType::H1(io) = io { actix_rt::spawn(CloseConnection::new( io, timeout, @@ -371,7 +364,7 @@ where fn release_close(&mut self, io: ConnectionType) { self.acquired -= 1; - if let Some(timeout) = self.disconnect_timeout { + if let Some(timeout) = self.config.disconnect_timeout { if let ConnectionType::H1(io) = io { actix_rt::spawn(CloseConnection::new(io, timeout)) } @@ -380,7 +373,7 @@ where } fn check_availibility(&self) { - if !self.waiters_queue.is_empty() && self.acquired < self.limit { + if !self.waiters_queue.is_empty() && self.acquired < self.config.limit { self.waker.wake(); } } @@ -422,6 +415,7 @@ where } } +#[pin_project] struct ConnectorPoolSupport where Io: AsyncRead + AsyncWrite + Unpin + 'static, @@ -439,7 +433,7 @@ where type Output = (); fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = unsafe { self.get_unchecked_mut() }; + let this = self.project(); let mut inner = this.inner.as_ref().borrow_mut(); inner.waker.register(cx.waker()); @@ -478,6 +472,7 @@ where tx, this.inner.clone(), this.connector.call(connect), + inner.config.clone(), ); } } @@ -488,10 +483,12 @@ where } } +#[pin_project::pin_project(PinnedDrop)] struct OpenWaitingConnection where Io: AsyncRead + AsyncWrite + Unpin + 'static, { + #[pin] fut: F, key: Key, h2: Option< @@ -502,6 +499,7 @@ where >, rx: Option, ConnectError>>>, inner: Option>>>, + config: ConnectorConfig, } impl OpenWaitingConnection @@ -514,6 +512,7 @@ where rx: oneshot::Sender, ConnectError>>, inner: Rc>>, fut: F, + config: ConnectorConfig, ) { actix_rt::spawn(OpenWaitingConnection { key, @@ -521,16 +520,18 @@ where h2: None, rx: Some(rx), inner: Some(inner), + config, }) } } -impl Drop for OpenWaitingConnection +#[pin_project::pinned_drop] +impl PinnedDrop for OpenWaitingConnection where Io: AsyncRead + AsyncWrite + Unpin + 'static, { - fn drop(&mut self) { - if let Some(inner) = self.inner.take() { + fn drop(self: Pin<&mut Self>) { + if let Some(inner) = self.project().inner.take() { let mut inner = inner.as_ref().borrow_mut(); inner.release(); inner.check_availibility(); @@ -545,8 +546,8 @@ where { type Output = (); - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = unsafe { self.get_unchecked_mut() }; + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.as_mut().project(); if let Some(ref mut h2) = this.h2 { return match Pin::new(h2).poll(cx) { @@ -571,7 +572,7 @@ where }; } - match unsafe { Pin::new_unchecked(&mut this.fut) }.poll(cx) { + match this.fut.poll(cx) { Poll::Ready(Err(err)) => { let _ = this.inner.take(); if let Some(rx) = this.rx.take() { @@ -589,8 +590,8 @@ where ))); Poll::Ready(()) } else { - this.h2 = Some(handshake(io).boxed_local()); - unsafe { Pin::new_unchecked(this) }.poll(cx) + *this.h2 = Some(handshake(io, this.config).boxed_local()); + self.poll(cx) } } Poll::Pending => Poll::Pending, diff --git a/actix-http/src/cloneable.rs b/actix-http/src/cloneable.rs index 65c6bec21..b64c299fc 100644 --- a/actix-http/src/cloneable.rs +++ b/actix-http/src/cloneable.rs @@ -1,4 +1,4 @@ -use std::cell::UnsafeCell; +use std::cell::RefCell; use std::rc::Rc; use std::task::{Context, Poll}; @@ -6,11 +6,15 @@ use actix_service::Service; #[doc(hidden)] /// Service that allows to turn non-clone service to a service with `Clone` impl -pub(crate) struct CloneableService(Rc>); +/// +/// # Panics +/// CloneableService might panic with some creative use of thread local storage. +/// See https://github.com/actix/actix-web/issues/1295 for example +pub(crate) struct CloneableService(Rc>); impl CloneableService { pub(crate) fn new(service: T) -> Self { - Self(Rc::new(UnsafeCell::new(service))) + Self(Rc::new(RefCell::new(service))) } } @@ -27,10 +31,10 @@ impl Service for CloneableService { type Future = T::Future; fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - unsafe { &mut *self.0.as_ref().get() }.poll_ready(cx) + self.0.borrow_mut().poll_ready(cx) } fn call(&mut self, req: T::Request) -> Self::Future { - unsafe { &mut *self.0.as_ref().get() }.call(req) + self.0.borrow_mut().call(req) } } diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index be949aaef..899046231 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -1,4 +1,4 @@ -use std::cell::UnsafeCell; +use std::cell::Cell; use std::fmt::Write; use std::rc::Rc; use std::time::Duration; @@ -7,7 +7,7 @@ use std::{fmt, net}; use actix_rt::time::{delay_for, delay_until, Delay, Instant}; use bytes::BytesMut; use futures_util::{future, FutureExt}; -use time; +use time::OffsetDateTime; // "Sun, 06 Nov 1994 08:49:37 GMT".len() const DATE_VALUE_LENGTH: usize = 29; @@ -211,7 +211,12 @@ impl Date { } fn update(&mut self) { self.pos = 0; - write!(self, "{}", time::at_utc(time::get_time()).rfc822()).unwrap(); + write!( + self, + "{}", + OffsetDateTime::now().format("%a, %d %b %Y %H:%M:%S GMT") + ) + .unwrap(); } } @@ -228,24 +233,24 @@ impl fmt::Write for Date { struct DateService(Rc); struct DateServiceInner { - current: UnsafeCell>, + current: Cell>, } impl DateServiceInner { fn new() -> Self { DateServiceInner { - current: UnsafeCell::new(None), + current: Cell::new(None), } } fn reset(&self) { - unsafe { (&mut *self.current.get()).take() }; + self.current.take(); } fn update(&self) { let now = Instant::now(); let date = Date::new(); - *(unsafe { &mut *self.current.get() }) = Some((date, now)); + self.current.set(Some((date, now))); } } @@ -255,7 +260,7 @@ impl DateService { } fn check_date(&self) { - if unsafe { (&*self.0.current.get()).is_none() } { + if self.0.current.get().is_none() { self.0.update(); // periodic date update @@ -269,12 +274,12 @@ impl DateService { fn now(&self) -> Instant { self.check_date(); - unsafe { (&*self.0.current.get()).as_ref().unwrap().1 } + self.0.current.get().unwrap().1 } fn set_date(&self, mut f: F) { self.check_date(); - f(&unsafe { (&*self.0.current.get()).as_ref().unwrap().0 }) + f(&self.0.current.get().unwrap().0); } } @@ -282,6 +287,16 @@ impl DateService { mod tests { use super::*; + // Test modifying the date from within the closure + // passed to `set_date` + #[test] + fn test_evil_date() { + let service = DateService::new(); + // Make sure that `check_date` doesn't try to spawn a task + service.0.update(); + service.set_date(|_| service.0.reset()); + } + #[test] fn test_date_len() { assert_eq!(DATE_VALUE_LENGTH, "Sun, 06 Nov 1994 08:49:37 GMT".len()); diff --git a/actix-http/src/cookie/builder.rs b/actix-http/src/cookie/builder.rs index f99d02b02..80e7ee71f 100644 --- a/actix-http/src/cookie/builder.rs +++ b/actix-http/src/cookie/builder.rs @@ -1,7 +1,6 @@ use std::borrow::Cow; -use chrono::Duration; -use time::Tm; +use time::{Duration, OffsetDateTime}; use super::{Cookie, SameSite}; @@ -64,13 +63,13 @@ impl CookieBuilder { /// use actix_http::cookie::Cookie; /// /// let c = Cookie::build("foo", "bar") - /// .expires(time::now()) + /// .expires(time::OffsetDateTime::now()) /// .finish(); /// /// assert!(c.expires().is_some()); /// ``` #[inline] - pub fn expires(mut self, when: Tm) -> CookieBuilder { + pub fn expires(mut self, when: OffsetDateTime) -> CookieBuilder { self.cookie.set_expires(when); self } @@ -108,7 +107,10 @@ impl CookieBuilder { /// ``` #[inline] pub fn max_age_time(mut self, value: Duration) -> CookieBuilder { - self.cookie.set_max_age(value); + // Truncate any nanoseconds from the Duration, as they aren't represented within `Max-Age` + // and would cause two otherwise identical `Cookie` instances to not be equivalent to one another. + self.cookie + .set_max_age(Duration::seconds(value.whole_seconds())); self } @@ -212,7 +214,7 @@ impl CookieBuilder { /// /// ```rust /// use actix_http::cookie::Cookie; - /// use chrono::Duration; + /// use time::Duration; /// /// let c = Cookie::build("foo", "bar") /// .permanent() diff --git a/actix-http/src/cookie/draft.rs b/actix-http/src/cookie/draft.rs index a2b039121..a6525a605 100644 --- a/actix-http/src/cookie/draft.rs +++ b/actix-http/src/cookie/draft.rs @@ -10,18 +10,26 @@ use std::fmt; /// attribute is "Strict", then the cookie is never sent in cross-site requests. /// If the `SameSite` attribute is "Lax", the cookie is only sent in cross-site /// requests with "safe" HTTP methods, i.e, `GET`, `HEAD`, `OPTIONS`, `TRACE`. -/// If the `SameSite` attribute is not present (made explicit via the -/// `SameSite::None` variant), then the cookie will be sent as normal. +/// If the `SameSite` attribute is not present then the cookie will be sent as +/// normal. In some browsers, this will implicitly handle the cookie as if "Lax" +/// and in others, "None". It's best to explicitly set the `SameSite` attribute +/// to avoid inconsistent behavior. +/// +/// **Note:** Depending on browser, the `Secure` attribute may be required for +/// `SameSite` "None" cookies to be accepted. /// /// **Note:** This cookie attribute is an HTTP draft! Its meaning and definition /// are subject to change. +/// +/// More info about these draft changes can be found in the draft spec: +/// - https://tools.ietf.org/html/draft-west-cookie-incrementalism-00 #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub enum SameSite { /// The "Strict" `SameSite` attribute. Strict, /// The "Lax" `SameSite` attribute. Lax, - /// No `SameSite` attribute. + /// The "None" `SameSite` attribute. None, } @@ -92,7 +100,7 @@ impl fmt::Display for SameSite { match *self { SameSite::Strict => write!(f, "Strict"), SameSite::Lax => write!(f, "Lax"), - SameSite::None => Ok(()), + SameSite::None => write!(f, "None"), } } } diff --git a/actix-http/src/cookie/jar.rs b/actix-http/src/cookie/jar.rs index dc2de4dfe..dd4ec477e 100644 --- a/actix-http/src/cookie/jar.rs +++ b/actix-http/src/cookie/jar.rs @@ -1,7 +1,7 @@ use std::collections::HashSet; use std::mem::replace; -use chrono::Duration; +use time::{Duration, OffsetDateTime}; use super::delta::DeltaCookie; use super::Cookie; @@ -188,7 +188,7 @@ impl CookieJar { /// /// ```rust /// use actix_http::cookie::{CookieJar, Cookie}; - /// use chrono::Duration; + /// use time::Duration; /// /// let mut jar = CookieJar::new(); /// @@ -202,7 +202,7 @@ impl CookieJar { /// let delta: Vec<_> = jar.delta().collect(); /// assert_eq!(delta.len(), 1); /// assert_eq!(delta[0].name(), "name"); - /// assert_eq!(delta[0].max_age(), Some(Duration::seconds(0))); + /// assert_eq!(delta[0].max_age(), Some(Duration::zero())); /// ``` /// /// Removing a new cookie does not result in a _removal_ cookie: @@ -220,8 +220,8 @@ impl CookieJar { pub fn remove(&mut self, mut cookie: Cookie<'static>) { if self.original_cookies.contains(cookie.name()) { cookie.set_value(""); - cookie.set_max_age(Duration::seconds(0)); - cookie.set_expires(time::now() - Duration::days(365)); + cookie.set_max_age(Duration::zero()); + cookie.set_expires(OffsetDateTime::now() - Duration::days(365)); self.delta_cookies.replace(DeltaCookie::removed(cookie)); } else { self.delta_cookies.remove(cookie.name()); @@ -239,7 +239,7 @@ impl CookieJar { /// /// ```rust /// use actix_http::cookie::{CookieJar, Cookie}; - /// use chrono::Duration; + /// use time::Duration; /// /// let mut jar = CookieJar::new(); /// @@ -533,8 +533,8 @@ mod test { #[test] #[cfg(feature = "secure-cookies")] fn delta() { - use chrono::Duration; use std::collections::HashMap; + use time::Duration; let mut c = CookieJar::new(); @@ -556,7 +556,7 @@ mod test { assert!(names.get("test2").unwrap().is_none()); assert!(names.get("test3").unwrap().is_none()); assert!(names.get("test4").unwrap().is_none()); - assert_eq!(names.get("original").unwrap(), &Some(Duration::seconds(0))); + assert_eq!(names.get("original").unwrap(), &Some(Duration::zero())); } #[test] diff --git a/actix-http/src/cookie/mod.rs b/actix-http/src/cookie/mod.rs index 13fd5cf4e..7f74abc95 100644 --- a/actix-http/src/cookie/mod.rs +++ b/actix-http/src/cookie/mod.rs @@ -47,7 +47,7 @@ //! ``` #![doc(html_root_url = "https://docs.rs/cookie/0.11")] -#![deny(missing_docs)] +#![warn(missing_docs)] mod builder; mod delta; @@ -65,9 +65,8 @@ use std::borrow::Cow; use std::fmt; use std::str::FromStr; -use chrono::Duration; use percent_encoding::{percent_encode, AsciiSet, CONTROLS}; -use time::Tm; +use time::{Duration, OffsetDateTime}; pub use self::builder::CookieBuilder; pub use self::draft::*; @@ -172,7 +171,7 @@ pub struct Cookie<'c> { /// The cookie's value. value: CookieStr, /// The cookie's expiration, if any. - expires: Option, + expires: Option, /// The cookie's maximum age, if any. max_age: Option, /// The cookie's domain, if any. @@ -479,7 +478,7 @@ impl<'c> Cookie<'c> { /// assert_eq!(c.max_age(), None); /// /// let c = Cookie::parse("name=value; Max-Age=3600").unwrap(); - /// assert_eq!(c.max_age().map(|age| age.num_hours()), Some(1)); + /// assert_eq!(c.max_age().map(|age| age.whole_hours()), Some(1)); /// ``` #[inline] pub fn max_age(&self) -> Option { @@ -544,10 +543,10 @@ impl<'c> Cookie<'c> { /// let expire_time = "Wed, 21 Oct 2017 07:28:00 GMT"; /// let cookie_str = format!("name=value; Expires={}", expire_time); /// let c = Cookie::parse(cookie_str).unwrap(); - /// assert_eq!(c.expires().map(|t| t.tm_year), Some(117)); + /// assert_eq!(c.expires().map(|t| t.year()), Some(2017)); /// ``` #[inline] - pub fn expires(&self) -> Option { + pub fn expires(&self) -> Option { self.expires } @@ -645,7 +644,7 @@ impl<'c> Cookie<'c> { /// /// ```rust /// use actix_http::cookie::Cookie; - /// use chrono::Duration; + /// use time::Duration; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.max_age(), None); @@ -698,18 +697,19 @@ impl<'c> Cookie<'c> { /// /// ```rust /// use actix_http::cookie::Cookie; + /// use time::{Duration, OffsetDateTime}; /// /// let mut c = Cookie::new("name", "value"); /// assert_eq!(c.expires(), None); /// - /// let mut now = time::now(); - /// now.tm_year += 1; + /// let mut now = OffsetDateTime::now(); + /// now += Duration::week(); /// /// c.set_expires(now); /// assert!(c.expires().is_some()) /// ``` #[inline] - pub fn set_expires(&mut self, time: Tm) { + pub fn set_expires(&mut self, time: OffsetDateTime) { self.expires = Some(time); } @@ -720,7 +720,7 @@ impl<'c> Cookie<'c> { /// /// ```rust /// use actix_http::cookie::Cookie; - /// use chrono::Duration; + /// use time::Duration; /// /// let mut c = Cookie::new("foo", "bar"); /// assert!(c.expires().is_none()); @@ -733,7 +733,7 @@ impl<'c> Cookie<'c> { pub fn make_permanent(&mut self) { let twenty_years = Duration::days(365 * 20); self.set_max_age(twenty_years); - self.set_expires(time::now() + twenty_years); + self.set_expires(OffsetDateTime::now() + twenty_years); } fn fmt_parameters(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -746,9 +746,7 @@ impl<'c> Cookie<'c> { } if let Some(same_site) = self.same_site() { - if !same_site.is_none() { - write!(f, "; SameSite={}", same_site)?; - } + write!(f, "; SameSite={}", same_site)?; } if let Some(path) = self.path() { @@ -760,11 +758,11 @@ impl<'c> Cookie<'c> { } if let Some(max_age) = self.max_age() { - write!(f, "; Max-Age={}", max_age.num_seconds())?; + write!(f, "; Max-Age={}", max_age.whole_seconds())?; } if let Some(time) = self.expires() { - write!(f, "; Expires={}", time.rfc822())?; + write!(f, "; Expires={}", time.format("%a, %d %b %Y %H:%M:%S GMT"))?; } Ok(()) @@ -992,7 +990,7 @@ impl<'a, 'b> PartialEq> for Cookie<'a> { #[cfg(test)] mod tests { use super::{Cookie, SameSite}; - use time::strptime; + use time::PrimitiveDateTime; #[test] fn format() { @@ -1017,7 +1015,9 @@ mod tests { assert_eq!(&cookie.to_string(), "foo=bar; Domain=www.rust-lang.org"); let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); + let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S") + .unwrap() + .assume_utc(); let cookie = Cookie::build("foo", "bar").expires(expires).finish(); assert_eq!( &cookie.to_string(), @@ -1037,7 +1037,7 @@ mod tests { let cookie = Cookie::build("foo", "bar") .same_site(SameSite::None) .finish(); - assert_eq!(&cookie.to_string(), "foo=bar"); + assert_eq!(&cookie.to_string(), "foo=bar; SameSite=None"); } #[test] diff --git a/actix-http/src/cookie/parse.rs b/actix-http/src/cookie/parse.rs index 20aee9507..ce261c758 100644 --- a/actix-http/src/cookie/parse.rs +++ b/actix-http/src/cookie/parse.rs @@ -5,11 +5,13 @@ use std::error::Error; use std::fmt; use std::str::Utf8Error; -use chrono::Duration; use percent_encoding::percent_decode; +use time::Duration; use super::{Cookie, CookieStr, SameSite}; +use crate::time_parser; + /// Enum corresponding to a parsing error. #[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum ParseError { @@ -147,7 +149,7 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { Ok(val) => { // Don't panic if the max age seconds is greater than what's supported by // `Duration`. - let val = cmp::min(val, Duration::max_value().num_seconds()); + let val = cmp::min(val, Duration::max_value().whole_seconds()); Some(Duration::seconds(val)) } Err(_) => continue, @@ -179,16 +181,14 @@ fn parse_inner<'c>(s: &str, decode: bool) -> Result, ParseError> { } } ("expires", Some(v)) => { - // Try strptime with three date formats according to + // Try parsing with three date formats according to // http://tools.ietf.org/html/rfc2616#section-3.3.1. Try // additional ones as encountered in the real world. - let tm = time::strptime(v, "%a, %d %b %Y %H:%M:%S %Z") - .or_else(|_| time::strptime(v, "%A, %d-%b-%y %H:%M:%S %Z")) - .or_else(|_| time::strptime(v, "%a, %d-%b-%Y %H:%M:%S %Z")) - .or_else(|_| time::strptime(v, "%a %b %d %H:%M:%S %Y")); + let tm = time_parser::parse_http_date(v) + .or_else(|| time::parse(v, "%a, %d-%b-%Y %H:%M:%S").ok()); - if let Ok(time) = tm { - cookie.expires = Some(time) + if let Some(time) = tm { + cookie.expires = Some(time.assume_utc()) } } _ => { @@ -216,8 +216,7 @@ where #[cfg(test)] mod tests { use super::{Cookie, SameSite}; - use chrono::Duration; - use time::strptime; + use time::{Duration, PrimitiveDateTime}; macro_rules! assert_eq_parse { ($string:expr, $expected:expr) => { @@ -377,7 +376,9 @@ mod tests { ); let time_str = "Wed, 21 Oct 2015 07:28:00 GMT"; - let expires = strptime(time_str, "%a, %d %b %Y %H:%M:%S %Z").unwrap(); + let expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%M:%S") + .unwrap() + .assume_utc(); expected.set_expires(expires); assert_eq_parse!( " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ @@ -386,7 +387,9 @@ mod tests { ); unexpected.set_domain("foo.com"); - let bad_expires = strptime(time_str, "%a, %d %b %Y %H:%S:%M %Z").unwrap(); + let bad_expires = PrimitiveDateTime::parse(time_str, "%a, %d %b %Y %H:%S:%M") + .unwrap() + .assume_utc(); expected.set_expires(bad_expires); assert_ne_parse!( " foo=bar ;HttpOnly; Secure; Max-Age=4; Path=/foo; \ @@ -414,8 +417,16 @@ mod tests { #[test] fn do_not_panic_on_large_max_ages() { - let max_seconds = Duration::max_value().num_seconds(); - let expected = Cookie::build("foo", "bar").max_age(max_seconds).finish(); - assert_eq_parse!(format!(" foo=bar; Max-Age={:?}", max_seconds + 1), expected); + let max_duration = Duration::max_value(); + let expected = Cookie::build("foo", "bar") + .max_age_time(max_duration) + .finish(); + let overflow_duration = max_duration + .checked_add(Duration::nanoseconds(1)) + .unwrap_or(max_duration); + assert_eq_parse!( + format!(" foo=bar; Max-Age={:?}", overflow_duration.whole_seconds()), + expected + ); } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index ca04845ab..72bb7d603 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -9,6 +9,7 @@ use brotli2::write::BrotliEncoder; use bytes::Bytes; use flate2::write::{GzEncoder, ZlibEncoder}; use futures_core::ready; +use pin_project::{pin_project, project}; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; @@ -19,8 +20,10 @@ use super::Writer; const INPLACE: usize = 1024; +#[pin_project] pub struct Encoder { eof: bool, + #[pin] body: EncoderBody, encoder: Option, fut: Option>, @@ -76,67 +79,88 @@ impl Encoder { } } +#[pin_project] enum EncoderBody { Bytes(Bytes), - Stream(B), - BoxedStream(Box), + Stream(#[pin] B), + BoxedStream(Box), +} + +impl MessageBody for EncoderBody { + fn size(&self) -> BodySize { + match self { + EncoderBody::Bytes(ref b) => b.size(), + EncoderBody::Stream(ref b) => b.size(), + EncoderBody::BoxedStream(ref b) => b.size(), + } + } + + #[project] + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + #[project] + match self.project() { + EncoderBody::Bytes(b) => { + if b.is_empty() { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(std::mem::replace(b, Bytes::new())))) + } + } + EncoderBody::Stream(b) => b.poll_next(cx), + EncoderBody::BoxedStream(ref mut b) => Pin::new(b.as_mut()).poll_next(cx), + } + } } impl MessageBody for Encoder { fn size(&self) -> BodySize { if self.encoder.is_none() { - match self.body { - EncoderBody::Bytes(ref b) => b.size(), - EncoderBody::Stream(ref b) => b.size(), - EncoderBody::BoxedStream(ref b) => b.size(), - } + self.body.size() } else { BodySize::Stream } } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let mut this = self.project(); loop { - if self.eof { + if *this.eof { return Poll::Ready(None); } - if let Some(ref mut fut) = self.fut { + if let Some(ref mut fut) = this.fut { let mut encoder = match ready!(Pin::new(fut).poll(cx)) { Ok(item) => item, Err(e) => return Poll::Ready(Some(Err(e.into()))), }; let chunk = encoder.take(); - self.encoder = Some(encoder); - self.fut.take(); + *this.encoder = Some(encoder); + this.fut.take(); if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } } - let result = match self.body { - EncoderBody::Bytes(ref mut b) => { - if b.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(std::mem::replace(b, Bytes::new())))) - } - } - EncoderBody::Stream(ref mut b) => b.poll_next(cx), - EncoderBody::BoxedStream(ref mut b) => b.poll_next(cx), - }; + let result = this.body.as_mut().poll_next(cx); + match result { Poll::Ready(Some(Ok(chunk))) => { - if let Some(mut encoder) = self.encoder.take() { + if let Some(mut encoder) = this.encoder.take() { if chunk.len() < INPLACE { encoder.write(&chunk)?; let chunk = encoder.take(); - self.encoder = Some(encoder); + *this.encoder = Some(encoder); if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } } else { - self.fut = Some(run(move || { + *this.fut = Some(run(move || { encoder.write(&chunk)?; Ok(encoder) })); @@ -146,12 +170,12 @@ impl MessageBody for Encoder { } } Poll::Ready(None) => { - if let Some(encoder) = self.encoder.take() { + if let Some(encoder) = this.encoder.take() { let chunk = encoder.finish()?; if chunk.is_empty() { return Poll::Ready(None); } else { - self.eof = true; + *this.eof = true; return Poll::Ready(Some(Ok(chunk))); } } else { diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index fd0fe927f..0850e18ff 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,5 +1,4 @@ //! Error and Result module -use std::any::TypeId; use std::cell::RefCell; use std::io::Write; use std::str::Utf8Error; @@ -15,7 +14,6 @@ use derive_more::{Display, From}; pub use futures_channel::oneshot::Canceled; use http::uri::InvalidUri; use http::{header, Error as HttpError, StatusCode}; -use httparse; use serde::de::value::Error as DeError; use serde_json::error::Error as JsonError; use serde_urlencoded::ser::Error as FormError; @@ -83,25 +81,10 @@ pub trait ResponseError: fmt::Debug + fmt::Display { resp.set_body(Body::from(buf)) } - #[doc(hidden)] - fn __private_get_type_id__(&self) -> TypeId - where - Self: 'static, - { - TypeId::of::() - } + downcast_get_type_id!(); } -impl dyn ResponseError + 'static { - /// Downcasts a response error to a specific type. - pub fn downcast_ref(&self) -> Option<&T> { - if self.__private_get_type_id__() == TypeId::of::() { - unsafe { Some(&*(self as *const dyn ResponseError as *const T)) } - } else { - None - } - } -} +downcast!(ResponseError); impl fmt::Display for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -967,6 +950,16 @@ where /// Compatibility for `failure::Error` impl ResponseError for fail_ure::Error {} +#[cfg(feature = "actors")] +/// `InternalServerError` for `actix::MailboxError` +/// This is supported on feature=`actors` only +impl ResponseError for actix::MailboxError {} + +#[cfg(feature = "actors")] +/// `InternalServerError` for `actix::ResolverError` +/// This is supported on feature=`actors` only +impl ResponseError for actix::actors::resolver::ResolverError {} + #[cfg(test)] mod tests { use super::*; diff --git a/actix-http/src/extensions.rs b/actix-http/src/extensions.rs index d85ca184d..5114ce140 100644 --- a/actix-http/src/extensions.rs +++ b/actix-http/src/extensions.rs @@ -28,33 +28,30 @@ impl Extensions { /// Check if container contains entry pub fn contains(&self) -> bool { - self.map.get(&TypeId::of::()).is_some() + self.map.contains_key(&TypeId::of::()) } /// Get a reference to a type previously inserted on this `Extensions`. pub fn get(&self) -> Option<&T> { self.map .get(&TypeId::of::()) - .and_then(|boxed| (&**boxed as &(dyn Any + 'static)).downcast_ref()) + .and_then(|boxed| boxed.downcast_ref()) } /// Get a mutable reference to a type previously inserted on this `Extensions`. pub fn get_mut(&mut self) -> Option<&mut T> { self.map .get_mut(&TypeId::of::()) - .and_then(|boxed| (&mut **boxed as &mut (dyn Any + 'static)).downcast_mut()) + .and_then(|boxed| boxed.downcast_mut()) } /// Remove a type from this `Extensions`. /// /// If a extension of this type existed, it will be returned. pub fn remove(&mut self) -> Option { - self.map.remove(&TypeId::of::()).and_then(|boxed| { - (boxed as Box) - .downcast() - .ok() - .map(|boxed| *boxed) - }) + self.map + .remove(&TypeId::of::()) + .and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed)) } /// Clear the `Extensions` of all inserted extensions. @@ -70,6 +67,92 @@ impl fmt::Debug for Extensions { } } +#[test] +fn test_remove() { + let mut map = Extensions::new(); + + map.insert::(123); + assert!(map.get::().is_some()); + + map.remove::(); + assert!(map.get::().is_none()); +} + +#[test] +fn test_clear() { + let mut map = Extensions::new(); + + map.insert::(8); + map.insert::(16); + map.insert::(32); + + assert!(map.contains::()); + assert!(map.contains::()); + assert!(map.contains::()); + + map.clear(); + + assert!(!map.contains::()); + assert!(!map.contains::()); + assert!(!map.contains::()); + + map.insert::(10); + assert_eq!(*map.get::().unwrap(), 10); +} + +#[test] +fn test_integers() { + let mut map = Extensions::new(); + + map.insert::(8); + map.insert::(16); + map.insert::(32); + map.insert::(64); + map.insert::(128); + map.insert::(8); + map.insert::(16); + map.insert::(32); + map.insert::(64); + map.insert::(128); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); + assert!(map.get::().is_some()); +} + +#[test] +fn test_composition() { + struct Magi(pub T); + + struct Madoka { + pub god: bool, + } + + struct Homura { + pub attempts: usize, + } + + struct Mami { + pub guns: usize, + } + + let mut map = Extensions::new(); + + map.insert(Magi(Madoka { god: false })); + map.insert(Magi(Homura { attempts: 0 })); + map.insert(Magi(Mami { guns: 999 })); + + assert!(!map.get::>().unwrap().0.god); + assert_eq!(0, map.get::>().unwrap().0.attempts); + assert_eq!(999, map.get::>().unwrap().0.guns); +} + #[test] fn test_extensions() { #[derive(Debug, PartialEq)] diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index e113fd52d..d3ccd8e5a 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -8,7 +8,6 @@ use actix_codec::Decoder; use bytes::{Buf, Bytes, BytesMut}; use http::header::{HeaderName, HeaderValue}; use http::{header, Method, StatusCode, Uri, Version}; -use httparse; use log::{debug, error, trace}; use crate::error::ParseError; diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 6f4c09915..ec01261e5 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1,3 +1,6 @@ +// Because MSRV is 1.39.0. +#![allow(clippy::mem_replace_with_default)] + use std::collections::VecDeque; use std::future::Future; use std::pin::Pin; @@ -10,6 +13,7 @@ use actix_service::Service; use bitflags::bitflags; use bytes::{Buf, BytesMut}; use log::{error, trace}; +use pin_project::pin_project; use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::cloneable::CloneableService; @@ -41,6 +45,7 @@ bitflags! { } } +#[pin_project::pin_project] /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where @@ -52,9 +57,11 @@ where U: Service), Response = ()>, U::Error: fmt::Display, { + #[pin] inner: DispatcherState, } +#[pin_project] enum DispatcherState where S: Service, @@ -65,11 +72,11 @@ where U: Service), Response = ()>, U::Error: fmt::Display, { - Normal(InnerDispatcher), - Upgrade(U::Future), - None, + Normal(#[pin] InnerDispatcher), + Upgrade(Pin>), } +#[pin_project] struct InnerDispatcher where S: Service, @@ -88,6 +95,7 @@ where peer_addr: Option, error: Option, + #[pin] state: State, payload: Option, messages: VecDeque, @@ -95,7 +103,7 @@ where ka_expire: Instant, ka_timer: Option, - io: T, + io: Option, read_buf: BytesMut, write_buf: BytesMut, codec: Codec, @@ -107,6 +115,7 @@ enum DispatcherMessage { Error(Response<()>), } +#[pin_project] enum State where S: Service, @@ -114,9 +123,9 @@ where B: MessageBody, { None, - ExpectCall(X::Future), - ServiceCall(S::Future), - SendPayload(ResponseBody), + ExpectCall(Pin>), + ServiceCall(Pin>), + SendPayload(#[pin] ResponseBody), } impl State @@ -141,7 +150,6 @@ where } } } - enum PollResponse { Upgrade(Request), DoNothing, @@ -236,7 +244,7 @@ where state: State::None, error: None, messages: VecDeque::new(), - io, + io: Some(io), codec, read_buf, service, @@ -278,10 +286,11 @@ where } // if checked is set to true, delay disconnect until all tasks have finished. - fn client_disconnected(&mut self) { - self.flags + fn client_disconnected(self: Pin<&mut Self>) { + let this = self.project(); + this.flags .insert(Flags::READ_DISCONNECT | Flags::WRITE_DISCONNECT); - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } } @@ -290,17 +299,22 @@ where /// /// true - got whouldblock /// false - didnt get whouldblock - fn poll_flush(&mut self, cx: &mut Context<'_>) -> Result { + #[pin_project::project] + fn poll_flush( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result { if self.write_buf.is_empty() { return Ok(false); } let len = self.write_buf.len(); let mut written = 0; + #[project] + let InnerDispatcher { io, write_buf, .. } = self.project(); + let mut io = Pin::new(io.as_mut().unwrap()); while written < len { - match unsafe { Pin::new_unchecked(&mut self.io) } - .poll_write(cx, &self.write_buf[written..]) - { + match io.as_mut().poll_write(cx, &write_buf[written..]) { Poll::Ready(Ok(0)) => { return Err(DispatchError::Io(io::Error::new( io::ErrorKind::WriteZero, @@ -312,112 +326,119 @@ where } Poll::Pending => { if written > 0 { - self.write_buf.advance(written); + write_buf.advance(written); } return Ok(true); } Poll::Ready(Err(err)) => return Err(DispatchError::Io(err)), } } - if written == self.write_buf.len() { - unsafe { self.write_buf.set_len(0) } + if written == write_buf.len() { + unsafe { write_buf.set_len(0) } } else { - self.write_buf.advance(written); + write_buf.advance(written); } Ok(false) } fn send_response( - &mut self, + self: Pin<&mut Self>, message: Response<()>, body: ResponseBody, ) -> Result, DispatchError> { - self.codec - .encode(Message::Item((message, body.size())), &mut self.write_buf) + let mut this = self.project(); + this.codec + .encode(Message::Item((message, body.size())), &mut this.write_buf) .map_err(|err| { - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } DispatchError::Io(err) })?; - self.flags.set(Flags::KEEPALIVE, self.codec.keepalive()); + this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); match body.size() { BodySize::None | BodySize::Empty => Ok(State::None), _ => Ok(State::SendPayload(body)), } } - fn send_continue(&mut self) { - self.write_buf + fn send_continue(self: Pin<&mut Self>) { + self.project() + .write_buf .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); } + #[pin_project::project] fn poll_response( - &mut self, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { loop { - let state = match self.state { - State::None => match self.messages.pop_front() { + let mut this = self.as_mut().project(); + #[project] + let state = match this.state.project() { + State::None => match this.messages.pop_front() { Some(DispatcherMessage::Item(req)) => { - Some(self.handle_request(req, cx)?) - } - Some(DispatcherMessage::Error(res)) => { - Some(self.send_response(res, ResponseBody::Other(Body::Empty))?) + Some(self.as_mut().handle_request(req, cx)?) } + Some(DispatcherMessage::Error(res)) => Some( + self.as_mut() + .send_response(res, ResponseBody::Other(Body::Empty))?, + ), Some(DispatcherMessage::Upgrade(req)) => { return Ok(PollResponse::Upgrade(req)); } None => None, }, - State::ExpectCall(ref mut fut) => { - match unsafe { Pin::new_unchecked(fut) }.poll(cx) { - Poll::Ready(Ok(req)) => { - self.send_continue(); - self.state = State::ServiceCall(self.service.call(req)); - continue; - } - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); - Some(self.send_response(res, body.into_body())?) - } - Poll::Pending => None, + State::ExpectCall(fut) => match fut.as_mut().poll(cx) { + Poll::Ready(Ok(req)) => { + self.as_mut().send_continue(); + this = self.as_mut().project(); + this.state + .set(State::ServiceCall(Box::pin(this.service.call(req)))); + continue; } - } - State::ServiceCall(ref mut fut) => { - match unsafe { Pin::new_unchecked(fut) }.poll(cx) { - Poll::Ready(Ok(res)) => { - let (res, body) = res.into().replace_body(()); - self.state = self.send_response(res, body)?; - continue; - } - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); - Some(self.send_response(res, body.into_body())?) - } - Poll::Pending => None, + Poll::Ready(Err(e)) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + Some(self.as_mut().send_response(res, body.into_body())?) } - } - State::SendPayload(ref mut stream) => { + Poll::Pending => None, + }, + State::ServiceCall(fut) => match fut.as_mut().poll(cx) { + Poll::Ready(Ok(res)) => { + let (res, body) = res.into().replace_body(()); + let state = self.as_mut().send_response(res, body)?; + this = self.as_mut().project(); + this.state.set(state); + continue; + } + Poll::Ready(Err(e)) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + Some(self.as_mut().send_response(res, body.into_body())?) + } + Poll::Pending => None, + }, + State::SendPayload(mut stream) => { loop { - if self.write_buf.len() < HW_BUFFER_SIZE { - match stream.poll_next(cx) { + if this.write_buf.len() < HW_BUFFER_SIZE { + match stream.as_mut().poll_next(cx) { Poll::Ready(Some(Ok(item))) => { - self.codec.encode( + this.codec.encode( Message::Chunk(Some(item)), - &mut self.write_buf, + &mut this.write_buf, )?; continue; } Poll::Ready(None) => { - self.codec.encode( + this.codec.encode( Message::Chunk(None), - &mut self.write_buf, + &mut this.write_buf, )?; - self.state = State::None; + this = self.as_mut().project(); + this.state.set(State::None); } Poll::Ready(Some(Err(_))) => { return Err(DispatchError::Unknown) @@ -433,9 +454,11 @@ where } }; + this = self.as_mut().project(); + // set new state if let Some(state) = state { - self.state = state; + this.state.set(state); if !self.state.is_empty() { continue; } @@ -443,7 +466,7 @@ where // if read-backpressure is enabled and we consumed some data. // we may read more data and retry if self.state.is_call() { - if self.poll_request(cx)? { + if self.as_mut().poll_request(cx)? { continue; } } else if !self.messages.is_empty() { @@ -457,16 +480,16 @@ where } fn handle_request( - &mut self, + mut self: Pin<&mut Self>, req: Request, cx: &mut Context<'_>, ) -> Result, DispatchError> { // Handle `EXPECT: 100-Continue` header let req = if req.head().expect() { - let mut task = self.expect.call(req); - match unsafe { Pin::new_unchecked(&mut task) }.poll(cx) { + let mut task = Box::pin(self.as_mut().project().expect.call(req)); + match task.as_mut().poll(cx) { Poll::Ready(Ok(req)) => { - self.send_continue(); + self.as_mut().send_continue(); req } Poll::Pending => return Ok(State::ExpectCall(task)), @@ -482,8 +505,8 @@ where }; // Call service - let mut task = self.service.call(req); - match unsafe { Pin::new_unchecked(&mut task) }.poll(cx) { + let mut task = Box::pin(self.as_mut().project().service.call(req)); + match task.as_mut().poll(cx) { Poll::Ready(Ok(res)) => { let (res, body) = res.into().replace_body(()); self.send_response(res, body) @@ -499,7 +522,7 @@ where /// Process one incoming requests pub(self) fn poll_request( - &mut self, + mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { // limit a mount of non processed requests @@ -508,24 +531,25 @@ where } let mut updated = false; + let mut this = self.as_mut().project(); loop { - match self.codec.decode(&mut self.read_buf) { + match this.codec.decode(&mut this.read_buf) { Ok(Some(msg)) => { updated = true; - self.flags.insert(Flags::STARTED); + this.flags.insert(Flags::STARTED); match msg { Message::Item(mut req) => { - let pl = self.codec.message_type(); - req.head_mut().peer_addr = self.peer_addr; + let pl = this.codec.message_type(); + req.head_mut().peer_addr = *this.peer_addr; // set on_connect data - if let Some(ref on_connect) = self.on_connect { + if let Some(ref on_connect) = this.on_connect { on_connect.set(&mut req.extensions_mut()); } - if pl == MessageType::Stream && self.upgrade.is_some() { - self.messages.push_back(DispatcherMessage::Upgrade(req)); + if pl == MessageType::Stream && this.upgrade.is_some() { + this.messages.push_back(DispatcherMessage::Upgrade(req)); break; } if pl == MessageType::Payload || pl == MessageType::Stream { @@ -533,41 +557,43 @@ where let (req1, _) = req.replace_payload(crate::Payload::H1(pl)); req = req1; - self.payload = Some(ps); + *this.payload = Some(ps); } // handle request early - if self.state.is_empty() { - self.state = self.handle_request(req, cx)?; + if this.state.is_empty() { + let state = self.as_mut().handle_request(req, cx)?; + this = self.as_mut().project(); + this.state.set(state); } else { - self.messages.push_back(DispatcherMessage::Item(req)); + this.messages.push_back(DispatcherMessage::Item(req)); } } Message::Chunk(Some(chunk)) => { - if let Some(ref mut payload) = self.payload { + if let Some(ref mut payload) = this.payload { payload.feed_data(chunk); } else { error!( "Internal server error: unexpected payload chunk" ); - self.flags.insert(Flags::READ_DISCONNECT); - self.messages.push_back(DispatcherMessage::Error( + this.flags.insert(Flags::READ_DISCONNECT); + this.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish().drop_body(), )); - self.error = Some(DispatchError::InternalError); + *this.error = Some(DispatchError::InternalError); break; } } Message::Chunk(None) => { - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.feed_eof(); } else { error!("Internal server error: unexpected eof"); - self.flags.insert(Flags::READ_DISCONNECT); - self.messages.push_back(DispatcherMessage::Error( + this.flags.insert(Flags::READ_DISCONNECT); + this.messages.push_back(DispatcherMessage::Error( Response::InternalServerError().finish().drop_body(), )); - self.error = Some(DispatchError::InternalError); + *this.error = Some(DispatchError::InternalError); break; } } @@ -575,44 +601,49 @@ where } Ok(None) => break, Err(ParseError::Io(e)) => { - self.client_disconnected(); - self.error = Some(DispatchError::Io(e)); + self.as_mut().client_disconnected(); + this = self.as_mut().project(); + *this.error = Some(DispatchError::Io(e)); break; } Err(e) => { - if let Some(mut payload) = self.payload.take() { + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::EncodingCorrupted); } // Malformed requests should be responded with 400 - self.messages.push_back(DispatcherMessage::Error( + this.messages.push_back(DispatcherMessage::Error( Response::BadRequest().finish().drop_body(), )); - self.flags.insert(Flags::READ_DISCONNECT); - self.error = Some(e.into()); + this.flags.insert(Flags::READ_DISCONNECT); + *this.error = Some(e.into()); break; } } } - if updated && self.ka_timer.is_some() { - if let Some(expire) = self.codec.config().keep_alive_expire() { - self.ka_expire = expire; + if updated && this.ka_timer.is_some() { + if let Some(expire) = this.codec.config().keep_alive_expire() { + *this.ka_expire = expire; } } Ok(updated) } /// keep-alive timer - fn poll_keepalive(&mut self, cx: &mut Context<'_>) -> Result<(), DispatchError> { - if self.ka_timer.is_none() { + fn poll_keepalive( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result<(), DispatchError> { + let mut this = self.as_mut().project(); + if this.ka_timer.is_none() { // shutdown timeout - if self.flags.contains(Flags::SHUTDOWN) { - if let Some(interval) = self.codec.config().client_disconnect_timer() { - self.ka_timer = Some(delay_until(interval)); + if this.flags.contains(Flags::SHUTDOWN) { + if let Some(interval) = this.codec.config().client_disconnect_timer() { + *this.ka_timer = Some(delay_until(interval)); } else { - self.flags.insert(Flags::READ_DISCONNECT); - if let Some(mut payload) = self.payload.take() { + this.flags.insert(Flags::READ_DISCONNECT); + if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); } return Ok(()); @@ -622,55 +653,56 @@ where } } - match Pin::new(&mut self.ka_timer.as_mut().unwrap()).poll(cx) { + match Pin::new(&mut this.ka_timer.as_mut().unwrap()).poll(cx) { Poll::Ready(()) => { // if we get timeout during shutdown, drop connection - if self.flags.contains(Flags::SHUTDOWN) { + if this.flags.contains(Flags::SHUTDOWN) { return Err(DispatchError::DisconnectTimeout); - } else if self.ka_timer.as_mut().unwrap().deadline() >= self.ka_expire { + } else if this.ka_timer.as_mut().unwrap().deadline() >= *this.ka_expire { // check for any outstanding tasks - if self.state.is_empty() && self.write_buf.is_empty() { - if self.flags.contains(Flags::STARTED) { + if this.state.is_empty() && this.write_buf.is_empty() { + if this.flags.contains(Flags::STARTED) { trace!("Keep-alive timeout, close connection"); - self.flags.insert(Flags::SHUTDOWN); + this.flags.insert(Flags::SHUTDOWN); // start shutdown timer if let Some(deadline) = - self.codec.config().client_disconnect_timer() + this.codec.config().client_disconnect_timer() { - if let Some(mut timer) = self.ka_timer.as_mut() { + if let Some(mut timer) = this.ka_timer.as_mut() { timer.reset(deadline); let _ = Pin::new(&mut timer).poll(cx); } } else { // no shutdown timeout, drop socket - self.flags.insert(Flags::WRITE_DISCONNECT); + this.flags.insert(Flags::WRITE_DISCONNECT); return Ok(()); } } else { // timeout on first request (slow request) return 408 - if !self.flags.contains(Flags::STARTED) { + if !this.flags.contains(Flags::STARTED) { trace!("Slow request timeout"); - let _ = self.send_response( + let _ = self.as_mut().send_response( Response::RequestTimeout().finish().drop_body(), ResponseBody::Other(Body::Empty), ); + this = self.as_mut().project(); } else { trace!("Keep-alive connection timeout"); } - self.flags.insert(Flags::STARTED | Flags::SHUTDOWN); - self.state = State::None; + this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); + this.state.set(State::None); } } else if let Some(deadline) = - self.codec.config().keep_alive_expire() + this.codec.config().keep_alive_expire() { - if let Some(mut timer) = self.ka_timer.as_mut() { + if let Some(mut timer) = this.ka_timer.as_mut() { timer.reset(deadline); let _ = Pin::new(&mut timer).poll(cx); } } - } else if let Some(mut timer) = self.ka_timer.as_mut() { - timer.reset(self.ka_expire); + } else if let Some(mut timer) = this.ka_timer.as_mut() { + timer.reset(*this.ka_expire); let _ = Pin::new(&mut timer).poll(cx); } } @@ -681,20 +713,6 @@ where } } -impl Unpin for Dispatcher -where - T: AsyncRead + AsyncWrite + Unpin, - S: Service, - S::Error: Into, - S::Response: Into>, - B: MessageBody, - X: Service, - X::Error: Into, - U: Service), Response = ()>, - U::Error: fmt::Display, -{ -} - impl Future for Dispatcher where T: AsyncRead + AsyncWrite + Unpin, @@ -709,22 +727,29 @@ where { type Output = Result<(), DispatchError>; + #[pin_project::project] #[inline] fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - match self.as_mut().inner { - DispatcherState::Normal(ref mut inner) => { - inner.poll_keepalive(cx)?; + let this = self.as_mut().project(); + #[project] + match this.inner.project() { + DispatcherState::Normal(mut inner) => { + inner.as_mut().poll_keepalive(cx)?; if inner.flags.contains(Flags::SHUTDOWN) { if inner.flags.contains(Flags::WRITE_DISCONNECT) { Poll::Ready(Ok(())) } else { // flush buffer - inner.poll_flush(cx)?; - if !inner.write_buf.is_empty() { + inner.as_mut().poll_flush(cx)?; + if !inner.write_buf.is_empty() || inner.io.is_none() { Poll::Pending } else { - match Pin::new(&mut inner.io).poll_shutdown(cx) { + match Pin::new(inner.project().io) + .as_pin_mut() + .unwrap() + .poll_shutdown(cx) + { Poll::Ready(res) => { Poll::Ready(res.map_err(DispatchError::from)) } @@ -736,53 +761,61 @@ where // read socket into a buf let should_disconnect = if !inner.flags.contains(Flags::READ_DISCONNECT) { - read_available(cx, &mut inner.io, &mut inner.read_buf)? + let mut inner_p = inner.as_mut().project(); + read_available( + cx, + inner_p.io.as_mut().unwrap(), + &mut inner_p.read_buf, + )? } else { None }; - inner.poll_request(cx)?; + inner.as_mut().poll_request(cx)?; if let Some(true) = should_disconnect { - inner.flags.insert(Flags::READ_DISCONNECT); - if let Some(mut payload) = inner.payload.take() { + let inner_p = inner.as_mut().project(); + inner_p.flags.insert(Flags::READ_DISCONNECT); + if let Some(mut payload) = inner_p.payload.take() { payload.feed_eof(); } }; loop { + let inner_p = inner.as_mut().project(); let remaining = - inner.write_buf.capacity() - inner.write_buf.len(); + inner_p.write_buf.capacity() - inner_p.write_buf.len(); if remaining < LW_BUFFER_SIZE { - inner.write_buf.reserve(HW_BUFFER_SIZE - remaining); + inner_p.write_buf.reserve(HW_BUFFER_SIZE - remaining); } - let result = inner.poll_response(cx)?; + let result = inner.as_mut().poll_response(cx)?; let drain = result == PollResponse::DrainWriteBuf; // switch to upgrade handler if let PollResponse::Upgrade(req) = result { - if let DispatcherState::Normal(inner) = - std::mem::replace(&mut self.inner, DispatcherState::None) - { - let mut parts = FramedParts::with_read_buf( - inner.io, - inner.codec, - inner.read_buf, - ); - parts.write_buf = inner.write_buf; - let framed = Framed::from_parts(parts); - self.inner = DispatcherState::Upgrade( - inner.upgrade.unwrap().call((req, framed)), - ); - return self.poll(cx); - } else { - panic!() - } + let inner_p = inner.as_mut().project(); + let mut parts = FramedParts::with_read_buf( + inner_p.io.take().unwrap(), + std::mem::replace(inner_p.codec, Codec::default()), + std::mem::replace(inner_p.read_buf, BytesMut::default()), + ); + parts.write_buf = std::mem::replace( + inner_p.write_buf, + BytesMut::default(), + ); + let framed = Framed::from_parts(parts); + let upgrade = + inner_p.upgrade.take().unwrap().call((req, framed)); + self.as_mut() + .project() + .inner + .set(DispatcherState::Upgrade(Box::pin(upgrade))); + return self.poll(cx); } // we didnt get WouldBlock from write operation, // so data get written to kernel completely (OSX) // and we have to write again otherwise response can get stuck - if inner.poll_flush(cx)? || !drain { + if inner.as_mut().poll_flush(cx)? || !drain { break; } } @@ -794,25 +827,26 @@ where let is_empty = inner.state.is_empty(); + let inner_p = inner.as_mut().project(); // read half is closed and we do not processing any responses - if inner.flags.contains(Flags::READ_DISCONNECT) && is_empty { - inner.flags.insert(Flags::SHUTDOWN); + if inner_p.flags.contains(Flags::READ_DISCONNECT) && is_empty { + inner_p.flags.insert(Flags::SHUTDOWN); } // keep-alive and stream errors - if is_empty && inner.write_buf.is_empty() { - if let Some(err) = inner.error.take() { + if is_empty && inner_p.write_buf.is_empty() { + if let Some(err) = inner_p.error.take() { Poll::Ready(Err(err)) } // disconnect if keep-alive is not enabled - else if inner.flags.contains(Flags::STARTED) - && !inner.flags.intersects(Flags::KEEPALIVE) + else if inner_p.flags.contains(Flags::STARTED) + && !inner_p.flags.intersects(Flags::KEEPALIVE) { - inner.flags.insert(Flags::SHUTDOWN); + inner_p.flags.insert(Flags::SHUTDOWN); self.poll(cx) } // disconnect if shutdown - else if inner.flags.contains(Flags::SHUTDOWN) { + else if inner_p.flags.contains(Flags::SHUTDOWN) { self.poll(cx) } else { Poll::Pending @@ -822,13 +856,10 @@ where } } } - DispatcherState::Upgrade(ref mut fut) => { - unsafe { Pin::new_unchecked(fut) }.poll(cx).map_err(|e| { - error!("Upgrade handler error: {}", e); - DispatchError::Upgrade - }) - } - DispatcherState::None => panic!(), + DispatcherState::Upgrade(fut) => fut.as_mut().poll(cx).map_err(|e| { + error!("Upgrade handler error: {}", e); + DispatchError::Upgrade + }), } } } @@ -918,9 +949,12 @@ mod tests { Poll::Ready(res) => assert!(res.is_err()), } - if let DispatcherState::Normal(ref inner) = h1.inner { + if let DispatcherState::Normal(ref mut inner) = h1.inner { assert!(inner.flags.contains(Flags::READ_DISCONNECT)); - assert_eq!(&inner.io.write_buf[..26], b"HTTP/1.1 400 Bad Request\r\n"); + assert_eq!( + &inner.io.take().unwrap().write_buf[..26], + b"HTTP/1.1 400 Bad Request\r\n" + ); } }) .await; diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 9ba4aa053..c44925c7a 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -13,6 +13,7 @@ use crate::response::Response; #[pin_project::pin_project] pub struct SendResponse { res: Option, BodySize)>>, + #[pin] body: Option>, framed: Option>, } @@ -35,24 +36,27 @@ where impl Future for SendResponse where T: AsyncRead + AsyncWrite, - B: MessageBody, + B: MessageBody + Unpin, { type Output = Result, Error>; + // TODO: rethink if we need loops in polls fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); + let mut this = self.project(); + let mut body_done = this.body.is_none(); loop { - let mut body_ready = this.body.is_some(); + let mut body_ready = !body_done; let framed = this.framed.as_mut().unwrap(); // send body - if this.res.is_none() && this.body.is_some() { - while body_ready && this.body.is_some() && !framed.is_write_buf_full() { - match this.body.as_mut().unwrap().poll_next(cx)? { + if this.res.is_none() && body_ready { + while body_ready && !body_done && !framed.is_write_buf_full() { + match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { Poll::Ready(item) => { - // body is done - if item.is_none() { + // body is done when item is None + body_done = item.is_none(); + if body_done { let _ = this.body.take(); } framed.write(Message::Chunk(item))?; @@ -82,7 +86,7 @@ where continue; } - if this.body.is_some() { + if !body_done { if body_ready { continue; } else { diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index a4ec15fab..b07764a03 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -158,15 +158,17 @@ where #[pin_project::pin_project] struct ServiceResponse { + #[pin] state: ServiceResponseState, config: ServiceConfig, buffer: Option, _t: PhantomData<(I, E)>, } +#[pin_project::pin_project] enum ServiceResponseState { - ServiceCall(F, Option>), - SendPayload(SendStream, ResponseBody), + ServiceCall(#[pin] F, Option>), + SendPayload(SendStream, #[pin] ResponseBody), } impl ServiceResponse @@ -247,68 +249,66 @@ where { type Output = (); + #[pin_project::project] fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let mut this = self.as_mut().project(); - match this.state { - ServiceResponseState::ServiceCall(ref mut call, ref mut send) => { - match unsafe { Pin::new_unchecked(call) }.poll(cx) { - Poll::Ready(Ok(res)) => { - let (res, body) = res.into().replace_body(()); + #[project] + match this.state.project() { + ServiceResponseState::ServiceCall(call, send) => match call.poll(cx) { + Poll::Ready(Ok(res)) => { + let (res, body) = res.into().replace_body(()); - let mut send = send.take().unwrap(); - let mut size = body.size(); - let h2_res = - self.as_mut().prepare_response(res.head(), &mut size); - this = self.as_mut().project(); + let mut send = send.take().unwrap(); + let mut size = body.size(); + let h2_res = self.as_mut().prepare_response(res.head(), &mut size); + this = self.as_mut().project(); - let stream = match send.send_response(h2_res, size.is_eof()) { - Err(e) => { - trace!("Error sending h2 response: {:?}", e); - return Poll::Ready(()); - } - Ok(stream) => stream, - }; - - if size.is_eof() { - Poll::Ready(()) - } else { - *this.state = - ServiceResponseState::SendPayload(stream, body); - self.poll(cx) + let stream = match send.send_response(h2_res, size.is_eof()) { + Err(e) => { + trace!("Error sending h2 response: {:?}", e); + return Poll::Ready(()); } - } - Poll::Pending => Poll::Pending, - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); + Ok(stream) => stream, + }; - let mut send = send.take().unwrap(); - let mut size = body.size(); - let h2_res = - self.as_mut().prepare_response(res.head(), &mut size); - this = self.as_mut().project(); - - let stream = match send.send_response(h2_res, size.is_eof()) { - Err(e) => { - trace!("Error sending h2 response: {:?}", e); - return Poll::Ready(()); - } - Ok(stream) => stream, - }; - - if size.is_eof() { - Poll::Ready(()) - } else { - *this.state = ServiceResponseState::SendPayload( - stream, - body.into_body(), - ); - self.poll(cx) - } + if size.is_eof() { + Poll::Ready(()) + } else { + this.state + .set(ServiceResponseState::SendPayload(stream, body)); + self.poll(cx) } } - } + Poll::Pending => Poll::Pending, + Poll::Ready(Err(e)) => { + let res: Response = e.into().into(); + let (res, body) = res.replace_body(()); + + let mut send = send.take().unwrap(); + let mut size = body.size(); + let h2_res = self.as_mut().prepare_response(res.head(), &mut size); + this = self.as_mut().project(); + + let stream = match send.send_response(h2_res, size.is_eof()) { + Err(e) => { + trace!("Error sending h2 response: {:?}", e); + return Poll::Ready(()); + } + Ok(stream) => stream, + }; + + if size.is_eof() { + Poll::Ready(()) + } else { + this.state.set(ServiceResponseState::SendPayload( + stream, + body.into_body(), + )); + self.poll(cx) + } + } + }, ServiceResponseState::SendPayload(ref mut stream, ref mut body) => loop { loop { if let Some(ref mut buffer) = this.buffer { @@ -335,7 +335,7 @@ where } } } else { - match body.poll_next(cx) { + match body.as_mut().poll_next(cx) { Poll::Pending => return Poll::Pending, Poll::Ready(None) => { if let Err(e) = stream.send_data(Bytes::new(), true) { diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index ff3f69faf..eef5dd02c 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -83,13 +83,11 @@ where Error = DispatchError, InitError = S::InitError, > { - pipeline_factory(fn_factory(|| { - async { - Ok::<_, S::InitError>(fn_service(|io: TcpStream| { - let peer_addr = io.peer_addr().ok(); - ok::<_, DispatchError>((io, peer_addr)) - })) - } + pipeline_factory(fn_factory(|| async { + Ok::<_, S::InitError>(fn_service(|io: TcpStream| { + let peer_addr = io.peer_addr().ok(); + ok::<_, DispatchError>((io, peer_addr)) + })) })) .and_then(self) } diff --git a/actix-http/src/header/common/accept_charset.rs b/actix-http/src/header/common/accept_charset.rs index 117e2015d..291ca53b6 100644 --- a/actix-http/src/header/common/accept_charset.rs +++ b/actix-http/src/header/common/accept_charset.rs @@ -63,7 +63,7 @@ header! { (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ test_accept_charset { - /// Test case from RFC + // Test case from RFC test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/actix-http/src/header/common/content_disposition.rs b/actix-http/src/header/common/content_disposition.rs index d0d5af765..aa2e00ec0 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/actix-http/src/header/common/content_disposition.rs @@ -423,7 +423,7 @@ impl ContentDisposition { /// Return the value of *name* if exists. pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).nth(0) + self.parameters.iter().filter_map(|p| p.as_name()).next() } /// Return the value of *filename* if exists. @@ -431,7 +431,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_filename()) - .nth(0) + .next() } /// Return the value of *filename\** if exists. @@ -439,7 +439,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_filename_ext()) - .nth(0) + .next() } /// Return the value of the parameter which the `name` matches. @@ -448,7 +448,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_unknown(name)) - .nth(0) + .next() } /// Return the value of the extended parameter which the `name` matches. @@ -457,7 +457,7 @@ impl ContentDisposition { self.parameters .iter() .filter_map(|p| p.as_unknown_ext(name)) - .nth(0) + .next() } } diff --git a/actix-http/src/header/shared/httpdate.rs b/actix-http/src/header/shared/httpdate.rs index 28d6a25ec..81caf6d53 100644 --- a/actix-http/src/header/shared/httpdate.rs +++ b/actix-http/src/header/shared/httpdate.rs @@ -1,59 +1,46 @@ use std::fmt::{self, Display}; use std::io::Write; use std::str::FromStr; -use std::time::{Duration, SystemTime, UNIX_EPOCH}; +use std::time::{SystemTime, UNIX_EPOCH}; use bytes::{buf::BufMutExt, BytesMut}; use http::header::{HeaderValue, InvalidHeaderValue}; +use time::{offset, OffsetDateTime, PrimitiveDateTime}; use crate::error::ParseError; use crate::header::IntoHeaderValue; +use crate::time_parser; /// A timestamp with HTTP formatting and parsing #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] -pub struct HttpDate(time::Tm); +pub struct HttpDate(OffsetDateTime); impl FromStr for HttpDate { type Err = ParseError; fn from_str(s: &str) -> Result { - match time::strptime(s, "%a, %d %b %Y %T %Z") - .or_else(|_| time::strptime(s, "%A, %d-%b-%y %T %Z")) - .or_else(|_| time::strptime(s, "%c")) - { - Ok(t) => Ok(HttpDate(t)), - Err(_) => Err(ParseError::Header), + match time_parser::parse_http_date(s) { + Some(t) => Ok(HttpDate(t.assume_utc())), + None => Err(ParseError::Header), } } } impl Display for HttpDate { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0.to_utc().rfc822(), f) + fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f) } } -impl From for HttpDate { - fn from(tm: time::Tm) -> HttpDate { - HttpDate(tm) +impl From for HttpDate { + fn from(dt: OffsetDateTime) -> HttpDate { + HttpDate(dt) } } impl From for HttpDate { fn from(sys: SystemTime) -> HttpDate { - let tmspec = match sys.duration_since(UNIX_EPOCH) { - Ok(dur) => { - time::Timespec::new(dur.as_secs() as i64, dur.subsec_nanos() as i32) - } - Err(err) => { - let neg = err.duration(); - time::Timespec::new( - -(neg.as_secs() as i64), - -(neg.subsec_nanos() as i32), - ) - } - }; - HttpDate(time::at_utc(tmspec)) + HttpDate(PrimitiveDateTime::from(sys).assume_utc()) } } @@ -62,56 +49,51 @@ impl IntoHeaderValue for HttpDate { fn try_into(self) -> Result { let mut wrt = BytesMut::with_capacity(29).writer(); - write!(wrt, "{}", self.0.rfc822()).unwrap(); + write!( + wrt, + "{}", + self.0 + .to_offset(offset!(UTC)) + .format("%a, %d %b %Y %H:%M:%S GMT") + ) + .unwrap(); HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze()) } } impl From for SystemTime { fn from(date: HttpDate) -> SystemTime { - let spec = date.0.to_timespec(); - if spec.sec >= 0 { - UNIX_EPOCH + Duration::new(spec.sec as u64, spec.nsec as u32) - } else { - UNIX_EPOCH - Duration::new(spec.sec as u64, spec.nsec as u32) - } + let dt = date.0; + let epoch = OffsetDateTime::unix_epoch(); + + UNIX_EPOCH + (dt - epoch) } } #[cfg(test)] mod tests { use super::HttpDate; - use time::Tm; - - const NOV_07: HttpDate = HttpDate(Tm { - tm_nsec: 0, - tm_sec: 37, - tm_min: 48, - tm_hour: 8, - tm_mday: 7, - tm_mon: 10, - tm_year: 94, - tm_wday: 0, - tm_isdst: 0, - tm_yday: 0, - tm_utcoff: 0, - }); + use time::{date, time, PrimitiveDateTime}; #[test] fn test_date() { + let nov_07 = HttpDate( + PrimitiveDateTime::new(date!(1994 - 11 - 07), time!(8:48:37)).assume_utc(), + ); + assert_eq!( "Sun, 07 Nov 1994 08:48:37 GMT".parse::().unwrap(), - NOV_07 + nov_07 ); assert_eq!( "Sunday, 07-Nov-94 08:48:37 GMT" .parse::() .unwrap(), - NOV_07 + nov_07 ); assert_eq!( "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - NOV_07 + nov_07 ); assert!("this-is-no-date".parse::().is_err()); } diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index 58ebff61f..86f8250b6 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -1,4 +1,4 @@ -use std::{io, mem, ptr, slice}; +use std::{io, ptr}; use bytes::{BufMut, BytesMut}; use http::Version; @@ -14,9 +14,7 @@ const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13; pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) { - let mut buf: [u8; STATUS_LINE_BUF_SIZE] = [ - b'H', b'T', b'T', b'P', b'/', b'1', b'.', b'1', b' ', b' ', b' ', b' ', b' ', - ]; + let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 "; match version { Version::HTTP_2 => buf[5] = b'2', Version::HTTP_10 => buf[7] = b'0', @@ -64,109 +62,104 @@ pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesM } } +const DIGITS_START: u8 = b'0'; + /// NOTE: bytes object has to contain enough space -pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) { +pub fn write_content_length(n: usize, bytes: &mut BytesMut) { + bytes.put_slice(b"\r\ncontent-length: "); + if n < 10 { - let mut buf: [u8; 21] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n', - ]; - buf[18] = (n as u8) + b'0'; - bytes.put_slice(&buf); + bytes.put_u8(DIGITS_START + (n as u8)); } else if n < 100 { - let mut buf: [u8; 22] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n', - ]; - let d1 = n << 1; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(18), - 2, - ); - } - bytes.put_slice(&buf); + let n = n as u8; + + let d10 = n / 10; + let d1 = n % 10; + + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); } else if n < 1000 { - let mut buf: [u8; 23] = [ - b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l', b'e', - b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r', b'\n', - ]; - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - unsafe { - ptr::copy_nonoverlapping( - DEC_DIGITS_LUT.as_ptr().add(d1), - buf.as_mut_ptr().offset(19), - 2, - ) - }; + let n = n as u16; - // decode last 1 - buf[18] = (n as u8) + b'0'; + let d100 = (n / 100) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; - bytes.put_slice(&buf); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 10_000 { + let n = n as u16; + + let d1000 = (n / 1000) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 100_000 { + let n = n as u32; + + let d10000 = (n / 10000) as u8; + let d1000 = ((n / 1000) % 10) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d10000); + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); + } else if n < 1_000_000 { + let n = n as u32; + + let d100000 = (n / 100_000) as u8; + let d10000 = ((n / 10000) % 10) as u8; + let d1000 = ((n / 1000) % 10) as u8; + let d100 = ((n / 100) % 10) as u8; + let d10 = ((n / 10) % 10) as u8; + let d1 = (n % 10) as u8; + + bytes.put_u8(DIGITS_START + d100000); + bytes.put_u8(DIGITS_START + d10000); + bytes.put_u8(DIGITS_START + d1000); + bytes.put_u8(DIGITS_START + d100); + bytes.put_u8(DIGITS_START + d10); + bytes.put_u8(DIGITS_START + d1); } else { - bytes.put_slice(b"\r\ncontent-length: "); - convert_usize(n, bytes); + write_usize(n, bytes); } + + bytes.put_slice(b"\r\n"); } -pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { - let mut curr: isize = 39; - let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() }; - buf[39] = b'\r'; - buf[40] = b'\n'; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); +pub(crate) fn write_usize(n: usize, bytes: &mut BytesMut) { + let mut n = n; - // eagerly decode 4 characters at a time - while n >= 10_000 { - let rem = (n % 10_000) as isize; - n /= 10_000; + // 20 chars is max length of a usize (2^64) + // digits will be added to the buffer from lsd to msd + let mut buf = BytesMut::with_capacity(20); - let d1 = (rem / 100) << 1; - let d2 = (rem % 100) << 1; - curr -= 4; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - ptr::copy_nonoverlapping(lut_ptr.offset(d2), buf_ptr.offset(curr + 2), 2); - } + while n > 9 { + // "pop" the least-significant digit + let lsd = (n % 10) as u8; + + // remove the lsd from n + n /= 10; + + buf.put_u8(DIGITS_START + lsd); } - // if we reach here numbers are <= 9999, so at most 4 chars long - let mut n = n as isize; // possibly reduce 64bit math + // put msd to result buffer + bytes.put_u8(DIGITS_START + (n as u8)); - // decode 2 more chars, if > 2 chars - if n >= 100 { - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2); - } - } - - unsafe { - bytes.extend_from_slice(slice::from_raw_parts( - buf_ptr.offset(curr), - 41 - curr as usize, - )); + // put, in reverse (msd to lsd), remaining digits to buffer + for i in (0..buf.len()).rev() { + bytes.put_u8(buf[i]); } } @@ -231,5 +224,48 @@ mod tests { bytes.reserve(50); write_content_length(5909, &mut bytes); assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 5909\r\n"[..]); + bytes.reserve(50); + write_content_length(9999, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 9999\r\n"[..]); + bytes.reserve(50); + write_content_length(10001, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 10001\r\n"[..]); + bytes.reserve(50); + write_content_length(59094, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 59094\r\n"[..]); + bytes.reserve(50); + write_content_length(99999, &mut bytes); + assert_eq!(bytes.split().freeze(), b"\r\ncontent-length: 99999\r\n"[..]); + + bytes.reserve(50); + write_content_length(590947, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 590947\r\n"[..] + ); + bytes.reserve(50); + write_content_length(999999, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 999999\r\n"[..] + ); + bytes.reserve(50); + write_content_length(5909471, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 5909471\r\n"[..] + ); + bytes.reserve(50); + write_content_length(59094718, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 59094718\r\n"[..] + ); + bytes.reserve(50); + write_content_length(4294973728, &mut bytes); + assert_eq!( + bytes.split().freeze(), + b"\r\ncontent-length: 4294973728\r\n"[..] + ); } } diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 7a47012f8..9f615a129 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,5 +1,5 @@ //! Basic http primitives for actix-net framework. -#![deny(rust_2018_idioms, warnings)] +#![warn(rust_2018_idioms, warnings)] #![allow( clippy::type_complexity, clippy::too_many_arguments, @@ -10,6 +10,9 @@ #[macro_use] extern crate log; +#[macro_use] +mod macros; + pub mod body; mod builder; pub mod client; @@ -27,6 +30,7 @@ mod payload; mod request; mod response; mod service; +mod time_parser; pub mod cookie; pub mod error; diff --git a/actix-http/src/macros.rs b/actix-http/src/macros.rs new file mode 100644 index 000000000..b970b14f2 --- /dev/null +++ b/actix-http/src/macros.rs @@ -0,0 +1,95 @@ +#[macro_export] +macro_rules! downcast_get_type_id { + () => { + /// A helper method to get the type ID of the type + /// this trait is implemented on. + /// This method is unsafe to *implement*, since `downcast_ref` relies + /// on the returned `TypeId` to perform a cast. + /// + /// Unfortunately, Rust has no notion of a trait method that is + /// unsafe to implement (marking it as `unsafe` makes it unsafe + /// to *call*). As a workaround, we require this method + /// to return a private type along with the `TypeId`. This + /// private type (`PrivateHelper`) has a private constructor, + /// making it impossible for safe code to construct outside of + /// this module. This ensures that safe code cannot violate + /// type-safety by implementing this method. + #[doc(hidden)] + fn __private_get_type_id__(&self) -> (std::any::TypeId, PrivateHelper) + where + Self: 'static, + { + (std::any::TypeId::of::(), PrivateHelper(())) + } + }; +} + +//Generate implementation for dyn $name +#[macro_export] +macro_rules! downcast { + ($name:ident) => { + /// A struct with a private constructor, for use with + /// `__private_get_type_id__`. Its single field is private, + /// ensuring that it can only be constructed from this module + #[doc(hidden)] + pub struct PrivateHelper(()); + + impl dyn $name + 'static { + /// Downcasts generic body to a specific type. + pub fn downcast_ref(&self) -> Option<&T> { + if self.__private_get_type_id__().0 == std::any::TypeId::of::() { + // Safety: external crates cannot override the default + // implementation of `__private_get_type_id__`, since + // it requires returning a private type. We can therefore + // rely on the returned `TypeId`, which ensures that this + // case is correct. + unsafe { Some(&*(self as *const dyn $name as *const T)) } + } else { + None + } + } + /// Downcasts a generic body to a mutable specific type. + pub fn downcast_mut(&mut self) -> Option<&mut T> { + if self.__private_get_type_id__().0 == std::any::TypeId::of::() { + // Safety: external crates cannot override the default + // implementation of `__private_get_type_id__`, since + // it requires returning a private type. We can therefore + // rely on the returned `TypeId`, which ensures that this + // case is correct. + unsafe { + Some(&mut *(self as *const dyn $name as *const T as *mut T)) + } + } else { + None + } + } + } + }; +} + +#[cfg(test)] +mod tests { + + trait MB { + downcast_get_type_id!(); + } + + downcast!(MB); + + impl MB for String {} + impl MB for () {} + + #[actix_rt::test] + async fn test_any_casting() { + let mut body = String::from("hello cast"); + let resp_body: &mut dyn MB = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push_str("!"); + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast!"); + let not_body = resp_body.downcast_ref::<()>(); + assert!(not_body.is_none()); + } +} diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index fcdcd7cdf..7a9b82df2 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -9,7 +9,6 @@ use std::{fmt, str}; use bytes::{Bytes, BytesMut}; use futures_core::Stream; use serde::Serialize; -use serde_json; use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; use crate::cookie::{Cookie, CookieJar}; @@ -637,7 +636,7 @@ impl ResponseBuilder { /// `ResponseBuilder` can not be used after this call. pub fn streaming(&mut self, stream: S) -> Response where - S: Stream> + 'static, + S: Stream> + Unpin + 'static, E: Into + 'static, { self.body(Body::from_message(BodyStream::new(stream))) diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs new file mode 100644 index 000000000..b5b07ccba --- /dev/null +++ b/actix-http/src/time_parser.rs @@ -0,0 +1,42 @@ +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 { + try_parse_rfc_1123(time) + .or_else(|| try_parse_rfc_850(time)) + .or_else(|| try_parse_asctime(time)) +} + +/// Attempt to parse a `time` string as a RFC 1123 formatted date time string. +fn try_parse_rfc_1123(time: &str) -> Option { + time::parse(time, "%a, %d %b %Y %H:%M:%S").ok() +} + +/// Attempt to parse a `time` string as a RFC 850 formatted date time string. +fn try_parse_rfc_850(time: &str) -> Option { + match PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S") { + Ok(dt) => { + // If the `time` string contains a two-digit year, then as per RFC 2616 ยง 19.3, + // we consider the year as part of this century if it's within the next 50 years, + // otherwise we consider as part of the previous century. + let now = OffsetDateTime::now(); + let century_start_year = (now.year() / 100) * 100; + let mut expanded_year = century_start_year + dt.year(); + + if expanded_year > now.year() + 50 { + expanded_year -= 100; + } + + match Date::try_from_ymd(expanded_year, dt.month(), dt.day()) { + Ok(date) => Some(PrimitiveDateTime::new(date, dt.time())), + Err(_) => None, + } + } + Err(_) => None, + } +} + +/// Attempt to parse a `time` string using ANSI C's `asctime` format. +fn try_parse_asctime(time: &str) -> Option { + time::parse(time, "%a %b %_d %H:%M:%S %Y").ok() +} diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 3c70eb2bd..8f7004f18 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -2,7 +2,6 @@ use std::convert::TryFrom; use bytes::{Buf, BufMut, BytesMut}; use log::debug; -use rand; use crate::ws::mask::apply_mask; use crate::ws::proto::{CloseCode, CloseReason, OpCode}; diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 60af6f08b..7b55cbf1a 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -1,5 +1,3 @@ -use base64; -use sha1; use std::convert::{From, Into}; use std::fmt; diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index b25f05272..77caa045b 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -97,11 +97,9 @@ async fn test_h2_body() -> io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let mut srv = test_server(move || { HttpService::build() - .h2(|mut req: Request<_>| { - async move { - let body = load_body(req.take_payload()).await?; - Ok::<_, Error>(Response::Ok().body(body)) - } + .h2(|mut req: Request<_>| async move { + let body = load_body(req.take_payload()).await?; + Ok::<_, Error>(Response::Ok().body(body)) }) .openssl(ssl_acceptor()) .map_err(|_| ()) diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index bc0c91cc3..933a6c894 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -104,11 +104,9 @@ async fn test_h2_body1() -> io::Result<()> { let data = "HELLOWORLD".to_owned().repeat(64 * 1024); let mut srv = test_server(move || { HttpService::build() - .h2(|mut req: Request<_>| { - async move { - let body = load_body(req.take_payload()).await?; - Ok::<_, Error>(Response::Ok().body(body)) - } + .h2(|mut req: Request<_>| async move { + let body = load_body(req.take_payload()).await?; + Ok::<_, Error>(Response::Ok().body(body)) }) .rustls(ssl_acceptor()) }); diff --git a/actix-identity/CHANGES.md b/actix-identity/CHANGES.md index 594c21388..0c9809ea1 100644 --- a/actix-identity/CHANGES.md +++ b/actix-identity/CHANGES.md @@ -1,5 +1,9 @@ # Changes +## [Unreleased] - 2020-xx-xx + +* Update the `time` dependency to 0.2.5 + ## [0.2.1] - 2020-01-10 * Fix panic with already borrowed: BorrowMutError #1263 diff --git a/actix-identity/Cargo.toml b/actix-identity/Cargo.toml index 8cd6b1271..f97b66291 100644 --- a/actix-identity/Cargo.toml +++ b/actix-identity/Cargo.toml @@ -17,13 +17,13 @@ path = "src/lib.rs" [dependencies] actix-web = { version = "2.0.0", default-features = false, features = ["secure-cookies"] } -actix-service = "1.0.2" +actix-service = "1.0.5" futures = "0.3.1" serde = "1.0" serde_json = "1.0" -time = "0.1.42" +time = { version = "0.2.5", default-features = false, features = ["std"] } [dev-dependencies] actix-rt = "1.0.0" actix-http = "1.0.1" -bytes = "0.5.3" \ No newline at end of file +bytes = "0.5.4" diff --git a/actix-identity/src/lib.rs b/actix-identity/src/lib.rs index 3b9626991..b584b1af7 100644 --- a/actix-identity/src/lib.rs +++ b/actix-identity/src/lib.rs @@ -428,14 +428,14 @@ impl CookieIdentityInner { let now = SystemTime::now(); if let Some(visit_deadline) = self.visit_deadline { if now.duration_since(value.visit_timestamp?).ok()? - > visit_deadline.to_std().ok()? + > visit_deadline { return None; } } if let Some(login_deadline) = self.login_deadline { if now.duration_since(value.login_timestamp?).ok()? - > login_deadline.to_std().ok()? + > login_deadline { return None; } @@ -855,7 +855,7 @@ mod tests { let cv: CookieValue = serde_json::from_str(cookie.value()).unwrap(); assert_eq!(cv.identity, identity); let now = SystemTime::now(); - let t30sec_ago = now - Duration::seconds(30).to_std().unwrap(); + let t30sec_ago = now - Duration::seconds(30); match login_timestamp { LoginTimestampCheck::NoTimestamp => assert_eq!(cv.login_timestamp, None), LoginTimestampCheck::NewTimestamp => assert!( @@ -997,7 +997,7 @@ mod tests { create_identity_server(|c| c.login_deadline(Duration::days(90))).await; let cookie = login_cookie( COOKIE_LOGIN, - Some(SystemTime::now() - Duration::days(180).to_std().unwrap()), + Some(SystemTime::now() - Duration::days(180)), None, ); let mut resp = test::call_service( @@ -1023,7 +1023,7 @@ mod tests { let cookie = login_cookie( COOKIE_LOGIN, None, - Some(SystemTime::now() - Duration::days(180).to_std().unwrap()), + Some(SystemTime::now() - Duration::days(180)), ); let mut resp = test::call_service( &mut srv, @@ -1065,7 +1065,7 @@ mod tests { .login_deadline(Duration::days(90)) }) .await; - let timestamp = SystemTime::now() - Duration::days(1).to_std().unwrap(); + let timestamp = SystemTime::now() - Duration::days(1); let cookie = login_cookie(COOKIE_LOGIN, Some(timestamp), Some(timestamp)); let mut resp = test::call_service( &mut srv, diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 31f326d05..ed5c8ad3f 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,5 +1,11 @@ # Changes +## [0.2.1] - 2020-01-xx + +* Remove the unused `time` dependency + +* Fix missing `std::error::Error` implement for `MultipartError`. + ## [0.2.0] - 2019-12-20 * Release @@ -44,4 +50,4 @@ * Split multipart support to separate crate -* Optimize multipart handling #634, #769 \ No newline at end of file +* Optimize multipart handling #634, #769 diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 6c683cb1a..3c8fe6de1 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -16,7 +16,7 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "2.0.0-rc", default-features = false } +actix-web = { version = "2.0.0", default-features = false } actix-service = "1.0.1" actix-utils = "1.0.3" bytes = "0.5.3" @@ -25,9 +25,8 @@ httparse = "1.3" futures = "0.3.1" log = "0.4" mime = "0.3" -time = "0.1" twoway = "0.2" [dev-dependencies] actix-rt = "1.0.0" -actix-http = "1.0.0" \ No newline at end of file +actix-http = "2.0.0-alpha.2" diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 6677f69c7..cdbb5d395 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -33,6 +33,8 @@ pub enum MultipartError { NotConsumed, } +impl std::error::Error for MultipartError {} + /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { diff --git a/actix-session/CHANGES.md b/actix-session/CHANGES.md index e4306fa9d..f6753ae58 100644 --- a/actix-session/CHANGES.md +++ b/actix-session/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [Unreleased] - 2020-01-xx + +* Update the `time` dependency to 0.2.5 +* [#1292](https://github.com/actix/actix-web/pull/1292) Long lasting auto-prolonged session + ## [0.3.0] - 2019-12-20 * Release diff --git a/actix-session/Cargo.toml b/actix-session/Cargo.toml index 5989cc0d6..b0a89ee29 100644 --- a/actix-session/Cargo.toml +++ b/actix-session/Cargo.toml @@ -22,14 +22,14 @@ default = ["cookie-session"] cookie-session = ["actix-web/secure-cookies"] [dependencies] -actix-web = "2.0.0-rc" -actix-service = "1.0.1" -bytes = "0.5.3" +actix-web = { version = "2.0.0" } +actix-service = "1.0.5" +bytes = "0.5.4" derive_more = "0.99.2" futures = "0.3.1" serde = "1.0" serde_json = "1.0" -time = "0.1.42" +time = { version = "0.2.5", default-features = false, features = ["std"] } [dev-dependencies] actix-rt = "1.0.0" diff --git a/actix-session/src/cookie.rs b/actix-session/src/cookie.rs index 75eef0c01..b5297f561 100644 --- a/actix-session/src/cookie.rs +++ b/actix-session/src/cookie.rs @@ -27,6 +27,7 @@ use actix_web::{Error, HttpMessage, ResponseError}; use derive_more::{Display, From}; use futures::future::{ok, FutureExt, LocalBoxFuture, Ready}; use serde_json::error::Error as JsonError; +use time::{Duration, OffsetDateTime}; use crate::{Session, SessionStatus}; @@ -56,7 +57,8 @@ struct CookieSessionInner { domain: Option, secure: bool, http_only: bool, - max_age: Option, + max_age: Option, + expires_in: Option, same_site: Option, } @@ -71,6 +73,7 @@ impl CookieSessionInner { secure: true, http_only: true, max_age: None, + expires_in: None, same_site: None, } } @@ -96,6 +99,10 @@ impl CookieSessionInner { cookie.set_domain(domain.clone()); } + if let Some(expires_in) = self.expires_in { + cookie.set_expires(OffsetDateTime::now() + expires_in); + } + if let Some(max_age) = self.max_age { cookie.set_max_age(max_age); } @@ -123,8 +130,8 @@ impl CookieSessionInner { fn remove_cookie(&self, res: &mut ServiceResponse) -> Result<(), Error> { let mut cookie = Cookie::named(self.name.clone()); cookie.set_value(""); - cookie.set_max_age(time::Duration::seconds(0)); - cookie.set_expires(time::now() - time::Duration::days(365)); + cookie.set_max_age(Duration::zero()); + cookie.set_expires(OffsetDateTime::now() - Duration::days(365)); let val = HeaderValue::from_str(&cookie.to_string())?; res.headers_mut().append(SET_COOKIE, val); @@ -263,7 +270,7 @@ impl CookieSession { /// Sets the `max-age` field in the session cookie being built. pub fn max_age(self, seconds: i64) -> CookieSession { - self.max_age_time(time::Duration::seconds(seconds)) + self.max_age_time(Duration::seconds(seconds)) } /// Sets the `max-age` field in the session cookie being built. @@ -271,6 +278,17 @@ impl CookieSession { Rc::get_mut(&mut self.0).unwrap().max_age = Some(value); self } + + /// Sets the `expires` field in the session cookie being built. + pub fn expires_in(self, seconds: i64) -> CookieSession { + self.expires_in_time(Duration::seconds(seconds)) + } + + /// Sets the `expires` field in the session cookie being built. + pub fn expires_in_time(mut self, value: Duration) -> CookieSession { + Rc::get_mut(&mut self.0).unwrap().expires_in = Some(value); + self + } } impl Transform for CookieSession @@ -323,6 +341,7 @@ where fn call(&mut self, mut req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); let (is_new, state) = self.inner.load(&req); + let prolong_expiration = self.inner.expires_in.is_some(); Session::set_session(state.into_iter(), &mut req); let fut = self.service.call(req); @@ -334,6 +353,9 @@ where | (SessionStatus::Renewed, Some(state)) => { res.checked_expr(|res| inner.set_cookie(res, state)) } + (SessionStatus::Unchanged, Some(state)) if prolong_expiration => { + res.checked_expr(|res| inner.set_cookie(res, state)) + } (SessionStatus::Unchanged, _) => // set a new session cookie upon first request (new client) { @@ -477,4 +499,47 @@ mod tests { let body = test::read_response(&mut app, request).await; assert_eq!(body, Bytes::from_static(b"counter: 100")); } + + #[actix_rt::test] + async fn prolong_expiration() { + let mut app = test::init_service( + App::new() + .wrap(CookieSession::signed(&[0; 32]).secure(false).expires_in(60)) + .service(web::resource("/").to(|ses: Session| { + async move { + let _ = ses.set("counter", 100); + "test" + } + })) + .service( + web::resource("/test/") + .to(|| async move { "no-changes-in-session" }), + ), + ) + .await; + + let request = test::TestRequest::get().to_request(); + let response = app.call(request).await.unwrap(); + let expires_1 = response + .response() + .cookies() + .find(|c| c.name() == "actix-session") + .expect("Cookie is set") + .expires() + .expect("Expiration is set"); + + actix_rt::time::delay_for(std::time::Duration::from_secs(1)).await; + + let request = test::TestRequest::with_uri("/test/").to_request(); + let response = app.call(request).await.unwrap(); + let expires_2 = response + .response() + .cookies() + .find(|c| c.name() == "actix-session") + .expect("Cookie is set") + .expires() + .expect("Expiration is set"); + + assert!(expires_2 - expires_1 >= Duration::seconds(1)); + } } diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 6f573e442..7941e192c 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -16,9 +16,9 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.9.0" -actix-web = "2.0.0-rc" -actix-http = "1.0.1" +actix = "0.10.0-alpha.1" +actix-web = "2.0.0" +actix-http = "2.0.0-alpha.2" actix-codec = "0.2.0" bytes = "0.5.2" futures = "0.3.1" diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index de676f688..941cd36de 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,5 +1,13 @@ # Changes +## [0.2.1] - 2020-02-25 + +* Add `#[allow(missing_docs)]` attribute to generated structs [#1368] +* Allow the handler function to be named as `config` [#1290] + +[#1368]: https://github.com/actix/actix-web/issues/1368 +[#1290]: https://github.com/actix/actix-web/issues/1290 + ## [0.2.0] - 2019-12-13 * Generate code for actix-web 2.0 diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 3fe561deb..0b926b807 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-codegen" -version = "0.2.0" +version = "0.2.1" description = "Actix web proc macros" readme = "README.md" authors = ["Nikolay Kim "] @@ -17,6 +17,6 @@ syn = { version = "^1", features = ["full", "parsing"] } proc-macro2 = "^1" [dev-dependencies] -actix-rt = { version = "1.0.0" } -actix-web = { version = "2.0.0-rc" } -futures = { version = "0.3.1" } +actix-rt = "1.0.0" +actix-web = "2.0.0" +futures = "0.3.1" diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index 16d3e8157..60b829595 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -191,19 +191,19 @@ impl Route { let extra_guards = &self.args.guards; let resource_type = &self.resource_type; let stream = quote! { - #[allow(non_camel_case_types)] + #[allow(non_camel_case_types, missing_docs)] pub struct #name; impl actix_web::dev::HttpServiceFactory for #name { - fn register(self, config: &mut actix_web::dev::AppService) { + fn register(self, __config: &mut actix_web::dev::AppService) { #ast - let resource = actix_web::Resource::new(#path) + let __resource = actix_web::Resource::new(#path) .name(#resource_name) .guard(actix_web::guard::#guard()) #(.guard(actix_web::guard::fn_guard(#extra_guards)))* .#resource_type(#name); - actix_web::dev::HttpServiceFactory::register(resource, config) + actix_web::dev::HttpServiceFactory::register(__resource, __config) } } }; diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 4ac1a8023..ffb50c11e 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -2,6 +2,12 @@ use actix_web::{http, test, web::Path, App, HttpResponse, Responder}; use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, trace}; use futures::{future, Future}; +// Make sure that we can name function as 'config' +#[get("/config")] +async fn config() -> impl Responder { + HttpResponse::Ok() +} + #[get("/test")] async fn test_handler() -> impl Responder { HttpResponse::Ok() diff --git a/awc/CHANGES.md b/awc/CHANGES.md index d9b26e453..1d6e03708 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,10 +1,15 @@ # Changes +## [NEXT] + +* ClientBuilder accepts initial_window_size and initial_connection_window_size HTTP2 configuration + +* ClientBuilder allowing to set max_http_version to limit HTTP version to be used + ## [1.0.1] - 2019-12-15 * Fix compilation with default features off - ## [1.0.0] - 2019-12-13 * Release diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 67e0a3ee4..efbe0abd6 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -36,7 +36,7 @@ compress = ["actix-http/compress"] [dependencies] actix-codec = "0.2.0" actix-service = "1.0.1" -actix-http = "1.0.0" +actix-http = "2.0.0-alpha.2" actix-rt = "1.0.0" base64 = "0.11" @@ -51,16 +51,16 @@ serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.6.1" open-ssl = { version="0.10", package="openssl", optional = true } -rust-tls = { version = "0.16.0", package="rustls", optional = true, features = ["dangerous_configuration"] } +rust-tls = { version = "0.17.0", package="rustls", optional = true, features = ["dangerous_configuration"] } [dev-dependencies] -actix-connect = { version = "1.0.1", features=["openssl"] } -actix-web = { version = "2.0.0-rc", features=["openssl"] } -actix-http = { version = "1.0.1", features=["openssl"] } +actix-connect = { version = "2.0.0-alpha.1", features=["openssl"] } +actix-web = { version = "2.0.0", features=["openssl"] } +actix-http = { version = "2.0.0-alpha.2", features=["openssl"] } actix-http-test = { version = "1.0.0", features=["openssl"] } actix-utils = "1.0.3" actix-server = "1.0.0" -actix-tls = { version = "1.0.0", features=["openssl", "rustls"] } +actix-tls = { version = "2.0.0-alpha.1", features=["openssl", "rustls"] } brotli2 = "0.3.2" flate2 = "1.0.13" futures = "0.3.1" diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 7bd0171ec..2b2e5df9f 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -4,11 +4,11 @@ use std::fmt; use std::rc::Rc; use std::time::Duration; -use actix_http::client::{Connect, ConnectError, Connection, Connector}; -use actix_http::http::{header, Error as HttpError, HeaderMap, HeaderName}; +use actix_http::client::{Connect as HttpConnect, ConnectError, Connection, Connector}; +use actix_http::http::{header, Error as HttpError, HeaderMap, HeaderName, self}; use actix_service::Service; -use crate::connect::ConnectorWrapper; +use crate::connect::{ConnectorWrapper, Connect}; use crate::{Client, ClientConfig}; /// An HTTP Client builder @@ -16,10 +16,15 @@ use crate::{Client, ClientConfig}; /// This type can be used to construct an instance of `Client` through a /// builder-like pattern. pub struct ClientBuilder { - config: ClientConfig, default_headers: bool, allow_redirects: bool, max_redirects: usize, + max_http_version: Option, + stream_window_size: Option, + conn_window_size: Option, + headers: HeaderMap, + timeout: Option, + connector: Option>>, } impl Default for ClientBuilder { @@ -34,25 +39,24 @@ impl ClientBuilder { default_headers: true, allow_redirects: true, max_redirects: 10, - config: ClientConfig { - headers: HeaderMap::new(), - timeout: Some(Duration::from_secs(5)), - connector: RefCell::new(Box::new(ConnectorWrapper( - Connector::new().finish(), - ))), - }, + headers: HeaderMap::new(), + timeout: Some(Duration::from_secs(5)), + connector: None, + max_http_version: None, + stream_window_size: None, + conn_window_size: None, } } /// Use custom connector service. pub fn connector(mut self, connector: T) -> Self where - T: Service + 'static, + T: Service + 'static, T::Response: Connection, ::Future: 'static, T::Future: 'static, { - self.config.connector = RefCell::new(Box::new(ConnectorWrapper(connector))); + self.connector = Some(RefCell::new(Box::new(ConnectorWrapper(connector)))); self } @@ -61,13 +65,13 @@ impl ClientBuilder { /// Request timeout is the total time before a response must be received. /// Default value is 5 seconds. pub fn timeout(mut self, timeout: Duration) -> Self { - self.config.timeout = Some(timeout); + self.timeout = Some(timeout); self } /// Disable request timeout. pub fn disable_timeout(mut self) -> Self { - self.config.timeout = None; + self.timeout = None; self } @@ -79,6 +83,31 @@ impl ClientBuilder { self } + /// Maximum supported http major version + /// Supported versions http/1.1, http/2 + pub fn max_http_version(mut self, val: http::Version) -> Self { + self.max_http_version = Some(val); + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 stream-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_window_size(mut self, size: u32) -> Self { + self.stream_window_size = Some(size); + self + } + + /// Indicates the initial window size (in octets) for + /// HTTP2 connection-level flow control for received data. + /// + /// The default value is 65,535 and is good for APIs, but not for big objects. + pub fn initial_connection_window_size(mut self, size: u32) -> Self { + self.conn_window_size = Some(size); + self + } + /// Set max number of redirects. /// /// Max redirects is set to 10 by default. @@ -106,7 +135,7 @@ impl ClientBuilder { match HeaderName::try_from(key) { Ok(key) => match value.try_into() { Ok(value) => { - self.config.headers.append(key, value); + self.headers.append(key, value); } Err(e) => log::error!("Header value error: {:?}", e), }, @@ -140,7 +169,27 @@ impl ClientBuilder { /// Finish build process and create `Client` instance. pub fn finish(self) -> Client { - Client(Rc::new(self.config)) + let connector = if let Some(connector) = self.connector { + connector + } else { + let mut connector = Connector::new(); + if let Some(val) = self.max_http_version { + connector = connector.max_http_version(val) + }; + if let Some(val) = self.conn_window_size { + connector = connector.initial_connection_window_size(val) + }; + if let Some(val) = self.stream_window_size { + connector = connector.initial_window_size(val) + }; + RefCell::new(Box::new(ConnectorWrapper(connector.finish())) as Box) + }; + let config = ClientConfig { + headers: self.headers, + timeout: self.timeout, + connector, + }; + Client(Rc::new(config)) } } @@ -153,7 +202,6 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", Some("password")); assert_eq!( client - .config .headers .get(header::AUTHORIZATION) .unwrap() @@ -165,7 +213,6 @@ mod tests { let client = ClientBuilder::new().basic_auth("username", None); assert_eq!( client - .config .headers .get(header::AUTHORIZATION) .unwrap() @@ -180,7 +227,6 @@ mod tests { let client = ClientBuilder::new().bearer_auth("someS3cr3tAutht0k3n"); assert_eq!( client - .config .headers .get(header::AUTHORIZATION) .unwrap() diff --git a/awc/src/lib.rs b/awc/src/lib.rs index 8944fe229..952a15369 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(rust_2018_idioms, warnings)] +#![warn(rust_2018_idioms, warnings)] #![allow( clippy::type_complexity, clippy::borrow_interior_mutable_const, diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs new file mode 100644 index 000000000..8b295cda7 --- /dev/null +++ b/awc/tests/test_connector.rs @@ -0,0 +1,61 @@ +#![cfg(feature = "openssl")] +use actix_http::HttpService; +use actix_http_test::test_server; +use actix_service::{map_config, ServiceFactory}; +use actix_web::http::Version; +use actix_web::{dev::AppConfig, web, App, HttpResponse}; +use open_ssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode}; + +fn ssl_acceptor() -> SslAcceptor { + // load ssl keys + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder + .set_private_key_file("../tests/key.pem", SslFiletype::PEM) + .unwrap(); + builder + .set_certificate_chain_file("../tests/cert.pem") + .unwrap(); + builder.set_alpn_select_callback(|_, protos| { + const H2: &[u8] = b"\x02h2"; + if protos.windows(3).any(|window| window == H2) { + Ok(b"h2") + } else { + Err(open_ssl::ssl::AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x02h2").unwrap(); + builder.build() +} + +#[actix_rt::test] +async fn test_connection_window_size() { + let srv = test_server(move || { + HttpService::build() + .h2(map_config( + App::new().service( + web::resource("/").route(web::to(|| HttpResponse::Ok())), + ), + |_| AppConfig::default(), + )) + .openssl(ssl_acceptor()) + .map_err(|_| ()) + }); + + // disable ssl verification + let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); + builder.set_verify(SslVerifyMode::NONE); + let _ = builder + .set_alpn_protos(b"\x02h2\x08http/1.1") + .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); + + let client = awc::Client::build() + .connector(awc::Connector::new().ssl(builder.build()).finish()) + .initial_window_size(100) + .initial_connection_window_size(100) + .finish(); + + let request = client.get(srv.surl("/")).send(); + let response = request.await.unwrap(); + assert!(response.status().is_success()); + assert_eq!(response.version(), Version::HTTP_2); +} diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 1d7eb7bc5..8863dfcbe 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -11,6 +11,7 @@ use futures::future::ok; use open_ssl::ssl::{SslAcceptor, SslFiletype, SslMethod, SslVerifyMode}; use rust_tls::ClientConfig; +#[allow(unused)] fn ssl_acceptor() -> SslAcceptor { // load ssl keys let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); diff --git a/benches/server.rs b/benches/server.rs new file mode 100644 index 000000000..93079a223 --- /dev/null +++ b/benches/server.rs @@ -0,0 +1,64 @@ +use actix_web::{test, web, App, HttpResponse}; +use awc::Client; +use criterion::{criterion_group, criterion_main, Criterion}; +use futures::future::join_all; + +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 \ + 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 \ + 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 \ + 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 \ + 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 \ + 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 \ + 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"; + +// benchmark sending all requests at the same time +fn bench_async_burst(c: &mut Criterion) { + let srv = test::start(|| { + App::new() + .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) + }); + + // We are using System here, since Runtime requires preinitialized tokio + // Maybe add to actix_rt docs + let url = srv.url("/"); + let mut rt = actix_rt::System::new("test"); + + c.bench_function("get_body_async_burst", move |b| { + b.iter_custom(|iters| { + let client = Client::new().get(url.clone()).freeze().unwrap(); + + let start = std::time::Instant::now(); + // benchmark body + let resps = rt.block_on(async move { + let burst = (0..iters).map(|_| client.send()); + join_all(burst).await + }); + let elapsed = start.elapsed(); + + // if there are failed requests that might be an issue + let failed = resps.iter().filter(|r| r.is_err()).count(); + if failed > 0 { + eprintln!("failed {} requests (might be bench timeout)", failed); + }; + + elapsed + }) + }); +} + +criterion_group!(server_benches, bench_async_burst); +criterion_main!(server_benches); diff --git a/benches/service.rs b/benches/service.rs new file mode 100644 index 000000000..8adbc8a0c --- /dev/null +++ b/benches/service.rs @@ -0,0 +1,108 @@ +use actix_service::Service; +use actix_web::dev::{ServiceRequest, ServiceResponse}; +use actix_web::{web, App, Error, HttpResponse}; +use criterion::{criterion_main, Criterion}; +use std::cell::RefCell; +use std::rc::Rc; + +use actix_web::test::{init_service, ok_service, TestRequest}; + +/// Criterion Benchmark for async Service +/// Should be used from within criterion group: +/// ```rust,ignore +/// let mut criterion: ::criterion::Criterion<_> = +/// ::criterion::Criterion::default().configure_from_args(); +/// bench_async_service(&mut criterion, ok_service(), "async_service_direct"); +/// ``` +/// +/// Usable for benching Service wrappers: +/// Using minimum service code implementation we first measure +/// time to run minimum service, then measure time with wrapper. +/// +/// Sample output +/// async_service_direct time: [1.0908 us 1.1656 us 1.2613 us] +pub fn bench_async_service(c: &mut Criterion, srv: S, name: &str) +where + S: Service + + 'static, +{ + let mut rt = actix_rt::System::new("test"); + let srv = Rc::new(RefCell::new(srv)); + + let req = TestRequest::default().to_srv_request(); + assert!(rt + .block_on(srv.borrow_mut().call(req)) + .unwrap() + .status() + .is_success()); + + // start benchmark loops + c.bench_function(name, move |b| { + b.iter_custom(|iters| { + let srv = srv.clone(); + // exclude request generation, it appears it takes significant time vs call (3us vs 1us) + let reqs: Vec<_> = (0..iters) + .map(|_| TestRequest::default().to_srv_request()) + .collect(); + let start = std::time::Instant::now(); + // benchmark body + rt.block_on(async move { + for req in reqs { + srv.borrow_mut().call(req).await.unwrap(); + } + }); + let elapsed = start.elapsed(); + // check that at least first request succeeded + elapsed + }) + }); +} + +async fn index(req: ServiceRequest) -> Result { + Ok(req.into_response(HttpResponse::Ok().finish())) +} + +// Benchmark basic WebService directly +// this approach is usable for benching WebService, though it adds some time to direct service call: +// Sample results on MacBook Pro '14 +// time: [2.0724 us 2.1345 us 2.2074 us] +fn async_web_service(c: &mut Criterion) { + let mut rt = actix_rt::System::new("test"); + let srv = Rc::new(RefCell::new(rt.block_on(init_service( + App::new().service(web::service("/").finish(index)), + )))); + + let req = TestRequest::get().uri("/").to_request(); + assert!(rt + .block_on(srv.borrow_mut().call(req)) + .unwrap() + .status() + .is_success()); + + // start benchmark loops + c.bench_function("async_web_service_direct", move |b| { + b.iter_custom(|iters| { + let srv = srv.clone(); + let reqs = (0..iters).map(|_| TestRequest::get().uri("/").to_request()); + + let start = std::time::Instant::now(); + // benchmark body + rt.block_on(async move { + for req in reqs { + srv.borrow_mut().call(req).await.unwrap(); + } + }); + let elapsed = start.elapsed(); + // check that at least first request succeeded + elapsed + }) + }); +} + +pub fn service_benches() { + let mut criterion: ::criterion::Criterion<_> = + ::criterion::Criterion::default().configure_from_args(); + bench_async_service(&mut criterion, ok_service(), "async_service_direct"); + async_web_service(&mut criterion); +} +criterion_main!(service_benches); diff --git a/codecov.yml b/codecov.yml new file mode 100644 index 000000000..90cdfab47 --- /dev/null +++ b/codecov.yml @@ -0,0 +1,5 @@ +ignore: # ignore codecoverage on following paths + - "**/tests" + - "test-server" + - "**/benches" + - "**/examples" diff --git a/examples/uds.rs b/examples/uds.rs index 77f245d99..06aa87f4d 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -1,6 +1,6 @@ -use actix_web::{ - get, middleware, web, App, Error, HttpRequest, HttpResponse, HttpServer, -}; +use actix_web::{get, web, HttpRequest}; +#[cfg(unix)] +use actix_web::{middleware, App, Error, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] async fn index(req: HttpRequest, name: web::Path) -> String { @@ -8,6 +8,7 @@ async fn index(req: HttpRequest, name: web::Path) -> String { format!("Hello: {}!\r\n", name) } +#[cfg(unix)] async fn index_async(req: HttpRequest) -> Result<&'static str, Error> { println!("REQ: {:?}", req); Ok("Hello world!\r\n") diff --git a/src/extract.rs b/src/extract.rs index c189bbf97..5289bd7db 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -193,57 +193,83 @@ impl FromRequest for () { macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { - /// FromRequest implementation for tuple - #[doc(hidden)] - #[allow(unused_parens)] - impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) - { - type Error = Error; - type Future = $fut_type<$($T),+>; - type Config = ($($T::Config),+); + // This module is a trick to get around the inability of + // `macro_rules!` macros to make new idents. We want to make + // a new `FutWrapper` struct for each distinct invocation of + // this macro. Ideally, we would name it something like + // `FutWrapper_$fut_type`, but this can't be done in a macro_rules + // macro. + // + // Instead, we put everything in a module named `$fut_type`, thus allowing + // us to use the name `FutWrapper` without worrying about conflicts. + // This macro only exists to generate trait impls for tuples - these + // are inherently global, so users don't have to care about this + // weird trick. + #[allow(non_snake_case)] + mod $fut_type { - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - $fut_type { - items: <($(Option<$T>,)+)>::default(), - futs: ($($T::from_request(req, payload),)+), + // Bring everything into scope, so we don't need + // redundant imports + use super::*; + + /// A helper struct to allow us to pin-project through + /// to individual fields + #[pin_project::pin_project] + struct FutWrapper<$($T: FromRequest),+>($(#[pin] $T::Future),+); + + /// FromRequest implementation for tuple + #[doc(hidden)] + #[allow(unused_parens)] + impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) + { + type Error = Error; + type Future = $fut_type<$($T),+>; + type Config = ($($T::Config),+); + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + $fut_type { + items: <($(Option<$T>,)+)>::default(), + futs: FutWrapper($($T::from_request(req, payload),)+), + } } } - } - #[doc(hidden)] - #[pin_project::pin_project] - pub struct $fut_type<$($T: FromRequest),+> { - items: ($(Option<$T>,)+), - futs: ($($T::Future,)+), - } + #[doc(hidden)] + #[pin_project::pin_project] + pub struct $fut_type<$($T: FromRequest),+> { + items: ($(Option<$T>,)+), + #[pin] + futs: FutWrapper<$($T,)+>, + } - impl<$($T: FromRequest),+> Future for $fut_type<$($T),+> - { - type Output = Result<($($T,)+), Error>; + impl<$($T: FromRequest),+> Future for $fut_type<$($T),+> + { + type Output = Result<($($T,)+), Error>; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); - let mut ready = true; - $( - if this.items.$n.is_none() { - match unsafe { Pin::new_unchecked(&mut this.futs.$n) }.poll(cx) { - Poll::Ready(Ok(item)) => { - this.items.$n = Some(item); + let mut ready = true; + $( + if this.items.$n.is_none() { + match this.futs.as_mut().project().$n.poll(cx) { + Poll::Ready(Ok(item)) => { + this.items.$n = Some(item); + } + Poll::Pending => ready = false, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())), } - Poll::Pending => ready = false, - Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())), } - } - )+ + )+ - 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 + } + } } } }); diff --git a/src/lib.rs b/src/lib.rs index d51005cfe..d7cb45074 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,4 +1,4 @@ -#![deny(rust_2018_idioms, warnings)] +#![warn(rust_2018_idioms, warnings)] #![allow( clippy::needless_doctest_main, clippy::type_complexity, @@ -7,6 +7,12 @@ //! Actix web is a small, pragmatic, and extremely fast web framework //! for Rust. //! +//! ## Example +//! +//! The `#[actix_rt::main]` macro in the example below is provided by the Actix runtime +//! crate, [`actix-rt`](https://crates.io/crates/actix-rt). You will need to include +//! `actix-rt` in your dependencies for it to run. +//! //! ```rust,no_run //! use actix_web::{web, App, Responder, HttpServer}; //! diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 97fa7463f..e40fe648a 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -14,7 +14,7 @@ use bytes::Bytes; use futures::future::{ok, Ready}; use log::debug; use regex::Regex; -use time; +use time::OffsetDateTime; use crate::dev::{BodySize, MessageBody, ResponseBody}; use crate::error::{Error, Result}; @@ -163,11 +163,11 @@ where LoggerResponse { fut: self.service.call(req), format: None, - time: time::now(), + time: OffsetDateTime::now(), _t: PhantomData, } } else { - let now = time::now(); + let now = OffsetDateTime::now(); let mut format = self.inner.format.clone(); for unit in &mut format.0 { @@ -192,7 +192,7 @@ where { #[pin] fut: S::Future, - time: time::Tm, + time: OffsetDateTime, format: Option, _t: PhantomData<(B,)>, } @@ -238,15 +238,20 @@ where } } +use pin_project::{pin_project, pinned_drop}; + +#[pin_project(PinnedDrop)] pub struct StreamLog { + #[pin] body: ResponseBody, format: Option, size: usize, - time: time::Tm, + time: OffsetDateTime, } -impl Drop for StreamLog { - fn drop(&mut self) { +#[pinned_drop] +impl PinnedDrop for StreamLog { + fn drop(self: Pin<&mut Self>) { if let Some(ref format) = self.format { let render = |fmt: &mut Formatter<'_>| { for unit in &format.0 { @@ -259,15 +264,17 @@ impl Drop for StreamLog { } } + impl MessageBody for StreamLog { fn size(&self) -> BodySize { self.body.size() } - fn poll_next(&mut self, cx: &mut Context<'_>) -> Poll>> { - match self.body.poll_next(cx) { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll>> { + let this = self.project(); + match this.body.poll_next(cx) { Poll::Ready(Some(Ok(chunk))) => { - self.size += chunk.len(); + *this.size += chunk.len(); Poll::Ready(Some(Ok(chunk))) } val => val, @@ -366,20 +373,20 @@ impl FormatText { &self, fmt: &mut Formatter<'_>, size: usize, - entry_time: time::Tm, + entry_time: OffsetDateTime, ) -> Result<(), fmt::Error> { match *self { FormatText::Str(ref string) => fmt.write_str(string), FormatText::Percent => "%".fmt(fmt), FormatText::ResponseSize => size.fmt(fmt), FormatText::Time => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000_000.0; + let rt = OffsetDateTime::now() - entry_time; + let rt = rt.as_seconds_f64(); fmt.write_fmt(format_args!("{:.6}", rt)) } FormatText::TimeMillis => { - let rt = time::now() - entry_time; - let rt = (rt.num_nanoseconds().unwrap_or(0) as f64) / 1_000_000.0; + let rt = OffsetDateTime::now() - entry_time; + let rt = (rt.whole_nanoseconds() as f64) / 1_000_000.0; fmt.write_fmt(format_args!("{:.6}", rt)) } FormatText::EnvironHeader(ref name) => { @@ -414,7 +421,7 @@ impl FormatText { } } - fn render_request(&mut self, now: time::Tm, req: &ServiceRequest) { + fn render_request(&mut self, now: OffsetDateTime, req: &ServiceRequest) { match *self { FormatText::RequestLine => { *self = if req.query_string().is_empty() { @@ -436,7 +443,7 @@ impl FormatText { } FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), FormatText::RequestTime => { - *self = FormatText::Str(now.rfc3339().to_string()) + *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")) } FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { @@ -513,7 +520,7 @@ mod tests { .uri("/test/route/yeah") .to_srv_request(); - let now = time::now(); + let now = OffsetDateTime::now(); for unit in &mut format.0 { unit.render_request(now, &req); } @@ -544,7 +551,7 @@ mod tests { ) .to_srv_request(); - let now = time::now(); + let now = OffsetDateTime::now(); for unit in &mut format.0 { unit.render_request(now, &req); } @@ -554,7 +561,7 @@ mod tests { unit.render_response(&resp); } - let entry_time = time::now(); + let entry_time = OffsetDateTime::now(); let render = |fmt: &mut Formatter<'_>| { for unit in &format.0 { unit.render(fmt, 1024, entry_time)?; @@ -572,7 +579,7 @@ mod tests { let mut format = Format::new("%t"); let req = TestRequest::default().to_srv_request(); - let now = time::now(); + let now = OffsetDateTime::now(); for unit in &mut format.0 { unit.render_request(now, &req); } @@ -589,6 +596,6 @@ mod tests { Ok(()) }; let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains(&format!("{}", now.rfc3339()))); + assert!(s.contains(&format!("{}", now.format("%Y-%m-%dT%H:%M:%S")))); } } diff --git a/src/server.rs b/src/server.rs index 11cfbb6bc..97dd9f7f7 100644 --- a/src/server.rs +++ b/src/server.rs @@ -443,8 +443,6 @@ where #[cfg(unix)] /// Start listening for unix domain connections on existing listener. - /// - /// This method is available with `uds` feature. pub fn listen_uds( mut self, lst: std::os::unix::net::UnixListener, @@ -483,8 +481,6 @@ where #[cfg(unix)] /// Start listening for incoming unix domain connections. - /// - /// This method is available with `uds` feature. pub fn bind_uds(mut self, addr: A) -> io::Result where A: AsRef, diff --git a/src/test.rs b/src/test.rs index 69ac1316c..44a88a315 100644 --- a/src/test.rs +++ b/src/test.rs @@ -95,11 +95,10 @@ where /// Calls service and waits for response future completion. /// /// ```rust -/// use actix_web::{test, App, HttpResponse, http::StatusCode}; -/// use actix_service::Service; +/// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[test] -/// fn test_response() { +/// #[actix_rt::test] +/// async fn test_response() { /// let mut app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| async { @@ -151,7 +150,7 @@ where pub async fn read_response(app: &mut S, req: Request) -> Bytes where S: Service, Error = Error>, - B: MessageBody, + B: MessageBody + Unpin, { let mut resp = app .call(req) @@ -194,7 +193,7 @@ where /// ``` pub async fn read_body(mut res: ServiceResponse) -> Bytes where - B: MessageBody, + B: MessageBody + Unpin, { let mut body = res.take_body(); let mut bytes = BytesMut::new(); @@ -300,7 +299,7 @@ where pub async fn read_response_json(app: &mut S, req: Request) -> T where S: Service, Error = Error>, - B: MessageBody, + B: MessageBody + Unpin, T: DeserializeOwned, { let body = read_response(app, req).await; @@ -1012,7 +1011,6 @@ impl Drop for TestServer { #[cfg(test)] mod tests { use actix_http::httpmessage::HttpMessage; - use futures::FutureExt; use serde::{Deserialize, Serialize}; use std::time::SystemTime; @@ -1261,6 +1259,13 @@ mod tests { assert!(res.status().is_success()); } +/* + + Comment out until actix decoupled of actix-http: + https://github.com/actix/actix/issues/321 + + use futures::FutureExt; + #[actix_rt::test] async fn test_actor() { use actix::Actor; @@ -1281,7 +1286,6 @@ mod tests { } } - let addr = MyActor.start(); let mut app = init_service(App::new().service(web::resource("/index.html").to( move || { @@ -1303,4 +1307,5 @@ mod tests { let res = app.call(req).await.unwrap(); assert!(res.status().is_success()); } +*/ } diff --git a/src/web.rs b/src/web.rs index 50d99479a..f47cf865e 100644 --- a/src/web.rs +++ b/src/web.rs @@ -96,7 +96,7 @@ pub fn route() -> Route { /// ); /// ``` /// -/// In the above example, one `GET` route get added: +/// In the above example, one `GET` route gets added: /// * /{project_id} /// pub fn get() -> Route { @@ -114,7 +114,7 @@ pub fn get() -> Route { /// ); /// ``` /// -/// In the above example, one `POST` route get added: +/// In the above example, one `POST` route gets added: /// * /{project_id} /// pub fn post() -> Route { @@ -132,7 +132,7 @@ pub fn post() -> Route { /// ); /// ``` /// -/// In the above example, one `PUT` route get added: +/// In the above example, one `PUT` route gets added: /// * /{project_id} /// pub fn put() -> Route { @@ -150,7 +150,7 @@ pub fn put() -> Route { /// ); /// ``` /// -/// In the above example, one `PATCH` route get added: +/// In the above example, one `PATCH` route gets added: /// * /{project_id} /// pub fn patch() -> Route { @@ -168,7 +168,7 @@ pub fn patch() -> Route { /// ); /// ``` /// -/// In the above example, one `DELETE` route get added: +/// In the above example, one `DELETE` route gets added: /// * /{project_id} /// pub fn delete() -> Route { @@ -186,13 +186,31 @@ pub fn delete() -> Route { /// ); /// ``` /// -/// In the above example, one `HEAD` route get added: +/// In the above example, one `HEAD` route gets added: /// * /{project_id} /// pub fn head() -> Route { method(Method::HEAD) } +/// Create *route* with `TRACE` method guard. +/// +/// ```rust +/// use actix_web::{web, App, HttpResponse}; +/// +/// let app = App::new().service( +/// web::resource("/{project_id}") +/// .route(web::trace().to(|| HttpResponse::Ok())) +/// ); +/// ``` +/// +/// In the above example, one `HEAD` route gets added: +/// * /{project_id} +/// +pub fn trace() -> Route { + method(Method::TRACE) +} + /// Create *route* and add method guard. /// /// ```rust @@ -204,7 +222,7 @@ pub fn head() -> Route { /// ); /// ``` /// -/// In the above example, one `GET` route get added: +/// In the above example, one `GET` route gets added: /// * /{project_id} /// pub fn method(method: Method) -> Route { diff --git a/test-server/CHANGES.md b/test-server/CHANGES.md index 5690afc64..3a990a594 100644 --- a/test-server/CHANGES.md +++ b/test-server/CHANGES.md @@ -1,5 +1,10 @@ # Changes +## [Unreleased] - 2020-xx-xx + +* Update the `time` dependency to 0.2.7 +* Update `actix-connect` dependency to 2.0.0-alpha.1 + ## [1.0.0] - 2019-12-13 ### Changed diff --git a/test-server/Cargo.toml b/test-server/Cargo.toml index 52a2da8da..894099720 100644 --- a/test-server/Cargo.toml +++ b/test-server/Cargo.toml @@ -32,12 +32,12 @@ openssl = ["open-ssl", "awc/openssl"] [dependencies] actix-service = "1.0.1" actix-codec = "0.2.0" -actix-connect = "1.0.0" +actix-connect = "2.0.0-alpha.1" actix-utils = "1.0.3" actix-rt = "1.0.0" actix-server = "1.0.0" actix-testing = "1.0.0" -awc = "1.0.0" +awc = "1.0.1" base64 = "0.11" bytes = "0.5.3" @@ -51,9 +51,9 @@ serde_json = "1.0" sha1 = "0.6" slab = "0.4" serde_urlencoded = "0.6.1" -time = "0.1" +time = { version = "0.2.7", default-features = false, features = ["std"] } open-ssl = { version="0.10", package="openssl", optional = true } [dev-dependencies] -actix-web = "2.0.0-rc" -actix-http = "1.0.1" +actix-web = "2.0.0" +actix-http = "2.0.0-alpha.2" diff --git a/tests/test_weird_poll.rs b/tests/test_weird_poll.rs new file mode 100644 index 000000000..571b69f45 --- /dev/null +++ b/tests/test_weird_poll.rs @@ -0,0 +1,30 @@ +// Regression test for #/1321 + +use futures::task::{noop_waker, Context}; +use futures::stream::once; +use actix_http::body::{MessageBody, BodyStream}; +use bytes::Bytes; + +/* +Disable weird poll until actix-web is based on actix-http 2.0.0 + +#[test] +fn weird_poll() { + let (sender, receiver) = futures::channel::oneshot::channel(); + let mut body_stream = Ok(BodyStream::new(once(async { + let x = Box::new(0); + let y = &x; + receiver.await.unwrap(); + let _z = **y; + Ok::<_, ()>(Bytes::new()) + }))); + + let waker = noop_waker(); + let mut context = Context::from_waker(&waker); + + let _ = body_stream.as_mut().unwrap().poll_next(&mut context); + sender.send(()).unwrap(); + let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context); +} + +*/