diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 000000000..606c30de7 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,12 @@ +[alias] +lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo" +lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo" + +# lib checking +ci-check-min = "hack --workspace check --no-default-features" +ci-check-default = "hack --workspace check" +ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check" +ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" + +# testing +ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index d8c6d66ca..bfa124ffd 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,15 +1,8 @@ blank_issues_enabled: true contact_links: - - name: GitHub Discussions - url: https://github.com/actix/actix-web/discussions - about: Actix Web Q&A - - name: Gitter chat (actix-web) - url: https://gitter.im/actix/actix-web - about: Actix Web Q&A - - name: Gitter chat (actix) - url: https://gitter.im/actix/actix - about: Actix (actor framework) Q&A - name: Actix Discord url: https://discord.gg/NWpN5mmg3x about: Actix developer discussion and community chat - + - name: GitHub Discussions + url: https://github.com/actix/actix-web/discussions + about: Actix Web Q&A diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index 42deadf5a..d617cf708 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -8,7 +8,7 @@ PR_TYPE ## PR Checklist - - [ ] Tests for the changes have been added / updated. diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index d19471a16..828d62561 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -1,4 +1,4 @@ -name: Benchmark (Linux) +name: Benchmark on: pull_request: diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 000000000..d9b98a7b8 --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,183 @@ +name: CI + +on: + pull_request: + types: [opened, synchronize, reopened] + push: + branches: [master] + +jobs: + build_and_test: + strategy: + fail-fast: false + matrix: + target: + - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } + - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } + - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } + version: + - 1.52.0 # MSRV + - stable + - nightly + + name: ${{ matrix.target.name }} / ${{ matrix.version }} + runs-on: ${{ matrix.target.os }} + + env: + CI: 1 + CARGO_INCREMENTAL: 0 + VCPKGRS_DYNAMIC: 1 + + steps: + - uses: actions/checkout@v2 + + # install OpenSSL on Windows + # TODO: GitHub actions docs state that OpenSSL is + # already installed on these Windows machines somewhere + - name: Set vcpkg root + if: matrix.target.triple == 'x86_64-pc-windows-msvc' + run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append + - name: Install OpenSSL + if: matrix.target.triple == 'x86_64-pc-windows-msvc' + run: vcpkg install openssl:x64-windows + + - name: Install ${{ matrix.version }} + uses: actions-rs/toolchain@v1 + with: + toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check minimal + uses: actions-rs/cargo@v1 + with: { command: ci-check-min } + + - name: check default + uses: actions-rs/cargo@v1 + with: { command: ci-check-default } + + - name: tests + timeout-minutes: 60 + run: | + cargo test --lib --tests -p=actix-router --all-features + cargo test --lib --tests -p=actix-http --all-features + cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls + cargo test --lib --tests -p=actix-web-codegen --all-features + cargo test --lib --tests -p=awc --all-features + cargo test --lib --tests -p=actix-http-test --all-features + cargo test --lib --tests -p=actix-test --all-features + cargo test --lib --tests -p=actix-files + cargo test --lib --tests -p=actix-multipart --all-features + cargo test --lib --tests -p=actix-web-actors --all-features + + - name: tests (io-uring) + if: matrix.target.os == 'ubuntu-latest' + timeout-minutes: 60 + run: > + sudo bash -c "ulimit -Sl 512 + && ulimit -Hl 512 + && PATH=$PATH:/usr/share/rust/.cargo/bin + && RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features" + + - name: Clear the cargo caches + run: | + cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean + cargo-cache + + ci_feature_powerset_check: + name: Verify Feature Combinations + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Install cargo-hack + uses: actions-rs/cargo@v1 + with: + command: install + args: cargo-hack + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset } + + - name: check feature combinations + uses: actions-rs/cargo@v1 + with: { command: ci-check-all-feature-powerset-linux } + + coverage: + name: coverage + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install stable + uses: actions-rs/toolchain@v1 + with: + toolchain: stable-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.2.0 + + - name: Generate coverage file + if: github.ref == 'refs/heads/master' + run: | + cargo install cargo-tarpaulin --vers "^0.13" + cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose + - name: Upload to Codecov + if: github.ref == 'refs/heads/master' + uses: codecov/codecov-action@v1 + with: { file: cobertura.xml } + + rustdoc: + name: doc tests + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Rust (nightly) + uses: actions-rs/toolchain@v1 + with: + toolchain: nightly-x86_64-unknown-linux-gnu + profile: minimal + override: true + + - name: Generate Cargo.lock + uses: actions-rs/cargo@v1 + with: { command: generate-lockfile } + - name: Cache Dependencies + uses: Swatinem/rust-cache@v1.3.0 + + - name: doc tests + uses: actions-rs/cargo@v1 + timeout-minutes: 60 + with: { command: ci-doctest } diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml index fb1ed7f32..957256d32 100644 --- a/.github/workflows/clippy-fmt.yml +++ b/.github/workflows/clippy-fmt.yml @@ -1,32 +1,39 @@ +name: Lint + on: pull_request: types: [opened, synchronize, reopened] -name: Clippy and rustfmt Check jobs: - clippy_check: + fmt: runs-on: ubuntu-latest steps: - uses: actions/checkout@v2 - - uses: actions-rs/toolchain@v1 + - name: Install Rust + uses: actions-rs/toolchain@v1 with: toolchain: stable components: rustfmt - override: true - name: Check with rustfmt uses: actions-rs/cargo@v1 with: command: fmt args: --all -- --check - - uses: actions-rs/toolchain@v1 + clippy: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + + - name: Install Rust + uses: actions-rs/toolchain@v1 with: - toolchain: nightly + toolchain: stable components: clippy override: true - name: Check with Clippy uses: actions-rs/clippy-check@v1 with: token: ${{ secrets.GITHUB_TOKEN }} - args: --all-features --all --tests + args: --workspace --all-features --tests diff --git a/.github/workflows/linux.yml b/.github/workflows/linux.yml deleted file mode 100644 index 53f22df63..000000000 --- a/.github/workflows/linux.yml +++ /dev/null @@ -1,81 +0,0 @@ -name: CI (Linux) - -on: - pull_request: - types: [opened, synchronize, reopened] - push: - branches: - - master - -jobs: - build_and_test: - strategy: - fail-fast: false - matrix: - version: - - 1.46.0 # MSRV - - stable - - nightly - - name: ${{ matrix.version }} - x86_64-unknown-linux-gnu - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - 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 Dependencies - uses: Swatinem/rust-cache@v1.0.1 - - - 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 --vers "^0.13" - cargo tarpaulin --out Xml - - name: Upload to Codecov - if: matrix.version == 'stable' && github.ref == 'refs/heads/master' - uses: codecov/codecov-action@v1 - with: - 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 deleted file mode 100644 index 6b5366faf..000000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -1,56 +0,0 @@ -name: CI (macOS) - -on: - pull_request: - types: [opened, synchronize, reopened] - push: - branches: - - master - -jobs: - build_and_test: - strategy: - fail-fast: false - matrix: - version: - - stable - - nightly - - name: ${{ matrix.version }} - x86_64-apple-darwin - runs-on: macOS-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install ${{ matrix.version }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.version }}-x86_64-apple-darwin - profile: minimal - override: true - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: - command: generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.0.1 - - - name: check build - uses: actions-rs/cargo@v1 - with: - command: check - args: --all --bins --examples --tests - - - name: tests - uses: actions-rs/cargo@v1 - with: - command: test - 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 index ba87a5637..94a2ddfbe 100644 --- a/.github/workflows/upload-doc.yml +++ b/.github/workflows/upload-doc.yml @@ -1,14 +1,12 @@ -name: Upload documentation +name: Upload Documentation on: push: - branches: - - master + branches: [master] jobs: build: runs-on: ubuntu-latest - if: github.repository == 'actix/actix-web' steps: - uses: actions/checkout@v2 @@ -20,14 +18,14 @@ jobs: profile: minimal override: true - - name: check build + - name: Build Docs uses: actions-rs/cargo@v1 with: command: doc - args: --no-deps --workspace --all-features + args: --workspace --all-features --no-deps - name: Tweak HTML - run: echo "" > target/doc/index.html + run: echo '' > target/doc/index.html - name: Deploy to GitHub Pages uses: JamesIves/github-pages-deploy-action@3.7.1 diff --git a/.github/workflows/windows.yml b/.github/workflows/windows.yml deleted file mode 100644 index d3de72a61..000000000 --- a/.github/workflows/windows.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: CI (Windows) - -on: - pull_request: - types: [opened, synchronize, reopened] - push: - branches: - - master - -env: - VCPKGRS_DYNAMIC: 1 - -jobs: - build_and_test: - strategy: - fail-fast: false - matrix: - version: - - stable - - nightly - - name: ${{ matrix.version }} - x86_64-pc-windows-msvc - runs-on: windows-latest - - steps: - - uses: actions/checkout@v2 - - - name: Install ${{ matrix.version }} - uses: actions-rs/toolchain@v1 - with: - toolchain: ${{ matrix.version }}-x86_64-pc-windows-msvc - profile: minimal - override: true - - - name: Install OpenSSL - 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: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: - command: generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.0.1 - - - name: check build - uses: actions-rs/cargo@v1 - with: - command: check - args: --all --bins --examples --tests - - - name: tests - uses: actions-rs/cargo@v1 - with: - command: test - args: --all --all-features --no-fail-fast -- --nocapture - --skip=test_h2_content_length - --skip=test_reading_deflate_encoding_large_random_rustls - --skip=test_params - --skip=test_simple - --skip=test_expect_continue - --skip=test_http10_keepalive - --skip=test_slow_request - --skip=test_connection_force_close - --skip=test_connection_server_close - --skip=test_connection_wait_queue_force_close - - - name: Clear the cargo caches - run: | - cargo install cargo-cache --no-default-features --features ci-autoclean - cargo-cache diff --git a/.gitignore b/.gitignore index 638a4397a..543403267 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,6 @@ guide/build/ # Configuration directory generated by CLion .idea + +# Configuration directory generated by VSCode +.vscode diff --git a/CHANGES.md b/CHANGES.md index a6bcd56cc..2b5a71001 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -2,18 +2,226 @@ ## Unreleased - 2021-xx-xx ### Added +* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] +* `AcceptEncoding` typed header. [#2482] +* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] +* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468] + +### Changed +* Rename `Accept::{mime_precedence => ranked}`. [#2480] +* Rename `Accept::{mime_preference => preference}`. [#2480] +* Un-deprecate `App::data_factory`. [#2484] + +### Fixed +* Accept wildcard `*` items in `AcceptLanguage`. [#2480] +* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] +* Typed headers containing lists that require one or more items now enforce this minimum. [#2482] + +[#2468]: https://github.com/actix/actix-web/pull/2468 +[#2480]: https://github.com/actix/actix-web/pull/2480 +[#2482]: https://github.com/actix/actix-web/pull/2482 +[#2484]: https://github.com/actix/actix-web/pull/2484 + + +## 4.0.0-beta.13 - 2021-11-30 +### Changed +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + +## 4.0.0-beta.12 - 2021-11-22 +### Changed +* Compress middleware's response type is now `AnyBody>`. [#2448] + +### Fixed +* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] + +### Removed +* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] + +[#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 + + +## 4.0.0-beta.11 - 2021-11-15 +### Added +* Re-export `dev::ServerHandle` from `actix-server`. [#2442] + +### Changed +* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] +* Update `actix-server` to `2.0.0-beta.9`. [#2442] + +[#2423]: https://github.com/actix/actix-web/pull/2423 +[#2442]: https://github.com/actix/actix-web/pull/2442 + + +## 4.0.0-beta.10 - 2021-10-20 +### Added +* Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] +* `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] + +### Changed +* Associated type `FromRequest::Config` was removed. [#2233] +* Inner field made private on `web::Payload`. [#2384] +* `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] +* Updated rustls to v0.20. [#2414] +* Minimum supported Rust version (MSRV) is now 1.52. + +### Removed +* Useless `ServiceResponse::checked_expr` method. [#2401] + +[#2233]: https://github.com/actix/actix-web/pull/2233 +[#2362]: https://github.com/actix/actix-web/pull/2362 +[#2384]: https://github.com/actix/actix-web/pull/2384 +[#2401]: https://github.com/actix/actix-web/pull/2401 +[#2403]: https://github.com/actix/actix-web/pull/2403 +[#2409]: https://github.com/actix/actix-web/pull/2409 +[#2414]: https://github.com/actix/actix-web/pull/2414 + + +## 4.0.0-beta.9 - 2021-09-09 +### Added +* Re-export actix-service `ServiceFactory` in `dev` module. [#2325] + +### Changed +* Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] +* Move `BaseHttpResponse` to `dev::Response`. [#2379] +* Enable `TestRequest::param` to accept more than just static strings. [#2172] +* Minimum supported Rust version (MSRV) is now 1.51. + +### Fixed +* Fix quality parse error in Accept-Encoding header. [#2344] +* Re-export correct type at `web::HttpResponse`. [#2379] + +[#2172]: https://github.com/actix/actix-web/pull/2172 +[#2325]: https://github.com/actix/actix-web/pull/2325 +[#2344]: https://github.com/actix/actix-web/pull/2344 +[#2379]: https://github.com/actix/actix-web/pull/2379 + + +## 4.0.0-beta.8 - 2021-06-26 +### Added +* Add `ServiceRequest::parts_mut`. [#2177] +* Add extractors for `Uri` and `Method`. [#2263] +* Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] +* Add `Route::service` for using hand-written services as handlers. [#2262] + +### Changed +* Change compression algorithm features flags. [#2250] +* Deprecate `App::data` and `App::data_factory`. [#2271] +* Smarter extraction of `ConnectionInfo` parts. [#2282] + +### Fixed +* Scope and Resource middleware can access data items set on their own layer. [#2288] + +[#2177]: https://github.com/actix/actix-web/pull/2177 +[#2250]: https://github.com/actix/actix-web/pull/2250 +[#2271]: https://github.com/actix/actix-web/pull/2271 +[#2262]: https://github.com/actix/actix-web/pull/2262 +[#2263]: https://github.com/actix/actix-web/pull/2263 +[#2282]: https://github.com/actix/actix-web/pull/2282 +[#2288]: https://github.com/actix/actix-web/pull/2288 + + +## 4.0.0-beta.7 - 2021-06-17 +### Added +* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] + +### Changed +* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] +[#2162]: (https://github.com/actix/actix-web/pull/2162) +* `ServiceResponse::error_response` now uses body type of `Body`. [#2201] +* `ServiceResponse::checked_expr` now returns a `Result`. [#2201] +* Update `language-tags` to `0.3`. +* `ServiceResponse::take_body`. [#2201] +* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody` types. [#2201] +* All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] +* All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] +* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] +* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] + +### Removed +* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] + +[#2200]: https://github.com/actix/actix-web/pull/2200 +[#2201]: https://github.com/actix/actix-web/pull/2201 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2246]: https://github.com/actix/actix-web/pull/2246 + + +## 4.0.0-beta.6 - 2021-04-17 +### Added +* `HttpResponse` and `HttpResponseBuilder` structs. [#2065] + +### Changed +* Most error types are now marked `#[non_exhaustive]`. [#2148] +* Methods on `ContentDisposition` that took `T: AsRef` now take `impl AsRef`. + +[#2065]: https://github.com/actix/actix-web/pull/2065 +[#2148]: https://github.com/actix/actix-web/pull/2148 + + +## 4.0.0-beta.5 - 2021-04-02 +### Added +* `Header` extractor for extracting common HTTP headers in handlers. [#2094] +* Added `TestServer::client_headers` method. [#2097] + +### Fixed +* Double ampersand in Logger format is escaped correctly. [#2067] + +### Changed +* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed + instead of skipping. (Only the first error is kept when multiple error occur) [#2093] + +### Removed +* The `client` mod was removed. Clients should now use `awc` directly. + [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) +* Integration testing was moved to new `actix-test` crate. Namely these items from the `test` + module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] + +[#2067]: https://github.com/actix/actix-web/pull/2067 +[#2093]: https://github.com/actix/actix-web/pull/2093 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2097]: https://github.com/actix/actix-web/pull/2097 +[#2112]: https://github.com/actix/actix-web/pull/2112 + + +## 4.0.0-beta.4 - 2021-03-09 +### Changed +* Feature `cookies` is now optional and enabled by default. [#1981] +* `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default + behaviour of the `web::Json` extractor. [#2010] + +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#2010]: https://github.com/actix/actix-web/pull/2010 + + +## 4.0.0-beta.3 - 2021-02-10 +* Update `actix-web-codegen` to `0.5.0-beta.1`. + + +## 4.0.0-beta.2 - 2021-02-10 +### Added * The method `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] +* Add `services!` macro for helping register multiple services to `App`. [#1933] +* Enable registering a vec of services of the same type to `App` [#1933] ### Changed * Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. - Making it more simple and performant. [#1891] -* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` would not fail. - `ServiceRequest::from_request` would not fail and no payload would be generated [#1893] + Making it simpler and more performant. [#1891] +* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] +* `ServiceRequest::from_request` can no longer fail. [#1893] * Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] +* `test::{call_service, read_response, read_response_json, send_request}` take `&Service` + in argument [#1905] +* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure + argument. [#1905] +* `web::block` no longer requires the output is a Result. [#1957] ### Fixed -* Multiple calls `App::data` with the same type now keeps the latest call's data. [#1906] +* Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906] ### Removed * Public field of `web::Path` has been made private. [#1894] @@ -27,7 +235,10 @@ [#1893]: https://github.com/actix/actix-web/pull/1893 [#1894]: https://github.com/actix/actix-web/pull/1894 [#1869]: https://github.com/actix/actix-web/pull/1869 +[#1905]: https://github.com/actix/actix-web/pull/1905 [#1906]: https://github.com/actix/actix-web/pull/1906 +[#1933]: https://github.com/actix/actix-web/pull/1933 +[#1957]: https://github.com/actix/actix-web/pull/1957 ## 4.0.0-beta.1 - 2021-01-07 @@ -144,7 +355,7 @@ ## 3.0.0-beta.4 - 2020-09-09 ### Added -* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing +* `middleware::NormalizePath` now has configurable behavior for either always having a trailing slash, or as the new addition, always trimming trailing slashes. [#1639] ### Changed @@ -472,7 +683,7 @@ ## [1.0.0-rc] - 2019-05-18 -### Add +### Added * Add `Query::from_query()` to extract parameters from a query string. #846 * `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. @@ -488,7 +699,7 @@ ## [1.0.0-beta.4] - 2019-05-12 -### Add +### Added * Allow to set/override app data on scope level @@ -514,7 +725,7 @@ * CORS handling without headers #702 -* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types. +* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. ### Fixed @@ -578,7 +789,7 @@ ### Changed -* Allow to use any service as default service. +* Allow using any service as default service. * Remove generic type for request payload, always use default. @@ -641,13 +852,13 @@ ### Added -* rustls support +* Rustls support ### Changed -* use forked cookie +* Use forked cookie -* multipart::Field renamed to MultipartField +* Multipart::Field renamed to MultipartField ## [1.0.0-alpha.1] - 2019-03-28 diff --git a/Cargo.toml b/Cargo.toml index bae6cb6cb..425bdbbb3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,121 +1,130 @@ [package] name = "actix-web" -version = "4.0.0-beta.1" +version = "4.0.0-beta.13" authors = ["Nikolay Kim "] description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" -readme = "README.md" keywords = ["actix", "http", "web", "framework", "async"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket" +] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-web/" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] -features = ["openssl", "rustls", "compress", "secure-cookies"] - -[badges] -travis-ci = { repository = "actix/actix-web", branch = "master" } -codecov = { repository = "actix/actix-web", branch = "master", service = "github" } +# features that docs.rs will build with +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] +rustdoc-args = ["--cfg", "docsrs"] [lib] name = "actix_web" path = "src/lib.rs" [workspace] +resolver = "2" members = [ - ".", - "awc", - "actix-http", - "actix-files", - "actix-multipart", - "actix-web-actors", - "actix-web-codegen", - "actix-http-test", + ".", + "awc", + "actix-http", + "actix-files", + "actix-multipart", + "actix-web-actors", + "actix-web-codegen", + "actix-http-test", + "actix-test", + "actix-router", ] [features] -default = ["compress"] +default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] -# content-encoding support -compress = ["actix-http/compress", "awc/compress"] +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] -# sessions feature -secure-cookies = ["actix-http/secure-cookies"] +# support for cookies +cookies = ["cookie"] + +# secure cookies feature +secure-cookies = ["cookie/secure"] # openssl -openssl = ["actix-tls/accept", "actix-tls/openssl", "awc/openssl", "open-ssl"] +openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] # rustls -rustls = ["actix-tls/accept", "actix-tls/rustls", "awc/rustls", "rust-tls"] +rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] -[[example]] -name = "basic" -required-features = ["compress"] +# Internal (PRIVATE!) features used to aid testing and checking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] -[[example]] -name = "uds" -required-features = ["compress"] - -[[test]] -name = "test_server" -required-features = ["compress"] - -[[example]] -name = "on_connect" -required-features = [] - -[[example]] -name = "client" -required-features = ["rustls"] +# io-uring feature only avaiable for Linux OSes. +experimental-io-uring = ["actix-server/io-uring"] [dependencies] -actix-codec = "0.4.0-beta.1" -actix-macros = "0.1.0" -actix-router = "0.2.4" -actix-rt = "2.0.0-beta.2" -actix-server = "2.0.0-beta.2" -actix-service = "2.0.0-beta.3" -actix-utils = "3.0.0-beta.1" -actix-tls = { version = "3.0.0-beta.2", default-features = false, optional = true } +actix-codec = "0.4.1" +actix-macros = "0.2.3" +actix-rt = "2.3" +actix-server = "2.0.0-beta.9" +actix-service = "2.0.0" +actix-utils = "3.0.0" +actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } -actix-web-codegen = "0.4.0" -actix-http = "3.0.0-beta.1" -awc = { version = "3.0.0-beta.1", default-features = false } +actix-http = "3.0.0-beta.14" +actix-router = "0.5.0-beta.2" +actix-web-codegen = "0.5.0-beta.5" -ahash = "0.6" +ahash = "0.7" bytes = "1" +cfg-if = "1" +cookie = { version = "0.15", features = ["percent-encode"], optional = true } derive_more = "0.99.5" either = "1.5.3" encoding_rs = "0.8" futures-core = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false } +itoa = "0.4" +language-tags = "0.3" +once_cell = "1.5" log = "0.4" mime = "0.3" -socket2 = "0.3.16" -pin-project = "1.0.0" +paste = "1" +pin-project-lite = "0.2.7" regex = "1.4" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" serde_urlencoded = "0.7" -time = { version = "0.2.7", default-features = false, features = ["std"] } +smallvec = "1.6.1" +socket2 = "0.4.0" +time = { version = "0.3", default-features = false, features = ["formatting"] } url = "2.1" -open-ssl = { package = "openssl", version = "0.10", optional = true } -rust-tls = { package = "rustls", version = "0.19.0", optional = true } -smallvec = "1.6" [dev-dependencies] -actix = "0.11.0-beta.1" -actix-http = { version = "3.0.0-beta.1", features = ["actors"] } -rand = "0.8" -env_logger = "0.8" -serde_derive = "1.0" +actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } +awc = { version = "3.0.0-beta.11", features = ["openssl"] } + brotli2 = "0.3.2" +criterion = { version = "0.3", features = ["html_reports"] } +env_logger = "0.9" flate2 = "1.0.13" -criterion = "0.3" +futures-util = { version = "0.3.7", default-features = false, features = ["std"] } +rand = "0.8" +rcgen = "0.8" +rustls-pemfile = "0.2" +tls-openssl = { package = "openssl", version = "0.10.9" } +tls-rustls = { package = "rustls", version = "0.20.0" } +zstd = "0.9" + +[profile.dev] +# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. +debug = 0 [profile.release] lto = true @@ -123,15 +132,42 @@ opt-level = 3 codegen-units = 1 [patch.crates-io] -actix-web = { path = "." } +actix-files = { path = "actix-files" } actix-http = { path = "actix-http" } actix-http-test = { path = "actix-http-test" } +actix-multipart = { path = "actix-multipart" } +actix-router = { path = "actix-router" } +actix-test = { path = "actix-test" } +actix-web = { path = "." } actix-web-actors = { path = "actix-web-actors" } actix-web-codegen = { path = "actix-web-codegen" } -actix-multipart = { path = "actix-multipart" } -actix-files = { path = "actix-files" } awc = { path = "awc" } +# uncomment for quick testing against local actix-net repo +# actix-service = { path = "../actix-net/actix-service" } +# actix-macros = { path = "../actix-net/actix-macros" } +# actix-rt = { path = "../actix-net/actix-rt" } +# actix-codec = { path = "../actix-net/actix-codec" } +# actix-utils = { path = "../actix-net/actix-utils" } +# actix-tls = { path = "../actix-net/actix-tls" } +# actix-server = { path = "../actix-net/actix-server" } + +[[test]] +name = "test_server" +required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] + +[[example]] +name = "basic" +required-features = ["compress-gzip"] + +[[example]] +name = "uds" +required-features = ["compress-gzip"] + +[[example]] +name = "on_connect" +required-features = [] + [[bench]] name = "server" harness = false diff --git a/LICENSE-MIT b/LICENSE-MIT index 95938ef15..d559b1cd1 100644 --- a/LICENSE-MIT +++ b/LICENSE-MIT @@ -1,4 +1,4 @@ -Copyright (c) 2017 Actix Team +Copyright (c) 2017-NOW Actix Team Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated diff --git a/MIGRATION.md b/MIGRATION.md index e01702868..d53bd7bf8 100644 --- a/MIGRATION.md +++ b/MIGRATION.md @@ -3,13 +3,28 @@ * The default `NormalizePath` behavior now strips trailing slashes by default. This was previously documented to be the case in v3 but the behavior now matches. The effect is that routes defined with trailing slashes will become inaccessible when - using `NormalizePath::default()`. + using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning. + It is advised that the `new` method be used instead. - Before: `#[get("/test/")` - After: `#[get("/test")` + Before: `#[get("/test/")]` + After: `#[get("/test")]` Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. +* The `type Config` of `FromRequest` was removed. + +* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). + By default all compression algorithms are enabled. + To select algorithm you want to include with `middleware::Compress` use following flags: + - `compress-brotli` + - `compress-gzip` + - `compress-zstd` + If you have set in your `Cargo.toml` dedicated `actix-web` features and you still want + to have compression enabled. Please change features selection like bellow: + + Before: `"compress"` + After: `"compress-brotli", "compress-gzip", "compress-zstd"` + ## 3.0.0 diff --git a/README.md b/README.md index 956bb3741..c363ece9b 100644 --- a/README.md +++ b/README.md @@ -1,20 +1,19 @@
-

Actix web

+

Actix Web

Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust

[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) -[![Documentation](https://docs.rs/actix-web/badge.svg?version=3.3.2)](https://docs.rs/actix-web/3.3.2) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) -![License](https://img.shields.io/crates/l/actix-web.svg) -[![Dependency Status](https://deps.rs/crate/actix-web/3.3.2/status.svg)](https://deps.rs/crate/actix-web/3.3.2) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.13)](https://docs.rs/actix-web/4.0.0-beta.13) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) +[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.13/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.13)
-[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) +[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) -[![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +![downloads](https://img.shields.io/crates/d/actix-web.svg) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)

@@ -26,15 +25,14 @@ * Streaming and pipelining * Keep-alive and slow requests handling * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -* Transparent content compression/decompression (br, gzip, deflate) +* Transparent content compression/decompression (br, gzip, deflate, zstd) * Powerful [request routing](https://actix.rs/docs/url-dispatch/) * Multipart streams * Static assets * SSL support using OpenSSL or Rustls * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) -* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) -* Supports [Actix actor framework](https://github.com/actix/actix) -* Runs on stable Rust 1.46+ +* Includes an async [HTTP client](https://docs.rs/awc/) +* Runs on stable Rust 1.52+ ## Documentation @@ -73,18 +71,18 @@ async fn main() -> std::io::Result<()> { ### More examples -* [Basic Setup](https://github.com/actix/examples/tree/master/basics/) -* [Application State](https://github.com/actix/examples/tree/master/state/) -* [JSON Handling](https://github.com/actix/examples/tree/master/json/) -* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/) -* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/) -* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/) -* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/) -* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/) -* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/) -* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/) -* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/) -* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/) +* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/) +* [Application State](https://github.com/actix/examples/tree/master/basics/state/) +* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/) +* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/) +* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/) +* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/) +* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/) +* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/) +* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/) +* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/) +* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/) +* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/) You may consider checking out [this directory](https://github.com/actix/examples/tree/master/) for more examples. @@ -92,16 +90,16 @@ You may consider checking out ## Benchmarks One of the fastest web frameworks available according to the -[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19). +[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite). ## License This project is licensed under either of * Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or - [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) + [http://www.apache.org/licenses/LICENSE-2.0]) * MIT license ([LICENSE-MIT](LICENSE-MIT) or - [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) + [http://opensource.org/licenses/MIT]) at your option. diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 9f606dfcd..63d8efc3f 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,9 +1,65 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 0.6.0-beta.9 - 2021-11-22 +* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] +* Add `NamedFile::open_async`. [#2408] +* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] +* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408] +* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408] +* Add `impl Clone` for `FilesService`. [#2408] + +[#2408]: https://github.com/actix/actix-web/pull/2408 +[#2453]: https://github.com/actix/actix-web/pull/2453 + + +## 0.6.0-beta.8 - 2021-10-20 +* Minimum supported Rust version (MSRV) is now 1.52. + + +## 0.6.0-beta.7 - 2021-09-09 +* Minimum supported Rust version (MSRV) is now 1.51. + + +## 0.6.0-beta.6 - 2021-06-26 +* Added `Files::path_filter()`. [#2274] +* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] + +[#2274]: https://github.com/actix/actix-web/pull/2274 +[#2228]: https://github.com/actix/actix-web/pull/2228 + + +## 0.6.0-beta.5 - 2021-06-17 +* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] +* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] +* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] +* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257] + +[#2135]: https://github.com/actix/actix-web/pull/2135 +[#2156]: https://github.com/actix/actix-web/pull/2156 +[#2225]: https://github.com/actix/actix-web/pull/2225 +[#2257]: https://github.com/actix/actix-web/pull/2257 + + +## 0.6.0-beta.4 - 2021-04-02 +* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] + +[#2046]: https://github.com/actix/actix-web/pull/2046 + + +## 0.6.0-beta.3 - 2021-03-09 +* No notable changes. + + +## 0.6.0-beta.2 - 2021-02-10 * Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] +* Replace `v_htmlescape` with `askama_escape`. [#1953] [#1887]: https://github.com/actix/actix-web/pull/1887 +[#1953]: https://github.com/actix/actix-web/pull/1953 + ## 0.6.0-beta.1 - 2021-01-07 * `HttpRange::parse` now has its own error type. diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index bde2cb717..6b6d6d245 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,13 +1,15 @@ [package] name = "actix-files" -version = "0.6.0-beta.1" -authors = ["Nikolay Kim "] +version = "0.6.0-beta.9" +authors = [ + "Nikolay Kim ", + "fakeshadow <24548779@qq.com>", + "Rob Ede ", +] description = "Static file serving for Actix Web" -readme = "README.md" keywords = ["actix", "http", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-files/" +repository = "https://github.com/actix/actix-web" categories = ["asynchronous", "web-programming::http-server"] license = "MIT OR Apache-2.0" edition = "2018" @@ -16,20 +18,30 @@ edition = "2018" name = "actix_files" path = "src/lib.rs" +[features] +experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] + [dependencies] -actix-web = { version = "4.0.0-beta.1", default-features = false } -actix-service = "2.0.0-beta.3" +actix-web = { version = "4.0.0-beta.11", default-features = false } +actix-http = "3.0.0-beta.14" +actix-service = "2" +actix-utils = "3" + +askama_escape = "0.10" bitflags = "1" bytes = "1" -futures-core = { version = "0.3.7", default-features = false } -futures-util = { version = "0.3.7", default-features = false } +futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } +http-range = "0.1.4" derive_more = "0.99.5" log = "0.4" mime = "0.3" mime_guess = "2.0.1" percent-encoding = "2.1" -v_htmlescape = "0.12" +pin-project-lite = "0.2.7" + +tokio-uring = { version = "0.1", optional = true } [dev-dependencies] -actix-rt = "2.0.0-beta.2" -actix-web = "4.0.0-beta.1" +actix-rt = "2.2" +actix-web = "4.0.0-beta.11" +actix-test = "0.1.0-beta.7" diff --git a/actix-files/README.md b/actix-files/README.md index 463f20224..84e556fa9 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -3,17 +3,16 @@ > Static file serving for Actix Web [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) -[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.5.0)](https://docs.rs/actix-files/0.5.0) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.9)](https://docs.rs/actix-files/0.6.0-beta.9) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.5.0/status.svg)](https://deps.rs/crate/actix-files/0.5.0) +[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.9/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.9) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-files/) -- [Example Project](https://github.com/actix/examples/tree/master/static_index) -- [Chat on Gitter](https://gitter.im/actix/actix-web) -- Minimum supported Rust version: 1.46 or later +- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-files/src/chunked.rs b/actix-files/src/chunked.rs index 5b7b17dc4..68221ccc3 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -1,86 +1,277 @@ use std::{ cmp, fmt, - fs::File, future::Future, - io::{self, Read, Seek}, + io, pin::Pin, task::{Context, Poll}, }; -use actix_web::{ - error::{Error, ErrorInternalServerError}, - rt::task::{spawn_blocking, JoinHandle}, -}; -use bytes::Bytes; +use actix_web::{error::Error, web::Bytes}; use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; -#[doc(hidden)] -/// A helper created from a `std::fs::File` which reads the file -/// chunk-by-chunk on a `ThreadPool`. -pub struct ChunkedReadFile { - pub(crate) size: u64, - pub(crate) offset: u64, - pub(crate) file: Option, - pub(crate) fut: Option>>, - pub(crate) counter: u64, +use super::named::File; + +pin_project! { + /// Adapter to read a `std::file::File` in chunks. + #[doc(hidden)] + pub struct ChunkedReadFile { + size: u64, + offset: u64, + #[pin] + state: ChunkedReadFileState, + counter: u64, + callback: F, + } } -impl fmt::Debug for ChunkedReadFile { +#[cfg(not(feature = "experimental-io-uring"))] +pin_project! { + #[project = ChunkedReadFileStateProj] + #[project_replace = ChunkedReadFileStateProjReplace] + enum ChunkedReadFileState { + File { file: Option, }, + Future { #[pin] fut: Fut }, + } +} + +#[cfg(feature = "experimental-io-uring")] +pin_project! { + #[project = ChunkedReadFileStateProj] + #[project_replace = ChunkedReadFileStateProjReplace] + enum ChunkedReadFileState { + File { file: Option<(File, BytesMut)> }, + Future { #[pin] fut: Fut }, + } +} + +impl fmt::Debug for ChunkedReadFile { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("ChunkedReadFile") } } -impl Stream for ChunkedReadFile { +pub(crate) fn new_chunked_read( + size: u64, + offset: u64, + file: File, +) -> impl Stream> { + ChunkedReadFile { + size, + offset, + #[cfg(not(feature = "experimental-io-uring"))] + state: ChunkedReadFileState::File { file: Some(file) }, + #[cfg(feature = "experimental-io-uring")] + state: ChunkedReadFileState::File { + file: Some((file, BytesMut::new())), + }, + counter: 0, + callback: chunked_read_file_callback, + } +} + +#[cfg(not(feature = "experimental-io-uring"))] +async fn chunked_read_file_callback( + mut file: File, + offset: u64, + max_bytes: usize, +) -> Result<(File, Bytes), Error> { + use io::{Read as _, Seek as _}; + + let res = actix_web::rt::task::spawn_blocking(move || { + let mut buf = Vec::with_capacity(max_bytes); + + file.seek(io::SeekFrom::Start(offset))?; + + let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; + + if n_bytes == 0 { + Err(io::Error::from(io::ErrorKind::UnexpectedEof)) + } else { + Ok((file, Bytes::from(buf))) + } + }) + .await + .map_err(|_| actix_web::error::BlockingError)??; + + Ok(res) +} + +#[cfg(feature = "experimental-io-uring")] +async fn chunked_read_file_callback( + file: File, + offset: u64, + max_bytes: usize, + mut bytes_mut: BytesMut, +) -> io::Result<(File, Bytes, BytesMut)> { + bytes_mut.reserve(max_bytes); + + let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await; + let n_bytes = res?; + + if n_bytes == 0 { + return Err(io::ErrorKind::UnexpectedEof.into()); + } + + let bytes = bytes_mut.split_to(n_bytes).freeze(); + + Ok((file, bytes, bytes_mut)) +} + +#[cfg(feature = "experimental-io-uring")] +impl Stream for ChunkedReadFile +where + F: Fn(File, u64, usize, BytesMut) -> Fut, + Fut: Future>, +{ type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - if let Some(ref mut fut) = self.fut { - let res = match ready!(Pin::new(fut).poll(cx)) { - Ok(Ok((file, bytes))) => { - self.fut.take(); - self.file = Some(file); + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.as_mut().project(); + match this.state.as_mut().project() { + ChunkedReadFileStateProj::File { file } => { + let size = *this.size; + let offset = *this.offset; + let counter = *this.counter; - self.offset += bytes.len() as u64; - self.counter += bytes.len() as u64; + if size == counter { + Poll::Ready(None) + } else { + let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - Ok(bytes) + let (file, bytes_mut) = file + .take() + .expect("ChunkedReadFile polled after completion"); + + let fut = (this.callback)(file, offset, max_bytes, bytes_mut); + + this.state + .project_replace(ChunkedReadFileState::Future { fut }); + + self.poll_next(cx) } - Ok(Err(e)) => Err(e.into()), - Err(_) => Err(ErrorInternalServerError("Unexpected error")), - }; - return Poll::Ready(Some(res)); - } + } + ChunkedReadFileStateProj::Future { fut } => { + let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?; - let size = self.size; - let offset = self.offset; - let counter = self.counter; + this.state.project_replace(ChunkedReadFileState::File { + file: Some((file, bytes_mut)), + }); - if size == counter { - Poll::Ready(None) - } else { - let mut file = self.file.take().expect("Use after completion"); + *this.offset += bytes.len() as u64; + *this.counter += bytes.len() as u64; - self.fut = Some(spawn_blocking(move || { - let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; - - let mut buf = Vec::with_capacity(max_bytes); - file.seek(io::SeekFrom::Start(offset))?; - - let n_bytes = - file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?; - - if n_bytes == 0 { - return Err(io::ErrorKind::UnexpectedEof.into()); - } - - Ok((file, Bytes::from(buf))) - })); - - self.poll_next(cx) + Poll::Ready(Some(Ok(bytes))) + } + } + } +} + +#[cfg(not(feature = "experimental-io-uring"))] +impl Stream for ChunkedReadFile +where + F: Fn(File, u64, usize) -> Fut, + Fut: Future>, +{ + type Item = Result; + + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.as_mut().project(); + match this.state.as_mut().project() { + ChunkedReadFileStateProj::File { file } => { + let size = *this.size; + let offset = *this.offset; + let counter = *this.counter; + + if size == counter { + Poll::Ready(None) + } else { + let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize; + + let file = file + .take() + .expect("ChunkedReadFile polled after completion"); + + let fut = (this.callback)(file, offset, max_bytes); + + this.state + .project_replace(ChunkedReadFileState::Future { fut }); + + self.poll_next(cx) + } + } + ChunkedReadFileStateProj::Future { fut } => { + let (file, bytes) = ready!(fut.poll(cx))?; + + this.state + .project_replace(ChunkedReadFileState::File { file: Some(file) }); + + *this.offset += bytes.len() as u64; + *this.counter += bytes.len() as u64; + + Poll::Ready(Some(Ok(bytes))) + } + } + } +} + +#[cfg(feature = "experimental-io-uring")] +use bytes_mut::BytesMut; + +// TODO: remove new type and use bytes::BytesMut directly +#[doc(hidden)] +#[cfg(feature = "experimental-io-uring")] +mod bytes_mut { + use std::ops::{Deref, DerefMut}; + + use tokio_uring::buf::{IoBuf, IoBufMut}; + + #[derive(Debug)] + pub struct BytesMut(bytes::BytesMut); + + impl BytesMut { + pub(super) fn new() -> Self { + Self(bytes::BytesMut::new()) + } + } + + impl Deref for BytesMut { + type Target = bytes::BytesMut; + + fn deref(&self) -> &Self::Target { + &self.0 + } + } + + impl DerefMut for BytesMut { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } + } + + unsafe impl IoBuf for BytesMut { + fn stable_ptr(&self) -> *const u8 { + self.0.as_ptr() + } + + fn bytes_init(&self) -> usize { + self.0.len() + } + + fn bytes_total(&self) -> usize { + self.0.capacity() + } + } + + unsafe impl IoBufMut for BytesMut { + fn stable_mut_ptr(&mut self) -> *mut u8 { + self.0.as_mut_ptr() + } + + unsafe fn set_init(&mut self, init_len: usize) { + if self.len() < init_len { + self.0.set_len(init_len); + } } } } diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs index 3717985d3..80e0c98d0 100644 --- a/actix-files/src/directory.rs +++ b/actix-files/src/directory.rs @@ -1,8 +1,8 @@ use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf}; use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; +use askama_escape::{escape as escape_html_entity, Html}; use percent_encoding::{utf8_percent_encode, CONTROLS}; -use v_htmlescape::escape as escape_html_entity; /// A directory; responds with the generated directory listing. #[derive(Debug)] @@ -50,7 +50,7 @@ macro_rules! encode_file_url { // " -- " & -- & ' -- ' < -- < > -- > / -- / macro_rules! encode_file_name { ($entry:ident) => { - escape_html_entity(&$entry.file_name().to_string_lossy()) + escape_html_entity(&$entry.file_name().to_string_lossy(), Html) }; } @@ -66,9 +66,7 @@ pub(crate) fn directory_listing( if dir.is_visible(&entry) { let entry = entry.unwrap(); let p = match entry.path().strip_prefix(&dir.path) { - Ok(p) if cfg!(windows) => { - base.join(p).to_string_lossy().replace("\\", "/") - } + Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"), Ok(p) => base.join(p).to_string_lossy().into_owned(), Err(_) => continue, }; diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index 9b30cbaa2..f8e32eef7 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -1,4 +1,4 @@ -use actix_web::{http::StatusCode, HttpResponse, ResponseError}; +use actix_web::{http::StatusCode, ResponseError}; use derive_more::Display; /// Errors which can occur when serving static files. @@ -16,11 +16,12 @@ pub enum FilesError { /// Return `NotFound` for `FilesError` impl ResponseError for FilesError { - fn error_response(&self) -> HttpResponse { - HttpResponse::new(StatusCode::NOT_FOUND) + fn status_code(&self) -> StatusCode { + StatusCode::NOT_FOUND } } +#[allow(clippy::enum_variant_names)] #[derive(Display, Debug, PartialEq)] pub enum UriSegmentError { /// The segment started with the wrapped invalid character. diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index 0cf0a91ba..06909bf08 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -1,27 +1,34 @@ -use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; +use std::{ + cell::RefCell, + fmt, io, + path::{Path, PathBuf}, + rc::Rc, +}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_web::{ dev::{ - AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse, + AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, + ServiceResponse, }, error::Error, guard::Guard, http::header::DispositionType, HttpRequest, }; -use futures_util::future::{ok, FutureExt, LocalBoxFuture}; +use futures_core::future::LocalBoxFuture; use crate::{ - directory_listing, named, Directory, DirectoryRenderer, FilesService, - HttpNewService, MimeOverride, + directory_listing, named, + service::{FilesService, FilesServiceInner}, + Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter, }; /// Static files handling service. /// /// `Files` service must be registered with `App::service()` method. /// -/// ```rust +/// ``` /// use actix_web::App; /// use actix_files::Files; /// @@ -37,8 +44,10 @@ pub struct Files { default: Rc>>>, renderer: Rc, mime_override: Option>, + path_filter: Option>, file_flags: named::Flags, - guards: Option>, + use_guards: Option>, + guards: Vec>, hidden_files: bool, } @@ -60,6 +69,8 @@ impl Clone for Files { file_flags: self.file_flags, path: self.path.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), + use_guards: self.use_guards.clone(), guards: self.guards.clone(), hidden_files: self.hidden_files, } @@ -81,10 +92,9 @@ impl Files { /// If the mount path is set as the root path `/`, services registered after this one will /// be inaccessible. Register more specific handlers and services first. /// - /// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a - /// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are - /// adjusted with work load. More threads would spawn when need and threads goes idle for a - /// period of time would be de-spawned. + /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations. + /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times + /// the number of server [workers](actix_web::HttpServer::workers), by default. pub fn new>(mount_path: &str, serve_from: T) -> Files { let orig_dir = serve_from.into(); let dir = match orig_dir.canonicalize() { @@ -96,7 +106,7 @@ impl Files { }; Files { - path: mount_path.to_owned(), + path: mount_path.trim_end_matches('/').to_owned(), directory: dir, index: None, show_index: false, @@ -104,8 +114,10 @@ impl Files { default: Rc::new(RefCell::new(None)), renderer: Rc::new(directory_listing), mime_override: None, + path_filter: None, file_flags: named::Flags::default(), - guards: None, + use_guards: None, + guards: Vec::new(), hidden_files: false, } } @@ -113,6 +125,9 @@ impl Files { /// Show files listing for directories. /// /// By default show files listing is disabled. + /// + /// When used with [`Files::index_file()`], files listing is shown as a fallback + /// when the index file is not found. pub fn show_files_listing(mut self) -> Self { self.show_index = true; self @@ -129,8 +144,8 @@ impl Files { /// Set custom directory renderer pub fn files_listing_renderer(mut self, f: F) -> Self where - for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result - + 'static, + for<'r, 's> F: + Fn(&'r Directory, &'s HttpRequest) -> Result + 'static, { self.renderer = Rc::new(f); self @@ -145,10 +160,45 @@ impl Files { self } + /// Sets path filtering closure. + /// + /// The path provided to the closure is relative to `serve_from` path. + /// You can safely join this path with the `serve_from` path to get the real path. + /// However, the real path may not exist since the filter is called before checking path existence. + /// + /// When a path doesn't pass the filter, [`Files::default_handler`] is called if set, otherwise, + /// `404 Not Found` is returned. + /// + /// # Examples + /// ``` + /// use std::path::Path; + /// use actix_files::Files; + /// + /// // prevent searching subdirectories and following symlinks + /// let files_service = Files::new("/", "./static").path_filter(|path, _| { + /// path.components().count() == 1 + /// && Path::new("./static") + /// .join(path) + /// .symlink_metadata() + /// .map(|m| !m.file_type().is_symlink()) + /// .unwrap_or(false) + /// }); + /// ``` + pub fn path_filter(mut self, f: F) -> Self + where + F: Fn(&Path, &RequestHead) -> bool + 'static, + { + self.path_filter = Some(Rc::new(f)); + self + } + /// Set index file /// - /// Shows specific index file for directory "/" instead of + /// Shows specific index file for directories instead of /// showing files listing. + /// + /// If the index file is not found, files listing is shown as a fallback if + /// [`Files::show_files_listing()`] is set. pub fn index_file>(mut self, index: T) -> Self { self.index = Some(index.into()); self @@ -157,7 +207,6 @@ impl Files { /// Specifies whether to use ETag or not. /// /// Default is true. - #[inline] pub fn use_etag(mut self, value: bool) -> Self { self.file_flags.set(named::Flags::ETAG, value); self @@ -166,7 +215,6 @@ impl Files { /// Specifies whether to use Last-Modified or not. /// /// Default is true. - #[inline] pub fn use_last_modified(mut self, value: bool) -> Self { self.file_flags.set(named::Flags::LAST_MD, value); self @@ -175,31 +223,80 @@ impl Files { /// Specifies whether text responses should signal a UTF-8 encoding. /// /// Default is false (but will default to true in a future version). - #[inline] pub fn prefer_utf8(mut self, value: bool) -> Self { self.file_flags.set(named::Flags::PREFER_UTF8, value); self } - /// Specifies custom guards to use for directory listings and files. + /// Adds a routing guard. /// - /// Default behaviour allows GET and HEAD. - #[inline] - pub fn use_guards(mut self, guards: G) -> Self { - self.guards = Some(Rc::new(guards)); + /// Use this to allow multiple chained file services that respond to strictly different + /// properties of a request. Due to the way routing works, if a guard check returns true and the + /// request starts being handled by the file service, it will not be able to back-out and try + /// the next service, you will simply get a 404 (or 405) error response. + /// + /// To allow `POST` requests to retrieve files, see [`Files::use_guards`]. + /// + /// # Examples + /// ``` + /// use actix_web::{guard::Header, App}; + /// use actix_files::Files; + /// + /// App::new().service( + /// Files::new("/","/my/site/files") + /// .guard(Header("Host", "example.com")) + /// ); + /// ``` + pub fn guard(mut self, guard: G) -> Self { + self.guards.push(Rc::new(guard)); self } + /// Specifies guard to check before fetching directory listings or files. + /// + /// Note that this guard has no effect on routing; it's main use is to guard on the request's + /// method just before serving the file, only allowing `GET` and `HEAD` requests by default. + /// See [`Files::guard`] for routing guards. + pub fn method_guard(mut self, guard: G) -> Self { + self.use_guards = Some(Rc::new(guard)); + self + } + + #[doc(hidden)] + #[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")] + /// See [`Files::method_guard`]. + pub fn use_guards(self, guard: G) -> Self { + self.method_guard(guard) + } + /// Disable `Content-Disposition` header. /// /// By default Content-Disposition` header is enabled. - #[inline] pub fn disable_content_disposition(mut self) -> Self { self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); self } /// Sets default handler which is used when no matched file could be found. + /// + /// # Examples + /// Setting a fallback static file handler: + /// ``` + /// use actix_files::{Files, NamedFile}; + /// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service}; + /// + /// # fn run() -> Result<(), actix_web::Error> { + /// let files = Files::new("/", "./static") + /// .index_file("index.html") + /// .default_handler(fn_service(|req: ServiceRequest| async { + /// let (req, _) = req.into_parts(); + /// let file = NamedFile::open_async("./static/404.html").await?; + /// let res = file.into_response(&req); + /// Ok(ServiceResponse::new(req, res)) + /// })); + /// # Ok(()) + /// # } + /// ``` pub fn default_handler(mut self, f: F) -> Self where F: IntoServiceFactory, @@ -219,7 +316,6 @@ impl Files { } /// Enables serving hidden files and directories, allowing a leading dots in url fragments. - #[inline] pub fn use_hidden_files(mut self) -> Self { self.hidden_files = true; self @@ -227,7 +323,19 @@ impl Files { } impl HttpServiceFactory for Files { - fn register(self, config: &mut AppService) { + fn register(mut self, config: &mut AppService) { + let guards = if self.guards.is_empty() { + None + } else { + let guards = std::mem::take(&mut self.guards); + Some( + guards + .into_iter() + .map(|guard| -> Box { Box::new(guard) }) + .collect::>(), + ) + }; + if self.default.borrow().is_none() { *self.default.borrow_mut() = Some(config.default_service()); } @@ -238,7 +346,7 @@ impl HttpServiceFactory for Files { ResourceDef::prefix(&self.path) }; - config.register_service(rdef, None, self, None) + config.register_service(rdef, guards, self, None) } } @@ -251,7 +359,7 @@ impl ServiceFactory for Files { type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - let mut srv = FilesService { + let mut inner = FilesServiceInner { directory: self.directory.clone(), index: self.index.clone(), show_index: self.show_index, @@ -259,24 +367,25 @@ impl ServiceFactory for Files { default: None, renderer: self.renderer.clone(), mime_override: self.mime_override.clone(), + path_filter: self.path_filter.clone(), file_flags: self.file_flags, - guards: self.guards.clone(), + guards: self.use_guards.clone(), hidden_files: self.hidden_files, }; if let Some(ref default) = *self.default.borrow() { - default - .new_service(()) - .map(move |result| match result { + let fut = default.new_service(()); + Box::pin(async { + match fut.await { Ok(default) => { - srv.default = Some(default); - Ok(srv) + inner.default = Some(default); + Ok(FilesService(Rc::new(inner))) } Err(_) => Err(()), - }) - .boxed_local() + } + }) } else { - ok(srv).boxed_local() + Box::pin(async move { Ok(FilesService(Rc::new(inner))) }) } } } diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 7b3e9adb5..3af5282f1 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -3,7 +3,7 @@ //! Provides a non-blocking service for serving static files from disk. //! //! # Example -//! ```rust +//! ``` //! use actix_web::App; //! use actix_files::Files; //! @@ -16,11 +16,12 @@ use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ - dev::{ServiceRequest, ServiceResponse}, + dev::{RequestHead, ServiceRequest, ServiceResponse}, error::Error, http::header::DispositionType, }; use mime_guess::from_ext; +use std::path::Path; mod chunked; mod directory; @@ -32,12 +33,12 @@ mod path_buf; mod range; mod service; -pub use crate::chunked::ChunkedReadFile; -pub use crate::directory::Directory; -pub use crate::files::Files; -pub use crate::named::NamedFile; -pub use crate::range::HttpRange; -pub use crate::service::FilesService; +pub use self::chunked::ChunkedReadFile; +pub use self::directory::Directory; +pub use self::files::Files; +pub use self::named::NamedFile; +pub use self::range::HttpRange; +pub use self::service::FilesService; use self::directory::{directory_listing, DirectoryRenderer}; use self::error::FilesError; @@ -56,10 +57,12 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime { type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType; +type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; + #[cfg(test)] mod tests { use std::{ - fs::{self, File}, + fs::{self}, ops::Add, time::{Duration, SystemTime}, }; @@ -73,13 +76,14 @@ mod tests { }, middleware::Compress, test::{self, TestRequest}, - web, App, HttpResponse, Responder, + web::{self, Bytes}, + App, HttpResponse, Responder, }; - use futures_util::future::ok; use super::*; + use crate::named::File; - #[actix_rt::test] + #[actix_web::test] async fn test_file_extension_to_mime() { let m = file_extension_to_mime(""); assert_eq!(m, mime::APPLICATION_OCTET_STREAM); @@ -96,9 +100,8 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_without_if_none_match() { - let file = NamedFile::open("Cargo.toml").unwrap(); - let since = - header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); + let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .insert_header((header::IF_MODIFIED_SINCE, since)) @@ -109,7 +112,7 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_without_if_none_match_same() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = file.last_modified().unwrap(); let req = TestRequest::default() @@ -121,9 +124,8 @@ mod tests { #[actix_rt::test] async fn test_if_modified_since_with_if_none_match() { - let file = NamedFile::open("Cargo.toml").unwrap(); - let since = - header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); + let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let req = TestRequest::default() .insert_header((header::IF_NONE_MATCH, "miss_etag")) @@ -135,7 +137,7 @@ mod tests { #[actix_rt::test] async fn test_if_unmodified_since() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = file.last_modified().unwrap(); let req = TestRequest::default() @@ -147,7 +149,7 @@ mod tests { #[actix_rt::test] async fn test_if_unmodified_since_failed() { - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let since = header::HttpDate::from(SystemTime::UNIX_EPOCH); let req = TestRequest::default() @@ -159,8 +161,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_text() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml").unwrap(); + assert!(NamedFile::open_async("test--").await.is_err()); + let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; @@ -183,8 +185,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_disposition() { - assert!(NamedFile::open("test--").is_err()); - let mut file = NamedFile::open("Cargo.toml").unwrap(); + assert!(NamedFile::open_async("test--").await.is_err()); + let mut file = NamedFile::open_async("Cargo.toml").await.unwrap(); { file.file(); let _f: &File = &file; @@ -200,7 +202,8 @@ mod tests { "inline; filename=\"Cargo.toml\"" ); - let file = NamedFile::open("Cargo.toml") + let file = NamedFile::open_async("Cargo.toml") + .await .unwrap() .disable_content_disposition(); let req = TestRequest::default().to_http_request(); @@ -210,9 +213,19 @@ mod tests { #[actix_rt::test] async fn test_named_file_non_ascii_file_name() { - let mut file = - NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml") - .unwrap(); + let file = { + #[cfg(feature = "experimental-io-uring")] + { + crate::named::File::open("Cargo.toml").await.unwrap() + } + + #[cfg(not(feature = "experimental-io-uring"))] + { + crate::named::File::open("Cargo.toml").unwrap() + } + }; + + let mut file = NamedFile::from_file(file, "貨物.toml").unwrap(); { file.file(); let _f: &File = &file; @@ -235,7 +248,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_set_content_type() { - let mut file = NamedFile::open("Cargo.toml") + let mut file = NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_content_type(mime::TEXT_XML); { @@ -260,7 +274,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_image() { - let mut file = NamedFile::open("tests/test.png").unwrap(); + let mut file = NamedFile::open_async("tests/test.png").await.unwrap(); { file.file(); let _f: &File = &file; @@ -281,13 +295,30 @@ mod tests { ); } + #[actix_rt::test] + async fn test_named_file_javascript() { + let file = NamedFile::open_async("tests/test.js").await.unwrap(); + + let req = TestRequest::default().to_http_request(); + let resp = file.respond_to(&req).await.unwrap(); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "application/javascript" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"test.js\"" + ); + } + #[actix_rt::test] async fn test_named_file_image_attachment() { let cd = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![DispositionParam::Filename(String::from("test.png"))], }; - let mut file = NamedFile::open("tests/test.png") + let mut file = NamedFile::open_async("tests/test.png") + .await .unwrap() .set_content_disposition(cd); { @@ -312,7 +343,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_binary() { - let mut file = NamedFile::open("tests/test.binary").unwrap(); + let mut file = NamedFile::open_async("tests/test.binary").await.unwrap(); { file.file(); let _f: &File = &file; @@ -335,7 +366,8 @@ mod tests { #[actix_rt::test] async fn test_named_file_status_code_text() { - let mut file = NamedFile::open("Cargo.toml") + let mut file = NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_status_code(StatusCode::NOT_FOUND); { @@ -365,7 +397,7 @@ mod tests { DispositionType::Attachment } - let mut srv = test::init_service( + let srv = test::init_service( App::new().service( Files::new("/", ".") .mime_override(all_attachment) @@ -375,7 +407,7 @@ mod tests { .await; let request = TestRequest::get().uri("/").to_request(); - let response = test::call_service(&mut srv, request).await; + let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::OK); let content_disposition = response @@ -390,7 +422,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_ranges_status_code() { - let mut srv = test::init_service( + let srv = test::init_service( App::new().service(Files::new("/test", ".").index_file("Cargo.toml")), ) .await; @@ -400,7 +432,7 @@ mod tests { .uri("/t%65st/Cargo.toml") .insert_header((header::RANGE, "bytes=10-20")) .to_request(); - let response = test::call_service(&mut srv, request).await; + let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT); // Invalid range header @@ -408,14 +440,14 @@ mod tests { .uri("/t%65st/Cargo.toml") .insert_header((header::RANGE, "bytes=1-0")) .to_request(); - let response = test::call_service(&mut srv, request).await; + let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE); } #[actix_rt::test] async fn test_named_file_content_range_headers() { - let srv = test::start(|| App::new().service(Files::new("/", "."))); + let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv @@ -440,7 +472,7 @@ mod tests { #[actix_rt::test] async fn test_named_file_content_length_headers() { - let srv = test::start(|| App::new().service(Files::new("/", "."))); + let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); // Valid range header let response = srv @@ -479,7 +511,7 @@ mod tests { #[actix_rt::test] async fn test_head_content_length_headers() { - let srv = test::start(|| App::new().service(Files::new("/", "."))); + let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); let response = srv.head("/tests/test.binary").send().await.unwrap(); @@ -495,14 +527,14 @@ mod tests { #[actix_rt::test] async fn test_static_files_with_spaces() { - let mut srv = test::init_service( + let srv = test::init_service( App::new().service(Files::new("/", ".").index_file("Cargo.toml")), ) .await; let request = TestRequest::get() .uri("/tests/test%20space.binary") .to_request(); - let response = test::call_service(&mut srv, request).await; + let response = test::call_service(&srv, request).await; assert_eq!(response.status(), StatusCode::OK); let bytes = test::read_body(response).await; @@ -512,29 +544,29 @@ mod tests { #[actix_rt::test] async fn test_files_not_allowed() { - let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await; + let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let req = TestRequest::default() .uri("/Cargo.toml") .method(Method::POST) .to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await; + let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let req = TestRequest::default() .method(Method::PUT) .uri("/Cargo.toml") .to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); } #[actix_rt::test] async fn test_files_guards() { - let mut srv = test::init_service( - App::new().service(Files::new("/", ".").use_guards(guard::Post())), + let srv = test::init_service( + App::new().service(Files::new("/", ".").method_guard(guard::Post())), ) .await; @@ -543,15 +575,16 @@ mod tests { .method(Method::POST) .to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_named_file_content_encoding() { - let mut srv = test::init_service(App::new().wrap(Compress::default()).service( + let srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { - NamedFile::open("Cargo.toml") + NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_content_encoding(header::ContentEncoding::Identity) }), @@ -562,16 +595,17 @@ mod tests { .uri("/") .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); - let res = test::call_service(&mut srv, request).await; + let res = test::call_service(&srv, request).await; assert_eq!(res.status(), StatusCode::OK); assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); } #[actix_rt::test] async fn test_named_file_content_encoding_gzip() { - let mut srv = test::init_service(App::new().wrap(Compress::default()).service( + let srv = test::init_service(App::new().wrap(Compress::default()).service( web::resource("/").to(|| async { - NamedFile::open("Cargo.toml") + NamedFile::open_async("Cargo.toml") + .await .unwrap() .set_content_encoding(header::ContentEncoding::Gzip) }), @@ -582,7 +616,7 @@ mod tests { .uri("/") .insert_header((header::ACCEPT_ENCODING, "gzip")) .to_request(); - let res = test::call_service(&mut srv, request).await; + let res = test::call_service(&srv, request).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( res.headers() @@ -597,34 +631,32 @@ mod tests { #[actix_rt::test] async fn test_named_file_allowed_method() { let req = TestRequest::default().method(Method::GET).to_http_request(); - let file = NamedFile::open("Cargo.toml").unwrap(); + let file = NamedFile::open_async("Cargo.toml").await.unwrap(); let resp = file.respond_to(&req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_static_files() { - let mut srv = test::init_service( - App::new().service(Files::new("/", ".").show_files_listing()), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) + .await; let req = TestRequest::with_uri("/missing").to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await; + let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let req = TestRequest::default().to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = test::init_service( - App::new().service(Files::new("/", ".").show_files_listing()), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) + .await; let req = TestRequest::with_uri("/tests").to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/html; charset=utf-8" @@ -636,17 +668,17 @@ mod tests { #[actix_rt::test] async fn test_redirect_to_slash_directory() { - // should not redirect if no index - let mut srv = test::init_service( + // should not redirect if no index and files listing is disabled + let srv = test::init_service( App::new().service(Files::new("/", ".").redirect_to_slash_directory()), ) .await; let req = TestRequest::with_uri("/tests").to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); // should redirect if index present - let mut srv = test::init_service( + let srv = test::init_service( App::new().service( Files::new("/", ".") .index_file("test.png") @@ -655,149 +687,270 @@ mod tests { ) .await; let req = TestRequest::with_uri("/tests").to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::FOUND); + + // should redirect if files listing is enabled + let srv = test::init_service( + App::new().service( + Files::new("/", ".") + .show_files_listing() + .redirect_to_slash_directory(), + ), + ) + .await; + let req = TestRequest::with_uri("/tests").to_request(); + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::FOUND); // should not redirect if the path is wrong let req = TestRequest::with_uri("/not_existing").to_request(); - let resp = test::call_service(&mut srv, req).await; + let resp = test::call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_static_files_bad_directory() { - let _st: Files = Files::new("/", "missing"); - let _st: Files = Files::new("/", "Cargo.toml"); + let service = Files::new("/", "./missing").new_service(()).await.unwrap(); + + let req = TestRequest::with_uri("/").to_srv_request(); + let resp = test::call_service(&service, req).await; + + assert_eq!(resp.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_default_handler_file_missing() { - let mut st = Files::new("/", ".") - .default_handler(|req: ServiceRequest| { - ok(req.into_response(HttpResponse::Ok().body("default content"))) + let st = Files::new("/", ".") + .default_handler(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) }) .new_service(()) .await .unwrap(); let req = TestRequest::with_uri("/missing").to_srv_request(); + let resp = test::call_service(&st, req).await; - let resp = test::call_service(&mut st, req).await; assert_eq!(resp.status(), StatusCode::OK); let bytes = test::read_body(resp).await; assert_eq!(bytes, web::Bytes::from_static(b"default content")); } - // #[actix_rt::test] - // async fn test_serve_index() { - // let st = Files::new(".").index_file("test.binary"); - // let req = TestRequest::default().uri("/tests").finish(); + #[actix_rt::test] + async fn test_serve_index_nested() { + let service = Files::new(".", ".") + .index_file("lib.rs") + .new_service(()) + .await + .unwrap(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers() - // .get(header::CONTENT_TYPE) - // .expect("content type"), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers() - // .get(header::CONTENT_DISPOSITION) - // .expect("content disposition"), - // "attachment; filename=\"test.binary\"" - // ); + let req = TestRequest::default().uri("/src").to_srv_request(); + let resp = test::call_service(&service, req).await; - // let req = TestRequest::default().uri("/tests/").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "application/octet-stream" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "attachment; filename=\"test.binary\"" - // ); + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-rust" + ); + assert_eq!( + resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"lib.rs\"" + ); + } - // // nonexistent index file - // let req = TestRequest::default().uri("/tests/unknown").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); + #[actix_rt::test] + async fn integration_serve_index() { + let srv = test::init_service( + App::new().service(Files::new("test", ".").index_file("Cargo.toml")), + ) + .await; - // let req = TestRequest::default().uri("/tests/unknown/").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::NOT_FOUND); - // } + let req = TestRequest::get().uri("/test").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); - // #[actix_rt::test] - // async fn test_serve_index_nested() { - // let st = Files::new(".").index_file("mod.rs"); - // let req = TestRequest::default().uri("/src/client").finish(); - // let resp = st.handle(&req).respond_to(&req).unwrap(); - // let resp = resp.as_msg(); - // assert_eq!(resp.status(), StatusCode::OK); - // assert_eq!( - // resp.headers().get(header::CONTENT_TYPE).unwrap(), - // "text/x-rust" - // ); - // assert_eq!( - // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), - // "inline; filename=\"mod.rs\"" - // ); - // } + let bytes = test::read_body(res).await; - // #[actix_rt::test] - // fn integration_serve_index() { - // let mut srv = test::TestServer::with_factory(|| { - // App::new().handler( - // "test", - // Files::new(".").index_file("Cargo.toml"), - // ) - // }); + let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + assert_eq!(bytes, data); - // let request = srv.get().uri(srv.url("/test")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - // let bytes = srv.execute(response.body()).unwrap(); - // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - // assert_eq!(bytes, data); + let req = TestRequest::get().uri("/test/").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); - // let request = srv.get().uri(srv.url("/test/")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - // let bytes = srv.execute(response.body()).unwrap(); - // let data = Bytes::from(fs::read("Cargo.toml").unwrap()); - // assert_eq!(bytes, data); + let bytes = test::read_body(res).await; + let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + assert_eq!(bytes, data); - // // nonexistent index file - // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::NOT_FOUND); + // nonexistent index file + let req = TestRequest::get().uri("/test/unknown").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); - // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::NOT_FOUND); - // } + let req = TestRequest::get().uri("/test/unknown/").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } - // #[actix_rt::test] - // fn integration_percent_encoded() { - // let mut srv = test::TestServer::with_factory(|| { - // App::new().handler( - // "test", - // Files::new(".").index_file("Cargo.toml"), - // ) - // }); + #[actix_rt::test] + async fn integration_percent_encoded() { + let srv = test::init_service( + App::new().service(Files::new("test", ".").index_file("Cargo.toml")), + ) + .await; - // let request = srv - // .get() - // .uri(srv.url("/test/%43argo.toml")) - // .finish() - // .unwrap(); - // let response = srv.execute(request.send()).unwrap(); - // assert_eq!(response.status(), StatusCode::OK); - // } + let req = TestRequest::get().uri("/test/%43argo.toml").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn test_serve_named_file() { + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let srv = test::init_service(App::new().service(factory)).await; + + let req = TestRequest::get().uri("/Cargo.toml").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let bytes = test::read_body(res).await; + let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + assert_eq!(bytes, data); + + let req = TestRequest::get().uri("/test/unknown").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_serve_named_file_prefix() { + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let srv = + test::init_service(App::new().service(web::scope("/test").service(factory))).await; + + let req = TestRequest::get().uri("/test/Cargo.toml").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let bytes = test::read_body(res).await; + let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + assert_eq!(bytes, data); + + let req = TestRequest::get().uri("/Cargo.toml").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_named_file_default_service() { + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let srv = test::init_service(App::new().default_service(factory)).await; + + for route in ["/foobar", "/baz", "/"].iter() { + let req = TestRequest::get().uri(route).to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + + let bytes = test::read_body(res).await; + let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + assert_eq!(bytes, data); + } + } + + #[actix_rt::test] + async fn test_default_handler_named_file() { + let factory = NamedFile::open_async("Cargo.toml").await.unwrap(); + let st = Files::new("/", ".") + .default_handler(factory) + .new_service(()) + .await + .unwrap(); + let req = TestRequest::with_uri("/missing").to_srv_request(); + let resp = test::call_service(&st, req).await; + + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + let data = Bytes::from(fs::read("Cargo.toml").unwrap()); + assert_eq!(bytes, data); + } + + #[actix_rt::test] + async fn test_symlinks() { + let srv = test::init_service(App::new().service(Files::new("test", "."))).await; + + let req = TestRequest::get() + .uri("/test/tests/symlink-test.png") + .to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(header::CONTENT_DISPOSITION).unwrap(), + "inline; filename=\"symlink-test.png\"" + ); + } + + #[actix_rt::test] + async fn test_index_with_show_files_listing() { + let service = Files::new(".", ".") + .index_file("lib.rs") + .show_files_listing() + .new_service(()) + .await + .unwrap(); + + // Serve the index if exists + let req = TestRequest::default().uri("/src").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!(resp.status(), StatusCode::OK); + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/x-rust" + ); + + // Show files listing, otherwise. + let req = TestRequest::default().uri("/tests").to_srv_request(); + let resp = test::call_service(&service, req).await; + assert_eq!( + resp.headers().get(header::CONTENT_TYPE).unwrap(), + "text/html; charset=utf-8" + ); + let bytes = test::read_body(resp).await; + assert!(format!("{:?}", bytes).contains("/tests/test.png")); + } + + #[actix_rt::test] + async fn test_path_filter() { + // prevent searching subdirectories + let st = Files::new("/", ".") + .path_filter(|path, _| path.components().count() == 1) + .new_service(()) + .await + .unwrap(); + + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/src/lib.rs").to_srv_request(); + let resp = test::call_service(&st, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } + + #[actix_rt::test] + async fn test_default_handler_filter() { + let st = Files::new("/", ".") + .default_handler(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().body("default content"))) + }) + .path_filter(|path, _| path.extension() == Some("png".as_ref())) + .new_service(()) + .await + .unwrap(); + let req = TestRequest::with_uri("/Cargo.toml").to_srv_request(); + let resp = test::call_service(&st, req).await; + + assert_eq!(resp.status(), StatusCode::OK); + let bytes = test::read_body(resp).await; + assert_eq!(bytes, web::Bytes::from_static(b"default content")); + } } diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index 8cd2a23f9..89775c6b3 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -1,27 +1,34 @@ -use std::fs::{File, Metadata}; -use std::io; -use std::ops::{Deref, DerefMut}; -use std::path::{Path, PathBuf}; -use std::time::{SystemTime, UNIX_EPOCH}; +use std::{ + fmt, + fs::Metadata, + io, + ops::{Deref, DerefMut}, + path::{Path, PathBuf}, + time::{SystemTime, UNIX_EPOCH}, +}; #[cfg(unix)] use std::os::unix::fs::MetadataExt; +use actix_service::{Service, ServiceFactory}; use actix_web::{ - dev::{BodyEncoding, SizedStream}, + body::{self, BoxBody, SizedStream}, + dev::{ + AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest, + ServiceResponse, + }, http::{ header::{ - self, Charset, ContentDisposition, DispositionParam, DispositionType, - ExtendedValue, + self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, }, ContentEncoding, StatusCode, }, - HttpMessage, HttpRequest, HttpResponse, Responder, + Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; use bitflags::bitflags; +use futures_core::future::LocalBoxFuture; use mime_guess::from_path; -use crate::ChunkedReadFile; use crate::{encoding::equiv_utf8_text, range::HttpRange}; bitflags! { @@ -40,7 +47,29 @@ impl Default for Flags { } /// A file with an associated name. -#[derive(Debug)] +/// +/// `NamedFile` can be registered as services: +/// ``` +/// use actix_web::App; +/// use actix_files::NamedFile; +/// +/// # async fn run() -> Result<(), Box> { +/// let file = NamedFile::open_async("./static/index.html").await?; +/// let app = App::new().service(file); +/// # Ok(()) +/// # } +/// ``` +/// +/// They can also be returned from handlers: +/// ``` +/// use actix_web::{Responder, get}; +/// use actix_files::NamedFile; +/// +/// #[get("/")] +/// async fn index() -> impl Responder { +/// NamedFile::open_async("./static/index.html").await +/// } +/// ``` pub struct NamedFile { path: PathBuf, file: File, @@ -53,6 +82,39 @@ pub struct NamedFile { pub(crate) encoding: Option, } +impl fmt::Debug for NamedFile { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("NamedFile") + .field("path", &self.path) + .field( + "file", + #[cfg(feature = "experimental-io-uring")] + { + &"tokio_uring::File" + }, + #[cfg(not(feature = "experimental-io-uring"))] + { + &self.file + }, + ) + .field("modified", &self.modified) + .field("md", &self.md) + .field("flags", &self.flags) + .field("status_code", &self.status_code) + .field("content_type", &self.content_type) + .field("content_disposition", &self.content_disposition) + .field("encoding", &self.encoding) + .finish() + } +} + +#[cfg(not(feature = "experimental-io-uring"))] +pub(crate) use std::fs::File; +#[cfg(feature = "experimental-io-uring")] +pub(crate) use tokio_uring::fs::File; + +use super::chunked; + impl NamedFile { /// Creates an instance from a previously opened file. /// @@ -60,8 +122,7 @@ impl NamedFile { /// `ContentDisposition` headers. /// /// # Examples - /// - /// ```rust + /// ```ignore /// use actix_files::NamedFile; /// use std::io::{self, Write}; /// use std::env; @@ -95,6 +156,11 @@ impl NamedFile { let disposition = match ct.type_() { mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, + mime::APPLICATION => match ct.subtype() { + mime::JAVASCRIPT | mime::JSON => DispositionType::Inline, + name if name == "wasm" => DispositionType::Inline, + _ => DispositionType::Attachment, + }, _ => DispositionType::Attachment, }; @@ -117,7 +183,30 @@ impl NamedFile { (ct, cd) }; - let md = file.metadata()?; + let md = { + #[cfg(not(feature = "experimental-io-uring"))] + { + file.metadata()? + } + + #[cfg(feature = "experimental-io-uring")] + { + use std::os::unix::prelude::{AsRawFd, FromRawFd}; + + let fd = file.as_raw_fd(); + + // SAFETY: fd is borrowed and lives longer than the unsafe block + unsafe { + let file = std::fs::File::from_raw_fd(fd); + let md = file.metadata(); + // SAFETY: forget the fd before exiting block in success or error case but don't + // run destructor (that would close file handle) + std::mem::forget(file); + md? + } + } + }; + let modified = md.modified().ok(); let encoding = None; @@ -134,17 +223,45 @@ impl NamedFile { }) } + #[cfg(not(feature = "experimental-io-uring"))] /// Attempts to open a file in read-only mode. /// /// # Examples - /// - /// ```rust + /// ``` /// use actix_files::NamedFile; - /// /// let file = NamedFile::open("foo.txt"); /// ``` pub fn open>(path: P) -> io::Result { - Self::from_file(File::open(&path)?, path) + let file = File::open(&path)?; + Self::from_file(file, path) + } + + /// Attempts to open a file asynchronously in read-only mode. + /// + /// When the `experimental-io-uring` crate feature is enabled, this will be async. + /// Otherwise, it will be just like [`open`][Self::open]. + /// + /// # Examples + /// ``` + /// use actix_files::NamedFile; + /// # async fn open() { + /// let file = NamedFile::open_async("foo.txt").await.unwrap(); + /// # } + /// ``` + pub async fn open_async>(path: P) -> io::Result { + let file = { + #[cfg(not(feature = "experimental-io-uring"))] + { + File::open(&path)? + } + + #[cfg(feature = "experimental-io-uring")] + { + File::open(&path).await? + } + }; + + Self::from_file(file, path) } /// Returns reference to the underlying `File` object. @@ -156,13 +273,12 @@ impl NamedFile { /// Retrieve the path of this file. /// /// # Examples - /// - /// ```rust + /// ``` /// # use std::io; /// use actix_files::NamedFile; /// - /// # fn path() -> io::Result<()> { - /// let file = NamedFile::open("test.txt")?; + /// # async fn path() -> io::Result<()> { + /// let file = NamedFile::open_async("test.txt").await?; /// assert_eq!(file.path().as_os_str(), "foo.txt"); /// # Ok(()) /// # } @@ -188,9 +304,11 @@ impl NamedFile { /// Set the Content-Disposition for serving this file. This allows /// changing the inline/attachment disposition as well as the filename - /// sent to the peer. By default the disposition is `inline` for text, - /// image, and video content types, and `attachment` otherwise, and - /// the filename is taken from the path provided in the `open` method + /// sent to the peer. + /// + /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and + /// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, + /// and the filename is taken from the path provided in the `open` method /// after converting it to UTF-8 using. /// [`std::ffi::OsStr::to_string_lossy`] #[inline] @@ -210,6 +328,8 @@ impl NamedFile { } /// Set content encoding for serving this file + /// + /// Must be used with [`actix_web::middleware::Compress`] to take effect. #[inline] pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { self.encoding = Some(enc); @@ -276,7 +396,7 @@ impl NamedFile { } /// Creates an `HttpResponse` with file as a streaming body. - pub fn into_response(self, req: &HttpRequest) -> HttpResponse { + pub fn into_response(self, req: &HttpRequest) -> HttpResponse { if self.status_code != StatusCode::OK { let mut res = HttpResponse::build(self.status_code); @@ -298,13 +418,7 @@ impl NamedFile { res.encoding(current_encoding); } - let reader = ChunkedReadFile { - size: self.md.len(), - offset: 0, - file: Some(self.file), - fut: None, - counter: 0, - }; + let reader = chunked::new_chunked_read(self.md.len(), 0, self.file); return res.streaming(reader); } @@ -327,8 +441,8 @@ impl NamedFile { } else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) = (last_modified, req.get_header()) { - let t1: SystemTime = m.clone().into(); - let t2: SystemTime = since.clone().into(); + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(), @@ -346,8 +460,8 @@ impl NamedFile { } else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) = (last_modified, req.get_header()) { - let t1: SystemTime = m.clone().into(); - let t2: SystemTime = since.clone().into(); + let t1: SystemTime = (*m).into(); + let t2: SystemTime = (*since).into(); match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { (Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(), @@ -401,18 +515,10 @@ impl NamedFile { resp.encoding(ContentEncoding::Identity); resp.insert_header(( header::CONTENT_RANGE, - format!( - "bytes {}-{}/{}", - offset, - offset + length - 1, - self.md.len() - ), + format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), )); } else { - resp.insert_header(( - header::CONTENT_RANGE, - format!("bytes */{}", length), - )); + resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); }; } else { @@ -423,16 +529,13 @@ impl NamedFile { if precondition_failed { return resp.status(StatusCode::PRECONDITION_FAILED).finish(); } else if not_modified { - return resp.status(StatusCode::NOT_MODIFIED).finish(); + return resp + .status(StatusCode::NOT_MODIFIED) + .body(body::None::new()) + .map_into_boxed_body(); } - let reader = ChunkedReadFile { - offset, - size: length, - file: Some(self.file), - fut: None, - counter: 0, - }; + let reader = chunked::new_chunked_read(length, offset, self.file); if offset != 0 || length != self.md.len() { resp.status(StatusCode::PARTIAL_CONTENT); @@ -442,20 +545,6 @@ impl NamedFile { } } -impl Deref for NamedFile { - type Target = File; - - fn deref(&self) -> &File { - &self.file - } -} - -impl DerefMut for NamedFile { - fn deref_mut(&mut self) -> &mut File { - &mut self.file - } -} - /// Returns true if `req` has no `If-Match` header or one which matches `etag`. fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { match req.get_header::() { @@ -496,8 +585,77 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { } } +impl Deref for NamedFile { + type Target = File; + + fn deref(&self) -> &Self::Target { + &self.file + } +} + +impl DerefMut for NamedFile { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.file + } +} + impl Responder for NamedFile { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { self.into_response(req) } } + +impl ServiceFactory for NamedFile { + type Response = ServiceResponse; + type Error = Error; + type Config = (); + type Service = NamedFileService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; + + fn new_service(&self, _: ()) -> Self::Future { + let service = NamedFileService { + path: self.path.clone(), + }; + + Box::pin(async move { Ok(service) }) + } +} + +#[doc(hidden)] +#[derive(Debug)] +pub struct NamedFileService { + path: PathBuf, +} + +impl Service for NamedFileService { + type Response = ServiceResponse; + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + actix_service::always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let (req, _) = req.into_parts(); + + let path = self.path.clone(); + Box::pin(async move { + let file = NamedFile::open_async(path).await?; + let res = file.into_response(&req); + Ok(ServiceResponse::new(req, res)) + }) + } +} + +impl HttpServiceFactory for NamedFile { + fn register(self, config: &mut AppService) { + config.register_service( + ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()), + None, + self, + None, + ) + } +} diff --git a/actix-files/src/path_buf.rs b/actix-files/src/path_buf.rs index dd8e5b503..0e0d4f51d 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -3,12 +3,12 @@ use std::{ str::FromStr, }; +use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, FromRequest, HttpRequest}; -use futures_util::future::{ready, Ready}; use crate::error::UriSegmentError; -#[derive(Debug)] +#[derive(Debug, PartialEq, Eq)] pub(crate) struct PathBufWrap(PathBuf); impl FromStr for PathBufWrap { @@ -21,6 +21,8 @@ impl FromStr for PathBufWrap { impl PathBufWrap { /// Parse a path, giving the choice of allowing hidden files to be considered valid segments. + /// + /// Path traversal is guarded by this method. pub fn parse_path(path: &str, hidden_files: bool) -> Result { let mut buf = PathBuf::new(); @@ -59,7 +61,6 @@ impl AsRef for PathBufWrap { impl FromRequest for PathBufWrap { type Error = UriSegmentError; type Future = Ready>; - type Config = (); fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { ready(req.match_info().path().parse()) @@ -116,4 +117,24 @@ mod tests { PathBuf::from_iter(vec!["test", ".tt"]) ); } + + #[test] + fn path_traversal() { + assert_eq!( + PathBufWrap::parse_path("/../README.md", false).unwrap().0, + PathBuf::from_iter(vec!["README.md"]) + ); + + assert_eq!( + PathBufWrap::parse_path("/../README.md", true).unwrap().0, + PathBuf::from_iter(vec!["README.md"]) + ); + + assert_eq!( + PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false) + .unwrap() + .0, + PathBuf::from_iter(vec!["etc/passwd"]) + ); + } } diff --git a/actix-files/src/range.rs b/actix-files/src/range.rs index e420ce414..8d9fe9445 100644 --- a/actix-files/src/range.rs +++ b/actix-files/src/range.rs @@ -10,9 +10,6 @@ pub struct HttpRange { pub length: u64, } -const PREFIX: &str = "bytes="; -const PREFIX_LEN: usize = 6; - #[derive(Debug, Clone, Display, Error)] #[display(fmt = "Parse HTTP Range failed")] pub struct ParseRangeErr(#[error(not(source))] ()); @@ -23,84 +20,16 @@ impl HttpRange { /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `size` is full size of response (file). pub fn parse(header: &str, size: u64) -> Result, ParseRangeErr> { - if header.is_empty() { - return Ok(Vec::new()); + match http_range::HttpRange::parse(header, size) { + Ok(ranges) => Ok(ranges + .iter() + .map(|range| HttpRange { + start: range.start, + length: range.length, + }) + .collect()), + Err(_) => Err(ParseRangeErr(())), } - if !header.starts_with(PREFIX) { - return Err(ParseRangeErr(())); - } - - let size_sig = size as i64; - let mut no_overlap = false; - - let all_ranges: Vec> = header[PREFIX_LEN..] - .split(',') - .map(|x| x.trim()) - .filter(|x| !x.is_empty()) - .map(|ra| { - let mut start_end_iter = ra.split('-'); - - let start_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim(); - let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim(); - - if start_str.is_empty() { - // If no start is specified, end specifies the - // range start relative to the end of the file. - let mut length: i64 = - end_str.parse().map_err(|_| ParseRangeErr(()))?; - - if length > size_sig { - length = size_sig; - } - - Ok(Some(HttpRange { - start: (size_sig - length) as u64, - length: length as u64, - })) - } else { - let start: i64 = start_str.parse().map_err(|_| ParseRangeErr(()))?; - - if start < 0 { - return Err(ParseRangeErr(())); - } - if start >= size_sig { - no_overlap = true; - return Ok(None); - } - - let length = if end_str.is_empty() { - // If no end is specified, range extends to end of the file. - size_sig - start - } else { - let mut end: i64 = - end_str.parse().map_err(|_| ParseRangeErr(()))?; - - if start > end { - return Err(ParseRangeErr(())); - } - - if end >= size_sig { - end = size_sig - 1; - } - - end - start + 1 - }; - - Ok(Some(HttpRange { - start: start as u64, - length: length as u64, - })) - } - }) - .collect::>()?; - - let ranges: Vec = all_ranges.into_iter().filter_map(|x| x).collect(); - - if no_overlap && ranges.is_empty() { - return Err(ParseRangeErr(())); - } - - Ok(ranges) } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index 25e285efc..f6e1c2e11 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -1,9 +1,4 @@ -use std::{ - fmt, io, - path::PathBuf, - rc::Rc, - task::{Context, Poll}, -}; +use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc}; use actix_service::Service; use actix_web::{ @@ -13,15 +8,26 @@ use actix_web::{ http::{header, Method}, HttpResponse, }; -use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; +use futures_core::future::LocalBoxFuture; use crate::{ - named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, - NamedFile, PathBufWrap, + named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, + PathBufWrap, PathFilter, }; /// Assembled file serving service. -pub struct FilesService { +#[derive(Clone)] +pub struct FilesService(pub(crate) Rc); + +impl Deref for FilesService { + type Target = FilesServiceInner; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +pub struct FilesServiceInner { pub(crate) directory: PathBuf, pub(crate) index: Option, pub(crate) show_index: bool, @@ -29,26 +35,56 @@ pub struct FilesService { pub(crate) default: Option, pub(crate) renderer: Rc, pub(crate) mime_override: Option>, + pub(crate) path_filter: Option>, pub(crate) file_flags: named::Flags, pub(crate) guards: Option>, pub(crate) hidden_files: bool, } -type FilesServiceFuture = Either< - Ready>, - LocalBoxFuture<'static, Result>, ->; +impl fmt::Debug for FilesServiceInner { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("FilesServiceInner") + } +} impl FilesService { - fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { - log::debug!("Failed to handle {}: {}", req.path(), e); + async fn handle_err( + &self, + err: io::Error, + req: ServiceRequest, + ) -> Result { + log::debug!("error handling {}: {}", req.path(), err); - if let Some(ref mut default) = self.default { - Either::Right(default.call(req)) + if let Some(ref default) = self.default { + default.call(req).await } else { - Either::Left(ok(req.error_response(e))) + Ok(req.error_response(err)) } } + + fn serve_named_file( + &self, + req: ServiceRequest, + mut named_file: NamedFile, + ) -> ServiceResponse { + if let Some(ref mime_override) = self.mime_override { + let new_disposition = mime_override(&named_file.content_type.type_()); + named_file.content_disposition.disposition = new_disposition; + } + named_file.flags = self.file_flags; + + let (req, _) = req.into_parts(); + let res = named_file.into_response(&req); + ServiceResponse::new(req, res) + } + + fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse { + let dir = Directory::new(self.directory.clone(), path); + + let (req, _) = req.into_parts(); + + (self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req)) + } } impl fmt::Debug for FilesService { @@ -60,13 +96,11 @@ impl fmt::Debug for FilesService { impl Service for FilesService { type Response = ServiceResponse; type Error = Error; - type Future = FilesServiceFuture; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_service::always_ready!(); - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { let is_method_valid = if let Some(guard) = &self.guards { // execute user defined guards (**guard).check(req.head()) @@ -75,88 +109,87 @@ impl Service for FilesService { matches!(*req.method(), Method::HEAD | Method::GET) }; - if !is_method_valid { - return Either::Left(ok(req.into_response( - actix_web::HttpResponse::MethodNotAllowed() - .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) - .body("Request did not meet this resource's requirements."), - ))); - } + let this = self.clone(); - let real_path = - match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { - Ok(item) => item, - Err(e) => return Either::Left(ok(req.error_response(e))), - }; + Box::pin(async move { + if !is_method_valid { + return Ok(req.into_response( + actix_web::HttpResponse::MethodNotAllowed() + .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) + .body("Request did not meet this resource's requirements."), + )); + } - // full file path - let path = match self.directory.join(&real_path).canonicalize() { - Ok(path) => path, - Err(e) => return self.handle_err(e, req), - }; + let real_path = + match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) { + Ok(item) => item, + Err(e) => return Ok(req.error_response(e)), + }; - if path.is_dir() { - if let Some(ref redir_index) = self.index { - if self.redirect_to_slash && !req.path().ends_with('/') { + if let Some(filter) = &this.path_filter { + if !filter(real_path.as_ref(), req.head()) { + if let Some(ref default) = this.default { + return default.call(req).await; + } else { + return Ok( + req.into_response(actix_web::HttpResponse::NotFound().finish()) + ); + } + } + } + + // full file path + let path = this.directory.join(&real_path); + if let Err(err) = path.canonicalize() { + return this.handle_err(err, req).await; + } + + if path.is_dir() { + if this.redirect_to_slash + && !req.path().ends_with('/') + && (this.index.is_some() || this.show_index) + { let redirect_to = format!("{}/", req.path()); - return Either::Left(ok(req.into_response( + return Ok(req.into_response( HttpResponse::Found() .insert_header((header::LOCATION, redirect_to)) - .body("") - .into_body(), - ))); + .finish(), + )); } - let path = path.join(redir_index); - - match NamedFile::open(path) { + match this.index { + Some(ref index) => { + let named_path = path.join(index); + match NamedFile::open_async(named_path).await { + Ok(named_file) => Ok(this.serve_named_file(req, named_file)), + Err(_) if this.show_index => Ok(this.show_index(req, path)), + Err(err) => this.handle_err(err, req).await, + } + } + None if this.show_index => Ok(this.show_index(req, path)), + _ => Ok(ServiceResponse::from_err( + FilesError::IsDirectory, + req.into_parts().0, + )), + } + } else { + match NamedFile::open_async(&path).await { Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { + if let Some(ref mime_override) = this.mime_override { let new_disposition = mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; } - named_file.flags = self.file_flags; + named_file.flags = this.file_flags; let (req, _) = req.into_parts(); let res = named_file.into_response(&req); - Either::Left(ok(ServiceResponse::new(req, res))) + Ok(ServiceResponse::new(req, res)) } - Err(e) => self.handle_err(e, req), + Err(err) => this.handle_err(err, req).await, } - } else if self.show_index { - let dir = Directory::new(self.directory.clone(), path); - - let (req, _) = req.into_parts(); - let x = (self.renderer)(&dir, &req); - - match x { - Ok(resp) => Either::Left(ok(resp)), - Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), - } - } else { - Either::Left(ok(ServiceResponse::from_err( - FilesError::IsDirectory, - req.into_parts().0, - ))) } - } else { - match NamedFile::open(path) { - Ok(mut named_file) => { - if let Some(ref mime_override) = self.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); - named_file.content_disposition.disposition = new_disposition; - } - named_file.flags = self.file_flags; - - let (req, _) = req.into_parts(); - let res = named_file.into_response(&req); - Either::Left(ok(ServiceResponse::new(req, res))) - } - Err(e) => self.handle_err(e, req), - } - } + }) } } diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index d7e01b305..652a7c12b 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -8,14 +8,13 @@ use actix_web::{ App, }; -#[actix_rt::test] +#[actix_web::test] async fn test_utf8_file_contents() { // use default ISO-8859-1 encoding - let mut srv = - test::init_service(App::new().service(Files::new("/", "./tests"))).await; + let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; let req = TestRequest::with_uri("/utf8.txt").to_request(); - let res = test::call_service(&mut srv, req).await; + let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( @@ -24,13 +23,12 @@ async fn test_utf8_file_contents() { ); // prefer UTF-8 encoding - let mut srv = test::init_service( - App::new().service(Files::new("/", "./tests").prefer_utf8(true)), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true))) + .await; let req = TestRequest::with_uri("/utf8.txt").to_request(); - let res = test::call_service(&mut srv, req).await; + let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); assert_eq!( diff --git a/actix-files/tests/fixtures/guards/first/index.txt b/actix-files/tests/fixtures/guards/first/index.txt new file mode 100644 index 000000000..fe4f02ad0 --- /dev/null +++ b/actix-files/tests/fixtures/guards/first/index.txt @@ -0,0 +1 @@ +first \ No newline at end of file diff --git a/actix-files/tests/fixtures/guards/second/index.txt b/actix-files/tests/fixtures/guards/second/index.txt new file mode 100644 index 000000000..2147e4188 --- /dev/null +++ b/actix-files/tests/fixtures/guards/second/index.txt @@ -0,0 +1 @@ +second \ No newline at end of file diff --git a/actix-files/tests/guard.rs b/actix-files/tests/guard.rs new file mode 100644 index 000000000..d053f3fdc --- /dev/null +++ b/actix-files/tests/guard.rs @@ -0,0 +1,36 @@ +use actix_files::Files; +use actix_web::{ + guard::Host, + http::StatusCode, + test::{self, TestRequest}, + App, +}; +use bytes::Bytes; + +#[actix_web::test] +async fn test_guard_filter() { + let srv = test::init_service( + App::new() + .service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com"))) + .service( + Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")), + ), + ) + .await; + + let req = TestRequest::with_uri("/index.txt") + .append_header(("Host", "first.com")) + .to_request(); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(test::read_body(res).await, Bytes::from("first")); + + let req = TestRequest::with_uri("/index.txt") + .append_header(("Host", "second.com")) + .to_request(); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(test::read_body(res).await, Bytes::from("second")); +} diff --git a/actix-files/tests/symlink-test.png b/actix-files/tests/symlink-test.png new file mode 120000 index 000000000..65c0dcfd6 --- /dev/null +++ b/actix-files/tests/symlink-test.png @@ -0,0 +1 @@ +test.png \ No newline at end of file diff --git a/actix-files/tests/test.js b/actix-files/tests/test.js new file mode 100644 index 000000000..2ee135561 --- /dev/null +++ b/actix-files/tests/test.js @@ -0,0 +1 @@ +// this file is empty. diff --git a/actix-files/tests/traversal.rs b/actix-files/tests/traversal.rs new file mode 100644 index 000000000..c890b3fe4 --- /dev/null +++ b/actix-files/tests/traversal.rs @@ -0,0 +1,27 @@ +use actix_files::Files; +use actix_web::{ + http::StatusCode, + test::{self, TestRequest}, + App, +}; + +#[actix_rt::test] +async fn test_directory_traversal_prevention() { + let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; + + let req = + TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri( + "/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd", + ) + .to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request(); + let res = test::call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); +} diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index ed6d87a60..6984e5962 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -3,6 +3,44 @@ ## Unreleased - 2021-xx-xx +## 3.0.0-beta.8 - 2021-11-30 +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + +## 3.0.0-beta.7 - 2021-11-22 +* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] + +[#2408]: https://github.com/actix/actix-web/pull/2408 + + +## 3.0.0-beta.6 - 2021-11-15 +* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] +* Update `actix-server` to `2.0.0-beta.9`. [#2442] +* Minimum supported Rust version (MSRV) is now 1.52. + +[#2442]: https://github.com/actix/actix-web/pull/2442 + + +## 3.0.0-beta.5 - 2021-09-09 +* Minimum supported Rust version (MSRV) is now 1.51. + + +## 3.0.0-beta.4 - 2021-04-02 +* Added `TestServer::client_headers` method. [#2097] + +[#2097]: https://github.com/actix/actix-web/pull/2097 + + +## 3.0.0-beta.3 - 2021-03-09 +* No notable changes. + + +## 3.0.0-beta.2 - 2021-02-10 +* No notable changes. + + ## 3.0.0-beta.1 - 2021-01-07 * Update `bytes` to `1.0`. [#1813] diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 772b60f76..8d347d4e9 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,18 +1,18 @@ [package] name = "actix-http-test" -version = "3.0.0-beta.1" +version = "3.0.0-beta.8" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" -readme = "README.md" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-http-test/" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" -exclude = [".gitignore", ".cargo/config"] edition = "2018" [package.metadata.docs.rs] @@ -26,30 +26,30 @@ path = "src/lib.rs" default = [] # openssl -openssl = ["open-ssl", "awc/openssl"] +openssl = ["tls-openssl", "awc/openssl"] [dependencies] -actix-service = "2.0.0-beta.3" -actix-codec = "0.4.0-beta.1" -actix-tls = "3.0.0-beta.2" -actix-utils = "3.0.0-beta.1" -actix-rt = "2.0.0-beta.2" -actix-server = "2.0.0-beta.2" -awc = "3.0.0-beta.1" +actix-service = "2.0.0" +actix-codec = "0.4.1" +actix-tls = "3.0.0-rc.1" +actix-utils = "3.0.0" +actix-rt = "2.2" +actix-server = "2.0.0-beta.9" +awc = { version = "3.0.0-beta.11", default-features = false } base64 = "0.13" bytes = "1" futures-core = { version = "0.3.7", default-features = false } -http = "0.2.2" +http = "0.2.5" log = "0.4" -socket2 = "0.3" +socket2 = "0.4" serde = "1.0" serde_json = "1.0" slab = "0.4" serde_urlencoded = "0.7" -time = { version = "0.2.7", default-features = false, features = ["std"] } -open-ssl = { version = "0.10", package = "openssl", optional = true } +tls-openssl = { version = "0.10.9", package = "openssl", optional = true } +tokio = { version = "1.2", features = ["sync"] } [dev-dependencies] -actix-web = "4.0.0-beta.1" -actix-http = "3.0.0-beta.1" +actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } +actix-http = "3.0.0-beta.14" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index bca9a7976..c3e99d259 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -3,13 +3,15 @@ > Various helpers for Actix applications to use during testing. [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) -[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=2.1.0)](https://docs.rs/actix-http-test/2.1.0) -![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http-test) -[![Dependency Status](https://deps.rs/crate/actix-http-test/2.1.0/status.svg)](https://deps.rs/crate/actix-http-test/2.1.0) -[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http-test/3.0.0-beta.8) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) +
+[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.8) +[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http-test) -- [Chat on Gitter](https://gitter.im/actix/actix-web) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 4fd74d6eb..7f55a0bf4 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -4,40 +4,43 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] -use std::sync::mpsc; -use std::{net, thread, time}; +#[cfg(feature = "openssl")] +extern crate tls_openssl as openssl; + +use std::{net, thread, time::Duration}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::{net::TcpStream, System}; use actix_server::{Server, ServiceFactory}; -use awc::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector}; +use awc::{ + error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector, +}; use bytes::Bytes; use futures_core::stream::Stream; use http::Method; use socket2::{Domain, Protocol, Socket, Type}; +use tokio::sync::mpsc; -/// Start test server +/// Start test server. /// -/// `TestServer` is very simple test server that simplify process of writing -/// integration tests cases for actix web applications. +/// `TestServer` is very simple test server that simplify process of writing integration tests cases +/// for HTTP applications. /// /// # Examples -/// -/// ```rust +/// ```no_run /// use actix_http::HttpService; -/// use actix_http_test::TestServer; +/// use actix_http_test::test_server; /// use actix_web::{web, App, HttpResponse, Error}; /// /// async fn my_handler() -> Result { /// Ok(HttpResponse::Ok().into()) /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_example() { -/// let mut srv = TestServer::start( -/// || HttpService::new( -/// App::new().service( -/// web::resource("/").to(my_handler)) +/// let mut srv = TestServer::start(|| +/// HttpService::new( +/// App::new().service(web::resource("/").to(my_handler)) /// ) /// ); /// @@ -51,86 +54,86 @@ pub async fn test_server>(factory: F) -> TestServer test_server_with_addr(tcp, factory).await } -/// Start [`test server`](test_server()) on a concrete Address +/// Start [`test server`](test_server()) on an existing address binding. pub async fn test_server_with_addr>( tcp: net::TcpListener, factory: F, ) -> TestServer { - let (tx, rx) = mpsc::channel(); + let (started_tx, started_rx) = std::sync::mpsc::channel(); + let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1); // run server in separate thread thread::spawn(move || { - let sys = System::new("actix-test-server"); - let local_addr = tcp.local_addr().unwrap(); + System::new().block_on(async move { + let local_addr = tcp.local_addr().unwrap(); - let srv = Server::build() - .listen("test", tcp, factory)? - .workers(1) - .disable_signals(); + let srv = Server::build() + .workers(1) + .disable_signals() + .system_exit() + .listen("test", tcp, factory) + .expect("test server could not be created"); - sys.block_on(async { - srv.start(); - tx.send((System::current(), local_addr)).unwrap(); + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); + + // drive server loop + srv.await.unwrap(); }); - sys.run() + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + let _ = thread_stop_tx.send(()); }); - let (system, addr) = rx.recv().unwrap(); + let (system, server, addr) = started_rx.recv().unwrap(); let client = { + #[cfg(feature = "openssl")] let connector = { - #[cfg(feature = "openssl")] - { - use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - 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)); - Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) - .ssl(builder.build()) - .finish() - } - #[cfg(not(feature = "openssl"))] - { - Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) - .finish() - } + 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)); + + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) + .ssl(builder.build()) + }; + + #[cfg(not(feature = "openssl"))] + let connector = { + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) }; Client::builder().connector(connector).finish() }; - actix_tls::connect::start_default_resolver().await.unwrap(); TestServer { - addr, + server, client, system, + addr, + thread_stop_rx, } } -/// Get first available unused address -pub fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = - Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); - socket.bind(&addr.into()).unwrap(); - socket.set_reuse_address(true).unwrap(); - let tcp = socket.into_tcp_listener(); - tcp.local_addr().unwrap() -} - /// Test server controller pub struct TestServer { + server: actix_server::ServerHandle, + client: awc::Client, + system: actix_rt::System, addr: net::SocketAddr, - client: Client, - system: System, + thread_stop_rx: mpsc::Receiver<()>, } impl TestServer { @@ -148,7 +151,7 @@ impl TestServer { } } - /// Construct test https server url + /// Construct test HTTPS server URL. pub fn surl(&self, uri: &str) -> String { if uri.starts_with('/') { format!("https://localhost:{}{}", self.addr.port(), uri) @@ -162,7 +165,7 @@ impl TestServer { self.client.get(self.url(path.as_ref()).as_str()) } - /// Create https `GET` request + /// Create HTTPS `GET` request pub fn sget>(&self, path: S) -> ClientRequest { self.client.get(self.surl(path.as_ref()).as_str()) } @@ -172,7 +175,7 @@ impl TestServer { self.client.post(self.url(path.as_ref()).as_str()) } - /// Create https `POST` request + /// Create HTTPS `POST` request pub fn spost>(&self, path: S) -> ClientRequest { self.client.post(self.surl(path.as_ref()).as_str()) } @@ -182,7 +185,7 @@ impl TestServer { self.client.head(self.url(path.as_ref()).as_str()) } - /// Create https `HEAD` request + /// Create HTTPS `HEAD` request pub fn shead>(&self, path: S) -> ClientRequest { self.client.head(self.surl(path.as_ref()).as_str()) } @@ -192,7 +195,7 @@ impl TestServer { self.client.put(self.url(path.as_ref()).as_str()) } - /// Create https `PUT` request + /// Create HTTPS `PUT` request pub fn sput>(&self, path: S) -> ClientRequest { self.client.put(self.surl(path.as_ref()).as_str()) } @@ -202,7 +205,7 @@ impl TestServer { self.client.patch(self.url(path.as_ref()).as_str()) } - /// Create https `PATCH` request + /// Create HTTPS `PATCH` request pub fn spatch>(&self, path: S) -> ClientRequest { self.client.patch(self.surl(path.as_ref()).as_str()) } @@ -212,7 +215,7 @@ impl TestServer { self.client.delete(self.url(path.as_ref()).as_str()) } - /// Create https `DELETE` request + /// Create HTTPS `DELETE` request pub fn sdelete>(&self, path: S) -> ClientRequest { self.client.delete(self.surl(path.as_ref()).as_str()) } @@ -222,12 +225,12 @@ impl TestServer { self.client.options(self.url(path.as_ref()).as_str()) } - /// Create https `OPTIONS` request + /// Create HTTPS `OPTIONS` request pub fn soptions>(&self, path: S) -> ClientRequest { self.client.options(self.surl(path.as_ref()).as_str()) } - /// Connect to test http server + /// Connect to test HTTP server pub fn request>(&self, method: Method, path: S) -> ClientRequest { self.client.request(method, path.as_ref()) } @@ -242,33 +245,66 @@ impl TestServer { response.body().limit(10_485_760).await } - /// Connect to websocket server at a given path + /// Connect to WebSocket server at a given path. pub async fn ws_at( &mut self, path: &str, - ) -> Result, awc::error::WsClientError> - { + ) -> Result, awc::error::WsClientError> { let url = self.url(path); let connect = self.client.ws(url).connect(); connect.await.map(|(_, framed)| framed) } - /// Connect to a websocket server + /// Connect to a WebSocket server. pub async fn ws( &mut self, - ) -> Result, awc::error::WsClientError> - { + ) -> Result, awc::error::WsClientError> { self.ws_at("/").await } - /// Stop http server - fn stop(&mut self) { + /// Get default HeaderMap of Client. + /// + /// Returns Some(&mut HeaderMap) when Client object is unique + /// (No other clone of client exists at the same time). + pub fn client_headers(&mut self) -> Option<&mut HeaderMap> { + self.client.headers() + } + + /// Stop HTTP server. + /// + /// Waits for spawned `Server` and `System` to (force) shutdown. + pub async fn stop(&mut self) { + // signal server to stop + self.server.stop(false).await; + + // also signal system to stop + // though this is handled by `ServerBuilder::exit_system` too self.system.stop(); + + // wait for thread to be stopped but don't care about result + let _ = self.thread_stop_rx.recv().await; } } impl Drop for TestServer { fn drop(&mut self) { - self.stop() + // calls in this Drop impl should be enough to shut down the server, system, and thread + // without needing to await anything + + // signal server to stop + let _ = self.server.stop(true); + + // signal system to stop + self.system.stop(); } } + +/// Get a localhost socket address with random, unused port. +pub fn unused_addr() -> net::SocketAddr { + let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); + let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap(); + socket.bind(&addr.into()).unwrap(); + socket.set_reuse_address(true).unwrap(); + let tcp = net::TcpListener::from(socket); + tcp.local_addr().unwrap() +} diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 34de7727a..23c15296a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -2,11 +2,250 @@ ## Unreleased - 2021-xx-xx ### Added +* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] +* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] +* `Response::map_into_boxed_body`. [#2468] +* `body::EitherBody` enum. [#2468] +* `body::None` struct. [#2468] +* Impl `MessageBody` for `bytestring::ByteString`. [#2468] +* `impl Clone for ws::HandshakeError`. [#2468] + +### Changed +* Rename `body::BoxBody::{from_body => new}`. [#2468] +* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] +* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] +* Error types using in service builders now require `Into>`. [#2468] +* `From` implementations on error types now return a `Response`. [#2468] +* `ResponseBuilder::body(B)` now returns `Response>`. [#2468] +* `ResponseBuilder::finish()` now returns `Response>`. [#2468] + +### Removed +* `ResponseBuilder::streaming`. [#2468] +* `impl Future` for `ResponseBuilder`. [#2468] +* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] +* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468] + +[#2483]: https://github.com/actix/actix-web/pull/2483 +[#2468]: https://github.com/actix/actix-web/pull/2468 + + +## 3.0.0-beta.14 - 2021-11-30 +### Changed +* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] +* Expose `header::map` module. [#2467] +* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2467]: https://github.com/actix/actix-web/pull/2467 +[#2470]: https://github.com/actix/actix-web/pull/2470 +[#2474]: https://github.com/actix/actix-web/pull/2474 + + +## 3.0.0-beta.13 - 2021-11-22 +### Added +* `body::AnyBody::empty` for quickly creating an empty body. [#2446] +* `body::AnyBody::none` for quickly creating a "none" body. [#2456] +* `impl Clone` for `body::AnyBody where S: Clone`. [#2448] +* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] + +### Changed +* Rename `body::AnyBody::{Message => Body}`. [#2446] +* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] +* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] +* Rename `body::{BoxAnyBody => BoxBody}`. [#2448] +* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448] +* `Encoder::response` now returns `AnyBody>`. [#2448] + +### Removed +* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] +* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] +* `EncoderError::Boxed`; it is no longer required. [#2446] +* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446] + +[#2446]: https://github.com/actix/actix-web/pull/2446 +[#2448]: https://github.com/actix/actix-web/pull/2448 +[#2456]: https://github.com/actix/actix-web/pull/2456 + + +## 3.0.0-beta.12 - 2021-11-15 +### Changed +* Update `actix-server` to `2.0.0-beta.9`. [#2442] + +### Removed +* `client` module. [#2425] +* `trust-dns` feature. [#2425] + +[#2425]: https://github.com/actix/actix-web/pull/2425 +[#2442]: https://github.com/actix/actix-web/pull/2442 + + +## 3.0.0-beta.11 - 2021-10-20 +### Changed +* Updated rustls to v0.20. [#2414] +* Minimum supported Rust version (MSRV) is now 1.52. + +[#2414]: https://github.com/actix/actix-web/pull/2414 + + +## 3.0.0-beta.10 - 2021-09-09 +### Changed +* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] +* Minimum supported Rust version (MSRV) is now 1.51. + +### Fixed +* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] +* Remove `Into` bound on `Encoder` body types. [#2375] +* Fix quality parse error in Accept-Encoding header. [#2344] + +[#2364]: https://github.com/actix/actix-web/pull/2364 +[#2375]: https://github.com/actix/actix-web/pull/2375 +[#2344]: https://github.com/actix/actix-web/pull/2344 +[#2377]: https://github.com/actix/actix-web/pull/2377 + + +## 3.0.0-beta.9 - 2021-08-09 +### Fixed +* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + + +## 3.0.0-beta.8 - 2021-06-26 +### Changed +* Change compression algorithm features flags. [#2250] + +### Removed +* `downcast` and `downcast_get_type_id` macros. [#2291] + +[#2291]: https://github.com/actix/actix-web/pull/2291 +[#2250]: https://github.com/actix/actix-web/pull/2250 + + +## 3.0.0-beta.7 - 2021-06-17 +### Added +* Alias `body::Body` as `body::AnyBody`. [#2215] +* `BoxAnyBody`: a boxed message body with boxed errors. [#2183] +* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] +* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171] +* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171] +* `Response::into_body` that consumes response and returns body type. [#2201] +* `impl Default` for `Response`. [#2201] +* Add zstd support for `ContentEncoding`. [#2244] + +### Changed +* The `MessageBody` trait now has an associated `Error` type. [#2183] +* All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] +* All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] +* Places in `Response` where `ResponseBody` was received or returned now simply use `B`. [#2201] +* `header` mod is now public. [#2171] +* `uri` mod is now public. [#2171] +* Update `language-tags` to `0.3`. +* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] +* `ResponseBuilder::message_body` now returns a `Result`. [#2201] +* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] +* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] + +### Removed +* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] +* Down-casting for `MessageBody` types. [#2183] +* `error::Result` alias. [#2201] +* Error field from `Response` and `Response::error`. [#2205] +* `impl Future` for `Response`. [#2201] +* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201] +* `InternalError` and all the error types it constructed. [#2215] +* Conversion (`impl Into`) of `Response` and `ResponseBuilder` to `Error`. [#2215] + +[#2171]: https://github.com/actix/actix-web/pull/2171 +[#2183]: https://github.com/actix/actix-web/pull/2183 +[#2196]: https://github.com/actix/actix-web/pull/2196 +[#2201]: https://github.com/actix/actix-web/pull/2201 +[#2205]: https://github.com/actix/actix-web/pull/2205 +[#2215]: https://github.com/actix/actix-web/pull/2215 +[#2253]: https://github.com/actix/actix-web/pull/2253 +[#2244]: https://github.com/actix/actix-web/pull/2244 + + + +## 3.0.0-beta.6 - 2021-04-17 +### Added +* `impl MessageBody for Pin>`. [#2152] +* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159] +* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158] + +### Changes +* The type parameter of `Response` no longer has a default. [#2152] +* The `Message` variant of `body::Body` is now `Pin>`. [#2152] +* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152] +* Error enum types are marked `#[non_exhaustive]`. [#2161] + +### Removed +* `cookies` feature flag. [#2065] +* Top-level `cookies` mod (re-export). [#2065] +* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065] +* `impl ResponseError for CookieParseError`. [#2065] +* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148] +* `ResponseBuilder::json`. [#2148] +* `ResponseBuilder::{set_header, header}`. [#2148] +* `impl From for Body`. [#2148] +* `Response::build_from`. [#2159] +* Most of the status code builders on `Response`. [#2159] + +[#2065]: https://github.com/actix/actix-web/pull/2065 +[#2148]: https://github.com/actix/actix-web/pull/2148 +[#2152]: https://github.com/actix/actix-web/pull/2152 +[#2159]: https://github.com/actix/actix-web/pull/2159 +[#2158]: https://github.com/actix/actix-web/pull/2158 +[#2161]: https://github.com/actix/actix-web/pull/2161 + + +## 3.0.0-beta.5 - 2021-04-02 +### Added +* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081] +* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] +* `client::ConnectionIo` trait alias [#2081] + +### Changed +* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] + +### Removed +* Common typed HTTP headers were moved to actix-web. [2094] +* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127] + +[#2063]: https://github.com/actix/actix-web/pull/2063 +[#2081]: https://github.com/actix/actix-web/pull/2081 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2127]: https://github.com/actix/actix-web/pull/2127 + + +## 3.0.0-beta.4 - 2021-03-08 +### Changed +* Feature `cookies` is now optional and disabled by default. [#1981] +* `ws::hash_key` now returns array. [#2035] +* `ResponseBuilder::json` now takes `impl Serialize`. [#2052] + +### Removed +* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] +* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994] + +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#1994]: https://github.com/actix/actix-web/pull/1994 +[#2035]: https://github.com/actix/actix-web/pull/2035 +[#2052]: https://github.com/actix/actix-web/pull/2052 + + +## 3.0.0-beta.3 - 2021-02-10 +* No notable changes. + + +## 3.0.0-beta.2 - 2021-02-10 +### Added * `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] * `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] * `ResponseBuilder::append_header` method which allows using typed headers. [#1869] * `TestRequest::insert_header` method which allows using typed headers. [#1869] * `ContentEncoding` implements all necessary header traits. [#1912] +* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964] +* `HeaderMap::drain` as an efficient draining iterator. [#1964] +* Implement `IntoIterator` for owned `HeaderMap`. [#1964] +* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed * `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed @@ -14,8 +253,15 @@ * Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std `TryInto` trait. [#1894] * `Extensions::insert` returns Option of replaced item. [#1904] -* Remove `HttpResponseBuilder::json2()` and make `HttpResponseBuilder::json()` take a value by - reference. [#1903] +* Remove `HttpResponseBuilder::json2()`. [#1903] +* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903] +* `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] +* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] +* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool + is dead. [#1957] +* `HeaderMap::len` now returns number of values instead of number of keys. [#1964] +* `HeaderMap::insert` now returns iterator of removed values. [#1964] +* `HeaderMap::remove` now returns iterator of removed values. [#1964] ### Removed * `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] @@ -23,12 +269,21 @@ * `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] * `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869] * `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] +* `actors` optional feature. [#1969] +* `ResponseError` impl for `actix::MailboxError`. [#1969] + +### Documentation +* Vastly improve docs and add examples for `HeaderMap`. [#1964] [#1869]: https://github.com/actix/actix-web/pull/1869 [#1894]: https://github.com/actix/actix-web/pull/1894 [#1903]: https://github.com/actix/actix-web/pull/1903 [#1904]: https://github.com/actix/actix-web/pull/1904 +[#1905]: https://github.com/actix/actix-web/pull/1905 [#1912]: https://github.com/actix/actix-web/pull/1912 +[#1957]: https://github.com/actix/actix-web/pull/1957 +[#1964]: https://github.com/actix/actix-web/pull/1964 +[#1969]: https://github.com/actix/actix-web/pull/1969 ## 3.0.0-beta.1 - 2021-01-07 @@ -60,6 +315,11 @@ [#1878]: https://github.com/actix/actix-web/pull/1878 +## 2.2.1 - 2021-08-09 +### Fixed +* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) + + ## 2.2.0 - 2020-11-25 ### Added * HttpResponse builders for 1xx status codes. [#1768] @@ -104,15 +364,14 @@ * Update actix-connect and actix-tls dependencies. -## [2.0.0-beta.3] - 2020-08-14 - +## 2.0.0-beta.3 - 2020-08-14 ### Fixed * Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] [#1626]: https://github.com/actix/actix-web/pull/1626 -## [2.0.0-beta.2] - 2020-07-21 +## 2.0.0-beta.2 - 2020-07-21 ### Fixed * Potential UB in h1 decoder using uninitialized memory. [#1614] @@ -123,10 +382,8 @@ [#1615]: https://github.com/actix/actix-web/pull/1615 -## [2.0.0-beta.1] - 2020-07-11 - +## 2.0.0-beta.1 - 2020-07-11 ### Changed - * Migrate cookie handling to `cookie` crate. [#1558] * Update `sha-1` to 0.9. [#1586] * Fix leak in client pool. [#1580] @@ -136,33 +393,30 @@ [#1586]: https://github.com/actix/actix-web/pull/1586 [#1580]: https://github.com/actix/actix-web/pull/1580 -## [2.0.0-alpha.4] - 2020-05-21 +## 2.0.0-alpha.4 - 2020-05-21 ### Changed - * Bump minimum supported Rust version to 1.40 -* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439] +* content_length function is removed, and you can set Content-Length by calling + no_chunking function [#1439] * `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. * Update `base64` dependency to 0.12 ### Fixed - * Support parsing of `SameSite=None` [#1503] [#1439]: https://github.com/actix/actix-web/pull/1439 [#1503]: https://github.com/actix/actix-web/pull/1503 -## [2.0.0-alpha.3] - 2020-05-08 +## 2.0.0-alpha.3 - 2020-05-08 ### Fixed - * Correct spelling of ConnectError::Unresolved [#1487] * Fix a mistake in the encoding of websocket continuation messages wherein Item::FirstText and Item::FirstBinary are each encoded as the other. ### Changed - * Implement `std::error::Error` for our custom errors [#1422] * Remove `failure` support for `ResponseError` since that crate will be deprecated in the near future. @@ -170,338 +424,247 @@ [#1422]: https://github.com/actix/actix-web/pull/1422 [#1487]: https://github.com/actix/actix-web/pull/1487 -## [2.0.0-alpha.2] - 2020-03-07 +## 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] - +* 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 +## 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(). +* 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 +## 1.0.1 - 2019-12-20 ### Fixed - * Poll upgrade service's readiness from HTTP service handlers - * Replace brotli with brotli2 #1224 -## [1.0.0] - 2019-12-13 +## 1.0.0 - 2019-12-13 ### Added - * Add websockets continuation frame support ### Changed - * Replace `flate2-xxx` features with `compress` -## [1.0.0-alpha.5] - 2019-12-09 +## 1.0.0-alpha.5 - 2019-12-09 ### Fixed - * Check `Upgrade` service readiness before calling it - -* Fix buffer remaining capacity calcualtion +* Fix buffer remaining capacity calculation ### Changed - * Websockets: Ping and Pong should have binary data #1049 -## [1.0.0-alpha.4] - 2019-12-08 +## 1.0.0-alpha.4 - 2019-12-08 ### Added - * Add impl ResponseBuilder for Error ### Changed - * Use rust based brotli compression library -## [1.0.0-alpha.3] - 2019-12-07 - +## 1.0.0-alpha.3 - 2019-12-07 ### Changed - * Migrate to tokio 0.2 - * Migrate to `std::future` -## [0.2.11] - 2019-11-06 - +## 0.2.11 - 2019-11-06 ### Added - * Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() - -* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) - +* Add an additional `filename*` param in the `Content-Disposition` header of + `actix_files::NamedFile` to be more compatible. (#1151) * Allow to use `std::convert::Infallible` as `actix_http::error::Error` ### Fixed +* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; + charset=utf-8` header [#1118] -* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118 +[#1878]: https://github.com/actix/actix-web/pull/1878 -## [0.2.10] - 2019-09-11 - +## 0.2.10 - 2019-09-11 ### Added - -* Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests with `RequestHead` +* Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests + with `RequestHead` ### Fixed - * h2 will use error response #1080 - * on_connect result isn't added to request extensions for http2 requests #1009 -## [0.2.9] - 2019-08-13 - +## 0.2.9 - 2019-08-13 ### Changed - * Dropped the `byteorder`-dependency in favor of `stdlib`-implementation - * Update percent-encoding to 2.1 - * Update serde_urlencoded to 0.6.1 ### Fixed - * Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) -## [0.2.8] - 2019-08-01 - +## 0.2.8 - 2019-08-01 ### Added - * Add `rustls` support - * Add `Clone` impl for `HeaderMap` ### Fixed - * awc client panic #1016 - -* Invalid response with compression middleware enabled, but compression-related features disabled #997 +* Invalid response with compression middleware enabled, but compression-related features + disabled #997 -## [0.2.7] - 2019-07-18 - +## 0.2.7 - 2019-07-18 ### Added - * Add support for downcasting response errors #986 -## [0.2.6] - 2019-07-17 - +## 0.2.6 - 2019-07-17 ### Changed - * Replace `ClonableService` with local copy - * Upgrade `rand` dependency version to 0.7 -## [0.2.5] - 2019-06-28 - +## 0.2.5 - 2019-06-28 ### Added - * Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 ### Changed - * Use `encoding_rs` crate instead of unmaintained `encoding` crate - * Add `Copy` and `Clone` impls for `ws::Codec` -## [0.2.4] - 2019-06-16 - +## 0.2.4 - 2019-06-16 ### Fixed - * Do not compress NoContent (204) responses #918 -## [0.2.3] - 2019-06-02 - +## 0.2.3 - 2019-06-02 ### Added - * Debug impl for ResponseBuilder - * From SizedStream and BodyStream for Body ### Changed - * SizedStream uses u64 -## [0.2.2] - 2019-05-29 - +## 0.2.2 - 2019-05-29 ### Fixed - * Parse incoming stream before closing stream on disconnect #868 -## [0.2.1] - 2019-05-25 - +## 0.2.1 - 2019-05-25 ### Fixed - * Handle socket read disconnect -## [0.2.0] - 2019-05-12 - +## 0.2.0 - 2019-05-12 ### Changed - * Update actix-service to 0.4 - * Expect and upgrade services accept `ServerConfig` config. ### Deleted - * `OneRequest` service -## [0.1.5] - 2019-05-04 - +## 0.1.5 - 2019-05-04 ### Fixed - * Clean up response extensions in response pool #817 -## [0.1.4] - 2019-04-24 - +## 0.1.4 - 2019-04-24 ### Added - * Allow to render h1 request headers in `Camel-Case` ### Fixed - * Read until eof for http/1.0 responses #771 -## [0.1.3] - 2019-04-23 - +## 0.1.3 - 2019-04-23 ### Fixed - * Fix http client pool management - * Fix http client wait queue management #794 -## [0.1.2] - 2019-04-23 - +## 0.1.2 - 2019-04-23 ### Fixed - * Fix BorrowMutError panic in client connector #793 -## [0.1.1] - 2019-04-19 - +## 0.1.1 - 2019-04-19 ### Changed - * Cookie::max_age() accepts value in seconds - * Cookie::max_age_time() accepts value in time::Duration - * Allow to specify server address for client connector -## [0.1.0] - 2019-04-16 - +## 0.1.0 - 2019-04-16 ### Added - * Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` ### Changed - * `actix_http::encoding` always available - * use trust-dns-resolver 0.11.0 -## [0.1.0-alpha.5] - 2019-04-12 - +## 0.1.0-alpha.5 - 2019-04-12 ### Added - * Allow to use custom service for upgrade requests - * Added `h1::SendResponse` future. ### Changed - * MessageBody::length() renamed to MessageBody::size() for consistency - * ws handshake verification functions take RequestHead instead of Request -## [0.1.0-alpha.4] - 2019-04-08 - +## 0.1.0-alpha.4 - 2019-04-08 ### Added - * Allow to use custom `Expect` handler - * Add minimal `std::error::Error` impl for `Error` ### Changed - * Export IntoHeaderValue - * Render error and return as response body - -* Use thread pool for response body comression +* Use thread pool for response body compression ### Deleted - * Removed PayloadBuffer -## [0.1.0-alpha.3] - 2019-04-02 - +## 0.1.0-alpha.3 - 2019-04-02 ### Added - * Warn when an unsealed private cookie isn't valid UTF-8 ### Fixed - * Rust 1.31.0 compatibility - * Preallocate read buffer for h1 codec - * Detect socket disconnection during protocol selection -## [0.1.0-alpha.2] - 2019-03-29 - +## 0.1.0-alpha.2 - 2019-03-29 ### Added - * Added ws::Message::Nop, no-op websockets message ### Changed - -* Do not use thread pool for decomression if chunk size is smaller than 2048. +* Do not use thread pool for decompression if chunk size is smaller than 2048. -## [0.1.0-alpha.1] - 2019-03-28 - +## 0.1.0-alpha.1 - 2019-03-28 * Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index af6209248..f8b15df75 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,21 +1,23 @@ [package] name = "actix-http" -version = "3.0.0-beta.1" +version = "3.0.0-beta.14" authors = ["Nikolay Kim "] description = "HTTP primitives for the Actix ecosystem" -readme = "README.md" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-http/" -categories = ["network-programming", "asynchronous", - "web-programming::http-server", - "web-programming::websocket"] +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] license = "MIT OR Apache-2.0" edition = "2018" [package.metadata.docs.rs] -features = ["openssl", "rustls", "compress", "secure-cookies", "actors"] +# features that docs.rs will build with +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] [lib] name = "actix_http" @@ -25,74 +27,79 @@ path = "src/lib.rs" default = [] # openssl -openssl = ["actix-tls/openssl"] +openssl = ["actix-tls/accept", "actix-tls/openssl"] # rustls support -rustls = ["actix-tls/rustls"] +rustls = ["actix-tls/accept", "actix-tls/rustls"] -# enable compressison support -compress = ["flate2", "brotli2"] +# enable compression support +compress-brotli = ["brotli2", "__compress"] +compress-gzip = ["flate2", "__compress"] +compress-zstd = ["zstd", "__compress"] -# support for secure cookies -secure-cookies = ["cookie/secure"] - -# support for actix Actor messages -actors = ["actix"] +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] [dependencies] -actix-service = "2.0.0-beta.3" -actix-codec = "0.4.0-beta.1" -actix-utils = "3.0.0-beta.1" -actix-rt = "2.0.0-beta.2" -actix-tls = "3.0.0-beta.2" -actix = { version = "0.11.0-beta.1", optional = true } +actix-service = "2.0.0" +actix-codec = "0.4.1" +actix-utils = "3.0.0" +actix-rt = "2.2" +ahash = "0.7" base64 = "0.13" bitflags = "1.2" bytes = "1" bytestring = "1" -cookie = { version = "0.14.1", features = ["percent-encode"] } derive_more = "0.99.5" -either = "1.5.3" encoding_rs = "0.8" -futures-channel = { version = "0.3.7", default-features = false } -futures-core = { version = "0.3.7", default-features = false } -futures-util = { version = "0.3.7", default-features = false, features = ["sink"] } -ahash = "0.6" -h2 = "0.3.0" -http = "0.2.2" -httparse = "1.3" -indexmap = "1.3" +futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } +futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } +h2 = "0.3.1" +http = "0.2.5" +httparse = "1.5.1" +httpdate = "1.0.1" itoa = "0.4" -lazy_static = "1.4" -language-tags = "0.2" +language-tags = "0.3" +local-channel = "0.1" log = "0.4" mime = "0.3" percent-encoding = "2.1" pin-project = "1.0.0" +pin-project-lite = "0.2" rand = "0.8" -regex = "1.3" -serde = "1.0" -serde_json = "1.0" sha-1 = "0.9" -smallvec = "1.6" -slab = "0.4" -serde_urlencoded = "0.7" -time = { version = "0.2.7", default-features = false, features = ["std"] } +smallvec = "1.6.1" + +# tls +actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true } # compression brotli2 = { version="0.3.2", optional = true } flate2 = { version = "1.0.13", optional = true } +zstd = { version = "0.9", optional = true } [dev-dependencies] -actix-server = "2.0.0-beta.2" -actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] } -actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] } -criterion = "0.3" -env_logger = "0.7" -serde_derive = "1.0" -open-ssl = { version="0.10", package = "openssl" } -rust-tls = { version="0.19", package = "rustls" } +actix-server = "2.0.0-beta.9" +actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } +actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] } +async-stream = "0.3" +criterion = { version = "0.3", features = ["html_reports"] } +env_logger = "0.9" +rcgen = "0.8" +regex = "1.3" +rustls-pemfile = "0.2" +serde = { version = "1.0", features = ["derive"] } +serde_json = "1.0" +static_assertions = "1" +tls-openssl = { package = "openssl", version = "0.10.9" } +tls-rustls = { package = "rustls", version = "0.20.0" } +tokio = { version = "1.2", features = ["net", "rt"] } + +[[example]] +name = "ws" +required-features = ["rustls"] [[bench]] name = "write-camel-case" diff --git a/actix-http/README.md b/actix-http/README.md index 9dfb85e24..92b86d2a3 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -3,16 +3,18 @@ > HTTP primitives for the Actix ecosystem. [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=2.2.0)](https://docs.rs/actix-http/2.2.0) -![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http) -[![Dependency Status](https://deps.rs/crate/actix-http/2.2.0/status.svg)](https://deps.rs/crate/actix-http/2.2.0) -[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.14)](https://docs.rs/actix-http/3.0.0-beta.14) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) +
+[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.14/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.14) +[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-http) -- [Chat on Gitter](https://gitter.im/actix/actix-web) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- Minimum Supported Rust Version (MSRV): 1.52 ## Example diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs index 83e74171c..53a2528ab 100644 --- a/actix-http/benches/uninit-headers.rs +++ b/actix-http/benches/uninit-headers.rs @@ -78,12 +78,12 @@ impl HeaderIndex { // test cases taken from: // https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs -const REQ_SHORT: &'static [u8] = b"\ +const REQ_SHORT: &[u8] = b"\ GET / HTTP/1.0\r\n\ Host: example.com\r\n\ Cookie: session=60; user_id=1\r\n\r\n"; -const REQ: &'static [u8] = b"\ +const REQ: &[u8] = b"\ GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\ Host: www.kittyhell.com\r\n\ User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\ @@ -119,6 +119,8 @@ mod _original { use std::mem::MaybeUninit; pub fn parse_headers(src: &mut BytesMut) -> usize { + #![allow(clippy::uninit_assumed_init)] + let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { MaybeUninit::uninit().assume_init() }; diff --git a/actix-http/benches/write-camel-case.rs b/actix-http/benches/write-camel-case.rs index fa4930eb9..ccf09b37e 100644 --- a/actix-http/benches/write-camel-case.rs +++ b/actix-http/benches/write-camel-case.rs @@ -18,7 +18,8 @@ fn bench_write_camel_case(c: &mut Criterion) { group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| { b.iter(|| { let mut buf = black_box([0; 24]); - _new::write_camel_case(black_box(bts), &mut buf) + let len = black_box(bts.len()); + _new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len) }); }); } @@ -30,9 +31,12 @@ criterion_group!(benches, bench_write_camel_case); criterion_main!(benches); mod _new { - pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) { + pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { // first copy entire (potentially wrong) slice to output - buffer[..value.len()].copy_from_slice(value); + let buffer = unsafe { + std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len); + std::slice::from_raw_parts_mut(buf, len) + }; let mut iter = value.iter(); diff --git a/actix-http/examples/echo.rs b/actix-http/examples/echo.rs index 90d768cbe..6cfe3a675 100644 --- a/actix-http/examples/echo.rs +++ b/actix-http/examples/echo.rs @@ -1,19 +1,17 @@ -use std::{env, io}; +use std::io; -use actix_http::{Error, HttpService, Request, Response}; +use actix_http::{http::StatusCode, Error, HttpService, Request, Response}; use actix_server::Server; use bytes::BytesMut; -use futures_util::StreamExt; +use futures_util::StreamExt as _; use http::header::HeaderValue; -use log::info; #[actix_rt::main] async fn main() -> io::Result<()> { - env::set_var("RUST_LOG", "echo=info"); - env_logger::init(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() - .bind("echo", "127.0.0.1:8080", || { + .bind("echo", ("127.0.0.1", 8080), || { HttpService::build() .client_timeout(1000) .client_disconnect(1000) @@ -23,9 +21,10 @@ async fn main() -> io::Result<()> { body.extend_from_slice(&item?); } - info!("request body: {:?}", body); + log::info!("request body: {:?}", body); + Ok::<_, Error>( - Response::Ok() + Response::build(StatusCode::OK) .insert_header(( "x-head", HeaderValue::from_static("dummy value!"), diff --git a/actix-http/examples/echo2.rs b/actix-http/examples/echo2.rs index bc932ce8f..6092c01ce 100644 --- a/actix-http/examples/echo2.rs +++ b/actix-http/examples/echo2.rs @@ -1,31 +1,32 @@ -use std::{env, io}; +use std::io; -use actix_http::http::HeaderValue; -use actix_http::{Error, HttpService, Request, Response}; +use actix_http::{ + body::MessageBody, http::HeaderValue, http::StatusCode, Error, HttpService, Request, + Response, +}; use actix_server::Server; use bytes::BytesMut; -use futures_util::StreamExt; -use log::info; +use futures_util::StreamExt as _; -async fn handle_request(mut req: Request) -> Result { +async fn handle_request(mut req: Request) -> Result, Error> { let mut body = BytesMut::new(); while let Some(item) = req.payload().next().await { body.extend_from_slice(&item?) } - info!("request body: {:?}", body); - Ok(Response::Ok() + log::info!("request body: {:?}", body); + + Ok(Response::build(StatusCode::OK) .insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .body(body)) } #[actix_rt::main] async fn main() -> io::Result<()> { - env::set_var("RUST_LOG", "echo=info"); - env_logger::init(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() - .bind("echo", "127.0.0.1:8080", || { + .bind("echo", ("127.0.0.1", 8080), || { HttpService::build().finish(handle_request).tcp() })? .run() diff --git a/actix-http/examples/hello-world.rs b/actix-http/examples/hello-world.rs index a84e9aac6..9a593c66a 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -1,29 +1,28 @@ -use std::{env, io}; +use std::{convert::Infallible, io}; -use actix_http::{HttpService, Response}; +use actix_http::{http::StatusCode, HttpService, Response}; use actix_server::Server; -use futures_util::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(); + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); Server::build() - .bind("hello-world", "127.0.0.1:8080", || { + .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(); + .finish(|req| async move { + log::info!("{:?}", req); + + let mut res = Response::build(StatusCode::OK); res.insert_header(( "x-head", HeaderValue::from_static("dummy value!"), )); - future::ok::<_, ()>(res.body("Hello world!")) + + Ok::<_, Infallible>(res.body("Hello world!")) }) .tcp() })? diff --git a/actix-http/examples/streaming-error.rs b/actix-http/examples/streaming-error.rs new file mode 100644 index 000000000..3988cbac2 --- /dev/null +++ b/actix-http/examples/streaming-error.rs @@ -0,0 +1,40 @@ +//! Example showing response body (chunked) stream erroring. +//! +//! Test using `nc` or `curl`. +//! ```sh +//! $ curl -vN 127.0.0.1:8080 +//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080 +//! ``` + +use std::{convert::Infallible, io, time::Duration}; + +use actix_http::{body::BodyStream, HttpService, Response}; +use actix_server::Server; +use async_stream::stream; +use bytes::Bytes; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + Server::build() + .bind("streaming-error", ("127.0.0.1", 8080), || { + HttpService::build() + .finish(|req| async move { + log::info!("{:?}", req); + let res = Response::ok(); + + Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! { + yield Ok(Bytes::from("123")); + yield Ok(Bytes::from("456")); + + actix_rt::time::sleep(Duration::from_millis(1000)).await; + + yield Err(io::Error::new(io::ErrorKind::Other, "")); + }))) + }) + .tcp() + })? + .run() + .await +} diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs new file mode 100644 index 000000000..b6be4d2f1 --- /dev/null +++ b/actix-http/examples/ws.rs @@ -0,0 +1,115 @@ +//! Sets up a WebSocket server over TCP and TLS. +//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames. + +extern crate tls_rustls as rustls; + +use std::{ + io, + pin::Pin, + task::{Context, Poll}, + time::Duration, +}; + +use actix_codec::Encoder; +use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response}; +use actix_rt::time::{interval, Interval}; +use actix_server::Server; +use bytes::{Bytes, BytesMut}; +use bytestring::ByteString; +use futures_core::{ready, Stream}; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + Server::build() + .bind("tcp", ("127.0.0.1", 8080), || { + HttpService::build().h1(handler).tcp() + })? + .bind("tls", ("127.0.0.1", 8443), || { + HttpService::build().finish(handler).rustls(tls_config()) + })? + .run() + .await +} + +async fn handler(req: Request) -> Result>, Error> { + log::info!("handshaking"); + let mut res = ws::handshake(req.head())?; + + // handshake will always fail under HTTP/2 + + log::info!("responding"); + Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?) +} + +struct Heartbeat { + codec: ws::Codec, + interval: Interval, +} + +impl Heartbeat { + fn new(codec: ws::Codec) -> Self { + Self { + codec, + interval: interval(Duration::from_secs(4)), + } + } +} + +impl Stream for Heartbeat { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + log::trace!("poll"); + + ready!(self.as_mut().interval.poll_tick(cx)); + + let mut buffer = BytesMut::new(); + + self.as_mut() + .codec + .encode( + ws::Message::Text(ByteString::from_static("hello world")), + &mut buffer, + ) + .unwrap(); + + Poll::Ready(Some(Ok(buffer.freeze()))) + } +} + +fn tls_config() -> rustls::ServerConfig { + use std::io::BufReader; + + use rustls::{Certificate, PrivateKey}; + use rustls_pemfile::{certs, pkcs8_private_keys}; + + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + + let cert_file = &mut BufReader::new(cert_file.as_bytes()); + let key_file = &mut BufReader::new(key_file.as_bytes()); + + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); + let mut keys = pkcs8_private_keys(key_file).unwrap(); + + let mut config = rustls::ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap(); + + config.alpn_protocols.push(b"http/1.1".to_vec()); + config.alpn_protocols.push(b"h2".to_vec()); + + config +} diff --git a/actix-http/src/body.rs b/actix-http/src/body.rs deleted file mode 100644 index d5d1989d4..000000000 --- a/actix-http/src/body.rs +++ /dev/null @@ -1,710 +0,0 @@ -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{fmt, mem}; - -use bytes::{Bytes, BytesMut}; -use futures_core::{ready, Stream}; -use pin_project::pin_project; - -use crate::error::Error; - -#[derive(Debug, PartialEq, Copy, Clone)] -/// Body size hint -pub enum BodySize { - None, - Empty, - Sized(u64), - Stream, -} - -impl BodySize { - pub fn is_eof(&self) -> bool { - matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) - } -} - -/// Type that provides this trait can be streamed to a peer. -pub trait MessageBody { - fn size(&self) -> BodySize; - - 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( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - Poll::Ready(None) - } -} - -impl MessageBody for Box { - fn size(&self) -> BodySize { - self.as_ref().size() - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - Pin::new(self.get_mut().as_mut()).poll_next(cx) - } -} - -#[pin_project(project = ResponseBodyProj)] -pub enum ResponseBody { - Body(#[pin] B), - Other(Body), -} - -impl ResponseBody { - pub fn into_body(self) -> ResponseBody { - match self { - ResponseBody::Body(b) => ResponseBody::Other(b), - ResponseBody::Other(b) => ResponseBody::Other(b), - } - } -} - -impl ResponseBody { - pub fn take_body(&mut self) -> ResponseBody { - std::mem::replace(self, ResponseBody::Other(Body::None)) - } -} - -impl ResponseBody { - pub fn as_ref(&self) -> Option<&B> { - if let ResponseBody::Body(ref b) = self { - Some(b) - } else { - None - } - } -} - -impl MessageBody for ResponseBody { - fn size(&self) -> BodySize { - match self { - ResponseBody::Body(ref body) => body.size(), - ResponseBody::Other(ref body) => body.size(), - } - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - match self.project() { - ResponseBodyProj::Body(body) => body.poll_next(cx), - ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), - } - } -} - -impl Stream for ResponseBody { - type Item = Result; - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.project() { - ResponseBodyProj::Body(body) => body.poll_next(cx), - ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), - } - } -} - -/// Represents various types of http message body. -pub enum Body { - /// Empty response. `Content-Length` header is not set. - None, - /// Zero sized response body. `Content-Length` header is set to `0`. - Empty, - /// Specific response body. - Bytes(Bytes), - /// Generic message body. - Message(Box), -} - -impl Body { - /// Create body from slice (copy) - pub fn from_slice(s: &[u8]) -> Body { - Body::Bytes(Bytes::copy_from_slice(s)) - } - - /// Create body from generic message body. - pub fn from_message(body: B) -> Body { - Body::Message(Box::new(body)) - } -} - -impl MessageBody for Body { - fn size(&self) -> BodySize { - match self { - Body::None => BodySize::None, - Body::Empty => BodySize::Empty, - Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64), - Body::Message(ref body) => body.size(), - } - } - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - match self.get_mut() { - Body::None => Poll::Ready(None), - Body::Empty => Poll::Ready(None), - Body::Bytes(ref mut bin) => { - let len = bin.len(); - if len == 0 { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(bin)))) - } - } - Body::Message(body) => Pin::new(&mut **body).poll_next(cx), - } - } -} - -impl PartialEq for Body { - fn eq(&self, other: &Body) -> bool { - match *self { - Body::None => matches!(*other, Body::None), - Body::Empty => matches!(*other, Body::Empty), - Body::Bytes(ref b) => match *other { - Body::Bytes(ref b2) => b == b2, - _ => false, - }, - Body::Message(_) => false, - } - } -} - -impl fmt::Debug for Body { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match *self { - Body::None => write!(f, "Body::None"), - Body::Empty => write!(f, "Body::Empty"), - Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b), - Body::Message(_) => write!(f, "Body::Message(_)"), - } - } -} - -impl From<&'static str> for Body { - fn from(s: &'static str) -> Body { - Body::Bytes(Bytes::from_static(s.as_ref())) - } -} - -impl From<&'static [u8]> for Body { - fn from(s: &'static [u8]) -> Body { - Body::Bytes(Bytes::from_static(s)) - } -} - -impl From> for Body { - fn from(vec: Vec) -> Body { - Body::Bytes(Bytes::from(vec)) - } -} - -impl From for Body { - fn from(s: String) -> Body { - s.into_bytes().into() - } -} - -impl<'a> From<&'a String> for Body { - fn from(s: &'a String) -> Body { - Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s))) - } -} - -impl From for Body { - fn from(s: Bytes) -> Body { - Body::Bytes(s) - } -} - -impl From for Body { - fn from(s: BytesMut) -> Body { - Body::Bytes(s.freeze()) - } -} - -impl From for Body { - fn from(v: serde_json::Value) -> Body { - Body::Bytes(v.to_string().into()) - } -} - -impl From> for Body -where - S: Stream> + Unpin + 'static, -{ - fn from(s: SizedStream) -> Body { - Body::from_message(s) - } -} - -impl From> for Body -where - S: Stream> + Unpin + 'static, - E: Into + 'static, -{ - fn from(s: BodyStream) -> Body { - Body::from_message(s) - } -} - -impl MessageBody for Bytes { - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut())))) - } - } -} - -impl MessageBody for BytesMut { - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - fn poll_next( - self: Pin<&mut Self>, - _: &mut Context<'_>, - ) -> Poll>> { - if self.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze()))) - } - } -} - -impl MessageBody for &'static str { - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - 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::take(self.get_mut()).as_ref(), - )))) - } - } -} - -impl MessageBody for Vec { - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - 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::take(self.get_mut()))))) - } - } -} - -impl MessageBody for String { - fn size(&self) -> BodySize { - BodySize::Sized(self.len() as u64) - } - - 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::take(self.get_mut()).into_bytes(), - )))) - } - } -} - -/// Type represent streaming body. -/// Response does not contain `content-length` header and appropriate transfer encoding is used. -pub struct BodyStream { - stream: S, -} - -impl BodyStream -where - S: Stream> + Unpin, - E: Into, -{ - pub fn new(stream: S) -> Self { - BodyStream { stream } - } -} - -impl MessageBody for BodyStream -where - S: Stream> + Unpin, - E: Into, -{ - fn size(&self) -> BodySize { - BodySize::Stream - } - - /// 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( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - loop { - let stream = &mut self.as_mut().stream; - return Poll::Ready(match ready!(Pin::new(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. -pub struct SizedStream { - size: u64, - stream: S, -} - -impl SizedStream -where - S: Stream> + Unpin, -{ - pub fn new(size: u64, stream: S) -> Self { - SizedStream { size, stream } - } -} - -impl MessageBody for SizedStream -where - S: Stream> + Unpin, -{ - fn size(&self) -> BodySize { - BodySize::Sized(self.size as u64) - } - - /// 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( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll>> { - loop { - let stream = &mut self.as_mut().stream; - return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) { - Some(Ok(ref bytes)) if bytes.is_empty() => continue, - val => val, - }); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use futures_util::future::poll_fn; - use futures_util::pin_mut; - use futures_util::stream; - - impl Body { - pub(crate) fn get_ref(&self) -> &[u8] { - match *self { - Body::Bytes(ref bin) => &bin, - _ => panic!(), - } - } - } - - impl ResponseBody { - pub(crate) fn get_ref(&self) -> &[u8] { - match *self { - ResponseBody::Body(ref b) => b.get_ref(), - ResponseBody::Other(ref b) => b.get_ref(), - } - } - } - - #[actix_rt::test] - async fn test_static_str() { - assert_eq!(Body::from("").size(), BodySize::Sized(0)); - assert_eq!(Body::from("test").size(), BodySize::Sized(4)); - assert_eq!(Body::from("test").get_ref(), b"test"); - - assert_eq!("test".size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| Pin::new(&mut "test").poll_next(cx)) - .await - .unwrap() - .ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_static_bytes() { - assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4)); - assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test"); - assert_eq!( - Body::from_slice(b"test".as_ref()).size(), - 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!(sb.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - 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!(test_vec.size(), BodySize::Sized(4)); - assert_eq!( - poll_fn(|cx| test_vec.as_mut().poll_next(cx)) - .await - .unwrap() - .ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_bytes() { - 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.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_bytes_mut() { - 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.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_string() { - 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.as_mut().poll_next(cx)).await.unwrap().ok(), - Some(Bytes::from("test")) - ); - } - - #[actix_rt::test] - async fn test_unit() { - assert_eq!(().size(), BodySize::Empty); - assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx)) - .await - .is_none()); - } - - #[actix_rt::test] - async fn test_box() { - let val = Box::new(()); - pin_mut!(val); - assert_eq!(val.size(), BodySize::Empty); - assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); - } - - #[actix_rt::test] - async fn test_body_eq() { - assert!( - Body::Bytes(Bytes::from_static(b"1")) - == Body::Bytes(Bytes::from_static(b"1")) - ); - assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None); - } - - #[actix_rt::test] - async fn test_body_debug() { - assert!(format!("{:?}", Body::None).contains("Body::None")); - assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); - assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1')); - } - - #[actix_rt::test] - async fn test_serde_json() { - use serde_json::json; - assert_eq!( - Body::from(serde_json::Value::String("test".into())).size(), - BodySize::Sized(6) - ); - assert_eq!( - Body::from(json!({"test-key":"test-value"})).size(), - 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('!'); - 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/body/body_stream.rs b/actix-http/src/body/body_stream.rs new file mode 100644 index 000000000..1da7a848a --- /dev/null +++ b/actix-http/src/body/body_stream.rs @@ -0,0 +1,214 @@ +use std::{ + error::Error as StdError, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; + +use super::{BodySize, MessageBody}; + +pin_project! { + /// Streaming response wrapper. + /// + /// Response does not contain `Content-Length` header and appropriate transfer encoding is used. + pub struct BodyStream { + #[pin] + stream: S, + } +} + +// TODO: from_infallible method + +impl BodyStream +where + S: Stream>, + E: Into> + 'static, +{ + pub fn new(stream: S) -> Self { + BodyStream { stream } + } +} + +impl MessageBody for BodyStream +where + S: Stream>, + E: Into> + 'static, +{ + type Error = E; + + fn size(&self) -> BodySize { + BodySize::Stream + } + + /// 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( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + loop { + let stream = self.as_mut().project().stream; + + let chunk = match ready!(stream.poll_next(cx)) { + Some(Ok(ref bytes)) if bytes.is_empty() => continue, + opt => opt, + }; + + return Poll::Ready(chunk); + } + } +} + +#[cfg(test)] +mod tests { + use std::{convert::Infallible, time::Duration}; + + use actix_rt::{ + pin, + time::{sleep, Sleep}, + }; + use actix_utils::future::poll_fn; + use derive_more::{Display, Error}; + use futures_core::ready; + use futures_util::{stream, FutureExt as _}; + use pin_project_lite::pin_project; + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + assert_impl_all!(BodyStream>>: MessageBody); + + assert_not_impl_all!(BodyStream>: MessageBody); + assert_not_impl_all!(BodyStream>: MessageBody); + // crate::Error is not Clone + assert_not_impl_all!(BodyStream>>: MessageBody); + + #[actix_rt::test] + async fn skips_empty_chunks() { + let body = BodyStream::new(stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), + )); + pin!(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 read_to_bytes() { + let body = BodyStream::new(stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), + )); + + assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); + } + #[derive(Debug, Display, Error)] + #[display(fmt = "stream error")] + struct StreamErr; + + #[actix_rt::test] + async fn stream_immediate_error() { + let body = BodyStream::new(stream::once(async { Err(StreamErr) })); + assert!(matches!(to_bytes(body).await, Err(StreamErr))); + } + + #[actix_rt::test] + async fn stream_string_error() { + // `&'static str` does not impl `Error` + // but it does impl `Into>` + + let body = BodyStream::new(stream::once(async { Err("stringy error") })); + assert!(matches!(to_bytes(body).await, Err("stringy error"))); + } + + #[actix_rt::test] + async fn stream_boxed_error() { + // `Box` does not impl `Error` + // but it does impl `Into>` + + let body = BodyStream::new(stream::once(async { + Err(Box::::from("stringy error")) + })); + + assert_eq!( + to_bytes(body).await.unwrap_err().to_string(), + "stringy error" + ); + } + + #[actix_rt::test] + async fn stream_delayed_error() { + let body = + BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); + assert!(matches!(to_bytes(body).await, Err(StreamErr))); + + pin_project! { + #[derive(Debug)] + #[project = TimeDelayStreamProj] + enum TimeDelayStream { + Start, + Sleep { delay: Pin> }, + Done, + } + } + + impl Stream for TimeDelayStream { + type Item = Result; + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + match self.as_mut().get_mut() { + TimeDelayStream::Start => { + let sleep = sleep(Duration::from_millis(1)); + self.as_mut().set(TimeDelayStream::Sleep { + delay: Box::pin(sleep), + }); + cx.waker().wake_by_ref(); + Poll::Pending + } + + TimeDelayStream::Sleep { ref mut delay } => { + ready!(delay.poll_unpin(cx)); + self.set(TimeDelayStream::Done); + cx.waker().wake_by_ref(); + Poll::Pending + } + + TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))), + } + } + } + + let body = BodyStream::new(TimeDelayStream::Start); + assert!(matches!(to_bytes(body).await, Err(StreamErr))); + } +} diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs new file mode 100644 index 000000000..9442bd1df --- /dev/null +++ b/actix-http/src/body/boxed.rs @@ -0,0 +1,80 @@ +use std::{ + error::Error as StdError, + fmt, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; + +use super::{BodySize, MessageBody, MessageBodyMapErr}; +use crate::Error; + +/// A boxed message body with boxed errors. +pub struct BoxBody(Pin>>>); + +impl BoxBody { + /// Boxes a `MessageBody` and any errors it generates. + pub fn new(body: B) -> Self + where + B: MessageBody + 'static, + { + let body = MessageBodyMapErr::new(body, Into::into); + Self(Box::pin(body)) + } + + /// Returns a mutable pinned reference to the inner message body type. + pub fn as_pin_mut( + &mut self, + ) -> Pin<&mut (dyn MessageBody>)> { + self.0.as_mut() + } +} + +impl fmt::Debug for BoxBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("BoxBody(dyn MessageBody)") + } +} + +impl MessageBody for BoxBody { + type Error = Error; + + fn size(&self) -> BodySize { + self.0.size() + } + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.0 + .as_mut() + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)) + } +} + +#[cfg(test)] +mod tests { + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin); + + assert_not_impl_all!(BoxBody: Send, Sync, Unpin); + + #[actix_rt::test] + async fn nested_boxed_body() { + let body = Bytes::from_static(&[1, 2, 3]); + let boxed_body = BoxBody::new(BoxBody::new(body)); + + assert_eq!( + to_bytes(boxed_body).await.unwrap(), + Bytes::from(vec![1, 2, 3]), + ); + } +} diff --git a/actix-http/src/body/either.rs b/actix-http/src/body/either.rs new file mode 100644 index 000000000..6169ee627 --- /dev/null +++ b/actix-http/src/body/either.rs @@ -0,0 +1,83 @@ +use std::{ + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use pin_project_lite::pin_project; + +use super::{BodySize, BoxBody, MessageBody}; +use crate::Error; + +pin_project! { + #[project = EitherBodyProj] + #[derive(Debug, Clone)] + pub enum EitherBody { + /// A body of type `L`. + Left { #[pin] body: L }, + + /// A body of type `R`. + Right { #[pin] body: R }, + } +} + +impl EitherBody { + /// Creates new `EitherBody` using left variant and boxed right variant. + pub fn new(body: L) -> Self { + Self::Left { body } + } +} + +impl EitherBody { + /// Creates new `EitherBody` using left variant. + pub fn left(body: L) -> Self { + Self::Left { body } + } + + /// Creates new `EitherBody` using right variant. + pub fn right(body: R) -> Self { + Self::Right { body } + } +} + +impl MessageBody for EitherBody +where + L: MessageBody + 'static, + R: MessageBody + 'static, +{ + type Error = Error; + + fn size(&self) -> BodySize { + match self { + EitherBody::Left { body } => body.size(), + EitherBody::Right { body } => body.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + EitherBodyProj::Left { body } => body + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), + EitherBodyProj::Right { body } => body + .poll_next(cx) + .map_err(|err| Error::new_body().with_cause(err)), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn type_parameter_inference() { + let _body: EitherBody<(), _> = EitherBody::new(()); + + let _body: EitherBody<_, ()> = EitherBody::left(()); + let _body: EitherBody<(), _> = EitherBody::right(()); + } +} diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs new file mode 100644 index 000000000..053b6f286 --- /dev/null +++ b/actix-http/src/body/message_body.rs @@ -0,0 +1,425 @@ +//! [`MessageBody`] trait and foreign implementations. + +use std::{ + convert::Infallible, + error::Error as StdError, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::{Bytes, BytesMut}; +use futures_core::ready; +use pin_project_lite::pin_project; + +use super::BodySize; + +/// An interface types that can converted to bytes and used as response bodies. +// TODO: examples +pub trait MessageBody { + // TODO: consider this bound to only fmt::Display since the error type is not really used + // and there is an impl for Into> on String + type Error: Into>; + + /// Body size hint. + fn size(&self) -> BodySize; + + /// Attempt to pull out the next chunk of body bytes. + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>>; +} + +mod foreign_impls { + use super::*; + + impl MessageBody for Infallible { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + match *self {} + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + match *self {} + } + } + + impl MessageBody for () { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + BodySize::Sized(0) + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(None) + } + } + + impl MessageBody for Box + where + B: MessageBody + Unpin, + { + type Error = B::Error; + + #[inline] + fn size(&self) -> BodySize { + self.as_ref().size() + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + Pin::new(self.get_mut().as_mut()).poll_next(cx) + } + } + + impl MessageBody for Pin> + where + B: MessageBody, + { + type Error = B::Error; + + #[inline] + fn size(&self) -> BodySize { + self.as_ref().size() + } + + #[inline] + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + self.as_mut().poll_next(cx) + } + } + + impl MessageBody for &'static [u8] { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + let bytes = Bytes::from_static(bytes); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for Bytes { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for BytesMut { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()).freeze(); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for Vec { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let bytes = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(Bytes::from(bytes)))) + } + } + } + + impl MessageBody for &'static str { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let string = mem::take(self.get_mut()); + let bytes = Bytes::from_static(string.as_bytes()); + Poll::Ready(Some(Ok(bytes))) + } + } + } + + impl MessageBody for String { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + if self.is_empty() { + Poll::Ready(None) + } else { + let string = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(Bytes::from(string)))) + } + } + } + + impl MessageBody for bytestring::ByteString { + type Error = Infallible; + + fn size(&self) -> BodySize { + BodySize::Sized(self.len() as u64) + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + let string = mem::take(self.get_mut()); + Poll::Ready(Some(Ok(string.into_bytes()))) + } + } +} + +pin_project! { + pub(crate) struct MessageBodyMapErr { + #[pin] + body: B, + mapper: Option, + } +} + +impl MessageBodyMapErr +where + B: MessageBody, + F: FnOnce(B::Error) -> E, +{ + pub(crate) fn new(body: B, mapper: F) -> Self { + Self { + body, + mapper: Some(mapper), + } + } +} + +impl MessageBody for MessageBodyMapErr +where + B: MessageBody, + F: FnOnce(B::Error) -> E, + E: Into>, +{ + type Error = E; + + fn size(&self) -> BodySize { + self.body.size() + } + + fn poll_next( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + let this = self.as_mut().project(); + + match ready!(this.body.poll_next(cx)) { + Some(Err(err)) => { + let f = self.as_mut().project().mapper.take().unwrap(); + let mapped_err = (f)(err); + Poll::Ready(Some(Err(mapped_err))) + } + Some(Ok(val)) => Poll::Ready(Some(Ok(val))), + None => Poll::Ready(None), + } + } +} + +#[cfg(test)] +mod tests { + use actix_rt::pin; + use actix_utils::future::poll_fn; + use bytes::{Bytes, BytesMut}; + + use super::*; + + macro_rules! assert_poll_next { + ($pin:expr, $exp:expr) => { + assert_eq!( + poll_fn(|cx| $pin.as_mut().poll_next(cx)) + .await + .unwrap() // unwrap option + .unwrap(), // unwrap result + $exp + ); + }; + } + + macro_rules! assert_poll_next_none { + ($pin:expr) => { + assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none()); + }; + } + + #[actix_rt::test] + async fn boxing_equivalence() { + assert_eq!(().size(), BodySize::Sized(0)); + assert_eq!(().size(), Box::new(()).size()); + assert_eq!(().size(), Box::pin(()).size()); + + let pl = Box::new(()); + pin!(pl); + assert_poll_next_none!(pl); + + let mut pl = Box::pin(()); + assert_poll_next_none!(pl); + } + + #[actix_rt::test] + async fn test_unit() { + let pl = (); + assert_eq!(pl.size(), BodySize::Sized(0)); + pin!(pl); + assert_poll_next_none!(pl); + } + + #[actix_rt::test] + async fn test_static_str() { + assert_eq!("".size(), BodySize::Sized(0)); + assert_eq!("test".size(), BodySize::Sized(4)); + + let pl = "test"; + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_static_bytes() { + assert_eq!(b"".as_ref().size(), BodySize::Sized(0)); + assert_eq!(b"test".as_ref().size(), BodySize::Sized(4)); + + let pl = b"test".as_ref(); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_vec() { + assert_eq!(vec![0; 0].size(), BodySize::Sized(0)); + assert_eq!(Vec::from("test").size(), BodySize::Sized(4)); + + let pl = Vec::from("test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_bytes() { + assert_eq!(Bytes::new().size(), BodySize::Sized(0)); + assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4)); + + let pl = Bytes::from_static(b"test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_bytes_mut() { + assert_eq!(BytesMut::new().size(), BodySize::Sized(0)); + assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4)); + + let pl = BytesMut::from("test"); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + #[actix_rt::test] + async fn test_string() { + assert_eq!(String::new().size(), BodySize::Sized(0)); + assert_eq!("test".to_owned().size(), BodySize::Sized(4)); + + let pl = "test".to_owned(); + pin!(pl); + assert_poll_next!(pl, Bytes::from("test")); + } + + // down-casting used to be done with a method on MessageBody trait + // test is kept to demonstrate equivalence of Any trait + #[actix_rt::test] + async fn test_body_casting() { + let mut body = String::from("hello cast"); + // let mut resp_body: &mut dyn MessageBody = &mut body; + let resp_body: &mut dyn std::any::Any = &mut body; + let body = resp_body.downcast_ref::().unwrap(); + assert_eq!(body, "hello cast"); + let body = &mut resp_body.downcast_mut::().unwrap(); + body.push('!'); + 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/body/mod.rs b/actix-http/src/body/mod.rs new file mode 100644 index 000000000..af7c4626f --- /dev/null +++ b/actix-http/src/body/mod.rs @@ -0,0 +1,20 @@ +//! Traits and structures to aid consuming and writing HTTP payloads. + +mod body_stream; +mod boxed; +mod either; +mod message_body; +mod none; +mod size; +mod sized_stream; +mod utils; + +pub use self::body_stream::BodyStream; +pub use self::boxed::BoxBody; +pub use self::either::EitherBody; +pub use self::message_body::MessageBody; +pub(crate) use self::message_body::MessageBodyMapErr; +pub use self::none::None; +pub use self::size::BodySize; +pub use self::sized_stream::SizedStream; +pub use self::utils::to_bytes; diff --git a/actix-http/src/body/none.rs b/actix-http/src/body/none.rs new file mode 100644 index 000000000..0fc7c8c9f --- /dev/null +++ b/actix-http/src/body/none.rs @@ -0,0 +1,43 @@ +use std::{ + convert::Infallible, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; + +use super::{BodySize, MessageBody}; + +/// Body type for responses that forbid payloads. +/// +/// Distinct from an empty response which would contain a Content-Length header. +/// +/// For an "empty" body, use `()` or `Bytes::new()`. +#[derive(Debug, Clone, Copy, Default)] +#[non_exhaustive] +pub struct None; + +impl None { + /// Constructs new "none" body. + #[inline] + pub fn new() -> Self { + None + } +} + +impl MessageBody for None { + type Error = Infallible; + + #[inline] + fn size(&self) -> BodySize { + BodySize::None + } + + #[inline] + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + Poll::Ready(Option::None) + } +} diff --git a/actix-http/src/body/size.rs b/actix-http/src/body/size.rs new file mode 100644 index 000000000..d64af9d44 --- /dev/null +++ b/actix-http/src/body/size.rs @@ -0,0 +1,36 @@ +/// Body size hint. +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum BodySize { + /// Absence of body can be assumed from method or status code. + /// + /// Will skip writing Content-Length header. + None, + + /// Known size body. + /// + /// Will write `Content-Length: N` header. + Sized(u64), + + /// Unknown size body. + /// + /// Will not write Content-Length header. Can be used with chunked Transfer-Encoding. + Stream, +} + +impl BodySize { + /// Returns true if size hint indicates omitted or empty body. + /// + /// Streams will return false because it cannot be known without reading the stream. + /// + /// ``` + /// # use actix_http::body::BodySize; + /// assert!(BodySize::None.is_eof()); + /// assert!(BodySize::Sized(0).is_eof()); + /// + /// assert!(!BodySize::Sized(64).is_eof()); + /// assert!(!BodySize::Stream.is_eof()); + /// ``` + pub fn is_eof(&self) -> bool { + matches!(self, BodySize::None | BodySize::Sized(0)) + } +} diff --git a/actix-http/src/body/sized_stream.rs b/actix-http/src/body/sized_stream.rs new file mode 100644 index 000000000..c8606897d --- /dev/null +++ b/actix-http/src/body/sized_stream.rs @@ -0,0 +1,169 @@ +use std::{ + error::Error as StdError, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::Bytes; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; + +use super::{BodySize, MessageBody}; + +pin_project! { + /// Known sized streaming response wrapper. + /// + /// This body implementation should be used if total size of stream is known. Data is sent as-is + /// without using chunked transfer encoding. + pub struct SizedStream { + size: u64, + #[pin] + stream: S, + } +} + +impl SizedStream +where + S: Stream>, + E: Into> + 'static, +{ + pub fn new(size: u64, stream: S) -> Self { + SizedStream { size, stream } + } +} + +// TODO: from_infallible method + +impl MessageBody for SizedStream +where + S: Stream>, + E: Into> + 'static, +{ + type Error = E; + + fn size(&self) -> BodySize { + BodySize::Sized(self.size as u64) + } + + /// 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( + mut self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + loop { + let stream = self.as_mut().project().stream; + + let chunk = match ready!(stream.poll_next(cx)) { + Some(Ok(ref bytes)) if bytes.is_empty() => continue, + val => val, + }; + + return Poll::Ready(chunk); + } + } +} + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use actix_rt::pin; + use actix_utils::future::poll_fn; + use futures_util::stream; + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + use crate::body::to_bytes; + + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + assert_impl_all!(SizedStream>>: MessageBody); + + assert_not_impl_all!(SizedStream>: MessageBody); + assert_not_impl_all!(SizedStream>: MessageBody); + // crate::Error is not Clone + assert_not_impl_all!(SizedStream>>: MessageBody); + + #[actix_rt::test] + async fn skips_empty_chunks() { + let body = SizedStream::new( + 2, + stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), + ), + ); + + pin!(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 read_to_bytes() { + let body = SizedStream::new( + 2, + stream::iter( + ["1", "", "2"] + .iter() + .map(|&v| Ok::<_, Infallible>(Bytes::from(v))), + ), + ); + + assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); + } + + #[actix_rt::test] + async fn stream_string_error() { + // `&'static str` does not impl `Error` + // but it does impl `Into>` + + let body = SizedStream::new(0, stream::once(async { Err("stringy error") })); + assert_eq!(to_bytes(body).await, Ok(Bytes::new())); + + let body = SizedStream::new(1, stream::once(async { Err("stringy error") })); + assert!(matches!(to_bytes(body).await, Err("stringy error"))); + } + + #[actix_rt::test] + async fn stream_boxed_error() { + // `Box` does not impl `Error` + // but it does impl `Into>` + + let body = SizedStream::new( + 0, + stream::once(async { Err(Box::::from("stringy error")) }), + ); + assert_eq!(to_bytes(body).await.unwrap(), Bytes::new()); + + let body = SizedStream::new( + 1, + stream::once(async { Err(Box::::from("stringy error")) }), + ); + assert_eq!( + to_bytes(body).await.unwrap_err().to_string(), + "stringy error" + ); + } +} diff --git a/actix-http/src/body/utils.rs b/actix-http/src/body/utils.rs new file mode 100644 index 000000000..a421ffd76 --- /dev/null +++ b/actix-http/src/body/utils.rs @@ -0,0 +1,78 @@ +use std::task::Poll; + +use actix_rt::pin; +use actix_utils::future::poll_fn; +use bytes::{Bytes, BytesMut}; +use futures_core::ready; + +use super::{BodySize, MessageBody}; + +/// Collects the body produced by a `MessageBody` implementation into `Bytes`. +/// +/// Any errors produced by the body stream are returned immediately. +/// +/// # Examples +/// ``` +/// use actix_http::body::{self, to_bytes}; +/// use bytes::Bytes; +/// +/// # async fn test_to_bytes() { +/// let body = body::None::new(); +/// let bytes = to_bytes(body).await.unwrap(); +/// assert!(bytes.is_empty()); +/// +/// let body = Bytes::from_static(b"123"); +/// let bytes = to_bytes(body).await.unwrap(); +/// assert_eq!(bytes, b"123"[..]); +/// # } +/// ``` +pub async fn to_bytes(body: B) -> Result { + let cap = match body.size() { + BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), + BodySize::Sized(size) => size as usize, + // good enough first guess for chunk size + BodySize::Stream => 32_768, + }; + + let mut buf = BytesMut::with_capacity(cap); + + pin!(body); + + poll_fn(|cx| loop { + let body = body.as_mut(); + + match ready!(body.poll_next(cx)) { + Some(Ok(bytes)) => buf.extend_from_slice(&*bytes), + None => return Poll::Ready(Ok(())), + Some(Err(err)) => return Poll::Ready(Err(err)), + } + }) + .await?; + + Ok(buf.freeze()) +} + +#[cfg(test)] +mod test { + use futures_util::{stream, StreamExt as _}; + + use super::*; + use crate::{body::BodyStream, Error}; + + #[actix_rt::test] + async fn test_to_bytes() { + let bytes = to_bytes(()).await.unwrap(); + assert!(bytes.is_empty()); + + let body = Bytes::from_static(b"123"); + let bytes = to_bytes(body).await.unwrap(); + assert_eq!(bytes, b"123"[..]); + + let stream = + stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) + .map(Ok::<_, Error>); + let body = BodyStream::new(stream); + let bytes = to_bytes(body).await.unwrap(); + assert_eq!(bytes, b"123abc"[..]); + } +} diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index fa430c4fe..ca821f1d9 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -1,19 +1,16 @@ -use std::marker::PhantomData; -use std::rc::Rc; -use std::{fmt, net}; +use std::{fmt, marker::PhantomData, net, rc::Rc}; use actix_codec::Framed; use actix_service::{IntoServiceFactory, Service, ServiceFactory}; -use crate::body::MessageBody; -use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::Error; -use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; -use crate::h2::H2Service; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpService; -use crate::{ConnectCallback, Extensions}; +use crate::{ + body::{BoxBody, MessageBody}, + config::{KeepAlive, ServiceConfig}, + h1::{self, ExpectHandler, H1Service, UpgradeHandler}, + h2::H2Service, + service::HttpService, + ConnectCallback, Extensions, Request, Response, +}; /// A HTTP service builder /// @@ -34,7 +31,7 @@ pub struct HttpServiceBuilder { impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, { @@ -57,17 +54,15 @@ where impl HttpServiceBuilder where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, >::Future: 'static, X: ServiceFactory, - X::Error: Into, + X::Error: Into>, X::InitError: fmt::Debug, - >::Future: 'static, - U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, + U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U::Error: fmt::Display, U::InitError: fmt::Debug, - )>>::Future: 'static, { /// Set server keep-alive setting. /// @@ -125,9 +120,8 @@ where where F: IntoServiceFactory, X1: ServiceFactory, - X1::Error: Into, + X1::Error: Into>, X1::InitError: fmt::Debug, - >::Future: 'static, { HttpServiceBuilder { keep_alive: self.keep_alive, @@ -148,11 +142,10 @@ where /// and this service get called with original request and framed object. pub fn upgrade(self, upgrade: F) -> HttpServiceBuilder where - F: IntoServiceFactory)>, - U1: ServiceFactory<(Request, Framed), Config = (), Response = ()>, + F: IntoServiceFactory)>, + U1: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U1::Error: fmt::Display, U1::InitError: fmt::Debug, - )>>::Future: 'static, { HttpServiceBuilder { keep_alive: self.keep_alive, @@ -185,7 +178,7 @@ where where B: MessageBody, F: IntoServiceFactory, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, { @@ -206,12 +199,12 @@ where /// Finish service configuration and create a HTTP service for HTTP/2 protocol. pub fn h2(self, service: F) -> H2Service where - B: MessageBody + 'static, F: IntoServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, - >::Future: 'static, + + B: MessageBody + 'static, { let cfg = ServiceConfig::new( self.keep_alive, @@ -228,12 +221,12 @@ where /// Finish service configuration and create `HttpService` instance. pub fn finish(self, service: F) -> HttpService where - B: MessageBody + 'static, F: IntoServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, - >::Future: 'static, + + B: MessageBody + 'static, { let cfg = ServiceConfig::new( self.keep_alive, diff --git a/actix-http/src/client/connection.rs b/actix-http/src/client/connection.rs deleted file mode 100644 index d22f2c7ac..000000000 --- a/actix-http/src/client/connection.rs +++ /dev/null @@ -1,267 +0,0 @@ -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{fmt, io, time}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; -use bytes::Bytes; -use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready}; -use h2::client::SendRequest; -use pin_project::pin_project; - -use crate::body::MessageBody; -use crate::h1::ClientCodec; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::Payload; - -use super::error::SendRequestError; -use super::pool::{Acquired, Protocol}; -use super::{h1proto, h2proto}; - -pub(crate) enum ConnectionType { - H1(Io), - H2(SendRequest), -} - -pub trait Connection { - type Io: AsyncRead + AsyncWrite + Unpin; - type Future: Future>; - - fn protocol(&self) -> Protocol; - - /// Send request and body - fn send_request>( - self, - head: H, - body: B, - ) -> Self::Future; - - type TunnelFuture: Future< - Output = Result<(ResponseHead, Framed), SendRequestError>, - >; - - /// Send request, returns Response and Framed - fn open_tunnel>(self, head: H) -> Self::TunnelFuture; -} - -pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { - /// Close connection - fn close(self: Pin<&mut Self>); - - /// Release connection to the connection pool - fn release(self: Pin<&mut Self>); -} - -#[doc(hidden)] -/// HTTP client connection -pub struct IoConnection { - io: Option>, - created: time::Instant, - pool: Option>, -} - -impl fmt::Debug for IoConnection -where - T: fmt::Debug, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.io { - Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io), - Some(ConnectionType::H2(_)) => write!(f, "H2Connection"), - None => write!(f, "Connection(Empty)"), - } - } -} - -impl IoConnection { - pub(crate) fn new( - io: ConnectionType, - created: time::Instant, - pool: Option>, - ) -> Self { - IoConnection { - pool, - created, - io: Some(io), - } - } - - pub(crate) fn into_inner(self) -> (ConnectionType, time::Instant) { - (self.io.unwrap(), self.created) - } -} - -impl Connection for IoConnection -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - type Io = T; - type Future = - LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>; - - fn protocol(&self) -> Protocol { - match self.io { - Some(ConnectionType::H1(_)) => Protocol::Http1, - Some(ConnectionType::H2(_)) => Protocol::Http2, - None => Protocol::Http1, - } - } - - fn send_request>( - mut self, - head: H, - body: B, - ) -> Self::Future { - match self.io.take().unwrap() { - ConnectionType::H1(io) => { - h1proto::send_request(io, head.into(), body, self.created, self.pool) - .boxed_local() - } - ConnectionType::H2(io) => { - h2proto::send_request(io, head.into(), body, self.created, self.pool) - .boxed_local() - } - } - } - - type TunnelFuture = Either< - LocalBoxFuture< - 'static, - Result<(ResponseHead, Framed), SendRequestError>, - >, - Ready), SendRequestError>>, - >; - - /// Send request, returns Response and Framed - fn open_tunnel>(mut self, head: H) -> Self::TunnelFuture { - match self.io.take().unwrap() { - ConnectionType::H1(io) => { - Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local()) - } - ConnectionType::H2(io) => { - if let Some(mut pool) = self.pool.take() { - pool.release(IoConnection::new( - ConnectionType::H2(io), - self.created, - None, - )); - } - Either::Right(err(SendRequestError::TunnelNotSupported)) - } - } - } -} - -#[allow(dead_code)] -pub(crate) enum EitherConnection { - A(IoConnection), - B(IoConnection), -} - -impl Connection for EitherConnection -where - A: AsyncRead + AsyncWrite + Unpin + 'static, - B: AsyncRead + AsyncWrite + Unpin + 'static, -{ - type Io = EitherIo; - type Future = - LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>; - - fn protocol(&self) -> Protocol { - match self { - EitherConnection::A(con) => con.protocol(), - EitherConnection::B(con) => con.protocol(), - } - } - - fn send_request>( - self, - head: H, - body: RB, - ) -> Self::Future { - match self { - EitherConnection::A(con) => con.send_request(head, body), - EitherConnection::B(con) => con.send_request(head, body), - } - } - - type TunnelFuture = LocalBoxFuture< - 'static, - Result<(ResponseHead, Framed), SendRequestError>, - >; - - /// Send request, returns Response and Framed - fn open_tunnel>(self, head: H) -> Self::TunnelFuture { - match self { - EitherConnection::A(con) => con - .open_tunnel(head) - .map(|res| { - res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A))) - }) - .boxed_local(), - EitherConnection::B(con) => con - .open_tunnel(head) - .map(|res| { - res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B))) - }) - .boxed_local(), - } - } -} - -#[pin_project(project = EitherIoProj)] -pub enum EitherIo { - A(#[pin] A), - B(#[pin] B), -} - -impl AsyncRead for EitherIo -where - A: AsyncRead, - B: AsyncRead, -{ - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_read(cx, buf), - EitherIoProj::B(val) => val.poll_read(cx, buf), - } - } -} - -impl AsyncWrite for EitherIo -where - A: AsyncWrite, - B: AsyncWrite, -{ - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_write(cx, buf), - EitherIoProj::B(val) => val.poll_write(cx, buf), - } - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_flush(cx), - EitherIoProj::B(val) => val.poll_flush(cx), - } - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - match self.project() { - EitherIoProj::A(val) => val.poll_shutdown(cx), - EitherIoProj::B(val) => val.poll_shutdown(cx), - } - } -} diff --git a/actix-http/src/client/connector.rs b/actix-http/src/client/connector.rs deleted file mode 100644 index 326a2fc60..000000000 --- a/actix-http/src/client/connector.rs +++ /dev/null @@ -1,534 +0,0 @@ -use std::fmt; -use std::marker::PhantomData; -use std::time::Duration; - -use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::net::TcpStream; -use actix_service::{apply_fn, Service, ServiceExt}; -use actix_tls::connect::{ - default_connector, Connect as TcpConnect, Connection as TcpConnection, -}; -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}; -use super::Connect; - -#[cfg(feature = "openssl")] -use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; - -#[cfg(feature = "rustls")] -use actix_tls::connect::ssl::rustls::ClientConfig; -#[cfg(feature = "rustls")] -use std::sync::Arc; - -#[cfg(any(feature = "openssl", feature = "rustls"))] -enum SslConnector { - #[cfg(feature = "openssl")] - Openssl(OpensslConnector), - #[cfg(feature = "rustls")] - Rustls(Arc), -} -#[cfg(not(any(feature = "openssl", feature = "rustls")))] -type SslConnector = (); - -/// Manages http client network connectivity -/// The `Connector` type uses a builder-like combinator pattern for service -/// construction that finishes by calling the `.finish()` method. -/// -/// ```rust,ignore -/// use std::time::Duration; -/// use actix_http::client::Connector; -/// -/// let connector = Connector::new() -/// .timeout(Duration::from_secs(5)) -/// .finish(); -/// ``` -pub struct Connector { - connector: T, - config: ConnectorConfig, - #[allow(dead_code)] - ssl: SslConnector, - _phantom: PhantomData, -} - -trait Io: AsyncRead + AsyncWrite + Unpin {} -impl Io for T {} - -impl Connector<(), ()> { - #[allow(clippy::new_ret_no_self, clippy::let_unit_value)] - pub fn new() -> Connector< - impl Service< - TcpConnect, - Response = TcpConnection, - Error = actix_tls::connect::ConnectError, - > + Clone, - TcpStream, - > { - Connector { - ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), - connector: default_connector(), - config: ConnectorConfig::default(), - _phantom: PhantomData, - } - } - - // Build Ssl connector with openssl, based on supplied alpn protocols - #[cfg(feature = "openssl")] - fn build_ssl(protocols: Vec>) -> SslConnector { - use actix_tls::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::accept::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 { - /// Use custom connector. - pub fn connector(self, connector: T1) -> Connector - where - U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug, - T1: Service< - TcpConnect, - Response = TcpConnection, - Error = actix_tls::connect::ConnectError, - > + Clone, - { - Connector { - connector, - config: self.config, - ssl: self.ssl, - _phantom: PhantomData, - } - } -} - -impl Connector -where - U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, - T: Service< - TcpConnect, - Response = TcpConnection, - Error = actix_tls::connect::ConnectError, - > + Clone - + 'static, -{ - /// 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.config.timeout = timeout; - self - } - - #[cfg(feature = "openssl")] - /// Use custom `SslConnector` instance. - pub fn ssl(mut self, connector: OpensslConnector) -> Self { - self.ssl = SslConnector::Openssl(connector); - self - } - - #[cfg(feature = "rustls")] - pub fn rustls(mut self, connector: Arc) -> Self { - self.ssl = SslConnector::Rustls(connector); - 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.config.limit = limit; - self - } - - /// Set keep-alive period for opened connection. - /// - /// Keep-alive period is the period between connection usage. If - /// the delay between repeated usages of the same connection - /// 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.config.conn_keep_alive = dur; - self - } - - /// Set max lifetime period for connection. - /// - /// Connection lifetime is max lifetime of any opened connection - /// 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.config.conn_lifetime = dur; - self - } - - /// Set server connection disconnect timeout in milliseconds. - /// - /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete - /// within this time, the socket get dropped. This timeout affects only secure connections. - /// - /// To disable timeout set value to 0. - /// - /// By default disconnect timeout is set to 3000 milliseconds. - pub fn disconnect_timeout(mut self, dur: Duration) -> Self { - self.config.disconnect_timeout = Some(dur); - self - } - - /// Finish configuration process and create connector service. - /// The Connector builder always concludes by calling `finish()` last in - /// its combinator chain. - pub fn finish( - self, - ) -> impl Service + Clone - { - #[cfg(not(any(feature = "openssl", feature = "rustls")))] - { - let connector = TimeoutService::new( - self.config.timeout, - apply_fn(self.connector, |msg: Connect, srv| { - srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) - }) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectError::Timeout, - }); - - connect_impl::InnerConnector { - tcp_pool: ConnectionPool::new( - connector, - self.config.no_disconnect_timeout(), - ), - } - } - #[cfg(any(feature = "openssl", feature = "rustls"))] - { - const H2: &[u8] = b"h2"; - use actix_service::{boxed::service, pipeline}; - #[cfg(feature = "openssl")] - use actix_tls::connect::ssl::openssl::OpensslConnector; - #[cfg(feature = "rustls")] - use actix_tls::connect::ssl::rustls::{RustlsConnector, Session}; - - let ssl_service = TimeoutService::new( - self.config.timeout, - pipeline( - apply_fn(self.connector.clone(), |msg: Connect, srv| { - srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) - }) - .map_err(ConnectError::from), - ) - .and_then(match self.ssl { - #[cfg(feature = "openssl")] - SslConnector::Openssl(ssl) => service( - OpensslConnector::service(ssl) - .map(|stream| { - let sock = stream.into_parts().0; - let h2 = sock - .ssl() - .selected_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (Box::new(sock) as Box, Protocol::Http2) - } else { - (Box::new(sock) as Box, Protocol::Http1) - } - }) - .map_err(ConnectError::from), - ), - #[cfg(feature = "rustls")] - SslConnector::Rustls(ssl) => service( - RustlsConnector::service(ssl) - .map_err(ConnectError::from) - .map(|stream| { - let sock = stream.into_parts().0; - let h2 = sock - .get_ref() - .1 - .get_alpn_protocol() - .map(|protos| protos.windows(2).any(|w| w == H2)) - .unwrap_or(false); - if h2 { - (Box::new(sock) as Box, Protocol::Http2) - } else { - (Box::new(sock) as Box, Protocol::Http1) - } - }), - ), - }), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectError::Timeout, - }); - - let tcp_service = TimeoutService::new( - self.config.timeout, - apply_fn(self.connector, |msg: Connect, srv| { - srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) - }) - .map_err(ConnectError::from) - .map(|stream| (stream.into_parts().0, Protocol::Http1)), - ) - .map_err(|e| match e { - TimeoutError::Service(e) => e, - TimeoutError::Timeout => ConnectError::Timeout, - }); - - connect_impl::InnerConnector { - tcp_pool: ConnectionPool::new( - tcp_service, - self.config.no_disconnect_timeout(), - ), - ssl_pool: ConnectionPool::new(ssl_service, self.config), - } - } - } -} - -#[cfg(not(any(feature = "openssl", feature = "rustls")))] -mod connect_impl { - use std::task::{Context, Poll}; - - use futures_util::future::{err, Either, Ready}; - - use super::*; - use crate::client::connection::IoConnection; - - pub(crate) struct InnerConnector - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - pub(crate) tcp_pool: ConnectionPool, - } - - impl Clone for InnerConnector - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - fn clone(&self) -> Self { - InnerConnector { - tcp_pool: self.tcp_pool.clone(), - } - } - } - - impl Service for InnerConnector - where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - type Response = IoConnection; - type Error = ConnectError; - type Future = Either< - as Service>::Future, - Ready, ConnectError>>, - >; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.tcp_pool.poll_ready(cx) - } - - fn call(&mut self, req: Connect) -> Self::Future { - match req.uri.scheme_str() { - Some("https") | Some("wss") => { - Either::Right(err(ConnectError::SslIsNotSupported)) - } - _ => Either::Left(self.tcp_pool.call(req)), - } - } - } -} - -#[cfg(any(feature = "openssl", feature = "rustls"))] -mod connect_impl { - use std::future::Future; - use std::marker::PhantomData; - use std::pin::Pin; - use std::task::{Context, Poll}; - - use futures_core::ready; - use futures_util::future::Either; - - use super::*; - use crate::client::connection::EitherConnection; - - pub(crate) struct InnerConnector - where - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - T1: Service, - T2: Service, - { - pub(crate) tcp_pool: ConnectionPool, - pub(crate) ssl_pool: ConnectionPool, - } - - impl Clone for InnerConnector - where - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - T1: Service + 'static, - T2: Service + 'static, - { - fn clone(&self) -> Self { - InnerConnector { - tcp_pool: self.tcp_pool.clone(), - ssl_pool: self.ssl_pool.clone(), - } - } - } - - impl Service for InnerConnector - where - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - T1: Service + 'static, - T2: Service + 'static, - { - type Response = EitherConnection; - type Error = ConnectError; - type Future = Either< - InnerConnectorResponseA, - InnerConnectorResponseB, - >; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.tcp_pool.poll_ready(cx) - } - - fn call(&mut self, req: Connect) -> Self::Future { - match req.uri.scheme_str() { - Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB { - fut: self.ssl_pool.call(req), - _phantom: PhantomData, - }), - _ => Either::Left(InnerConnectorResponseA { - fut: self.tcp_pool.call(req), - _phantom: PhantomData, - }), - } - } - } - - #[pin_project::pin_project] - pub(crate) struct InnerConnectorResponseA - where - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - #[pin] - fut: as Service>::Future, - _phantom: PhantomData, - } - - impl Future for InnerConnectorResponseA - where - T: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - type Output = Result, ConnectError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Poll::Ready( - ready!(Pin::new(&mut self.get_mut().fut).poll(cx)) - .map(EitherConnection::A), - ) - } - } - - #[pin_project::pin_project] - pub(crate) struct InnerConnectorResponseB - where - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, - { - #[pin] - fut: as Service>::Future, - _phantom: PhantomData, - } - - impl Future for InnerConnectorResponseB - where - T: Service + 'static, - Io1: AsyncRead + AsyncWrite + Unpin + 'static, - Io2: AsyncRead + AsyncWrite + Unpin + 'static, - { - type Output = Result, ConnectError>; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - Poll::Ready( - ready!(Pin::new(&mut self.get_mut().fut).poll(cx)) - .map(EitherConnection::B), - ) - } - } -} diff --git a/actix-http/src/client/h1proto.rs b/actix-http/src/client/h1proto.rs deleted file mode 100644 index 24f4207e8..000000000 --- a/actix-http/src/client/h1proto.rs +++ /dev/null @@ -1,291 +0,0 @@ -use std::io::Write; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{io, time}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; -use bytes::buf::BufMut; -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; -use futures_util::future::poll_fn; -use futures_util::{pin_mut, SinkExt, StreamExt}; - -use crate::error::PayloadError; -use crate::h1; -use crate::header::HeaderMap; -use crate::http::header::{IntoHeaderValue, HOST}; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::{Payload, PayloadStream}; - -use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; -use super::error::{ConnectError, SendRequestError}; -use super::pool::Acquired; -use crate::body::{BodySize, MessageBody}; - -pub(crate) async fn send_request( - io: T, - mut head: RequestHeadType, - body: B, - created: time::Instant, - pool: Option>, -) -> Result<(ResponseHead, Payload), SendRequestError> -where - T: AsyncRead + AsyncWrite + Unpin + 'static, - B: MessageBody, -{ - // set request host header - if !head.as_ref().headers.contains_key(HOST) - && !head.extra_headers().iter().any(|h| h.contains_key(HOST)) - { - if let Some(host) = head.as_ref().uri.host() { - let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); - - let _ = match head.as_ref().uri.port_u16() { - None | Some(80) | Some(443) => write!(wrt, "{}", host), - Some(port) => write!(wrt, "{}:{}", host, port), - }; - - match wrt.get_mut().split().freeze().try_into_value() { - Ok(value) => match head { - RequestHeadType::Owned(ref mut head) => { - head.headers.insert(HOST, value) - } - RequestHeadType::Rc(_, ref mut extra_headers) => { - let headers = extra_headers.get_or_insert(HeaderMap::new()); - headers.insert(HOST, value) - } - }, - Err(e) => log::error!("Can not set HOST header {}", e), - } - } - } - - let io = H1Connection { - created, - pool, - io: Some(io), - }; - - // create Framed and send request - let mut framed_inner = Framed::new(io, h1::ClientCodec::default()); - framed_inner.send((head, body.size()).into()).await?; - - // send request body - match body.size() { - BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} - _ => send_body(body, Pin::new(&mut framed_inner)).await?, - }; - - // read response and init read body - let res = Pin::new(&mut framed_inner).into_future().await; - let (head, framed) = if let (Some(result), framed) = res { - let item = result.map_err(SendRequestError::from)?; - (item, framed) - } else { - return Err(SendRequestError::from(ConnectError::Disconnected)); - }; - - match framed.codec_ref().message_type() { - h1::MessageType::None => { - let force_close = !framed.codec_ref().keepalive(); - release_connection(framed, force_close); - Ok((head, Payload::None)) - } - _ => { - let pl: PayloadStream = PlStream::new(framed_inner).boxed_local(); - Ok((head, pl.into())) - } - } -} - -pub(crate) async fn open_tunnel( - io: T, - head: RequestHeadType, -) -> Result<(ResponseHead, Framed), SendRequestError> -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - // create Framed and send request - let mut framed = Framed::new(io, h1::ClientCodec::default()); - framed.send((head, BodySize::None).into()).await?; - - // read response - if let (Some(result), framed) = framed.into_future().await { - let head = result.map_err(SendRequestError::from)?; - Ok((head, framed)) - } else { - Err(SendRequestError::from(ConnectError::Disconnected)) - } -} - -/// send request body to the peer -pub(crate) async fn send_body( - body: B, - mut framed: Pin<&mut Framed>, -) -> Result<(), SendRequestError> -where - T: ConnectionLifetime + Unpin, - B: MessageBody, -{ - pin_mut!(body); - - let mut eof = false; - while !eof { - while !eof && !framed.as_ref().is_write_buf_full() { - match poll_fn(|cx| body.as_mut().poll_next(cx)).await { - Some(result) => { - framed.as_mut().write(h1::Message::Chunk(Some(result?)))?; - } - None => { - eof = true; - framed.as_mut().write(h1::Message::Chunk(None))?; - } - } - } - - if !framed.as_ref().is_write_buf_empty() { - poll_fn(|cx| match framed.as_mut().flush(cx) { - Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), - Poll::Ready(Err(err)) => Poll::Ready(Err(err)), - Poll::Pending => { - if !framed.as_ref().is_write_buf_full() { - Poll::Ready(Ok(())) - } else { - Poll::Pending - } - } - }) - .await?; - } - } - - SinkExt::flush(Pin::into_inner(framed)).await?; - Ok(()) -} - -#[doc(hidden)] -/// HTTP client connection -pub struct H1Connection { - /// T should be `Unpin` - io: Option, - created: time::Instant, - pool: Option>, -} - -impl ConnectionLifetime for H1Connection -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - /// Close connection - fn close(mut self: Pin<&mut Self>) { - if let Some(mut pool) = self.pool.take() { - if let Some(io) = self.io.take() { - pool.close(IoConnection::new( - ConnectionType::H1(io), - self.created, - None, - )); - } - } - } - - /// Release this connection to the connection pool - fn release(mut self: Pin<&mut Self>) { - if let Some(mut pool) = self.pool.take() { - if let Some(io) = self.io.take() { - pool.release(IoConnection::new( - ConnectionType::H1(io), - self.created, - None, - )); - } - } - } -} - -impl AsyncRead for H1Connection { - fn poll_read( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf) - } -} - -impl AsyncWrite for H1Connection { - fn poll_write( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf) - } - - fn poll_flush( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(self.io.as_mut().unwrap()).poll_flush(cx) - } - - fn poll_shutdown( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx) - } -} - -#[pin_project::pin_project] -pub(crate) struct PlStream { - #[pin] - framed: Option>, -} - -impl PlStream { - fn new(framed: Framed) -> Self { - let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); - - PlStream { - framed: Some(framed), - } - } -} - -impl Stream for PlStream { - type Item = Result; - - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - let mut this = self.project(); - - match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { - Poll::Pending => Poll::Pending, - Poll::Ready(Some(chunk)) => { - if let Some(chunk) = chunk { - Poll::Ready(Some(Ok(chunk))) - } else { - let framed = this.framed.as_mut().as_pin_mut().unwrap(); - let force_close = !framed.codec_ref().keepalive(); - release_connection(framed, force_close); - Poll::Ready(None) - } - } - Poll::Ready(None) => Poll::Ready(None), - } - } -} - -fn release_connection(framed: Pin<&mut Framed>, force_close: bool) -where - T: ConnectionLifetime, -{ - if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() { - framed.io_pin().release() - } else { - framed.io_pin().close() - } -} diff --git a/actix-http/src/client/pool.rs b/actix-http/src/client/pool.rs deleted file mode 100644 index af862cdd7..000000000 --- a/actix-http/src/client/pool.rs +++ /dev/null @@ -1,645 +0,0 @@ -use std::cell::RefCell; -use std::collections::VecDeque; -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::time::{Duration, Instant}; - -use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; -use actix_rt::time::{sleep, Sleep}; -use actix_service::Service; -use actix_utils::task::LocalWaker; -use ahash::AHashMap; -use bytes::Bytes; -use futures_channel::oneshot; -use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture}; -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)] -/// Protocol version -pub enum Protocol { - Http1, - Http2, -} - -#[derive(Hash, Eq, PartialEq, Clone, Debug)] -pub(crate) struct Key { - authority: Authority, -} - -impl From for Key { - fn from(authority: Authority) -> Key { - Key { authority } - } -} - -/// Connections pool -pub(crate) struct ConnectionPool(Rc>, Rc>>); - -impl ConnectionPool -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, -{ - pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self { - let connector_rc = Rc::new(RefCell::new(connector)); - let inner_rc = Rc::new(RefCell::new(Inner { - config, - acquired: 0, - waiters: Slab::new(), - waiters_queue: IndexSet::new(), - available: AHashMap::default(), - waker: LocalWaker::new(), - })); - - // start support future - actix_rt::spawn(ConnectorPoolSupport { - connector: Rc::clone(&connector_rc), - inner: Rc::clone(&inner_rc), - }); - - ConnectionPool(connector_rc, inner_rc) - } -} - -impl Clone for ConnectionPool -where - Io: 'static, -{ - fn clone(&self) -> Self { - ConnectionPool(self.0.clone(), self.1.clone()) - } -} - -impl Drop for ConnectionPool { - fn drop(&mut self) { - // wake up the ConnectorPoolSupport when dropping so it can exit properly. - self.1.borrow().waker.wake(); - } -} - -impl Service for ConnectionPool -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service + 'static, -{ - type Response = IoConnection; - type Error = ConnectError; - type Future = LocalBoxFuture<'static, Result, ConnectError>>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.0.poll_ready(cx) - } - - fn call(&mut self, req: Connect) -> Self::Future { - let mut connector = self.0.clone(); - let inner = self.1.clone(); - - let fut = async move { - let key = if let Some(authority) = req.uri.authority() { - authority.clone().into() - } else { - return Err(ConnectError::Unresolved); - }; - - // acquire connection - match poll_fn(|cx| Poll::Ready(inner.borrow_mut().acquire(&key, cx))).await { - Acquire::Acquired(io, created) => { - // use existing connection - Ok(IoConnection::new( - io, - created, - Some(Acquired(key, Some(inner))), - )) - } - Acquire::Available => { - // 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 { - Ok(IoConnection::new( - ConnectionType::H1(io), - Instant::now(), - Some(guard.consume()), - )) - } else { - let (snd, connection) = handshake(io, &config).await?; - actix_rt::spawn(connection.map(|_| ())); - Ok(IoConnection::new( - ConnectionType::H2(snd), - Instant::now(), - Some(guard.consume()), - )) - } - } - _ => { - // connection is not available, wait - let (rx, token) = inner.borrow_mut().wait_for(req); - - let guard = WaiterGuard::new(key, token, inner); - let res = match rx.await { - Err(_) => Err(ConnectError::Disconnected), - Ok(res) => res, - }; - guard.consume(); - res - } - } - }; - - fut.boxed_local() - } -} - -struct WaiterGuard -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - key: Key, - token: usize, - inner: Option>>>, -} - -impl WaiterGuard -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - fn new(key: Key, token: usize, inner: Rc>>) -> Self { - Self { - key, - token, - inner: Some(inner), - } - } - - fn consume(mut self) { - let _ = self.inner.take(); - } -} - -impl Drop for WaiterGuard -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - fn drop(&mut self) { - if let Some(i) = self.inner.take() { - let mut inner = i.as_ref().borrow_mut(); - inner.release_waiter(&self.key, self.token); - inner.check_availability(); - } - } -} - -struct OpenGuard -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - key: Key, - inner: Option>>>, -} - -impl OpenGuard -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - fn new(key: Key, inner: Rc>>) -> Self { - Self { - key, - inner: Some(inner), - } - } - - fn consume(mut self) -> Acquired { - Acquired(self.key.clone(), self.inner.take()) - } -} - -impl Drop for OpenGuard -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - fn drop(&mut self) { - if let Some(i) = self.inner.take() { - let mut inner = i.as_ref().borrow_mut(); - inner.release(); - inner.check_availability(); - } - } -} - -enum Acquire { - Acquired(ConnectionType, Instant), - Available, - NotAvailable, -} - -struct AvailableConnection { - io: ConnectionType, - used: Instant, - created: Instant, -} - -pub(crate) struct Inner { - config: ConnectorConfig, - acquired: usize, - available: AHashMap>>, - waiters: Slab< - Option<( - Connect, - oneshot::Sender, ConnectError>>, - )>, - >, - waiters_queue: IndexSet<(Key, usize)>, - waker: LocalWaker, -} - -impl Inner { - fn reserve(&mut self) { - self.acquired += 1; - } - - fn release(&mut self) { - self.acquired -= 1; - } - - fn release_waiter(&mut self, key: &Key, token: usize) { - self.waiters.remove(token); - let _ = self.waiters_queue.shift_remove(&(key.clone(), token)); - } -} - -impl Inner -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - /// connection is not available, wait - fn wait_for( - &mut self, - connect: Connect, - ) -> ( - oneshot::Receiver, ConnectError>>, - usize, - ) { - let (tx, rx) = oneshot::channel(); - - let key: Key = connect.uri.authority().unwrap().clone().into(); - let entry = self.waiters.vacant_entry(); - let token = entry.key(); - entry.insert(Some((connect, tx))); - assert!(self.waiters_queue.insert((key, token))); - - (rx, token) - } - - fn acquire(&mut self, key: &Key, cx: &mut Context<'_>) -> Acquire { - // check limits - if self.config.limit > 0 && self.acquired >= self.config.limit { - return Acquire::NotAvailable; - } - - self.reserve(); - - // check if open connection is available - // cleanup stale connections at the same time - if let Some(ref mut connections) = self.available.get_mut(key) { - let now = Instant::now(); - while let Some(conn) = connections.pop_back() { - // check if it still usable - if (now - conn.used) > self.config.conn_keep_alive - || (now - conn.created) > self.config.conn_lifetime - { - if let Some(timeout) = self.config.disconnect_timeout { - if let ConnectionType::H1(io) = conn.io { - actix_rt::spawn(CloseConnection::new(io, timeout)) - } - } - } else { - let mut io = conn.io; - let mut buf = [0; 2]; - let mut read_buf = ReadBuf::new(&mut buf); - if let ConnectionType::H1(ref mut s) = io { - match Pin::new(s).poll_read(cx, &mut read_buf) { - Poll::Pending => {} - Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => { - if let Some(timeout) = self.config.disconnect_timeout { - if let ConnectionType::H1(io) = io { - actix_rt::spawn(CloseConnection::new( - io, timeout, - )) - } - } - continue; - } - _ => continue, - } - } - return Acquire::Acquired(io, conn.created); - } - } - } - Acquire::Available - } - - fn release_conn(&mut self, key: &Key, io: ConnectionType, created: Instant) { - self.acquired -= 1; - self.available - .entry(key.clone()) - .or_insert_with(VecDeque::new) - .push_back(AvailableConnection { - io, - created, - used: Instant::now(), - }); - self.check_availability(); - } - - fn release_close(&mut self, io: ConnectionType) { - self.acquired -= 1; - if let Some(timeout) = self.config.disconnect_timeout { - if let ConnectionType::H1(io) = io { - actix_rt::spawn(CloseConnection::new(io, timeout)) - } - } - self.check_availability(); - } - - fn check_availability(&self) { - if !self.waiters_queue.is_empty() && self.acquired < self.config.limit { - self.waker.wake(); - } - } -} - -#[pin_project::pin_project] -struct CloseConnection { - io: T, - #[pin] - timeout: Sleep, -} - -impl CloseConnection -where - T: AsyncWrite + Unpin, -{ - fn new(io: T, timeout: Duration) -> Self { - CloseConnection { - io, - timeout: sleep(timeout), - } - } -} - -impl Future for CloseConnection -where - T: AsyncWrite + Unpin, -{ - type Output = (); - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { - let this = self.project(); - - match this.timeout.poll(cx) { - Poll::Ready(_) => Poll::Ready(()), - Poll::Pending => match Pin::new(this.io).poll_shutdown(cx) { - Poll::Ready(_) => Poll::Ready(()), - Poll::Pending => Poll::Pending, - }, - } - } -} - -#[pin_project] -struct ConnectorPoolSupport -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - connector: T, - inner: Rc>>, -} - -impl Future for ConnectorPoolSupport -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, - T: Service, - T::Future: 'static, -{ - type Output = (); - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); - - if Rc::strong_count(this.inner) == 1 { - // If we are last copy of Inner it means the ConnectionPool is already gone - // and we are safe to exit. - return Poll::Ready(()); - } - - let mut inner = this.inner.borrow_mut(); - inner.waker.register(cx.waker()); - - // check waiters - loop { - let (key, token) = { - if let Some((key, token)) = inner.waiters_queue.get_index(0) { - (key.clone(), *token) - } else { - break; - } - }; - if inner.waiters.get(token).unwrap().is_none() { - continue; - } - - match inner.acquire(&key, cx) { - Acquire::NotAvailable => break, - Acquire::Acquired(io, created) => { - let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1; - if let Err(conn) = tx.send(Ok(IoConnection::new( - io, - created, - Some(Acquired(key.clone(), Some(this.inner.clone()))), - ))) { - let (io, created) = conn.unwrap().into_inner(); - inner.release_conn(&key, io, created); - } - } - Acquire::Available => { - let (connect, tx) = - inner.waiters.get_mut(token).unwrap().take().unwrap(); - OpenWaitingConnection::spawn( - key.clone(), - tx, - this.inner.clone(), - this.connector.call(connect), - inner.config.clone(), - ); - } - } - let _ = inner.waiters_queue.swap_remove_index(0); - } - - Poll::Pending - } -} - -#[pin_project::pin_project(PinnedDrop)] -struct OpenWaitingConnection -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - #[pin] - fut: F, - key: Key, - h2: Option< - LocalBoxFuture< - 'static, - Result<(SendRequest, Connection), h2::Error>, - >, - >, - rx: Option, ConnectError>>>, - inner: Option>>>, - config: ConnectorConfig, -} - -impl OpenWaitingConnection -where - F: Future> + 'static, - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - fn spawn( - key: Key, - rx: oneshot::Sender, ConnectError>>, - inner: Rc>>, - fut: F, - config: ConnectorConfig, - ) { - actix_rt::spawn(OpenWaitingConnection { - key, - fut, - h2: None, - rx: Some(rx), - inner: Some(inner), - config, - }) - } -} - -#[pin_project::pinned_drop] -impl PinnedDrop for OpenWaitingConnection -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ - 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_availability(); - } - } -} - -impl Future for OpenWaitingConnection -where - F: Future>, - Io: AsyncRead + AsyncWrite + Unpin, -{ - type Output = (); - - 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) { - Poll::Ready(Ok((snd, connection))) => { - actix_rt::spawn(connection.map(|_| ())); - let rx = this.rx.take().unwrap(); - let _ = rx.send(Ok(IoConnection::new( - ConnectionType::H2(snd), - Instant::now(), - Some(Acquired(this.key.clone(), this.inner.take())), - ))); - Poll::Ready(()) - } - Poll::Pending => Poll::Pending, - Poll::Ready(Err(err)) => { - let _ = this.inner.take(); - if let Some(rx) = this.rx.take() { - let _ = rx.send(Err(ConnectError::H2(err))); - } - Poll::Ready(()) - } - }; - } - - match this.fut.poll(cx) { - Poll::Ready(Err(err)) => { - let _ = this.inner.take(); - if let Some(rx) = this.rx.take() { - let _ = rx.send(Err(err)); - } - Poll::Ready(()) - } - Poll::Ready(Ok((io, proto))) => { - if proto == Protocol::Http1 { - let rx = this.rx.take().unwrap(); - let _ = rx.send(Ok(IoConnection::new( - ConnectionType::H1(io), - Instant::now(), - Some(Acquired(this.key.clone(), this.inner.take())), - ))); - Poll::Ready(()) - } else { - *this.h2 = Some(handshake(io, this.config).boxed_local()); - self.poll(cx) - } - } - Poll::Pending => Poll::Pending, - } - } -} - -pub(crate) struct Acquired(Key, Option>>>); - -impl Acquired -where - T: AsyncRead + AsyncWrite + Unpin + 'static, -{ - pub(crate) fn close(&mut self, conn: IoConnection) { - if let Some(inner) = self.1.take() { - let (io, _) = conn.into_inner(); - inner.as_ref().borrow_mut().release_close(io); - } - } - pub(crate) fn release(&mut self, conn: IoConnection) { - if let Some(inner) = self.1.take() { - let (io, created) = conn.into_inner(); - inner - .as_ref() - .borrow_mut() - .release_conn(&self.0, io, created); - } - } -} - -impl Drop for Acquired { - fn drop(&mut self) { - if let Some(inner) = self.1.take() { - inner.as_ref().borrow_mut().release(); - } - } -} diff --git a/actix-http/src/clinu/mod.rs b/actix-http/src/clinu/mod.rs deleted file mode 100644 index e69de29bb..000000000 diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index f178db274..5d020edfc 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -1,24 +1,29 @@ -use std::cell::Cell; -use std::fmt::Write; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, net}; +use std::{ + cell::Cell, + fmt::{self, Write}, + net, + rc::Rc, + time::{Duration, SystemTime}, +}; -use actix_rt::time::{sleep, sleep_until, Instant, Sleep}; +use actix_rt::{ + task::JoinHandle, + time::{interval, sleep_until, Instant, Sleep}, +}; use bytes::BytesMut; -use futures_util::{future, FutureExt}; -use time::OffsetDateTime; /// "Sun, 06 Nov 1994 08:49:37 GMT".len() -const DATE_VALUE_LENGTH: usize = 29; +pub(crate) const DATE_VALUE_LENGTH: usize = 29; #[derive(Debug, PartialEq, Clone, Copy)] /// Server keep-alive setting pub enum KeepAlive { /// Keep alive in seconds Timeout(usize), + /// Rely on OS to shutdown tcp connection Os, + /// Disabled Disabled, } @@ -49,7 +54,7 @@ struct Inner { ka_enabled: bool, secure: bool, local_addr: Option, - timer: DateService, + date_service: DateService, } impl Clone for ServiceConfig { @@ -91,42 +96,42 @@ impl ServiceConfig { client_disconnect, secure, local_addr, - timer: DateService::new(), + date_service: DateService::new(), })) } + /// Returns true if connection is secure (HTTPS) #[inline] - /// Returns true if connection is secure(https) pub fn secure(&self) -> bool { self.0.secure } - #[inline] /// Returns the local address that this server is bound to. + /// + /// Returns `None` for connections via UDS (Unix Domain Socket). + #[inline] pub fn local_addr(&self) -> Option { self.0.local_addr } - #[inline] /// Keep alive duration if configured. + #[inline] pub fn keep_alive(&self) -> Option { self.0.keep_alive } - #[inline] /// Return state of connection keep-alive functionality + #[inline] pub fn keep_alive_enabled(&self) -> bool { self.0.ka_enabled } - #[inline] /// Client timeout for first request. + #[inline] pub fn client_timer(&self) -> Option { let delay_time = self.0.client_timeout; if delay_time != 0 { - Some(sleep_until( - self.0.timer.now() + Duration::from_millis(delay_time), - )) + Some(sleep_until(self.now() + Duration::from_millis(delay_time))) } else { None } @@ -136,7 +141,7 @@ impl ServiceConfig { pub fn client_timer_expire(&self) -> Option { let delay = self.0.client_timeout; if delay != 0 { - Some(self.0.timer.now() + Duration::from_millis(delay)) + Some(self.now() + Duration::from_millis(delay)) } else { None } @@ -146,34 +151,26 @@ impl ServiceConfig { pub fn client_disconnect_timer(&self) -> Option { let delay = self.0.client_disconnect; if delay != 0 { - Some(self.0.timer.now() + Duration::from_millis(delay)) + Some(self.now() + Duration::from_millis(delay)) } else { None } } - #[inline] /// Return keep-alive timer delay is configured. + #[inline] pub fn keep_alive_timer(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(sleep_until(self.0.timer.now() + ka)) - } else { - None - } + self.keep_alive().map(|ka| sleep_until(self.now() + ka)) } /// Keep-alive expire time pub fn keep_alive_expire(&self) -> Option { - if let Some(ka) = self.0.keep_alive { - Some(self.0.timer.now() + ka) - } else { - None - } + self.keep_alive().map(|ka| self.now() + ka) } #[inline] pub(crate) fn now(&self) -> Instant { - self.0.timer.now() + self.0.date_service.now() } #[doc(hidden)] @@ -181,7 +178,7 @@ impl ServiceConfig { let mut buf: [u8; 39] = [0; 39]; buf[..6].copy_from_slice(b"date: "); self.0 - .timer + .date_service .set_date(|date| buf[6..35].copy_from_slice(&date.bytes)); buf[35..].copy_from_slice(b"\r\n\r\n"); dst.extend_from_slice(&buf); @@ -189,7 +186,7 @@ impl ServiceConfig { pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { self.0 - .timer + .date_service .set_date(|date| dst.extend_from_slice(&date.bytes)); } } @@ -212,12 +209,7 @@ impl Date { fn update(&mut self) { self.pos = 0; - write!( - self, - "{}", - OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT") - ) - .unwrap(); + write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap(); } } @@ -230,57 +222,102 @@ impl fmt::Write for Date { } } -#[derive(Clone)] -struct DateService(Rc); - -struct DateServiceInner { - current: Cell>, +/// Service for update Date and Instant periodically at 500 millis interval. +struct DateService { + current: Rc>, + handle: JoinHandle<()>, } -impl DateServiceInner { - fn new() -> Self { - DateServiceInner { - current: Cell::new(None), - } - } - - fn reset(&self) { - self.current.take(); - } - - fn update(&self) { - let now = Instant::now(); - let date = Date::new(); - self.current.set(Some((date, now))); +impl Drop for DateService { + fn drop(&mut self) { + // stop the timer update async task on drop. + self.handle.abort(); } } impl DateService { fn new() -> Self { - DateService(Rc::new(DateServiceInner::new())) - } + // shared date and timer for DateService and update async task. + let current = Rc::new(Cell::new((Date::new(), Instant::now()))); + let current_clone = Rc::clone(¤t); + // spawn an async task sleep for 500 milli and update current date/timer in a loop. + // handle is used to stop the task on DateService drop. + let handle = actix_rt::spawn(async move { + #[cfg(test)] + let _notify = notify_on_drop::NotifyOnDrop::new(); - fn check_date(&self) { - if self.0.current.get().is_none() { - self.0.update(); + let mut interval = interval(Duration::from_millis(500)); + loop { + let now = interval.tick().await; + let date = Date::new(); + current_clone.set((date, now)); + } + }); - // periodic date update - let s = self.clone(); - actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| { - s.0.reset(); - future::ready(()) - })); - } + DateService { current, handle } } fn now(&self) -> Instant { - self.check_date(); - self.0.current.get().unwrap().1 + self.current.get().1 } fn set_date(&self, mut f: F) { - self.check_date(); - f(&self.0.current.get().unwrap().0); + f(&self.current.get().0); + } +} + +// TODO: move to a util module for testing all spawn handle drop style tasks. +/// Test Module for checking the drop state of certain async tasks that are spawned +/// with `actix_rt::spawn` +/// +/// The target task must explicitly generate `NotifyOnDrop` when spawn the task +#[cfg(test)] +mod notify_on_drop { + use std::cell::RefCell; + + thread_local! { + static NOTIFY_DROPPED: RefCell> = RefCell::new(None); + } + + /// Check if the spawned task is dropped. + /// + /// # Panics + /// Panics when there was no `NotifyOnDrop` instance on current thread. + pub(crate) fn is_dropped() -> bool { + NOTIFY_DROPPED.with(|bool| { + bool.borrow() + .expect("No NotifyOnDrop existed on current thread") + }) + } + + pub(crate) struct NotifyOnDrop; + + impl NotifyOnDrop { + /// # Panic: + /// + /// When construct multiple instances on any given thread. + pub(crate) fn new() -> Self { + NOTIFY_DROPPED.with(|bool| { + let mut bool = bool.borrow_mut(); + if bool.is_some() { + panic!("NotifyOnDrop existed on current thread"); + } else { + *bool = Some(false); + } + }); + + NotifyOnDrop + } + } + + impl Drop for NotifyOnDrop { + fn drop(&mut self) { + NOTIFY_DROPPED.with(|bool| { + if let Some(b) = bool.borrow_mut().as_mut() { + *b = true; + } + }); + } } } @@ -288,14 +325,67 @@ 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()); + use actix_rt::{task::yield_now, time::sleep}; + + #[actix_rt::test] + async fn test_date_service_update() { + let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None); + + yield_now().await; + + let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); + settings.set_date(&mut buf1); + let now1 = settings.now(); + + sleep_until(Instant::now() + Duration::from_secs(2)).await; + yield_now().await; + + let now2 = settings.now(); + let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10); + settings.set_date(&mut buf2); + + assert_ne!(now1, now2); + + assert_ne!(buf1, buf2); + + drop(settings); + + // Ensure the task will drop eventually + let mut times = 0; + while !notify_on_drop::is_dropped() { + sleep(Duration::from_millis(100)).await; + times += 1; + assert!(times < 10, "Timeout waiting for task drop"); + } + } + + #[actix_rt::test] + async fn test_date_service_drop() { + let service = Rc::new(DateService::new()); + + // yield so date service have a chance to register the spawned timer update task. + yield_now().await; + + let clone1 = service.clone(); + let clone2 = service.clone(); + let clone3 = service.clone(); + + drop(clone1); + assert!(!notify_on_drop::is_dropped()); + drop(clone2); + assert!(!notify_on_drop::is_dropped()); + drop(clone3); + assert!(!notify_on_drop::is_dropped()); + + drop(service); + + // Ensure the task will drop eventually + let mut times = 0; + while !notify_on_drop::is_dropped() { + sleep(Duration::from_millis(100)).await; + times += 1; + assert!(times < 10, "Timeout waiting for task drop"); + } } #[test] diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index b26609911..c32983fc7 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -1,19 +1,32 @@ -use std::future::Future; -use std::io::{self, Write}; -use std::pin::Pin; -use std::task::{Context, Poll}; +//! Stream decoders. + +use std::{ + future::Future, + io::{self, Write as _}, + pin::Pin, + task::{Context, Poll}, +}; use actix_rt::task::{spawn_blocking, JoinHandle}; -use brotli2::write::BrotliDecoder; use bytes::Bytes; -use flate2::write::{GzDecoder, ZlibDecoder}; use futures_core::{ready, Stream}; -use super::Writer; -use crate::error::{BlockingError, PayloadError}; -use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}; +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliDecoder; -const INPLACE: usize = 2049; +#[cfg(feature = "compress-gzip")] +use flate2::write::{GzDecoder, ZlibDecoder}; + +#[cfg(feature = "compress-zstd")] +use zstd::stream::write::Decoder as ZstdDecoder; + +use crate::{ + encoding::Writer, + error::{BlockingError, PayloadError}, + http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}, +}; + +const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; pub struct Decoder { decoder: Option, @@ -30,17 +43,28 @@ where #[inline] pub fn new(stream: S, encoding: ContentEncoding) -> Decoder { let decoder = match encoding { + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( BrotliDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ZlibDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( GzDecoder::new(Writer::new()), ))), + #[cfg(feature = "compress-zstd")] + ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new( + ZstdDecoder::new(Writer::new()).expect( + "Failed to create zstd decoder. This is a bug. \ + Please report it to the actix-web repository.", + ), + ))), _ => None, }; + Decoder { decoder, stream, @@ -53,15 +77,11 @@ where #[inline] pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder { // check content-encoding - let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) { - if let Ok(enc) = enc.to_str() { - ContentEncoding::from(enc) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - }; + let encoding = headers + .get(&CONTENT_ENCODING) + .and_then(|val| val.to_str().ok()) + .and_then(|x| x.parse().ok()) + .unwrap_or(ContentEncoding::Identity); Self::new(stream, encoding) } @@ -79,17 +99,12 @@ where ) -> Poll> { loop { if let Some(ref mut fut) = self.fut { - let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) { - Ok(Ok(item)) => item, - Ok(Err(e)) => { - return Poll::Ready(Some(Err(BlockingError::Error(e).into()))) - } - Err(_) => { - return Poll::Ready(Some(Err(BlockingError::Canceled.into()))) - } - }; + let (chunk, decoder) = + ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; + self.decoder = Some(decoder); self.fut.take(); + if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } @@ -99,13 +114,15 @@ where return Poll::Ready(None); } - match Pin::new(&mut self.stream).poll_next(cx) { - Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), - Poll::Ready(Some(Ok(chunk))) => { + match ready!(Pin::new(&mut self.stream).poll_next(cx)) { + Some(Err(err)) => return Poll::Ready(Some(Err(err))), + + Some(Ok(chunk)) => { if let Some(mut decoder) = self.decoder.take() { - if chunk.len() < INPLACE { + if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE { let chunk = decoder.feed_data(chunk)?; self.decoder = Some(decoder); + if let Some(chunk) = chunk { return Poll::Ready(Some(Ok(chunk))); } @@ -115,13 +132,16 @@ where Ok((chunk, decoder)) })); } + continue; } else { return Poll::Ready(Some(Ok(chunk))); } } - Poll::Ready(None) => { + + None => { self.eof = true; + return if let Some(mut decoder) = self.decoder.take() { match decoder.feed_eof() { Ok(Some(res)) => Poll::Ready(Some(Ok(res))), @@ -132,25 +152,32 @@ where Poll::Ready(None) }; } - Poll::Pending => break, } } - Poll::Pending } } enum ContentDecoder { + #[cfg(feature = "compress-gzip")] Deflate(Box>), + #[cfg(feature = "compress-gzip")] Gzip(Box>), + #[cfg(feature = "compress-brotli")] Br(Box>), + // We need explicit 'static lifetime here because ZstdDecoder need lifetime + // argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static` + #[cfg(feature = "compress-zstd")] + Zstd(Box>), } impl ContentDecoder { fn feed_eof(&mut self) -> io::Result> { match self { + #[cfg(feature = "compress-brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.flush() { Ok(()) => { let b = decoder.get_mut().take(); + if !b.is_empty() { Ok(Some(b)) } else { @@ -159,7 +186,23 @@ impl ContentDecoder { } Err(e) => Err(e), }, + + #[cfg(feature = "compress-gzip")] ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { + Ok(_) => { + let b = decoder.get_mut().take(); + + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + + #[cfg(feature = "compress-gzip")] + ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { Ok(_) => { let b = decoder.get_mut().take(); if !b.is_empty() { @@ -170,7 +213,9 @@ impl ContentDecoder { } Err(e) => Err(e), }, - ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { + + #[cfg(feature = "compress-zstd")] + ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() { Ok(_) => { let b = decoder.get_mut().take(); if !b.is_empty() { @@ -186,10 +231,12 @@ impl ContentDecoder { fn feed_data(&mut self, data: Bytes) -> io::Result> { match self { + #[cfg(feature = "compress-brotli")] ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; let b = decoder.get_mut().take(); + if !b.is_empty() { Ok(Some(b)) } else { @@ -198,9 +245,27 @@ impl ContentDecoder { } Err(e) => Err(e), }, + + #[cfg(feature = "compress-gzip")] ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; + let b = decoder.get_mut().take(); + + if !b.is_empty() { + Ok(Some(b)) + } else { + Ok(None) + } + } + Err(e) => Err(e), + }, + + #[cfg(feature = "compress-gzip")] + ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { + Ok(_) => { + decoder.flush()?; + let b = decoder.get_mut().take(); if !b.is_empty() { Ok(Some(b)) @@ -210,9 +275,12 @@ impl ContentDecoder { } Err(e) => Err(e), }, - ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { + + #[cfg(feature = "compress-zstd")] + ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) { Ok(_) => { decoder.flush()?; + let b = decoder.get_mut().take(); if !b.is_empty() { Ok(Some(b)) diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 28c757076..49e5663dc 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -1,122 +1,148 @@ -//! Stream encoder -use std::future::Future; -use std::io::{self, Write}; -use std::pin::Pin; -use std::task::{Context, Poll}; +//! Stream encoders. + +use std::{ + error::Error as StdError, + future::Future, + io::{self, Write as _}, + pin::Pin, + task::{Context, Poll}, +}; use actix_rt::task::{spawn_blocking, JoinHandle}; -use brotli2::write::BrotliEncoder; use bytes::Bytes; -use flate2::write::{GzEncoder, ZlibEncoder}; +use derive_more::Display; use futures_core::ready; -use pin_project::pin_project; +use pin_project_lite::pin_project; -use crate::body::{Body, BodySize, MessageBody, ResponseBody}; -use crate::http::header::{ContentEncoding, CONTENT_ENCODING}; -use crate::http::{HeaderValue, StatusCode}; -use crate::{Error, ResponseHead}; +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliEncoder; + +#[cfg(feature = "compress-gzip")] +use flate2::write::{GzEncoder, ZlibEncoder}; + +#[cfg(feature = "compress-zstd")] +use zstd::stream::write::Encoder as ZstdEncoder; use super::Writer; -use crate::error::BlockingError; +use crate::{ + body::{BodySize, MessageBody}, + error::BlockingError, + http::{ + header::{ContentEncoding, CONTENT_ENCODING}, + HeaderValue, StatusCode, + }, + ResponseHead, +}; -const INPLACE: usize = 1024; +const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; -#[pin_project] -pub struct Encoder { - eof: bool, - #[pin] - body: EncoderBody, - encoder: Option, - fut: Option>>, +pin_project! { + pub struct Encoder { + #[pin] + body: EncoderBody, + encoder: Option, + fut: Option>>, + eof: bool, + } } impl Encoder { + fn none() -> Self { + Encoder { + body: EncoderBody::None, + encoder: None, + fut: None, + eof: true, + } + } + pub fn response( encoding: ContentEncoding, head: &mut ResponseHead, - body: ResponseBody, - ) -> ResponseBody> { + body: B, + ) -> Self { let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) || head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::NO_CONTENT || encoding == ContentEncoding::Identity || encoding == ContentEncoding::Auto); - let body = match body { - ResponseBody::Other(b) => match b { - Body::None => return ResponseBody::Other(Body::None), - Body::Empty => return ResponseBody::Other(Body::Empty), - Body::Bytes(buf) => { - if can_encode { - EncoderBody::Bytes(buf) - } else { - return ResponseBody::Other(Body::Bytes(buf)); - } - } - Body::Message(stream) => EncoderBody::BoxedStream(stream), - }, - ResponseBody::Body(stream) => EncoderBody::Stream(stream), - }; + match body.size() { + // no need to compress an empty body + BodySize::None => return Self::none(), + + // we cannot assume that Sized is not a stream + BodySize::Sized(_) | BodySize::Stream => {} + } + + // TODO potentially some optimisation for single-chunk responses here by trying to read the + // payload eagerly, stopping after 2 polls if the first is a chunk and the second is None if can_encode { - // Modify response body only if encoder is not None + // Modify response body only if encoder is set if let Some(enc) = ContentEncoder::encoder(encoding) { update_head(encoding, head); head.no_chunking(false); - return ResponseBody::Body(Encoder { - body, - eof: false, - fut: None, + + return Encoder { + body: EncoderBody::Stream { body }, encoder: Some(enc), - }); + fut: None, + eof: false, + }; } } - ResponseBody::Body(Encoder { - body, - eof: false, - fut: None, + + Encoder { + body: EncoderBody::Stream { body }, encoder: None, - }) + fut: None, + eof: false, + } } } -#[pin_project(project = EncoderBodyProj)] -enum EncoderBody { - Bytes(Bytes), - Stream(#[pin] B), - BoxedStream(Box), +pin_project! { + #[project = EncoderBodyProj] + enum EncoderBody { + None, + Stream { #[pin] body: B }, + } } -impl MessageBody for EncoderBody { +impl MessageBody for EncoderBody +where + B: MessageBody, +{ + type Error = EncoderError; + fn size(&self) -> BodySize { match self { - EncoderBody::Bytes(ref b) => b.size(), - EncoderBody::Stream(ref b) => b.size(), - EncoderBody::BoxedStream(ref b) => b.size(), + EncoderBody::None => BodySize::None, + EncoderBody::Stream { body } => body.size(), } } fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { match self.project() { - EncoderBodyProj::Bytes(b) => { - if b.is_empty() { - Poll::Ready(None) - } else { - Poll::Ready(Some(Ok(std::mem::take(b)))) - } - } - EncoderBodyProj::Stream(b) => b.poll_next(cx), - EncoderBodyProj::BoxedStream(ref mut b) => { - Pin::new(b.as_mut()).poll_next(cx) - } + EncoderBodyProj::None => Poll::Ready(None), + + EncoderBodyProj::Stream { body } => body + .poll_next(cx) + .map_err(|err| EncoderError::Body(err.into())), } } } -impl MessageBody for Encoder { +impl MessageBody for Encoder +where + B: MessageBody, +{ + type Error = EncoderError; + fn size(&self) -> BodySize { if self.encoder.is_none() { self.body.size() @@ -128,7 +154,7 @@ impl MessageBody for Encoder { fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { let mut this = self.project(); loop { if *this.eof { @@ -136,34 +162,31 @@ impl MessageBody for Encoder { } if let Some(ref mut fut) = this.fut { - let mut encoder = match ready!(Pin::new(fut).poll(cx)) { - Ok(Ok(item)) => item, - Ok(Err(e)) => { - return Poll::Ready(Some(Err(BlockingError::Error(e).into()))) - } - Err(_) => { - return Poll::Ready(Some(Err( - BlockingError::::Canceled.into(), - ))) - } - }; + let mut encoder = ready!(Pin::new(fut).poll(cx)) + .map_err(|_| EncoderError::Blocking(BlockingError))? + .map_err(EncoderError::Io)?; + let chunk = encoder.take(); *this.encoder = Some(encoder); this.fut.take(); + if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } } - let result = this.body.as_mut().poll_next(cx); + let result = ready!(this.body.as_mut().poll_next(cx)); match result { - Poll::Ready(Some(Ok(chunk))) => { + Some(Err(err)) => return Poll::Ready(Some(Err(err))), + + Some(Ok(chunk)) => { if let Some(mut encoder) = this.encoder.take() { - if chunk.len() < INPLACE { - encoder.write(&chunk)?; + if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE { + encoder.write(&chunk).map_err(EncoderError::Io)?; let chunk = encoder.take(); *this.encoder = Some(encoder); + if !chunk.is_empty() { return Poll::Ready(Some(Ok(chunk))); } @@ -177,9 +200,11 @@ impl MessageBody for Encoder { return Poll::Ready(Some(Ok(chunk))); } } - Poll::Ready(None) => { + + None => { if let Some(encoder) = this.encoder.take() { - let chunk = encoder.finish()?; + let chunk = encoder.finish().map_err(EncoderError::Io)?; + if chunk.is_empty() { return Poll::Ready(None); } else { @@ -190,7 +215,6 @@ impl MessageBody for Encoder { return Poll::Ready(None); } } - val => return val, } } } @@ -204,25 +228,47 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { } enum ContentEncoder { + #[cfg(feature = "compress-gzip")] Deflate(ZlibEncoder), + + #[cfg(feature = "compress-gzip")] Gzip(GzEncoder), + + #[cfg(feature = "compress-brotli")] Br(BrotliEncoder), + + // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we + // use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`. + #[cfg(feature = "compress-zstd")] + Zstd(ZstdEncoder<'static, Writer>), } impl ContentEncoder { fn encoder(encoding: ContentEncoding) -> Option { match encoding { + #[cfg(feature = "compress-gzip")] ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + + #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( Writer::new(), flate2::Compression::fast(), ))), + + #[cfg(feature = "compress-brotli")] ContentEncoding::Br => { Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) } + + #[cfg(feature = "compress-zstd")] + ContentEncoding::Zstd => { + let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; + Some(ContentEncoder::Zstd(encoder)) + } + _ => None, } } @@ -230,31 +276,51 @@ impl ContentEncoder { #[inline] pub(crate) fn take(&mut self) -> Bytes { match *self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), + + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), + + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), + + #[cfg(feature = "compress-zstd")] + ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), } } fn finish(self) -> Result { match self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(encoder) => match encoder.finish() { Ok(writer) => Ok(writer.buf.freeze()), Err(err) => Err(err), }, + + #[cfg(feature = "compress-zstd")] + ContentEncoder::Zstd(encoder) => match encoder.finish() { + Ok(writer) => Ok(writer.buf.freeze()), + Err(err) => Err(err), + }, } } fn write(&mut self, data: &[u8]) -> Result<(), io::Error> { match *self { + #[cfg(feature = "compress-brotli")] ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -262,6 +328,8 @@ impl ContentEncoder { Err(err) } }, + + #[cfg(feature = "compress-gzip")] ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -269,6 +337,8 @@ impl ContentEncoder { Err(err) } }, + + #[cfg(feature = "compress-gzip")] ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { Ok(_) => Ok(()), Err(err) => { @@ -276,6 +346,44 @@ impl ContentEncoder { Err(err) } }, + + #[cfg(feature = "compress-zstd")] + ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { + Ok(_) => Ok(()), + Err(err) => { + trace!("Error decoding ztsd encoding: {}", err); + Err(err) + } + }, } } } + +#[derive(Debug, Display)] +#[non_exhaustive] +pub enum EncoderError { + #[display(fmt = "body")] + Body(Box), + + #[display(fmt = "blocking")] + Blocking(BlockingError), + + #[display(fmt = "io")] + Io(io::Error), +} + +impl StdError for EncoderError { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + match self { + EncoderError::Body(err) => Some(&**err), + EncoderError::Blocking(err) => Some(err), + EncoderError::Io(err) => Some(err), + } + } +} + +impl From for crate::Error { + fn from(err: EncoderError) -> Self { + crate::Error::new_encoder().with_cause(err) + } +} diff --git a/actix-http/src/encoding/mod.rs b/actix-http/src/encoding/mod.rs index 9eaf4104e..d51dd66c0 100644 --- a/actix-http/src/encoding/mod.rs +++ b/actix-http/src/encoding/mod.rs @@ -1,4 +1,5 @@ -//! Content-Encoding support +//! Content-Encoding support. + use std::io; use bytes::{Bytes, BytesMut}; @@ -9,6 +10,9 @@ mod encoder; pub use self::decoder::Decoder; pub use self::encoder::Encoder; +/// Special-purpose writer for streaming (de-)compression. +/// +/// Pre-allocates 8KiB of capacity. pub(self) struct Writer { buf: BytesMut, } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 9ff154240..231e90e57 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -1,262 +1,202 @@ //! Error and Result module -use std::cell::RefCell; -use std::io::Write; -use std::str::Utf8Error; -use std::string::FromUtf8Error; -use std::{fmt, io, result}; +use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; -use actix_codec::{Decoder, Encoder}; -use actix_utils::dispatcher::DispatcherError as FramedDispatcherError; -use actix_utils::timeout::TimeoutError; -use bytes::BytesMut; -use derive_more::{Display, From}; -pub use futures_channel::oneshot::Canceled; -use http::uri::InvalidUri; -use http::{header, Error as HttpError, StatusCode}; -use serde::de::value::Error as DeError; -use serde_json::error::Error as JsonError; -use serde_urlencoded::ser::Error as FormError; +use derive_more::{Display, Error, From}; +use http::{uri::InvalidUri, StatusCode}; -use crate::body::Body; -pub use crate::cookie::ParseError as CookieParseError; -use crate::helpers::Writer; -use crate::response::{Response, ResponseBuilder}; +use crate::{body::BoxBody, ws, Response}; -/// A specialized [`std::result::Result`] -/// for actix web operations -/// -/// This typedef is generally used to avoid writing out -/// `actix_http::error::Error` directly and is otherwise a direct mapping to -/// `Result`. -pub type Result = result::Result; +pub use http::Error as HttpError; -/// General purpose actix web error. -/// -/// An actix web error is used to carry errors from `std::error` -/// through actix in a convenient way. It can be created through -/// converting errors with `into()`. -/// -/// Whenever it is created from an external object a response error is created -/// for it that can be used to create an http response from it this means that -/// if you have access to an actix `Error` you can always get a -/// `ResponseError` reference from it. pub struct Error { - cause: Box, + inner: Box, +} + +pub(crate) struct ErrorInner { + #[allow(dead_code)] + kind: Kind, + cause: Option>, } impl Error { - /// Returns the reference to the underlying `ResponseError`. - pub fn as_response_error(&self) -> &dyn ResponseError { - self.cause.as_ref() + fn new(kind: Kind) -> Self { + Self { + inner: Box::new(ErrorInner { kind, cause: None }), + } } - /// Similar to `as_response_error` but downcasts. - pub fn as_error(&self) -> Option<&T> { - ResponseError::downcast_ref(self.cause.as_ref()) + pub(crate) fn with_cause(mut self, cause: impl Into>) -> Self { + self.inner.cause = Some(cause.into()); + self + } + + pub(crate) fn new_http() -> Self { + Self::new(Kind::Http) + } + + pub(crate) fn new_parse() -> Self { + Self::new(Kind::Parse) + } + + pub(crate) fn new_payload() -> Self { + Self::new(Kind::Payload) + } + + pub(crate) fn new_body() -> Self { + Self::new(Kind::Body) + } + + pub(crate) fn new_send_response() -> Self { + Self::new(Kind::SendResponse) + } + + #[allow(unused)] // reserved for future use (TODO: remove allow when being used) + pub(crate) fn new_io() -> Self { + Self::new(Kind::Io) + } + + #[allow(unused)] // used in encoder behind feature flag so ignore unused warning + pub(crate) fn new_encoder() -> Self { + Self::new(Kind::Encoder) + } + + pub(crate) fn new_ws() -> Self { + Self::new(Kind::Ws) } } -/// Error that can be converted to `Response` -pub trait ResponseError: fmt::Debug + fmt::Display { - /// Response's status code - /// - /// Internal server error is generated by default. - fn status_code(&self) -> StatusCode { - StatusCode::INTERNAL_SERVER_ERROR - } +impl From for Response { + fn from(err: Error) -> Self { + // TODO: more appropriate error status codes, usage assessment needed + let status_code = match err.inner.kind { + Kind::Parse => StatusCode::BAD_REQUEST, + _ => StatusCode::INTERNAL_SERVER_ERROR, + }; - /// Create response for error - /// - /// Internal server error is generated by default. - fn error_response(&self) -> Response { - let mut resp = Response::new(self.status_code()); - let mut buf = BytesMut::new(); - let _ = write!(Writer(&mut buf), "{}", self); - resp.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); - resp.set_body(Body::from(buf)) + Response::new(status_code).set_body(BoxBody::new(err.to_string())) } - - downcast_get_type_id!(); } -downcast!(ResponseError); +#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] +pub(crate) enum Kind { + #[display(fmt = "error processing HTTP")] + Http, -impl fmt::Display for Error { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } + #[display(fmt = "error parsing HTTP message")] + Parse, + + #[display(fmt = "request payload read error")] + Payload, + + #[display(fmt = "response body write error")] + Body, + + #[display(fmt = "send response error")] + SendResponse, + + #[display(fmt = "error in WebSocket process")] + Ws, + + #[display(fmt = "connection error")] + Io, + + #[display(fmt = "encoder error")] + Encoder, } impl fmt::Debug for Error { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "{:?}", &self.cause) + // TODO: more detail + f.write_str("actix_http::Error") } } -impl std::error::Error for Error { - fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { - None +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self.inner.cause.as_ref() { + Some(err) => write!(f, "{}: {}", &self.inner.kind, err), + None => write!(f, "{}", &self.inner.kind), + } } } -impl From<()> for Error { - fn from(_: ()) -> Self { - Error::from(UnitError) +impl StdError for Error { + fn source(&self) -> Option<&(dyn StdError + 'static)> { + self.inner.cause.as_ref().map(Box::as_ref) } } impl From for Error { - fn from(_: std::convert::Infallible) -> Self { - // `std::convert::Infallible` indicates an error - // that will never happen - unreachable!() + fn from(err: std::convert::Infallible) -> Self { + match err {} } } -/// Convert `Error` to a `Response` instance -impl From for Response { - fn from(err: Error) -> Self { - Response::from_error(err) +impl From for Error { + fn from(err: HttpError) -> Self { + Self::new_http().with_cause(err) } } -/// `Error` for any error that implements `ResponseError` -impl From for Error { - fn from(err: T) -> Error { - Error { - cause: Box::new(err), - } +impl From for Error { + fn from(err: ws::HandshakeError) -> Self { + Self::new_ws().with_cause(err) } } -/// Convert Response to a Error -impl From for Error { - fn from(res: Response) -> Error { - InternalError::from_response("", res).into() +impl From for Error { + fn from(err: ws::ProtocolError) -> Self { + Self::new_ws().with_cause(err) } } -/// Convert ResponseBuilder to a Error -impl From for Error { - fn from(mut res: ResponseBuilder) -> Error { - InternalError::from_response("", res.finish()).into() - } -} - -/// Return `GATEWAY_TIMEOUT` for `TimeoutError` -impl ResponseError for TimeoutError { - fn status_code(&self) -> StatusCode { - match self { - TimeoutError::Service(e) => e.status_code(), - TimeoutError::Timeout => StatusCode::GATEWAY_TIMEOUT, - } - } -} - -#[derive(Debug, Display)] -#[display(fmt = "UnknownError")] -struct UnitError; - -/// `InternalServerError` for `UnitError` -impl ResponseError for UnitError {} - -/// `InternalServerError` for `JsonError` -impl ResponseError for JsonError {} - -/// `InternalServerError` for `FormError` -impl ResponseError for FormError {} - -#[cfg(feature = "openssl")] -/// `InternalServerError` for `openssl::ssl::Error` -impl ResponseError for actix_tls::accept::openssl::SslError {} - -/// Return `BAD_REQUEST` for `de::value::Error` -impl ResponseError for DeError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -/// `InternalServerError` for `Canceled` -impl ResponseError for Canceled {} - -/// Return `BAD_REQUEST` for `Utf8Error` -impl ResponseError for Utf8Error { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -/// Return `InternalServerError` for `HttpError`, -/// Response generation can return `HttpError`, so it is internal error -impl ResponseError for HttpError {} - -/// Return `InternalServerError` for `io::Error` -impl ResponseError for io::Error { - fn status_code(&self) -> StatusCode { - match self.kind() { - io::ErrorKind::NotFound => StatusCode::NOT_FOUND, - io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - -/// `BadRequest` for `InvalidHeaderValue` -impl ResponseError for header::InvalidHeaderValue { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -/// A set of errors that can occur during parsing HTTP streams -#[derive(Debug, Display)] +/// A set of errors that can occur during parsing HTTP streams. +#[derive(Debug, Display, Error)] +#[non_exhaustive] pub enum ParseError { /// An invalid `Method`, such as `GE.T`. #[display(fmt = "Invalid Method specified")] Method, + /// An invalid `Uri`, such as `exam ple.domain`. #[display(fmt = "Uri error: {}", _0)] Uri(InvalidUri), + /// An invalid `HttpVersion`, such as `HTP/1.1` #[display(fmt = "Invalid HTTP version specified")] Version, + /// An invalid `Header`. #[display(fmt = "Invalid Header provided")] Header, + /// A message head is too large to be reasonable. #[display(fmt = "Message head is too large")] TooLarge, + /// A message reached EOF, but is not complete. #[display(fmt = "Message is incomplete")] Incomplete, + /// An invalid `Status`, such as `1337 ELITE`. #[display(fmt = "Invalid Status provided")] Status, + /// A timeout occurred waiting for an IO event. #[allow(dead_code)] #[display(fmt = "Timeout")] Timeout, - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. + + /// An `io::Error` that occurred while trying to read or write to a network stream. #[display(fmt = "IO error: {}", _0)] Io(io::Error), - /// Parsing a field as string failed + + /// Parsing a field as string failed. #[display(fmt = "UTF8 error: {}", _0)] Utf8(Utf8Error), } -/// Return `BadRequest` for `ParseError` -impl ResponseError for ParseError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - impl From for ParseError { fn from(err: io::Error) -> ParseError { ParseError::Io(err) @@ -295,22 +235,26 @@ impl From for ParseError { } } -/// A set of errors that can occur running blocking tasks in thread pool. -#[derive(Debug, Display)] -pub enum BlockingError { - #[display(fmt = "{:?}", _0)] - Error(E), - #[display(fmt = "Thread pool is gone")] - Canceled, +impl From for Error { + fn from(err: ParseError) -> Self { + Self::new_parse().with_cause(err) + } } -impl std::error::Error for BlockingError {} +impl From for Response { + fn from(err: ParseError) -> Self { + Error::from(err).into() + } +} -/// `InternalServerError` for `BlockingError` -impl ResponseError for BlockingError {} +/// A set of errors that can occur running blocking tasks in thread pool. +#[derive(Debug, Display, Error)] +#[display(fmt = "Blocking thread pool is gone")] +pub struct BlockingError; -#[derive(Display, Debug)] -/// A set of errors that can occur during payload parsing +/// A set of errors that can occur during payload parsing. +#[derive(Debug, Display)] +#[non_exhaustive] pub enum PayloadError { /// A payload reached EOF, but is not complete. #[display( @@ -372,49 +316,39 @@ impl From for PayloadError { } } -impl From> for PayloadError { - fn from(err: BlockingError) -> Self { - match err { - BlockingError::Error(e) => PayloadError::Io(e), - BlockingError::Canceled => PayloadError::Io(io::Error::new( - io::ErrorKind::Other, - "Operation is canceled", - )), - } +impl From for PayloadError { + fn from(_: BlockingError) -> Self { + PayloadError::Io(io::Error::new( + io::ErrorKind::Other, + "Operation is canceled", + )) } } -/// `PayloadError` returns two possible results: -/// -/// - `Overflow` returns `PayloadTooLarge` -/// - Other errors returns `BadRequest` -impl ResponseError for PayloadError { - fn status_code(&self) -> StatusCode { - match *self { - PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE, - _ => StatusCode::BAD_REQUEST, - } +impl From for Error { + fn from(err: PayloadError) -> Self { + Self::new_payload().with_cause(err) } } -/// Return `BadRequest` for `cookie::ParseError` -impl ResponseError for crate::cookie::ParseError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -#[derive(Debug, Display, From)] -/// A set of errors that can occur during dispatching http requests +/// A set of errors that can occur during dispatching HTTP requests. +#[derive(Debug, Display, Error, From)] +#[non_exhaustive] pub enum DispatchError { /// Service error - Service(Error), + // FIXME: display and error type + #[display(fmt = "Service Error")] + Service(#[error(not(source))] Response), + + /// Body error + // FIXME: display and error type + #[display(fmt = "Body Error")] + Body(#[error(not(source))] Box), /// Upgrade service error Upgrade, - /// An `io::Error` that occurred while trying to read or write to a network - /// stream. + /// An `io::Error` that occurred while trying to read or write to a network stream. #[display(fmt = "IO error: {}", _0)] Io(io::Error), @@ -451,529 +385,35 @@ pub enum DispatchError { Unknown, } -/// A set of error that can occur during parsing content type -#[derive(PartialEq, Debug, Display)] +/// A set of error that can occur during parsing content type. +#[derive(Debug, Display, Error)] +#[non_exhaustive] pub enum ContentTypeError { /// Can not parse content type #[display(fmt = "Can not parse content type")] ParseError, + /// Unknown content encoding #[display(fmt = "Unknown content encoding")] UnknownEncoding, } -impl std::error::Error for ContentTypeError {} +#[cfg(test)] +mod content_type_test_impls { + use super::*; -/// Return `BadRequest` for `ContentTypeError` -impl ResponseError for ContentTypeError { - fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST - } -} - -impl + Decoder, I> ResponseError for FramedDispatcherError -where - E: fmt::Debug + fmt::Display, - >::Error: fmt::Debug, - ::Error: fmt::Debug, -{ -} - -/// Helper type that can wrap any error and generate custom response. -/// -/// In following example any `io::Error` will be converted into "BAD REQUEST" -/// response as opposite to *INTERNAL SERVER ERROR* which is defined by -/// default. -/// -/// ```rust -/// # use std::io; -/// # use actix_http::*; -/// -/// fn index(req: Request) -> Result<&'static str> { -/// Err(error::ErrorBadRequest(io::Error::new(io::ErrorKind::Other, "error"))) -/// } -/// ``` -pub struct InternalError { - cause: T, - status: InternalErrorType, -} - -enum InternalErrorType { - Status(StatusCode), - Response(RefCell>), -} - -impl InternalError { - /// Create `InternalError` instance - pub fn new(cause: T, status: StatusCode) -> Self { - InternalError { - cause, - status: InternalErrorType::Status(status), - } - } - - /// Create `InternalError` with predefined `Response`. - pub fn from_response(cause: T, response: Response) -> Self { - InternalError { - cause, - status: InternalErrorType::Response(RefCell::new(Some(response))), - } - } -} - -impl fmt::Debug for InternalError -where - T: fmt::Debug + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Debug::fmt(&self.cause, f) - } -} - -impl fmt::Display for InternalError -where - T: fmt::Display + 'static, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.cause, f) - } -} - -impl ResponseError for InternalError -where - T: fmt::Debug + fmt::Display + 'static, -{ - fn status_code(&self) -> StatusCode { - match self.status { - InternalErrorType::Status(st) => st, - InternalErrorType::Response(ref resp) => { - if let Some(resp) = resp.borrow().as_ref() { - resp.head().status - } else { - StatusCode::INTERNAL_SERVER_ERROR - } - } - } - } - - fn error_response(&self) -> Response { - match self.status { - InternalErrorType::Status(st) => { - let mut res = Response::new(st); - let mut buf = BytesMut::new(); - let _ = write!(Writer(&mut buf), "{}", self); - res.headers_mut().insert( - header::CONTENT_TYPE, - header::HeaderValue::from_static("text/plain; charset=utf-8"), - ); - res.set_body(Body::from(buf)) - } - InternalErrorType::Response(ref resp) => { - if let Some(resp) = resp.borrow_mut().take() { - resp - } else { - Response::new(StatusCode::INTERNAL_SERVER_ERROR) + impl std::cmp::PartialEq for ContentTypeError { + fn eq(&self, other: &Self) -> bool { + match self { + Self::ParseError => matches!(other, ContentTypeError::ParseError), + Self::UnknownEncoding => { + matches!(other, ContentTypeError::UnknownEncoding) } } } } } -/// Helper function that creates wrapper of any error and generate *BAD -/// REQUEST* response. -#[allow(non_snake_case)] -pub fn ErrorBadRequest(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::BAD_REQUEST).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNAUTHORIZED* response. -#[allow(non_snake_case)] -pub fn ErrorUnauthorized(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNAUTHORIZED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PAYMENT_REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorPaymentRequired(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PAYMENT_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate *FORBIDDEN* -/// response. -#[allow(non_snake_case)] -pub fn ErrorForbidden(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::FORBIDDEN).into() -} - -/// Helper function that creates wrapper of any error and generate *NOT FOUND* -/// response. -#[allow(non_snake_case)] -pub fn ErrorNotFound(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_FOUND).into() -} - -/// Helper function that creates wrapper of any error and generate *METHOD NOT -/// ALLOWED* response. -#[allow(non_snake_case)] -pub fn ErrorMethodNotAllowed(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED).into() -} - -/// Helper function that creates wrapper of any error and generate *NOT -/// ACCEPTABLE* response. -#[allow(non_snake_case)] -pub fn ErrorNotAcceptable(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_ACCEPTABLE).into() -} - -/// Helper function that creates wrapper of any error and generate *PROXY -/// AUTHENTICATION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorProxyAuthenticationRequired(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PROXY_AUTHENTICATION_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate *REQUEST -/// TIMEOUT* response. -#[allow(non_snake_case)] -pub fn ErrorRequestTimeout(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::REQUEST_TIMEOUT).into() -} - -/// Helper function that creates wrapper of any error and generate *CONFLICT* -/// response. -#[allow(non_snake_case)] -pub fn ErrorConflict(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::CONFLICT).into() -} - -/// Helper function that creates wrapper of any error and generate *GONE* -/// response. -#[allow(non_snake_case)] -pub fn ErrorGone(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::GONE).into() -} - -/// Helper function that creates wrapper of any error and generate *LENGTH -/// REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorLengthRequired(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LENGTH_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PAYLOAD TOO LARGE* response. -#[allow(non_snake_case)] -pub fn ErrorPayloadTooLarge(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PAYLOAD_TOO_LARGE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *URI TOO LONG* response. -#[allow(non_snake_case)] -pub fn ErrorUriTooLong(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::URI_TOO_LONG).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNSUPPORTED MEDIA TYPE* response. -#[allow(non_snake_case)] -pub fn ErrorUnsupportedMediaType(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNSUPPORTED_MEDIA_TYPE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *RANGE NOT SATISFIABLE* response. -#[allow(non_snake_case)] -pub fn ErrorRangeNotSatisfiable(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::RANGE_NOT_SATISFIABLE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *IM A TEAPOT* response. -#[allow(non_snake_case)] -pub fn ErrorImATeapot(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::IM_A_TEAPOT).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *MISDIRECTED REQUEST* response. -#[allow(non_snake_case)] -pub fn ErrorMisdirectedRequest(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::MISDIRECTED_REQUEST).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNPROCESSABLE ENTITY* response. -#[allow(non_snake_case)] -pub fn ErrorUnprocessableEntity(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNPROCESSABLE_ENTITY).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *LOCKED* response. -#[allow(non_snake_case)] -pub fn ErrorLocked(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LOCKED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *FAILED DEPENDENCY* response. -#[allow(non_snake_case)] -pub fn ErrorFailedDependency(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::FAILED_DEPENDENCY).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UPGRADE REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorUpgradeRequired(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UPGRADE_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PRECONDITION FAILED* response. -#[allow(non_snake_case)] -pub fn ErrorPreconditionFailed(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PRECONDITION_FAILED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *PRECONDITION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorPreconditionRequired(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::PRECONDITION_REQUIRED).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *TOO MANY REQUESTS* response. -#[allow(non_snake_case)] -pub fn ErrorTooManyRequests(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::TOO_MANY_REQUESTS).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *REQUEST HEADER FIELDS TOO LARGE* response. -#[allow(non_snake_case)] -pub fn ErrorRequestHeaderFieldsTooLarge(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *UNAVAILABLE FOR LEGAL REASONS* response. -#[allow(non_snake_case)] -pub fn ErrorUnavailableForLegalReasons(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS).into() -} - -/// Helper function that creates wrapper of any error and generate -/// *EXPECTATION FAILED* response. -#[allow(non_snake_case)] -pub fn ErrorExpectationFailed(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::EXPECTATION_FAILED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *INTERNAL SERVER ERROR* response. -#[allow(non_snake_case)] -pub fn ErrorInternalServerError(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NOT IMPLEMENTED* response. -#[allow(non_snake_case)] -pub fn ErrorNotImplemented(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_IMPLEMENTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *BAD GATEWAY* response. -#[allow(non_snake_case)] -pub fn ErrorBadGateway(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::BAD_GATEWAY).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *SERVICE UNAVAILABLE* response. -#[allow(non_snake_case)] -pub fn ErrorServiceUnavailable(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::SERVICE_UNAVAILABLE).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *GATEWAY TIMEOUT* response. -#[allow(non_snake_case)] -pub fn ErrorGatewayTimeout(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::GATEWAY_TIMEOUT).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *HTTP VERSION NOT SUPPORTED* response. -#[allow(non_snake_case)] -pub fn ErrorHttpVersionNotSupported(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::HTTP_VERSION_NOT_SUPPORTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *VARIANT ALSO NEGOTIATES* response. -#[allow(non_snake_case)] -pub fn ErrorVariantAlsoNegotiates(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::VARIANT_ALSO_NEGOTIATES).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *INSUFFICIENT STORAGE* response. -#[allow(non_snake_case)] -pub fn ErrorInsufficientStorage(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::INSUFFICIENT_STORAGE).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *LOOP DETECTED* response. -#[allow(non_snake_case)] -pub fn ErrorLoopDetected(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::LOOP_DETECTED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NOT EXTENDED* response. -#[allow(non_snake_case)] -pub fn ErrorNotExtended(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NOT_EXTENDED).into() -} - -/// Helper function that creates wrapper of any error and -/// generate *NETWORK AUTHENTICATION REQUIRED* response. -#[allow(non_snake_case)] -pub fn ErrorNetworkAuthenticationRequired(err: T) -> Error -where - T: fmt::Debug + fmt::Display + 'static, -{ - InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() -} - -#[cfg(feature = "actors")] -/// `InternalServerError` for `actix::MailboxError` -/// This is supported on feature=`actors` only -impl ResponseError for actix::MailboxError {} - #[cfg(test)] mod tests { use super::*; @@ -982,48 +422,36 @@ mod tests { #[test] fn test_into_response() { - let resp: Response = ParseError::Incomplete.error_response(); + let resp: Response = ParseError::Incomplete.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); - let resp: Response = err.error_response(); + let resp: Response = Error::new_http().with_cause(err).into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } - #[test] - fn test_cookie_parse() { - let resp: Response = CookieParseError::EmptyName.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - } - #[test] fn test_as_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let e: Error = ParseError::Io(orig).into(); - assert_eq!(format!("{}", e.as_response_error()), "IO error: other"); - } - - #[test] - fn test_error_cause() { - let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.to_string(); - let e = Error::from(orig); - assert_eq!(format!("{}", e.as_response_error()), desc); + let err: Error = ParseError::Io(orig).into(); + assert_eq!( + format!("{}", err), + "error parsing HTTP message: IO error: other" + ); } #[test] fn test_error_display() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let desc = orig.to_string(); - let e = Error::from(orig); - assert_eq!(format!("{}", e), desc); + let err = Error::new_io().with_cause(orig); + assert_eq!("connection error: other", err.to_string()); } #[test] fn test_error_http_response() { let orig = io::Error::new(io::ErrorKind::Other, "other"); - let e = Error::from(orig); - let resp: Response = e.into(); + let err = Error::new_io().with_cause(orig); + let resp: Response = err.into(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } @@ -1075,142 +503,4 @@ mod tests { from!(httparse::Error::TooManyHeaders => ParseError::TooLarge); from!(httparse::Error::Version => ParseError::Version); } - - #[test] - fn test_internal_error() { - let err = - InternalError::from_response(ParseError::Method, Response::Ok().into()); - let resp: Response = err.error_response(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_error_casting() { - let err = PayloadError::Overflow; - let resp_err: &dyn ResponseError = &err; - let err = resp_err.downcast_ref::().unwrap(); - assert_eq!(err.to_string(), "Payload reached size limit."); - let not_err = resp_err.downcast_ref::(); - assert!(not_err.is_none()); - } - - #[test] - fn test_error_helpers() { - let r: Response = ErrorBadRequest("err").into(); - assert_eq!(r.status(), StatusCode::BAD_REQUEST); - - let r: Response = ErrorUnauthorized("err").into(); - assert_eq!(r.status(), StatusCode::UNAUTHORIZED); - - let r: Response = ErrorPaymentRequired("err").into(); - assert_eq!(r.status(), StatusCode::PAYMENT_REQUIRED); - - let r: Response = ErrorForbidden("err").into(); - assert_eq!(r.status(), StatusCode::FORBIDDEN); - - let r: Response = ErrorNotFound("err").into(); - assert_eq!(r.status(), StatusCode::NOT_FOUND); - - let r: Response = ErrorMethodNotAllowed("err").into(); - assert_eq!(r.status(), StatusCode::METHOD_NOT_ALLOWED); - - let r: Response = ErrorNotAcceptable("err").into(); - assert_eq!(r.status(), StatusCode::NOT_ACCEPTABLE); - - let r: Response = ErrorProxyAuthenticationRequired("err").into(); - assert_eq!(r.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); - - let r: Response = ErrorRequestTimeout("err").into(); - assert_eq!(r.status(), StatusCode::REQUEST_TIMEOUT); - - let r: Response = ErrorConflict("err").into(); - assert_eq!(r.status(), StatusCode::CONFLICT); - - let r: Response = ErrorGone("err").into(); - assert_eq!(r.status(), StatusCode::GONE); - - let r: Response = ErrorLengthRequired("err").into(); - assert_eq!(r.status(), StatusCode::LENGTH_REQUIRED); - - let r: Response = ErrorPreconditionFailed("err").into(); - assert_eq!(r.status(), StatusCode::PRECONDITION_FAILED); - - let r: Response = ErrorPayloadTooLarge("err").into(); - assert_eq!(r.status(), StatusCode::PAYLOAD_TOO_LARGE); - - let r: Response = ErrorUriTooLong("err").into(); - assert_eq!(r.status(), StatusCode::URI_TOO_LONG); - - let r: Response = ErrorUnsupportedMediaType("err").into(); - assert_eq!(r.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); - - let r: Response = ErrorRangeNotSatisfiable("err").into(); - assert_eq!(r.status(), StatusCode::RANGE_NOT_SATISFIABLE); - - let r: Response = ErrorExpectationFailed("err").into(); - assert_eq!(r.status(), StatusCode::EXPECTATION_FAILED); - - let r: Response = ErrorImATeapot("err").into(); - assert_eq!(r.status(), StatusCode::IM_A_TEAPOT); - - let r: Response = ErrorMisdirectedRequest("err").into(); - assert_eq!(r.status(), StatusCode::MISDIRECTED_REQUEST); - - let r: Response = ErrorUnprocessableEntity("err").into(); - assert_eq!(r.status(), StatusCode::UNPROCESSABLE_ENTITY); - - let r: Response = ErrorLocked("err").into(); - assert_eq!(r.status(), StatusCode::LOCKED); - - let r: Response = ErrorFailedDependency("err").into(); - assert_eq!(r.status(), StatusCode::FAILED_DEPENDENCY); - - let r: Response = ErrorUpgradeRequired("err").into(); - assert_eq!(r.status(), StatusCode::UPGRADE_REQUIRED); - - let r: Response = ErrorPreconditionRequired("err").into(); - assert_eq!(r.status(), StatusCode::PRECONDITION_REQUIRED); - - let r: Response = ErrorTooManyRequests("err").into(); - assert_eq!(r.status(), StatusCode::TOO_MANY_REQUESTS); - - let r: Response = ErrorRequestHeaderFieldsTooLarge("err").into(); - assert_eq!(r.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); - - let r: Response = ErrorUnavailableForLegalReasons("err").into(); - assert_eq!(r.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); - - let r: Response = ErrorInternalServerError("err").into(); - assert_eq!(r.status(), StatusCode::INTERNAL_SERVER_ERROR); - - let r: Response = ErrorNotImplemented("err").into(); - assert_eq!(r.status(), StatusCode::NOT_IMPLEMENTED); - - let r: Response = ErrorBadGateway("err").into(); - assert_eq!(r.status(), StatusCode::BAD_GATEWAY); - - let r: Response = ErrorServiceUnavailable("err").into(); - assert_eq!(r.status(), StatusCode::SERVICE_UNAVAILABLE); - - let r: Response = ErrorGatewayTimeout("err").into(); - assert_eq!(r.status(), StatusCode::GATEWAY_TIMEOUT); - - let r: Response = ErrorHttpVersionNotSupported("err").into(); - assert_eq!(r.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); - - let r: Response = ErrorVariantAlsoNegotiates("err").into(); - assert_eq!(r.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); - - let r: Response = ErrorInsufficientStorage("err").into(); - assert_eq!(r.status(), StatusCode::INSUFFICIENT_STORAGE); - - let r: Response = ErrorLoopDetected("err").into(); - assert_eq!(r.status(), StatusCode::LOOP_DETECTED); - - let r: Response = ErrorNotExtended("err").into(); - assert_eq!(r.status(), StatusCode::NOT_EXTENDED); - - let r: Response = ErrorNetworkAuthenticationRequired("err").into(); - assert_eq!(r.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); - } } diff --git a/actix-http/src/h1/chunked.rs b/actix-http/src/h1/chunked.rs new file mode 100644 index 000000000..e5b734fff --- /dev/null +++ b/actix-http/src/h1/chunked.rs @@ -0,0 +1,432 @@ +use std::{io, task::Poll}; + +use bytes::{Buf as _, Bytes, BytesMut}; + +macro_rules! byte ( + ($rdr:ident) => ({ + if $rdr.len() > 0 { + let b = $rdr[0]; + $rdr.advance(1); + b + } else { + return Poll::Pending + } + }) +); + +#[derive(Debug, PartialEq, Clone)] +pub(super) enum ChunkedState { + Size, + SizeLws, + Extension, + SizeLf, + Body, + BodyCr, + BodyLf, + EndCr, + EndLf, + End, +} + +impl ChunkedState { + pub(super) fn step( + &self, + body: &mut BytesMut, + size: &mut u64, + buf: &mut Option, + ) -> Poll> { + use self::ChunkedState::*; + match *self { + Size => ChunkedState::read_size(body, size), + SizeLws => ChunkedState::read_size_lws(body), + Extension => ChunkedState::read_extension(body), + SizeLf => ChunkedState::read_size_lf(body, *size), + Body => ChunkedState::read_body(body, size, buf), + BodyCr => ChunkedState::read_body_cr(body), + BodyLf => ChunkedState::read_body_lf(body), + EndCr => ChunkedState::read_end_cr(body), + EndLf => ChunkedState::read_end_lf(body), + End => Poll::Ready(Ok(ChunkedState::End)), + } + } + + fn read_size( + rdr: &mut BytesMut, + size: &mut u64, + ) -> Poll> { + let radix = 16; + + let rem = match byte!(rdr) { + b @ b'0'..=b'9' => b - b'0', + b @ b'a'..=b'f' => b + 10 - b'a', + b @ b'A'..=b'F' => b + 10 - b'A', + b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), + b';' => return Poll::Ready(Ok(ChunkedState::Extension)), + b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), + _ => { + return Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size line: Invalid Size", + ))); + } + }; + + match size.checked_mul(radix) { + Some(n) => { + *size = n as u64; + *size += rem as u64; + + Poll::Ready(Ok(ChunkedState::Size)) + } + None => { + log::debug!("chunk size would overflow u64"); + Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size line: Size is too big", + ))) + } + } + } + + fn read_size_lws(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + // LWS can follow the chunk size, but no more digits can come + b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)), + b';' => Poll::Ready(Ok(ChunkedState::Extension)), + b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size linear white space", + ))), + } + } + fn read_extension(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), + // strictly 0x20 (space) should be disallowed but we don't parse quoted strings here + 0x00..=0x08 | 0x0a..=0x1f | 0x7f => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid character in chunk extension", + ))), + _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions + } + } + fn read_size_lf( + rdr: &mut BytesMut, + size: u64, + ) -> Poll> { + match byte!(rdr) { + b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)), + b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk size LF", + ))), + } + } + + fn read_body( + rdr: &mut BytesMut, + rem: &mut u64, + buf: &mut Option, + ) -> Poll> { + log::trace!("Chunked read, remaining={:?}", rem); + + let len = rdr.len() as u64; + if len == 0 { + Poll::Ready(Ok(ChunkedState::Body)) + } else { + let slice; + if *rem > len { + slice = rdr.split().freeze(); + *rem -= len; + } else { + slice = rdr.split_to(*rem as usize).freeze(); + *rem = 0; + } + *buf = Some(slice); + if *rem > 0 { + Poll::Ready(Ok(ChunkedState::Body)) + } else { + Poll::Ready(Ok(ChunkedState::BodyCr)) + } + } + } + + fn read_body_cr(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk body CR", + ))), + } + } + fn read_body_lf(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\n' => Poll::Ready(Ok(ChunkedState::Size)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk body LF", + ))), + } + } + fn read_end_cr(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk end CR", + ))), + } + } + fn read_end_lf(rdr: &mut BytesMut) -> Poll> { + match byte!(rdr) { + b'\n' => Poll::Ready(Ok(ChunkedState::End)), + _ => Poll::Ready(Err(io::Error::new( + io::ErrorKind::InvalidInput, + "Invalid chunk end LF", + ))), + } + } +} + +#[cfg(test)] +mod tests { + use actix_codec::Decoder as _; + use bytes::{Bytes, BytesMut}; + use http::Method; + + use crate::{ + error::ParseError, + h1::decoder::{MessageDecoder, PayloadItem}, + HttpMessage as _, Request, + }; + + macro_rules! parse_ready { + ($e:expr) => {{ + match MessageDecoder::::default().decode($e) { + Ok(Some((msg, _))) => msg, + Ok(_) => unreachable!("Eof during parsing http request"), + Err(err) => unreachable!("Error during parsing http request: {:?}", err), + } + }}; + } + + macro_rules! expect_parse_err { + ($e:expr) => {{ + match MessageDecoder::::default().decode($e) { + Err(err) => match err { + ParseError::Io(_) => unreachable!("Parse error expected"), + _ => {} + }, + _ => unreachable!("Error expected"), + } + }}; + } + + #[test] + fn test_parse_chunked_payload_chunk_extension() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\ + \r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(msg.chunked().unwrap()); + + buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"data")); + let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); + assert_eq!(chunk, Bytes::from_static(b"line")); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + } + + #[test] + fn test_request_chunked() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let req = parse_ready!(&mut buf); + + if let Ok(val) = req.chunked() { + assert!(val); + } else { + unreachable!("Error"); + } + + // intentional typo in "chunked" + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chnked\r\n\r\n", + ); + expect_parse_err!(&mut buf); + } + + #[test] + fn test_http_request_chunked_payload() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"data" + ); + assert_eq!( + pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), + b"line" + ); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn test_http_request_chunked_payload_and_next_message() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend( + b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ + POST /test2 HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n" + .iter(), + ); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"line"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert!(msg.eof()); + + let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); + assert!(req.chunked().unwrap()); + assert_eq!(*req.method(), Method::POST); + assert!(req.chunked().unwrap()); + } + + #[test] + fn test_http_request_chunked_payload_chunks() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + transfer-encoding: chunked\r\n\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + assert!(req.chunked().unwrap()); + + buf.extend(b"4\r\n1111\r\n"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"1111"); + + buf.extend(b"4\r\ndata\r"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"data"); + + buf.extend(b"\n4"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + buf.extend(b"\n"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"li"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"li"); + + //trailers + //buf.feed_data("test: test\r\n"); + //not_ready!(reader.parse(&mut buf, &mut readbuf)); + + buf.extend(b"ne\r\n0\r\n"); + let msg = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(msg.chunk().as_ref(), b"ne"); + assert!(pl.decode(&mut buf).unwrap().is_none()); + + buf.extend(b"\r\n"); + assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); + } + + #[test] + fn chunk_extension_quoted() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 2;hello=b;one=\"1 2 3\"\r\n\ + xx", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let chunk = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"xx"))); + } + + #[test] + fn hrs_chunk_extension_invalid() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: localhost:8080\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 2;x\nx\r\n\ + 4c\r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let err = pl.decode(&mut buf).unwrap_err(); + assert!(err + .to_string() + .contains("Invalid character in chunk extension")); + } + + #[test] + fn hrs_chunk_size_overflow() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + f0000000000000003\r\n\ + abc\r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let err = pl.decode(&mut buf).unwrap_err(); + assert!(err + .to_string() + .contains("Invalid chunk size line: Size is too big")); + } +} diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index 2e0103409..bec167971 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -120,7 +120,7 @@ impl Decoder for ClientCodec { debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); if let Some((req, payload)) = self.inner.decoder.decode(src)? { - if let Some(ctype) = req.ctype() { + if let Some(ctype) = req.conn_type() { // do not use peer's keep-alive self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.ctype @@ -223,15 +223,3 @@ impl Encoder> for ClientCodec { Ok(()) } } - -pub struct Writer<'a>(pub &'a mut BytesMut); - -impl<'a> io::Write for Writer<'a> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index d5035df26..29f6f4170 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -29,7 +29,7 @@ pub struct Codec { decoder: decoder::MessageDecoder, payload: Option, version: Version, - ctype: ConnectionType, + conn_type: ConnectionType, // encoder part flags: Flags, @@ -65,7 +65,7 @@ impl Codec { decoder: decoder::MessageDecoder::default(), payload: None, version: Version::HTTP_11, - ctype: ConnectionType::Close, + conn_type: ConnectionType::Close, encoder: encoder::MessageEncoder::default(), } } @@ -73,13 +73,13 @@ impl Codec { /// Check if request is upgrade. #[inline] pub fn upgrade(&self) -> bool { - self.ctype == ConnectionType::Upgrade + self.conn_type == ConnectionType::Upgrade } /// Check if last response is keep-alive. #[inline] pub fn keepalive(&self) -> bool { - self.ctype == ConnectionType::KeepAlive + self.conn_type == ConnectionType::KeepAlive } /// Check if keep-alive enabled on server level. @@ -124,11 +124,11 @@ impl Decoder for Codec { let head = req.head(); self.flags.set(Flags::HEAD, head.method == Method::HEAD); self.version = head.version; - self.ctype = head.connection_type(); - if self.ctype == ConnectionType::KeepAlive + self.conn_type = head.connection_type(); + if self.conn_type == ConnectionType::KeepAlive && !self.flags.contains(Flags::KEEPALIVE_ENABLED) { - self.ctype = ConnectionType::Close + self.conn_type = ConnectionType::Close } match payload { PayloadType::None => self.payload = None, @@ -159,14 +159,14 @@ impl Encoder, BodySize)>> for Codec { res.head_mut().version = self.version; // connection status - self.ctype = if let Some(ct) = res.head().ctype() { + self.conn_type = if let Some(ct) = res.head().conn_type() { if ct == ConnectionType::KeepAlive { - self.ctype + self.conn_type } else { ct } } else { - self.ctype + self.conn_type }; // encode message @@ -177,10 +177,9 @@ impl Encoder, BodySize)>> for Codec { self.flags.contains(Flags::STREAM), self.version, length, - self.ctype, + self.conn_type, &self.config, )?; - // self.headers_size = (dst.len() - len) as u32; } Message::Chunk(Some(bytes)) => { self.encoder.encode_chunk(bytes.as_ref(), dst)?; @@ -189,6 +188,7 @@ impl Encoder, BodySize)>> for Codec { self.encoder.encode_eof(dst)?; } } + Ok(()) } } @@ -199,10 +199,10 @@ mod tests { use http::Method; use super::*; - use crate::httpmessage::HttpMessage; + use crate::HttpMessage; - #[test] - fn test_http_request_chunked_payload_and_next_message() { + #[actix_rt::test] + async fn test_http_request_chunked_payload_and_next_message() { let mut codec = Codec::default(); let mut buf = BytesMut::from( diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 85379b084..f25c35a76 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,20 +1,20 @@ -use std::convert::TryFrom; -use std::io; -use std::marker::PhantomData; -use std::task::Poll; +use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll}; use actix_codec::Decoder; -use bytes::{Buf, Bytes, BytesMut}; +use bytes::{Bytes, BytesMut}; use http::header::{HeaderName, HeaderValue}; use http::{header, Method, StatusCode, Uri, Version}; use log::{debug, error, trace}; -use crate::error::ParseError; -use crate::header::HeaderMap; -use crate::message::{ConnectionType, ResponseHead}; -use crate::request::Request; +use super::chunked::ChunkedState; +use crate::{ + error::ParseError, + header::HeaderMap, + message::{ConnectionType, ResponseHead}, + request::Request, +}; -const MAX_BUFFER_SIZE: usize = 131_072; +pub(crate) const MAX_BUFFER_SIZE: usize = 131_072; const MAX_HEADERS: usize = 96; /// Incoming message decoder @@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized { let mut has_upgrade_websocket = false; let mut expect = false; let mut chunked = false; + let mut seen_te = false; let mut content_length = None; { @@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized { }; match name { - header::CONTENT_LENGTH => { - if let Ok(s) = value.to_str() { + header::CONTENT_LENGTH if content_length.is_some() => { + debug!("multiple Content-Length"); + return Err(ParseError::Header); + } + + header::CONTENT_LENGTH => match value.to_str() { + Ok(s) if s.trim().starts_with('+') => { + debug!("illegal Content-Length: {:?}", s); + return Err(ParseError::Header); + } + Ok(s) => { if let Ok(len) = s.parse::() { if len != 0 { content_length = Some(len); @@ -95,22 +105,38 @@ pub(crate) trait MessageType: Sized { debug!("illegal Content-Length: {:?}", s); return Err(ParseError::Header); } - } else { + } + Err(_) => { debug!("illegal Content-Length: {:?}", value); return Err(ParseError::Header); } - } + }, + // transfer-encoding + header::TRANSFER_ENCODING if seen_te => { + debug!("multiple Transfer-Encoding not allowed"); + return Err(ParseError::Header); + } + header::TRANSFER_ENCODING => { - if let Ok(s) = value.to_str().map(|s| s.trim()) { - chunked = s.eq_ignore_ascii_case("chunked"); + seen_te = true; + + if let Ok(s) = value.to_str().map(str::trim) { + if s.eq_ignore_ascii_case("chunked") { + chunked = true; + } else if s.eq_ignore_ascii_case("identity") { + // allow silently since multiple TE headers are already checked + } else { + debug!("illegal Transfer-Encoding: {:?}", s); + return Err(ParseError::Header); + } } else { return Err(ParseError::Header); } } // connection keep-alive state header::CONNECTION => { - ka = if let Ok(conn) = value.to_str().map(|conn| conn.trim()) { + ka = if let Ok(conn) = value.to_str().map(str::trim) { if conn.eq_ignore_ascii_case("keep-alive") { Some(ConnectionType::KeepAlive) } else if conn.eq_ignore_ascii_case("close") { @@ -125,7 +151,7 @@ pub(crate) trait MessageType: Sized { }; } header::UPGRADE => { - if let Ok(val) = value.to_str().map(|val| val.trim()) { + if let Ok(val) = value.to_str().map(str::trim) { if val.eq_ignore_ascii_case("websocket") { has_upgrade_websocket = true; } @@ -148,7 +174,7 @@ pub(crate) trait MessageType: Sized { self.set_expect() } - // https://tools.ietf.org/html/rfc7230#section-3.3.3 + // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3 if chunked { // Chunked encoding Ok(PayloadLength::Payload(PayloadType::Payload( @@ -186,10 +212,17 @@ impl MessageType for Request { let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let (len, method, uri, ver, h_len) = { - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; + // SAFETY: + // Create an uninitialized array of `MaybeUninit`. The `assume_init` is + // safe because the type we are claiming to have initialized here is a + // bunch of `MaybeUninit`s, which do not require initialization. + let mut parsed = unsafe { + MaybeUninit::<[MaybeUninit>; MAX_HEADERS]>::uninit() + .assume_init() + }; - let mut req = httparse::Request::new(&mut parsed); - match req.parse(src)? { + let mut req = httparse::Request::new(&mut []); + match req.parse_with_uninit_headers(src, &mut parsed)? { httparse::Status::Complete(len) => { let method = Method::from_bytes(req.method.unwrap().as_bytes()) .map_err(|_| ParseError::Method)?; @@ -203,7 +236,15 @@ impl MessageType for Request { (len, method, uri, version, req.headers.len()) } - httparse::Status::Partial => return Ok(None), + httparse::Status::Partial => { + return if src.len() >= MAX_BUFFER_SIZE { + trace!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + Err(ParseError::TooLarge) + } else { + // Return None to notify more read are needed for parsing request + Ok(None) + }; + } } }; @@ -216,15 +257,12 @@ impl MessageType for Request { let decoder = match length { PayloadLength::Payload(pl) => pl, PayloadLength::UpgradeWebSocket => { - // upgrade(websocket) + // upgrade (WebSocket) PayloadType::Stream(PayloadDecoder::eof()) } PayloadLength::None => { if method == Method::CONNECT { PayloadType::Stream(PayloadDecoder::eof()) - } else if src.len() >= MAX_BUFFER_SIZE { - trace!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - return Err(ParseError::TooLarge); } else { PayloadType::None } @@ -273,7 +311,14 @@ impl MessageType for ResponseHead { (len, version, status, res.headers.len()) } - httparse::Status::Partial => return Ok(None), + httparse::Status::Partial => { + return if src.len() >= MAX_BUFFER_SIZE { + error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); + Err(ParseError::TooLarge) + } else { + Ok(None) + } + } } }; @@ -289,9 +334,6 @@ impl MessageType for ResponseHead { } else if status == StatusCode::SWITCHING_PROTOCOLS { // switching protocol or connect PayloadType::Stream(PayloadDecoder::eof()) - } else if src.len() >= MAX_BUFFER_SIZE { - error!("MAX_BUFFER_SIZE unprocessed data reached, closing"); - return Err(ParseError::TooLarge); } else { // for HTTP/1.0 read to eof and close connection if msg.version == Version::HTTP_10 { @@ -399,20 +441,6 @@ enum Kind { Eof, } -#[derive(Debug, PartialEq, Clone)] -enum ChunkedState { - Size, - SizeLws, - Extension, - SizeLf, - Body, - BodyCr, - BodyLf, - EndCr, - EndLf, - End, -} - impl Decoder for PayloadDecoder { type Item = PayloadItem; type Error = io::Error; @@ -442,19 +470,23 @@ impl Decoder for PayloadDecoder { Kind::Chunked(ref mut state, ref mut size) => { loop { let mut buf = None; + // advances the chunked state *state = match state.step(src, size, &mut buf) { Poll::Pending => return Ok(None), Poll::Ready(Ok(state)) => state, Poll::Ready(Err(e)) => return Err(e), }; + if *state == ChunkedState::End { trace!("End of chunked stream"); return Ok(Some(PayloadItem::Eof)); } + if let Some(buf) = buf { return Ok(Some(PayloadItem::Chunk(buf))); } + if src.is_empty() { return Ok(None); } @@ -471,201 +503,40 @@ impl Decoder for PayloadDecoder { } } -macro_rules! byte ( - ($rdr:ident) => ({ - if $rdr.len() > 0 { - let b = $rdr[0]; - $rdr.advance(1); - b - } else { - return Poll::Pending - } - }) -); - -impl ChunkedState { - fn step( - &self, - body: &mut BytesMut, - size: &mut u64, - buf: &mut Option, - ) -> Poll> { - use self::ChunkedState::*; - match *self { - Size => ChunkedState::read_size(body, size), - SizeLws => ChunkedState::read_size_lws(body), - Extension => ChunkedState::read_extension(body), - SizeLf => ChunkedState::read_size_lf(body, size), - Body => ChunkedState::read_body(body, size, buf), - BodyCr => ChunkedState::read_body_cr(body), - BodyLf => ChunkedState::read_body_lf(body), - EndCr => ChunkedState::read_end_cr(body), - EndLf => ChunkedState::read_end_lf(body), - End => Poll::Ready(Ok(ChunkedState::End)), - } - } - - fn read_size( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { - let radix = 16; - match byte!(rdr) { - b @ b'0'..=b'9' => { - *size *= radix; - *size += u64::from(b - b'0'); - } - b @ b'a'..=b'f' => { - *size *= radix; - *size += u64::from(b + 10 - b'a'); - } - b @ b'A'..=b'F' => { - *size *= radix; - *size += u64::from(b + 10 - b'A'); - } - b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), - b';' => return Poll::Ready(Ok(ChunkedState::Extension)), - b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => { - return Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size line: Invalid Size", - ))); - } - } - Poll::Ready(Ok(ChunkedState::Size)) - } - - fn read_size_lws(rdr: &mut BytesMut) -> Poll> { - trace!("read_size_lws"); - match byte!(rdr) { - // LWS can follow the chunk size, but no more digits can come - b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)), - b';' => Poll::Ready(Ok(ChunkedState::Extension)), - b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size linear white space", - ))), - } - } - fn read_extension(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), - _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions - } - } - fn read_size_lf( - rdr: &mut BytesMut, - size: &mut u64, - ) -> Poll> { - match byte!(rdr) { - b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)), - b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk size LF", - ))), - } - } - - fn read_body( - rdr: &mut BytesMut, - rem: &mut u64, - buf: &mut Option, - ) -> Poll> { - trace!("Chunked read, remaining={:?}", rem); - - let len = rdr.len() as u64; - if len == 0 { - Poll::Ready(Ok(ChunkedState::Body)) - } else { - let slice; - if *rem > len { - slice = rdr.split().freeze(); - *rem -= len; - } else { - slice = rdr.split_to(*rem as usize).freeze(); - *rem = 0; - } - *buf = Some(slice); - if *rem > 0 { - Poll::Ready(Ok(ChunkedState::Body)) - } else { - Poll::Ready(Ok(ChunkedState::BodyCr)) - } - } - } - - fn read_body_cr(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body CR", - ))), - } - } - fn read_body_lf(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\n' => Poll::Ready(Ok(ChunkedState::Size)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk body LF", - ))), - } - } - fn read_end_cr(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end CR", - ))), - } - } - fn read_end_lf(rdr: &mut BytesMut) -> Poll> { - match byte!(rdr) { - b'\n' => Poll::Ready(Ok(ChunkedState::End)), - _ => Poll::Ready(Err(io::Error::new( - io::ErrorKind::InvalidInput, - "Invalid chunk end LF", - ))), - } - } -} - #[cfg(test)] mod tests { use bytes::{Bytes, BytesMut}; use http::{Method, Version}; use super::*; - use crate::error::ParseError; - use crate::http::header::{HeaderName, SET_COOKIE}; - use crate::httpmessage::HttpMessage; + use crate::{ + error::ParseError, + http::header::{HeaderName, SET_COOKIE}, + HttpMessage as _, + }; impl PayloadType { - fn unwrap(self) -> PayloadDecoder { + pub(crate) fn unwrap(self) -> PayloadDecoder { match self { PayloadType::Payload(pl) => pl, _ => panic!(), } } - fn is_unhandled(&self) -> bool { + pub(crate) fn is_unhandled(&self) -> bool { matches!(self, PayloadType::Stream(_)) } } impl PayloadItem { - fn chunk(self) -> Bytes { + pub(crate) fn chunk(self) -> Bytes { match self { PayloadItem::Chunk(chunk) => chunk, _ => panic!("error"), } } - fn eof(&self) -> bool { + + pub(crate) fn eof(&self) -> bool { matches!(*self, PayloadItem::Eof) } } @@ -821,8 +692,8 @@ mod tests { .get_all(SET_COOKIE) .map(|v| v.to_str().unwrap().to_owned()) .collect(); - assert_eq!(val[1], "c1=cookie1"); - assert_eq!(val[0], "c2=cookie2"); + assert_eq!(val[0], "c1=cookie1"); + assert_eq!(val[1], "c2=cookie2"); } #[test] @@ -958,34 +829,6 @@ mod tests { assert!(req.upgrade()); } - #[test] - fn test_request_chunked() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(val); - } else { - unreachable!("Error"); - } - - // intentional typo in "chunked" - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chnked\r\n\r\n", - ); - let req = parse_ready!(&mut buf); - - if let Ok(val) = req.chunked() { - assert!(!val); - } else { - unreachable!("Error"); - } - } - #[test] fn test_headers_content_length_err_1() { let mut buf = BytesMut::from( @@ -1103,128 +946,9 @@ mod tests { expect_parse_err!(&mut buf); } - #[test] - fn test_http_request_chunked_payload() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"data" - ); - assert_eq!( - pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(), - b"line" - ); - assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_http_request_chunked_payload_and_next_message() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend( - b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\ - POST /test2 HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n" - .iter(), - ); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"line"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - - let (req, _) = reader.decode(&mut buf).unwrap().unwrap(); - assert!(req.chunked().unwrap()); - assert_eq!(*req.method(), Method::POST); - assert!(req.chunked().unwrap()); - } - - #[test] - fn test_http_request_chunked_payload_chunks() { - let mut buf = BytesMut::from( - "GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n", - ); - - let mut reader = MessageDecoder::::default(); - let (req, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(req.chunked().unwrap()); - - buf.extend(b"4\r\n1111\r\n"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"1111"); - - buf.extend(b"4\r\ndata\r"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"data"); - - buf.extend(b"\n4"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - buf.extend(b"\n"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"li"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"li"); - - //trailers - //buf.feed_data("test: test\r\n"); - //not_ready!(reader.parse(&mut buf, &mut readbuf)); - - buf.extend(b"ne\r\n0\r\n"); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert_eq!(msg.chunk().as_ref(), b"ne"); - assert!(pl.decode(&mut buf).unwrap().is_none()); - - buf.extend(b"\r\n"); - assert!(pl.decode(&mut buf).unwrap().unwrap().eof()); - } - - #[test] - fn test_parse_chunked_payload_chunk_extension() { - let mut buf = BytesMut::from( - &"GET /test HTTP/1.1\r\n\ - transfer-encoding: chunked\r\n\r\n"[..], - ); - - let mut reader = MessageDecoder::::default(); - let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); - let mut pl = pl.unwrap(); - assert!(msg.chunked().unwrap()); - - buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n") - let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"data")); - let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk(); - assert_eq!(chunk, Bytes::from_static(b"line")); - let msg = pl.decode(&mut buf).unwrap().unwrap(); - assert!(msg.eof()); - } - #[test] fn test_response_http10_read_until_eof() { - let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]); + let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data"); let mut reader = MessageDecoder::::default(); let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); @@ -1233,4 +957,84 @@ mod tests { let chunk = pl.decode(&mut buf).unwrap().unwrap(); assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data"))); } + + #[test] + fn hrs_multiple_content_length() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 4\r\n\ + Content-Length: 2\r\n\ + \r\n\ + abcd", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_content_length_plus() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: +3\r\n\ + \r\n\ + 000", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_unknown_transfer_encoding() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Transfer-Encoding: JUNK\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 5\r\n\ + hello\r\n\ + 0", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn hrs_multiple_transfer_encoding() { + let mut buf = BytesMut::from( + "GET / HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 51\r\n\ + Transfer-Encoding: identity\r\n\ + Transfer-Encoding: chunked\r\n\ + \r\n\ + 0\r\n\ + \r\n\ + GET /forbidden HTTP/1.1\r\n\ + Host: example.com\r\n\r\n", + ); + + expect_parse_err!(&mut buf); + } + + #[test] + fn transfer_encoding_agrees() { + let mut buf = BytesMut::from( + "GET /test HTTP/1.1\r\n\ + Host: example.com\r\n\ + Content-Length: 3\r\n\ + Transfer-Encoding: identity\r\n\ + \r\n\ + 0\r\n", + ); + + let mut reader = MessageDecoder::::default(); + let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); + let mut pl = pl.unwrap(); + + let chunk = pl.decode(&mut buf).unwrap().unwrap(); + assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n"))); + } } diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index eb5f48105..6695d1bf3 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -1,5 +1,4 @@ use std::{ - cell::RefCell, collections::VecDeque, fmt, future::Future, @@ -14,47 +13,50 @@ use actix_rt::time::{sleep_until, Instant, Sleep}; use actix_service::Service; use bitflags::bitflags; use bytes::{Buf, BytesMut}; +use futures_core::ready; use log::{error, trace}; use pin_project::pin_project; -use crate::body::{Body, BodySize, MessageBody, ResponseBody}; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::error::{ParseError, PayloadError}; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::OnConnectData; +use crate::{ + body::{BodySize, BoxBody, MessageBody}, + config::ServiceConfig, + error::{DispatchError, ParseError, PayloadError}, + service::HttpFlow, + OnConnectData, Request, Response, StatusCode, +}; -use super::codec::Codec; -use super::payload::{Payload, PayloadSender, PayloadStatus}; -use super::{Message, MessageType}; +use super::{ + codec::Codec, + payload::{Payload, PayloadSender, PayloadStatus}, + Message, MessageType, +}; -const LW_BUFFER_SIZE: usize = 4096; -const HW_BUFFER_SIZE: usize = 32_768; +const LW_BUFFER_SIZE: usize = 1024; +const HW_BUFFER_SIZE: usize = 1024 * 8; const MAX_PIPELINED_MESSAGES: usize = 16; bitflags! { pub struct Flags: u8 { const STARTED = 0b0000_0001; const KEEPALIVE = 0b0000_0010; - const POLLED = 0b0000_0100; - const SHUTDOWN = 0b0000_1000; - const READ_DISCONNECT = 0b0001_0000; - const WRITE_DISCONNECT = 0b0010_0000; - const UPGRADE = 0b0100_0000; + const SHUTDOWN = 0b0000_0100; + const READ_DISCONNECT = 0b0000_1000; + const WRITE_DISCONNECT = 0b0001_0000; } } -#[pin_project::pin_project] +#[pin_project] /// Dispatcher for HTTP/1.1 protocol pub struct Dispatcher where S: Service, - S::Error: Into, + S::Error: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { @@ -69,10 +71,13 @@ where enum DispatcherState where S: Service, - S::Error: Into, + S::Error: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { @@ -84,14 +89,17 @@ where struct InnerDispatcher where S: Service, - S::Error: Into, + S::Error: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { - flow: Rc>>, + flow: Rc>, on_connect_data: OnConnectData, flags: Flags, peer_addr: Option, @@ -123,114 +131,82 @@ enum State where S: Service, X: Service, + B: MessageBody, { None, ExpectCall(#[pin] X::Future), ServiceCall(#[pin] S::Future), - SendPayload(#[pin] ResponseBody), + SendPayload(#[pin] B), + SendErrorPayload(#[pin] BoxBody), } impl State where S: Service, + X: Service, + B: MessageBody, { fn is_empty(&self) -> bool { matches!(self, State::None) } - - fn is_call(&self) -> bool { - matches!(self, State::ServiceCall(_)) - } } + enum PollResponse { Upgrade(Request), DoNothing, DrainWriteBuf, } -impl PartialEq for PollResponse { - fn eq(&self, other: &PollResponse) -> bool { - match self { - PollResponse::DrainWriteBuf => matches!(other, PollResponse::DrainWriteBuf), - PollResponse::DoNothing => matches!(other, PollResponse::DoNothing), - _ => false, - } - } -} - impl Dispatcher where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { /// Create HTTP/1 dispatcher. pub(crate) fn new( - stream: T, - config: ServiceConfig, - services: Rc>>, - on_connect_data: OnConnectData, - peer_addr: Option, - ) -> Self { - Dispatcher::with_timeout( - stream, - Codec::new(config.clone()), - config, - BytesMut::with_capacity(HW_BUFFER_SIZE), - None, - services, - on_connect_data, - peer_addr, - ) - } - - /// Create http/1 dispatcher with slow request timeout. - pub(crate) fn with_timeout( io: T, - codec: Codec, config: ServiceConfig, - read_buf: BytesMut, - timeout: Option, - services: Rc>>, + flow: Rc>, on_connect_data: OnConnectData, peer_addr: Option, ) -> Self { - let keepalive = config.keep_alive_enabled(); - let flags = if keepalive { + let flags = if config.keep_alive_enabled() { Flags::KEEPALIVE } else { Flags::empty() }; // keep-alive timer - let (ka_expire, ka_timer) = if let Some(delay) = timeout { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = config.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (config.now(), None) + let (ka_expire, ka_timer) = match config.keep_alive_timer() { + Some(delay) => (delay.deadline(), Some(delay)), + None => (config.now(), None), }; Dispatcher { inner: DispatcherState::Normal(InnerDispatcher { + read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), write_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), payload: None, state: State::None, error: None, messages: VecDeque::new(), io: Some(io), - codec, - read_buf, - flow: services, + codec: Codec::new(config), + flow, on_connect_data, flags, peer_addr, @@ -247,20 +223,21 @@ where impl InnerDispatcher where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { fn can_read(&self, cx: &mut Context<'_>) -> bool { - if self - .flags - .intersects(Flags::READ_DISCONNECT | Flags::UPGRADE) - { + if self.flags.contains(Flags::READ_DISCONNECT) { false } else if let Some(ref info) = self.payload { info.need_read(cx) == PayloadStatus::Read @@ -279,55 +256,48 @@ where } } - /// Flush stream - /// - /// true - got WouldBlock - /// false - didn't get WouldBlock fn poll_flush( self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Result { - let len = self.write_buf.len(); - if len == 0 { - return Ok(false); - } - + ) -> Poll> { let InnerDispatcherProj { io, write_buf, .. } = self.project(); let mut io = Pin::new(io.as_mut().unwrap()); + let len = write_buf.len(); let mut written = 0; + while written < len { - match io.as_mut().poll_write(cx, &write_buf[written..]) { - Poll::Ready(Ok(0)) => { - return Err(DispatchError::Io(io::Error::new( + match io.as_mut().poll_write(cx, &write_buf[written..])? { + Poll::Ready(0) => { + return Poll::Ready(Err(io::Error::new( io::ErrorKind::WriteZero, "", ))) } - Poll::Ready(Ok(n)) => written += n, + Poll::Ready(n) => written += n, Poll::Pending => { write_buf.advance(written); - return Ok(true); + return Poll::Pending; } - Poll::Ready(Err(err)) => return Err(DispatchError::Io(err)), } } - // SAFETY: setting length to 0 is safe - // skips one length check vs truncate - unsafe { write_buf.set_len(0) } + // everything has written to io. clear buffer. + write_buf.clear(); - Ok(false) + // flush the io and check if get blocked. + io.poll_flush(cx) } - fn send_response( + fn send_response_inner( self: Pin<&mut Self>, message: Response<()>, - body: ResponseBody, - ) -> Result<(), DispatchError> { - let mut this = self.project(); + body: &impl MessageBody, + ) -> Result { + let size = body.size(); + let this = self.project(); this.codec - .encode(Message::Item((message, body.size())), &mut this.write_buf) + .encode(Message::Item((message, size)), this.write_buf) .map_err(|err| { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::Incomplete(None)); @@ -336,10 +306,35 @@ where })?; this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); - match body.size() { - BodySize::None | BodySize::Empty => this.state.set(State::None), - _ => this.state.set(State::SendPayload(body)), + + Ok(size) + } + + fn send_response( + mut self: Pin<&mut Self>, + message: Response<()>, + body: B, + ) -> Result<(), DispatchError> { + let size = self.as_mut().send_response_inner(message, &body)?; + let state = match size { + BodySize::None | BodySize::Sized(0) => State::None, + _ => State::SendPayload(body), }; + self.project().state.set(state); + Ok(()) + } + + fn send_error_response( + mut self: Pin<&mut Self>, + message: Response<()>, + body: BoxBody, + ) -> Result<(), DispatchError> { + let size = self.as_mut().send_response_inner(message, &body)?; + let state = match size { + BodySize::None | BodySize::Sized(0) => State::None, + _ => State::SendErrorPayload(body), + }; + self.project().state.set(state); Ok(()) } @@ -353,109 +348,157 @@ where mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { - loop { + 'res: loop { let mut this = self.as_mut().project(); - // state is not changed on Poll::Pending. - // other variant and conditions always trigger a state change(or an error). - let state_change = match this.state.project() { + match this.state.as_mut().project() { + // no future is in InnerDispatcher state. pop next message. StateProj::None => match this.messages.pop_front() { + // handle request message. Some(DispatcherMessage::Item(req)) => { - self.as_mut().handle_request(req, cx)?; - true + // Handle `EXPECT: 100-Continue` header + if req.head().expect() { + // set InnerDispatcher state and continue loop to poll it. + let task = this.flow.expect.call(req); + this.state.set(State::ExpectCall(task)); + } else { + // the same as expect call. + let task = this.flow.service.call(req); + this.state.set(State::ServiceCall(task)); + }; } + + // handle error message. Some(DispatcherMessage::Error(res)) => { - self.as_mut() - .send_response(res, ResponseBody::Other(Body::Empty))?; - true + // send_response would update InnerDispatcher state to SendPayload or + // None(If response body is empty). + // continue loop to poll it. + self.as_mut().send_error_response(res, BoxBody::new(()))?; } + + // return with upgrade request and poll it exclusively. Some(DispatcherMessage::Upgrade(req)) => { return Ok(PollResponse::Upgrade(req)); } - None => false, - }, - StateProj::ExpectCall(fut) => match fut.poll(cx) { - Poll::Ready(Ok(req)) => { - self.as_mut().send_continue(); - this = self.as_mut().project(); - let fut = this.flow.borrow_mut().service.call(req); - this.state.set(State::ServiceCall(fut)); - continue; - } - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); - let (res, body) = res.replace_body(()); - self.as_mut().send_response(res, body.into_body())?; - true - } - Poll::Pending => false, + + // all messages are dealt with. + None => return Ok(PollResponse::DoNothing), }, StateProj::ServiceCall(fut) => match fut.poll(cx) { + // service call resolved. send response. Poll::Ready(Ok(res)) => { let (res, body) = res.into().replace_body(()); self.as_mut().send_response(res, body)?; - continue; } - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); + + // send service call error as response + Poll::Ready(Err(err)) => { + let res: Response = err.into(); let (res, body) = res.replace_body(()); - self.as_mut().send_response(res, body.into_body())?; - true + self.as_mut().send_error_response(res, body)?; } - Poll::Pending => false, - }, - StateProj::SendPayload(mut stream) => { - loop { - if this.write_buf.len() < HW_BUFFER_SIZE { - match stream.as_mut().poll_next(cx) { - Poll::Ready(Some(Ok(item))) => { - this.codec.encode( - Message::Chunk(Some(item)), - &mut this.write_buf, - )?; - continue; - } - Poll::Ready(None) => { - this.codec.encode( - Message::Chunk(None), - &mut this.write_buf, - )?; - this = self.as_mut().project(); - this.state.set(State::None); - } - Poll::Ready(Some(Err(_))) => { - return Err(DispatchError::Unknown) - } - Poll::Pending => return Ok(PollResponse::DoNothing), - } - } else { - return Ok(PollResponse::DrainWriteBuf); + + // service call pending and could be waiting for more chunk messages. + // (pipeline message limit and/or payload can_read limit) + Poll::Pending => { + // no new message is decoded and no new payload is feed. + // nothing to do except waiting for new incoming data from client. + if !self.as_mut().poll_request(cx)? { + return Ok(PollResponse::DoNothing); } - break; + // otherwise keep loop. } - continue; - } - }; + }, - // state is changed and continue when the state is not Empty - if state_change { - if !self.state.is_empty() { - continue; - } - } else { - // if read-backpressure is enabled and we consumed some data. - // we may read more data and retry - if self.state.is_call() { - if self.as_mut().poll_request(cx)? { - continue; + StateProj::SendPayload(mut stream) => { + // keep populate writer buffer until buffer size limit hit, + // get blocked or finished. + while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { + match stream.as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(item))) => { + this.codec.encode( + Message::Chunk(Some(item)), + this.write_buf, + )?; + } + + Poll::Ready(None) => { + this.codec + .encode(Message::Chunk(None), this.write_buf)?; + // payload stream finished. + // set state to None and handle next message + this.state.set(State::None); + continue 'res; + } + + Poll::Ready(Some(Err(err))) => { + return Err(DispatchError::Body(err.into())) + } + + Poll::Pending => return Ok(PollResponse::DoNothing), + } } - } else if !self.messages.is_empty() { - continue; + // buffer is beyond max size. + // return and try to write the whole buffer to io stream. + return Ok(PollResponse::DrainWriteBuf); } + + StateProj::SendErrorPayload(mut stream) => { + // TODO: de-dupe impl with SendPayload + + // keep populate writer buffer until buffer size limit hit, + // get blocked or finished. + while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE { + match stream.as_mut().poll_next(cx) { + Poll::Ready(Some(Ok(item))) => { + this.codec.encode( + Message::Chunk(Some(item)), + this.write_buf, + )?; + } + + Poll::Ready(None) => { + this.codec + .encode(Message::Chunk(None), this.write_buf)?; + // payload stream finished. + // set state to None and handle next message + this.state.set(State::None); + continue 'res; + } + + Poll::Ready(Some(Err(err))) => { + return Err(DispatchError::Service(err.into())) + } + + Poll::Pending => return Ok(PollResponse::DoNothing), + } + } + // buffer is beyond max size. + // return and try to write the whole buffer to io stream. + return Ok(PollResponse::DrainWriteBuf); + } + + StateProj::ExpectCall(fut) => match fut.poll(cx) { + // expect resolved. write continue to buffer and set InnerDispatcher state + // to service call. + Poll::Ready(Ok(req)) => { + this.write_buf + .extend_from_slice(b"HTTP/1.1 100 Continue\r\n\r\n"); + let fut = this.flow.service.call(req); + this.state.set(State::ServiceCall(fut)); + } + + // send expect error as response + Poll::Ready(Err(err)) => { + let res: Response = err.into(); + let (res, body) = res.replace_body(()); + self.as_mut().send_error_response(res, body)?; + } + + // expect must be solved before progress can be made. + Poll::Pending => return Ok(PollResponse::DoNothing), + }, } - break; } - - Ok(PollResponse::DoNothing) } fn handle_request( @@ -464,15 +507,14 @@ where cx: &mut Context<'_>, ) -> Result<(), DispatchError> { // Handle `EXPECT: 100-Continue` header + let mut this = self.as_mut().project(); if req.head().expect() { // set dispatcher state so the future is pinned. - let mut this = self.as_mut().project(); - let task = this.flow.borrow_mut().expect.call(req); + let task = this.flow.expect.call(req); this.state.set(State::ExpectCall(task)); } else { // the same as above. - let mut this = self.as_mut().project(); - let task = this.flow.borrow_mut().service.call(req); + let task = this.flow.service.call(req); this.state.set(State::ServiceCall(task)); }; @@ -485,7 +527,7 @@ where Poll::Ready(Ok(req)) => { self.as_mut().send_continue(); let mut this = self.as_mut().project(); - let task = this.flow.borrow_mut().service.call(req); + let task = this.flow.service.call(req); this.state.set(State::ServiceCall(task)); continue; } @@ -495,11 +537,10 @@ where // future is error. send response and return a result. On success // to notify the dispatcher a new state is set and the outer loop // should be continue. - Poll::Ready(Err(e)) => { - let e = e.into(); - let res: Response = e.into(); + Poll::Ready(Err(err)) => { + let res: Response = err.into(); let (res, body) = res.replace_body(()); - return self.send_response(res, body.into_body()); + return self.send_error_response(res, body); } } } @@ -515,11 +556,11 @@ where } // see the comment on ExpectCall state branch's Pending. Poll::Pending => Ok(()), - // see the comment on ExpectCall state branch's Ready(Err(e)). - Poll::Ready(Err(e)) => { - let res: Response = e.into().into(); + // see the comment on ExpectCall state branch's Ready(Err(err)). + Poll::Ready(Err(err)) => { + let res: Response = err.into(); let (res, body) = res.replace_body(()); - self.send_response(res, body.into_body()) + self.send_error_response(res, body) } }; } @@ -531,7 +572,7 @@ where } /// Process one incoming request. - pub(self) fn poll_request( + fn poll_request( mut self: Pin<&mut Self>, cx: &mut Context<'_>, ) -> Result { @@ -543,34 +584,50 @@ where let mut updated = false; let mut this = self.as_mut().project(); loop { - match this.codec.decode(&mut this.read_buf) { + match this.codec.decode(this.read_buf) { Ok(Some(msg)) => { updated = true; this.flags.insert(Flags::STARTED); match msg { Message::Item(mut req) => { - let pl = this.codec.message_type(); req.head_mut().peer_addr = *this.peer_addr; // merge on_connect_ext data into request extensions this.on_connect_data.merge_into(&mut req); - if pl == MessageType::Stream - && this.flow.borrow().upgrade.is_some() - { - this.messages.push_back(DispatcherMessage::Upgrade(req)); - break; - } - if pl == MessageType::Payload || pl == MessageType::Stream { - let (ps, pl) = Payload::create(false); - let (req1, _) = - req.replace_payload(crate::Payload::H1(pl)); - req = req1; - *this.payload = Some(ps); + match this.codec.message_type() { + // Request is upgradable. add upgrade message and break. + // everything remain in read buffer would be handed to + // upgraded Request. + MessageType::Stream if this.flow.upgrade.is_some() => { + this.messages + .push_back(DispatcherMessage::Upgrade(req)); + break; + } + + // Request is not upgradable. + MessageType::Payload | MessageType::Stream => { + /* + PayloadSender and Payload are smart pointers share the + same state. + PayloadSender is attached to dispatcher and used to sink + new chunked request data to state. + Payload is attached to Request and passed to Service::call + where the state can be collected and consumed. + */ + let (ps, pl) = Payload::create(false); + let (req1, _) = + req.replace_payload(crate::Payload::H1(pl)); + req = req1; + *this.payload = Some(ps); + } + + // Request has no payload. + MessageType::None => {} } - // handle request early + // handle request early when no future in InnerDispatcher state. if this.state.is_empty() { self.as_mut().handle_request(req, cx)?; this = self.as_mut().project(); @@ -587,7 +644,7 @@ where ); this.flags.insert(Flags::READ_DISCONNECT); this.messages.push_back(DispatcherMessage::Error( - Response::InternalServerError().finish().drop_body(), + Response::internal_server_error().drop_body(), )); *this.error = Some(DispatchError::InternalError); break; @@ -600,7 +657,7 @@ where error!("Internal server error: unexpected eof"); this.flags.insert(Flags::READ_DISCONNECT); this.messages.push_back(DispatcherMessage::Error( - Response::InternalServerError().finish().drop_body(), + Response::internal_server_error().drop_body(), )); *this.error = Some(DispatchError::InternalError); break; @@ -608,24 +665,41 @@ where } } } + // decode is partial and buffer is not full yet. + // break and wait for more read. Ok(None) => break, - Err(ParseError::Io(e)) => { + Err(ParseError::Io(err)) => { self.as_mut().client_disconnected(); this = self.as_mut().project(); - *this.error = Some(DispatchError::Io(e)); + *this.error = Some(DispatchError::Io(err)); break; } - Err(e) => { + Err(ParseError::TooLarge) => { + if let Some(mut payload) = this.payload.take() { + payload.set_error(PayloadError::Overflow); + } + // Requests overflow buffer size should be responded with 431 + this.messages.push_back(DispatcherMessage::Error( + Response::with_body( + StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE, + (), + ), + )); + this.flags.insert(Flags::READ_DISCONNECT); + *this.error = Some(ParseError::TooLarge.into()); + break; + } + Err(err) => { if let Some(mut payload) = this.payload.take() { payload.set_error(PayloadError::EncodingCorrupted); } // Malformed requests should be responded with 400 this.messages.push_back(DispatcherMessage::Error( - Response::BadRequest().finish().drop_body(), + Response::bad_request().drop_body(), )); this.flags.insert(Flags::READ_DISCONNECT); - *this.error = Some(e.into()); + *this.error = Some(err.into()); break; } } @@ -658,11 +732,6 @@ where // go into Some> branch this.ka_timer.set(Some(sleep_until(deadline))); return self.poll_keepalive(cx); - } else { - this.flags.insert(Flags::READ_DISCONNECT); - if let Some(mut payload) = this.payload.take() { - payload.set_error(PayloadError::Incomplete(None)); - } } } } @@ -672,7 +741,7 @@ where // got timeout during shutdown, drop connection if this.flags.contains(Flags::SHUTDOWN) { return Err(DispatchError::DisconnectTimeout); - // exceed deadline. check for any outstanding tasks + // exceed deadline. check for any outstanding tasks } else if timer.deadline() >= *this.ka_expire { // have no task at hand. if this.state.is_empty() && this.write_buf.is_empty() { @@ -692,28 +761,23 @@ where } } else { // timeout on first request (slow request) return 408 - if !this.flags.contains(Flags::STARTED) { - trace!("Slow request timeout"); - let _ = self.as_mut().send_response( - Response::RequestTimeout().finish().drop_body(), - ResponseBody::Other(Body::Empty), - ); - this = self.project(); - } else { - trace!("Keep-alive connection timeout"); - } + trace!("Slow request timeout"); + let _ = self.as_mut().send_error_response( + Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), + BoxBody::new(()), + ); + this = self.project(); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); - this.state.set(State::None); } - // still have unfinished task. try to reset and register keep-alive. + // still have unfinished task. try to reset and register keep-alive. } else if let Some(deadline) = this.codec.config().keep_alive_expire() { timer.as_mut().reset(deadline); let _ = timer.poll(cx); } - // timer resolved but still have not met the keep-alive expire deadline. - // reset and register for later wakeup. + // timer resolved but still have not met the keep-alive expire deadline. + // reset and register for later wakeup. } else { timer.as_mut().reset(*this.ka_expire); let _ = timer.poll(cx); @@ -723,17 +787,128 @@ where } Ok(()) } + + /// Returns true when io stream can be disconnected after write to it. + /// + /// It covers these conditions: + /// + /// - `std::io::ErrorKind::ConnectionReset` after partial read. + /// - all data read done. + #[inline(always)] + fn read_available( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Result { + let this = self.project(); + + if this.flags.contains(Flags::READ_DISCONNECT) { + return Ok(false); + }; + + let mut io = Pin::new(this.io.as_mut().unwrap()); + + let mut read_some = false; + + loop { + // Return early when read buf exceed decoder's max buffer size. + if this.read_buf.len() >= super::decoder::MAX_BUFFER_SIZE { + /* + At this point it's not known IO stream is still scheduled + to be waked up. so force wake up dispatcher just in case. + + Reason: + AsyncRead mostly would only have guarantee wake up + when the poll_read return Poll::Pending. + + Case: + When read_buf is beyond max buffer size the early return + could be successfully be parsed as a new Request. + This case would not generate ParseError::TooLarge + and at this point IO stream is not fully read to Pending + and would result in dispatcher stuck until timeout (KA) + + Note: + This is a perf choice to reduce branch on + ::decode. + + A Request head too large to parse is only checked on + httparse::Status::Partial condition. + */ + if this.payload.is_none() { + /* + When dispatcher has a payload the responsibility of + wake up it would be shift to h1::payload::Payload. + + Reason: + Self wake up when there is payload would waste poll + and/or result in over read. + + Case: + When payload is (partial) dropped by user there is + no need to do read anymore. + At this case read_buf could always remain beyond + MAX_BUFFER_SIZE and self wake up would be busy poll + dispatcher and waste resource. + + */ + cx.waker().wake_by_ref(); + } + + return Ok(false); + } + + // grow buffer if necessary. + let remaining = this.read_buf.capacity() - this.read_buf.len(); + if remaining < LW_BUFFER_SIZE { + this.read_buf.reserve(HW_BUFFER_SIZE - remaining); + } + + match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) { + Poll::Ready(Ok(n)) => { + if n == 0 { + return Ok(true); + } + read_some = true; + } + Poll::Pending => return Ok(false), + Poll::Ready(Err(err)) => { + return match err.kind() { + io::ErrorKind::WouldBlock => Ok(false), + io::ErrorKind::ConnectionReset if read_some => Ok(true), + _ => Err(DispatchError::Io(err)), + } + } + } + } + } + + /// call upgrade service with request. + fn upgrade(self: Pin<&mut Self>, req: Request) -> U::Future { + let this = self.project(); + let mut parts = FramedParts::with_read_buf( + this.io.take().unwrap(), + mem::take(this.codec), + mem::take(this.read_buf), + ); + parts.write_buf = mem::take(this.write_buf); + let framed = Framed::from_parts(parts); + this.flow.upgrade.as_ref().unwrap().call((req, framed)) + } } impl Future for Dispatcher where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { @@ -756,72 +931,43 @@ where if inner.flags.contains(Flags::WRITE_DISCONNECT) { Poll::Ready(Ok(())) } else { - // flush buffer - inner.as_mut().poll_flush(cx)?; - if !inner.write_buf.is_empty() { - Poll::Pending - } else { - Pin::new(inner.project().io.as_mut().unwrap()) - .poll_shutdown(cx) - .map_err(DispatchError::from) - } + // flush buffer and wait on blocked. + ready!(inner.as_mut().poll_flush(cx))?; + Pin::new(inner.project().io.as_mut().unwrap()) + .poll_shutdown(cx) + .map_err(DispatchError::from) } } else { - // read socket into a buf - let should_disconnect = - if !inner.flags.contains(Flags::READ_DISCONNECT) { - let mut inner_p = inner.as_mut().project(); - read_available( - cx, - inner_p.io.as_mut().unwrap(), - &mut inner_p.read_buf, - )? - } else { - None - }; + // read from io stream and fill read buffer. + let should_disconnect = inner.as_mut().read_available(cx)?; inner.as_mut().poll_request(cx)?; - if let Some(true) = should_disconnect { - let inner_p = inner.as_mut().project(); - inner_p.flags.insert(Flags::READ_DISCONNECT); - if let Some(mut payload) = inner_p.payload.take() { + + // io stream should to be closed. + if should_disconnect { + let inner = inner.as_mut().project(); + inner.flags.insert(Flags::READ_DISCONNECT); + if let Some(mut payload) = inner.payload.take() { payload.feed_eof(); } }; loop { - let inner_p = inner.as_mut().project(); - let remaining = - inner_p.write_buf.capacity() - inner_p.write_buf.len(); - if remaining < LW_BUFFER_SIZE { - inner_p.write_buf.reserve(HW_BUFFER_SIZE - remaining); - } - let result = inner.as_mut().poll_response(cx)?; - let drain = result == PollResponse::DrainWriteBuf; - - // switch to upgrade handler - if let PollResponse::Upgrade(req) = result { - let inner_p = inner.as_mut().project(); - let mut parts = FramedParts::with_read_buf( - inner_p.io.take().unwrap(), - mem::take(inner_p.codec), - mem::take(inner_p.read_buf), - ); - parts.write_buf = mem::take(inner_p.write_buf); - let framed = Framed::from_parts(parts); - let upgrade = inner_p - .flow - .borrow_mut() - .upgrade - .take() - .unwrap() - .call((req, framed)); - self.as_mut() - .project() - .inner - .set(DispatcherState::Upgrade(upgrade)); - return self.poll(cx); - } + // poll_response and populate write buffer. + // drain indicate if write buffer should be emptied before next run. + let drain = match inner.as_mut().poll_response(cx)? { + PollResponse::DrainWriteBuf => true, + PollResponse::DoNothing => false, + // upgrade request and goes Upgrade variant of DispatcherState. + PollResponse::Upgrade(req) => { + let upgrade = inner.upgrade(req); + self.as_mut() + .project() + .inner + .set(DispatcherState::Upgrade(upgrade)); + return self.poll(cx); + } + }; // we didn't get WouldBlock from write operation, // so data get written to kernel completely (macOS) @@ -829,7 +975,7 @@ where // // TODO: what? is WouldBlock good or bad? // want to find a reference for this macOS behavior - if inner.as_mut().poll_flush(cx)? || !drain { + if inner.as_mut().poll_flush(cx)?.is_pending() || !drain { break; } } @@ -878,74 +1024,22 @@ where } } -/// Returns either: -/// - `Ok(Some(true))` - data was read and done reading all data. -/// - `Ok(Some(false))` - data was read but there should be more to read. -/// - `Ok(None)` - no data was read but there should be more to read later. -/// - Unhandled Errors -fn read_available( - cx: &mut Context<'_>, - io: &mut T, - buf: &mut BytesMut, -) -> Result, io::Error> -where - T: AsyncRead + Unpin, -{ - let mut read_some = false; - - loop { - // If buf is full return but do not disconnect since - // there is more reading to be done - if buf.len() >= HW_BUFFER_SIZE { - return Ok(Some(false)); - } - - let remaining = buf.capacity() - buf.len(); - if remaining < LW_BUFFER_SIZE { - buf.reserve(HW_BUFFER_SIZE - remaining); - } - - match actix_codec::poll_read_buf(Pin::new(io), cx, buf) { - Poll::Pending => { - return if read_some { Ok(Some(false)) } else { Ok(None) }; - } - Poll::Ready(Ok(n)) => { - if n == 0 { - return Ok(Some(true)); - } else { - read_some = true; - } - } - Poll::Ready(Err(err)) => { - return if err.kind() == io::ErrorKind::WouldBlock { - if read_some { - Ok(Some(false)) - } else { - Ok(None) - } - } else if err.kind() == io::ErrorKind::ConnectionReset && read_some { - Ok(Some(true)) - } else { - Err(err) - } - } - } - } -} - #[cfg(test)] mod tests { use std::str; use actix_service::fn_service; - use futures_util::future::{lazy, ready}; + use actix_utils::future::{ready, Ready}; + use bytes::Bytes; + use futures_util::future::lazy; use super::*; - use crate::test::TestBuffer; - use crate::{error::Error, KeepAlive}; use crate::{ + error::Error, h1::{ExpectHandler, UpgradeHandler}, - test::TestSeqBuffer, + http::Method, + test::{TestBuffer, TestSeqBuffer}, + HttpMessage, KeepAlive, }; fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option { @@ -957,26 +1051,32 @@ mod tests { fn stabilize_date_header(payload: &mut [u8]) { let mut from = 0; - while let Some(pos) = find_slice(&payload, b"date", from) { + while let Some(pos) = find_slice(payload, b"date", from) { payload[(from + pos)..(from + pos + 35)] .copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC"); from += 35; } } - fn ok_service() -> impl Service { - fn_service(|_req: Request| ready(Ok::<_, Error>(Response::Ok().finish()))) + fn ok_service( + ) -> impl Service, Error = Error> + { + fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) } - fn echo_path_service() -> impl Service { + fn echo_path_service( + ) -> impl Service, Error = Error> + { fn_service(|req: Request| { let path = req.path().as_bytes(); - ready(Ok::<_, Error>(Response::Ok().body(Body::from_slice(path)))) + ready(Ok::<_, Error>( + Response::ok().set_body(Bytes::copy_from_slice(path)), + )) }) } - fn echo_payload_service() -> impl Service - { + fn echo_payload_service( + ) -> impl Service, Error = Error> { fn_service(|mut req: Request| { Box::pin(async move { use futures_util::stream::StreamExt as _; @@ -987,7 +1087,7 @@ mod tests { body.extend_from_slice(chunk.unwrap().chunk()) } - Ok::<_, Error>(Response::Ok().body(body)) + Ok::<_, Error>(Response::ok().set_body(body.freeze())) }) }) } @@ -1007,7 +1107,7 @@ mod tests { None, ); - futures_util::pin_mut!(h1); + actix_rt::pin!(h1); match h1.as_mut().poll(cx) { Poll::Pending => panic!(), @@ -1047,7 +1147,7 @@ mod tests { None, ); - futures_util::pin_mut!(h1); + actix_rt::pin!(h1); assert!(matches!(&h1.inner, DispatcherState::Normal(_))); @@ -1101,7 +1201,7 @@ mod tests { None, ); - futures_util::pin_mut!(h1); + actix_rt::pin!(h1); assert!(matches!(&h1.inner, DispatcherState::Normal(_))); @@ -1160,7 +1260,7 @@ mod tests { ", ); - futures_util::pin_mut!(h1); + actix_rt::pin!(h1); assert!(h1.as_mut().poll(cx).is_pending()); assert!(matches!(&h1.inner, DispatcherState::Normal(_))); @@ -1232,7 +1332,7 @@ mod tests { ", ); - futures_util::pin_mut!(h1); + actix_rt::pin!(h1); assert!(h1.as_mut().poll(cx).is_ready()); assert!(matches!(&h1.inner, DispatcherState::Normal(_))); @@ -1269,14 +1369,30 @@ mod tests { #[actix_rt::test] async fn test_upgrade() { + struct TestUpgrade; + + impl Service<(Request, Framed)> for TestUpgrade { + type Response = (); + type Error = Error; + type Future = Ready>; + + actix_service::always_ready!(); + + fn call(&self, (req, _framed): (Request, Framed)) -> Self::Future { + assert_eq!(req.method(), Method::GET); + assert!(req.upgrade()); + assert_eq!(req.headers().get("upgrade").unwrap(), "websocket"); + ready(Ok(())) + } + } + lazy(|cx| { let mut buf = TestSeqBuffer::empty(); let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); - let services = - HttpFlow::new(ok_service(), ExpectHandler, Some(UpgradeHandler)); + let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade)); - let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( + let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new( buf.clone(), cfg, services, @@ -1293,7 +1409,7 @@ mod tests { ", ); - futures_util::pin_mut!(h1); + actix_rt::pin!(h1); assert!(h1.as_mut().poll(cx).is_ready()); assert!(matches!(&h1.inner, DispatcherState::Upgrade(_))); diff --git a/actix-http/src/h1/encoder.rs b/actix-http/src/h1/encoder.rs index bb89905fb..60880cd7d 100644 --- a/actix-http/src/h1/encoder.rs +++ b/actix-http/src/h1/encoder.rs @@ -6,19 +6,21 @@ use std::{cmp, io}; use bytes::{BufMut, BytesMut}; -use crate::body::BodySize; -use crate::config::ServiceConfig; -use crate::header::{map::Value, HeaderName}; -use crate::helpers; -use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; -use crate::http::{HeaderMap, StatusCode, Version}; -use crate::message::{ConnectionType, RequestHeadType}; -use crate::response::Response; +use crate::{ + body::BodySize, + config::ServiceConfig, + header::{map::Value, HeaderMap, HeaderName}, + header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}, + helpers, + message::{ConnectionType, RequestHeadType}, + Response, StatusCode, Version, +}; const AVERAGE_HEADER_SIZE: usize = 30; #[derive(Debug)] pub(crate) struct MessageEncoder { + #[allow(dead_code)] pub length: BodySize, pub te: TransferEncoding, _phantom: PhantomData, @@ -54,7 +56,7 @@ pub(crate) trait MessageType: Sized { dst: &mut BytesMut, version: Version, mut length: BodySize, - ctype: ConnectionType, + conn_type: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { let chunked = self.chunked(); @@ -69,17 +71,28 @@ pub(crate) trait MessageType: Sized { | StatusCode::PROCESSING | StatusCode::NO_CONTENT => { // skip content-length and transfer-encoding headers - // See https://tools.ietf.org/html/rfc7230#section-3.3.1 - // and https://tools.ietf.org/html/rfc7230#section-3.3.2 + // see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1 + // and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2 skip_len = true; length = BodySize::None } + + StatusCode::NOT_MODIFIED => { + // 304 responses should never have a body but should retain a manually set + // content-length header + // see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 + skip_len = false; + length = BodySize::None; + } + _ => {} } } + match length { BodySize::Stream => { if chunked { + skip_len = true; if camel_case { dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") } else { @@ -90,19 +103,16 @@ pub(crate) trait MessageType: Sized { dst.put_slice(b"\r\n"); } } - BodySize::Empty => { - if camel_case { - dst.put_slice(b"\r\nContent-Length: 0\r\n"); - } else { - dst.put_slice(b"\r\ncontent-length: 0\r\n"); - } + BodySize::Sized(0) if camel_case => { + dst.put_slice(b"\r\nContent-Length: 0\r\n") } + BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"), BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::None => dst.put_slice(b"\r\n"), } // Connection - match ctype { + match conn_type { ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"), ConnectionType::KeepAlive if version < Version::HTTP_11 => { if camel_case { @@ -144,104 +154,54 @@ pub(crate) trait MessageType: Sized { let k = key.as_str().as_bytes(); let k_len = k.len(); - match value { - Value::One(ref val) => { - let v = val.as_ref(); - let v_len = v.len(); + // TODO: drain? + for val in value.iter() { + let v = val.as_ref(); + let v_len = v.len(); - // key length + value length + colon + space + \r\n - let len = k_len + v_len + 4; + // key length + value length + colon + space + \r\n + let len = k_len + v_len + 4; - if len > remaining { - // not enough room in buffer for this header; reserve more space - - // SAFETY: all the bytes written up to position "pos" are initialized - // the written byte count and pointer advancement are kept in sync - unsafe { - dst.advance_mut(pos); - } - - pos = 0; - dst.reserve(len * 2); - remaining = dst.capacity() - dst.len(); - - // re-assign buf raw pointer since it's possible that the buffer was - // reallocated and/or resized - buf = dst.chunk_mut().as_mut_ptr(); - } - - // SAFETY: on each write, it is enough to ensure that the advancement of the - // cursor matches the number of bytes written + if len > remaining { + // SAFETY: all the bytes written up to position "pos" are initialized + // the written byte count and pointer advancement are kept in sync unsafe { - // use upper Camel-Case - if camel_case { - write_camel_case(k, from_raw_parts_mut(buf, k_len)) - } else { - write_data(k, buf, k_len) - } - - buf = buf.add(k_len); - - write_data(b": ", buf, 2); - buf = buf.add(2); - - write_data(v, buf, v_len); - buf = buf.add(v_len); - - write_data(b"\r\n", buf, 2); - buf = buf.add(2); + dst.advance_mut(pos); } - pos += len; - remaining -= len; + pos = 0; + dst.reserve(len * 2); + remaining = dst.capacity() - dst.len(); + + // re-assign buf raw pointer since it's possible that the buffer was + // reallocated and/or resized + buf = dst.chunk_mut().as_mut_ptr(); } - Value::Multi(ref vec) => { - for val in vec { - let v = val.as_ref(); - let v_len = v.len(); - let len = k_len + v_len + 4; - - if len > remaining { - // SAFETY: all the bytes written up to position "pos" are initialized - // the written byte count and pointer advancement are kept in sync - unsafe { - dst.advance_mut(pos); - } - pos = 0; - dst.reserve(len * 2); - remaining = dst.capacity() - dst.len(); - - // re-assign buf raw pointer since it's possible that the buffer was - // reallocated and/or resized - buf = dst.chunk_mut().as_mut_ptr(); - } - - // SAFETY: on each write, it is enough to ensure that the advancement of - // the cursor matches the number of bytes written - unsafe { - if camel_case { - write_camel_case(k, from_raw_parts_mut(buf, k_len)); - } else { - write_data(k, buf, k_len); - } - - buf = buf.add(k_len); - - write_data(b": ", buf, 2); - buf = buf.add(2); - - write_data(v, buf, v_len); - buf = buf.add(v_len); - - write_data(b"\r\n", buf, 2); - buf = buf.add(2); - }; - - pos += len; - remaining -= len; + // SAFETY: on each write, it is enough to ensure that the advancement of + // the cursor matches the number of bytes written + unsafe { + if camel_case { + // use Camel-Case headers + write_camel_case(k, buf, k_len); + } else { + write_data(k, buf, k_len); } - } + + buf = buf.add(k_len); + + write_data(b": ", buf, 2); + buf = buf.add(2); + + write_data(v, buf, v_len); + buf = buf.add(v_len); + + write_data(b"\r\n", buf, 2); + buf = buf.add(2); + }; + + pos += len; + remaining -= len; } }); @@ -337,7 +297,7 @@ impl MessageType for RequestHeadType { let head = self.as_ref(); dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE); write!( - Writer(dst), + helpers::MutWriter(dst), "{} {} {}", head.method, head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), @@ -377,13 +337,13 @@ impl MessageEncoder { stream: bool, version: Version, length: BodySize, - ctype: ConnectionType, + conn_type: ConnectionType, config: &ServiceConfig, ) -> io::Result<()> { // transfer encoding if !head { self.te = match length { - BodySize::Empty => TransferEncoding::empty(), + BodySize::Sized(0) => TransferEncoding::empty(), BodySize::Sized(len) => TransferEncoding::length(len), BodySize::Stream => { if message.chunked() && !stream { @@ -399,7 +359,7 @@ impl MessageEncoder { } message.encode_status(dst)?; - message.encode_headers(dst, version, length, ctype, config) + message.encode_headers(dst, version, length, conn_type, config) } } @@ -413,10 +373,12 @@ pub(crate) struct TransferEncoding { enum TransferEncodingKind { /// An Encoder for when Transfer-Encoding includes `chunked`. Chunked(bool), + /// An Encoder for when Content-Length is set. /// /// Enforces that the body is not longer than the Content-Length header. Length(u64), + /// An Encoder for when Content-Length is not known. /// /// Application decides when to stop writing. @@ -470,7 +432,7 @@ impl TransferEncoding { *eof = true; buf.extend_from_slice(b"0\r\n\r\n"); } else { - writeln!(Writer(buf), "{:X}\r", msg.len()) + writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len()) .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; buf.reserve(msg.len() + 2); @@ -520,28 +482,23 @@ impl TransferEncoding { } } -struct Writer<'a>(pub &'a mut BytesMut); - -impl<'a> io::Write for Writer<'a> { - fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); - Ok(buf.len()) - } - fn flush(&mut self) -> io::Result<()> { - Ok(()) - } -} - /// # Safety -/// Callers must ensure that the given length matches given value length. +/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is +/// valid for writes of at least `len` bytes. unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { debug_assert_eq!(value.len(), len); copy_nonoverlapping(value.as_ptr(), buf, len); } -fn write_camel_case(value: &[u8], buffer: &mut [u8]) { +/// # Safety +/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is +/// valid for writes of at least `len` bytes. +unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { // first copy entire (potentially wrong) slice to output - buffer[..value.len()].copy_from_slice(value); + write_data(value, buf, len); + + // SAFETY: We just initialized the buffer with `value` + let buffer = from_raw_parts_mut(buf, len); let mut iter = value.iter(); @@ -591,8 +548,8 @@ mod tests { ); } - #[test] - fn test_camel_case() { + #[actix_rt::test] + async fn test_camel_case() { let mut bytes = BytesMut::with_capacity(2048); let mut head = RequestHead::default(); head.set_camel_case_headers(true); @@ -605,13 +562,12 @@ mod tests { let _ = head.encode_headers( &mut bytes, Version::HTTP_11, - BodySize::Empty, + BodySize::Sized(0), ConnectionType::Close, &ServiceConfig::default(), ); let data = String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); - eprintln!("{}", &data); assert!(data.contains("Content-Length: 0\r\n")); assert!(data.contains("Connection: close\r\n")); @@ -655,8 +611,8 @@ mod tests { assert!(data.contains("date: date\r\n")); } - #[test] - fn test_extra_headers() { + #[actix_rt::test] + async fn test_extra_headers() { let mut bytes = BytesMut::with_capacity(2048); let mut head = RequestHead::default(); @@ -677,7 +633,7 @@ mod tests { let _ = head.encode_headers( &mut bytes, Version::HTTP_11, - BodySize::Empty, + BodySize::Sized(0), ConnectionType::Close, &ServiceConfig::default(), ); @@ -689,16 +645,14 @@ mod tests { assert!(data.contains("date: date\r\n")); } - #[test] - fn test_no_content_length() { + #[actix_rt::test] + async fn test_no_content_length() { let mut bytes = BytesMut::with_capacity(2048); - let mut res: Response<()> = - Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>(); + let mut res = Response::with_body(StatusCode::SWITCHING_PROTOCOLS, ()); + res.headers_mut().insert(DATE, HeaderValue::from_static("")); res.headers_mut() - .insert(DATE, HeaderValue::from_static(&"")); - res.headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static(&"0")); + .insert(CONTENT_LENGTH, HeaderValue::from_static("0")); let _ = res.encode_headers( &mut bytes, diff --git a/actix-http/src/h1/expect.rs b/actix-http/src/h1/expect.rs index c3e4ccdaa..bb8e28e95 100644 --- a/actix-http/src/h1/expect.rs +++ b/actix-http/src/h1/expect.rs @@ -1,7 +1,5 @@ -use std::task::{Context, Poll}; - use actix_service::{Service, ServiceFactory}; -use futures_util::future::{ready, Ready}; +use actix_utils::future::{ready, Ready}; use crate::error::Error; use crate::request::Request; @@ -26,11 +24,9 @@ impl Service for ExpectHandler { type Error = Error; type Future = Ready>; - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_service::always_ready!(); - fn call(&mut self, req: Request) -> Self::Future { + fn call(&self, req: Request) -> Self::Future { ready(Ok(req)) // TODO: add some way to trigger error // Err(error::ErrorExpectationFailed("test")) diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 3d5dea5d6..17cbfb90f 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -1,6 +1,8 @@ -//! HTTP/1 implementation +//! HTTP/1 protocol implementation. + use bytes::{Bytes, BytesMut}; +mod chunked; mod client; mod codec; mod decoder; diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index d4cfee146..cc771f28a 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -3,9 +3,8 @@ use std::cell::RefCell; use std::collections::VecDeque; use std::pin::Pin; use std::rc::{Rc, Weak}; -use std::task::{Context, Poll}; +use std::task::{Context, Poll, Waker}; -use actix_utils::task::LocalWaker; use bytes::Bytes; use futures_core::Stream; @@ -134,7 +133,7 @@ impl PayloadSender { if shared.borrow().need_read { PayloadStatus::Read } else { - shared.borrow_mut().io_task.register(cx.waker()); + shared.borrow_mut().register_io(cx); PayloadStatus::Pause } } else { @@ -150,8 +149,8 @@ struct Inner { err: Option, need_read: bool, items: VecDeque, - task: LocalWaker, - io_task: LocalWaker, + task: Option, + io_task: Option, } impl Inner { @@ -162,8 +161,46 @@ impl Inner { err: None, items: VecDeque::new(), need_read: true, - task: LocalWaker::new(), - io_task: LocalWaker::new(), + task: None, + io_task: None, + } + } + + /// Wake up future waiting for payload data to be available. + fn wake(&mut self) { + if let Some(waker) = self.task.take() { + waker.wake(); + } + } + + /// Wake up future feeding data to Payload. + fn wake_io(&mut self) { + if let Some(waker) = self.io_task.take() { + waker.wake(); + } + } + + /// Register future waiting data from payload. + /// Waker would be used in `Inner::wake` + fn register(&mut self, cx: &mut Context<'_>) { + if self + .task + .as_ref() + .map_or(true, |w| !cx.waker().will_wake(w)) + { + self.task = Some(cx.waker().clone()); + } + } + + // Register future feeding data to payload. + /// Waker would be used in `Inner::wake_io` + fn register_io(&mut self, cx: &mut Context<'_>) { + if self + .io_task + .as_ref() + .map_or(true, |w| !cx.waker().will_wake(w)) + { + self.io_task = Some(cx.waker().clone()); } } @@ -182,7 +219,7 @@ impl Inner { self.len += data.len(); self.items.push_back(data); self.need_read = self.len < MAX_BUFFER_SIZE; - self.task.wake(); + self.wake(); } #[cfg(test)] @@ -199,9 +236,9 @@ impl Inner { self.need_read = self.len < MAX_BUFFER_SIZE; if self.need_read && !self.eof { - self.task.register(cx.waker()); + self.register(cx); } - self.io_task.wake(); + self.wake_io(); Poll::Ready(Some(Ok(data))) } else if let Some(err) = self.err.take() { Poll::Ready(Some(Err(err))) @@ -209,8 +246,8 @@ impl Inner { Poll::Ready(None) } else { self.need_read = true; - self.task.register(cx.waker()); - self.io_task.wake(); + self.register(cx); + self.wake_io(); Poll::Pending } } @@ -224,7 +261,7 @@ impl Inner { #[cfg(test)] mod tests { use super::*; - use futures_util::future::poll_fn; + use actix_utils::future::poll_fn; #[actix_rt::test] async fn test_unread_data() { diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index aed700eed..70e83901c 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -1,28 +1,28 @@ -use std::cell::RefCell; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{fmt, net}; +use std::{ + fmt, + marker::PhantomData, + net, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; -use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; -use futures_core::ready; -use futures_util::future::ready; +use actix_service::{ + fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, +}; +use actix_utils::future::ready; +use futures_core::future::LocalBoxFuture; -use crate::body::MessageBody; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::{ConnectCallback, OnConnectData}; +use crate::{ + body::{BoxBody, MessageBody}, + config::ServiceConfig, + error::DispatchError, + service::HttpServiceHandler, + ConnectCallback, OnConnectData, Request, Response, +}; -use super::codec::Codec; -use super::dispatcher::Dispatcher; -use super::{ExpectHandler, UpgradeHandler}; +use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler}; /// `ServiceFactory` implementation for HTTP1 transport pub struct H1Service { @@ -37,7 +37,7 @@ pub struct H1Service { impl H1Service where S: ServiceFactory, - S::Error: Into, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, B: MessageBody, @@ -61,15 +61,21 @@ where impl H1Service where S: ServiceFactory, - S::Error: Into, + S::Future: 'static, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, + B: MessageBody, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, + U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { /// Create simple tcp stream service @@ -82,7 +88,7 @@ where Error = DispatchError, InitError = (), > { - pipeline_factory(|io: TcpStream| { + fn_service(|io: TcpStream| { let peer_addr = io.peer_addr().ok(); ready(Ok((io, peer_addr))) }) @@ -94,29 +100,39 @@ where mod openssl { use super::*; - use actix_service::ServiceFactoryExt; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; - use actix_tls::accept::TlsError; + use actix_tls::accept::{ + openssl::{ + reexports::{Error as SslError, SslAcceptor}, + Acceptor, TlsStream, + }, + TlsError, + }; - impl H1Service, S, B, X, U> + impl H1Service, S, B, X, U> where S: ServiceFactory, - S::Error: Into, + S::Future: 'static, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, + B: MessageBody, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, + U: ServiceFactory< - (Request, Framed, Codec>), + (Request, Framed, Codec>), Config = (), Response = (), >, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create openssl based service + /// Create OpenSSL based service. pub fn openssl( self, acceptor: SslAcceptor, @@ -127,47 +143,58 @@ mod openssl { Error = TlsError, InitError = (), > { - pipeline_factory( - Acceptor::new(acceptor) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()), - ) - .and_then(|io: SslStream| { - let peer_addr = io.get_ref().peer_addr().ok(); - ready(Ok((io, peer_addr))) - }) - .and_then(self.map_err(TlsError::Service)) + Acceptor::new(acceptor) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let peer_addr = io.get_ref().peer_addr().ok(); + (io, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) } } } #[cfg(feature = "rustls")] mod rustls { + + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + use super::*; - use actix_service::ServiceFactoryExt; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::accept::TlsError; - use std::{fmt, io}; impl H1Service, S, B, X, U> where S: ServiceFactory, - S::Error: Into, + S::Future: 'static, + S::Error: Into>, S::InitError: fmt::Debug, S::Response: Into>, + B: MessageBody, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, + U: ServiceFactory< (Request, Framed, Codec>), Config = (), Response = (), >, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create rustls based service + /// Create Rustls based service. pub fn rustls( self, config: ServerConfig, @@ -178,16 +205,16 @@ mod rustls { Error = TlsError, InitError = (), > { - pipeline_factory( - Acceptor::new(config) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()), - ) - .and_then(|io: TlsStream| { - let peer_addr = io.get_ref().0.peer_addr().ok(); - ready(Ok((io, peer_addr))) - }) - .and_then(self.map_err(TlsError::Service)) + Acceptor::new(config) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let peer_addr = io.get_ref().0.peer_addr().ok(); + (io, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) } } } @@ -195,7 +222,7 @@ mod rustls { impl H1Service where S: ServiceFactory, - S::Error: Into, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, B: MessageBody, @@ -203,7 +230,7 @@ where pub fn expect(self, expect: X1) -> H1Service where X1: ServiceFactory, - X1::Error: Into, + X1::Error: Into>, X1::InitError: fmt::Debug, { H1Service { @@ -242,17 +269,24 @@ where impl ServiceFactory<(T, Option)> for H1Service where - T: AsyncRead + AsyncWrite + Unpin, + T: AsyncRead + AsyncWrite + Unpin + 'static, + S: ServiceFactory, - S::Error: Into, + S::Future: 'static, + S::Error: Into>, S::Response: Into>, S::InitError: fmt::Debug, + B: MessageBody, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, + U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { type Response = (); @@ -260,207 +294,77 @@ where type Config = (); type Service = H1ServiceHandler; type InitError = (); - type Future = H1ServiceResponse; + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - H1ServiceResponse { - fut: self.srv.new_service(()), - fut_ex: Some(self.expect.new_service(())), - fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), - expect: None, - upgrade: None, - on_connect_ext: self.on_connect_ext.clone(), - cfg: Some(self.cfg.clone()), - _phantom: PhantomData, - } - } -} + let service = self.srv.new_service(()); + let expect = self.expect.new_service(()); + let upgrade = self.upgrade.as_ref().map(|s| s.new_service(())); + let on_connect_ext = self.on_connect_ext.clone(); + let cfg = self.cfg.clone(); -#[doc(hidden)] -#[pin_project::pin_project] -pub struct H1ServiceResponse -where - S: ServiceFactory, - S::Error: Into, - S::InitError: fmt::Debug, - X: ServiceFactory, - X::Error: Into, - X::InitError: fmt::Debug, - U: ServiceFactory<(Request, Framed), Response = ()>, - U::Error: fmt::Display, - U::InitError: fmt::Debug, -{ - #[pin] - fut: S::Future, - #[pin] - fut_ex: Option, - #[pin] - fut_upg: Option, - expect: Option, - upgrade: Option, - on_connect_ext: Option>>, - cfg: Option, - _phantom: PhantomData, -} + Box::pin(async move { + let expect = expect + .await + .map_err(|e| log::error!("Init http expect service error: {:?}", e))?; -impl Future for H1ServiceResponse -where - T: AsyncRead + AsyncWrite + Unpin, - S: ServiceFactory, - S::Error: Into, - S::Response: Into>, - S::InitError: fmt::Debug, - B: MessageBody, - X: ServiceFactory, - X::Error: Into, - X::InitError: fmt::Debug, - U: ServiceFactory<(Request, Framed), Response = ()>, - U::Error: fmt::Display, - U::InitError: fmt::Debug, -{ - type Output = Result, ()>; + let upgrade = match upgrade { + Some(upgrade) => { + let upgrade = upgrade.await.map_err(|e| { + log::error!("Init http upgrade service error: {:?}", e) + })?; + Some(upgrade) + } + None => None, + }; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.as_mut().project(); + let service = service + .await + .map_err(|e| log::error!("Init http service error: {:?}", e))?; - if let Some(fut) = this.fut_ex.as_pin_mut() { - let expect = ready!(fut - .poll(cx) - .map_err(|e| log::error!("Init http service error: {:?}", e)))?; - this = self.as_mut().project(); - *this.expect = Some(expect); - this.fut_ex.set(None); - } - - if let Some(fut) = this.fut_upg.as_pin_mut() { - let upgrade = ready!(fut - .poll(cx) - .map_err(|e| log::error!("Init http service error: {:?}", e)))?; - this = self.as_mut().project(); - *this.upgrade = Some(upgrade); - this.fut_upg.set(None); - } - - let result = ready!(this - .fut - .poll(cx) - .map_err(|e| log::error!("Init http service error: {:?}", e))); - - Poll::Ready(result.map(|service| { - let this = self.as_mut().project(); - - H1ServiceHandler::new( - this.cfg.take().unwrap(), + Ok(H1ServiceHandler::new( + cfg, service, - this.expect.take().unwrap(), - this.upgrade.take(), - this.on_connect_ext.clone(), - ) - })) + expect, + upgrade, + on_connect_ext, + )) + }) } } /// `Service` implementation for HTTP/1 transport -pub struct H1ServiceHandler -where - S: Service, - X: Service, - U: Service<(Request, Framed)>, -{ - flow: Rc>>, - on_connect_ext: Option>>, - cfg: ServiceConfig, - _phantom: PhantomData, -} - -impl H1ServiceHandler -where - S: Service, - S::Error: Into, - S::Response: Into>, - B: MessageBody, - X: Service, - X::Error: Into, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - fn new( - cfg: ServiceConfig, - service: S, - expect: X, - upgrade: Option, - on_connect_ext: Option>>, - ) -> H1ServiceHandler { - H1ServiceHandler { - flow: HttpFlow::new(service, expect, upgrade), - cfg, - on_connect_ext, - _phantom: PhantomData, - } - } -} +pub type H1ServiceHandler = HttpServiceHandler; impl Service<(T, Option)> - for H1ServiceHandler + for HttpServiceHandler where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into, + S::Error: Into>, S::Response: Into>, + B: MessageBody, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; type Future = Dispatcher; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - let mut flow = self.flow.borrow_mut(); - let ready = flow - .expect - .poll_ready(cx) - .map_err(|e| { - let e = e.into(); - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service(e) - })? - .is_ready(); - - let ready = flow - .service - .poll_ready(cx) - .map_err(|e| { - let e = e.into(); - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service(e) - })? - .is_ready() - && ready; - - let ready = if let Some(ref mut upg) = flow.upgrade { - upg.poll_ready(cx) - .map_err(|e| { - let e = e.into(); - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service(e) - })? - .is_ready() - && ready - } else { - ready - }; - - if ready { - Poll::Ready(Ok(())) - } else { - Poll::Pending - } + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + self._poll_ready(cx).map_err(|e| { + log::error!("HTTP/1 service readiness error: {:?}", e); + DispatchError::Service(e) + }) } - fn call(&mut self, (io, addr): (T, Option)) -> Self::Future { + fn call(&self, (io, addr): (T, Option)) -> Self::Future { let on_connect_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); diff --git a/actix-http/src/h1/upgrade.rs b/actix-http/src/h1/upgrade.rs index 007aff1bf..e57ea8ae9 100644 --- a/actix-http/src/h1/upgrade.rs +++ b/actix-http/src/h1/upgrade.rs @@ -1,8 +1,6 @@ -use std::task::{Context, Poll}; - use actix_codec::Framed; use actix_service::{Service, ServiceFactory}; -use futures_util::future::{ready, Ready}; +use futures_core::future::LocalBoxFuture; use crate::error::Error; use crate::h1::Codec; @@ -16,7 +14,7 @@ impl ServiceFactory<(Request, Framed)> for UpgradeHandler { type Config = (); type Service = UpgradeHandler; type InitError = Error; - type Future = Ready>; + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { unimplemented!() @@ -26,13 +24,11 @@ impl ServiceFactory<(Request, Framed)> for UpgradeHandler { impl Service<(Request, Framed)> for UpgradeHandler { type Response = (); type Error = Error; - type Future = Ready>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } + actix_service::always_ready!(); - fn call(&mut self, _: (Request, Framed)) -> Self::Future { - ready(Ok(())) + fn call(&self, _: (Request, Framed)) -> Self::Future { + unimplemented!() } } diff --git a/actix-http/src/h1/utils.rs b/actix-http/src/h1/utils.rs index 9e9c57137..905585a32 100644 --- a/actix-http/src/h1/utils.rs +++ b/actix-http/src/h1/utils.rs @@ -1,27 +1,36 @@ -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + future::Future, + pin::Pin, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; +use pin_project_lite::pin_project; -use crate::body::{BodySize, MessageBody, ResponseBody}; -use crate::error::Error; -use crate::h1::{Codec, Message}; -use crate::response::Response; +use crate::{ + body::{BodySize, MessageBody}, + error::Error, + h1::{Codec, Message}, + response::Response, +}; -/// Send HTTP/1 response -#[pin_project::pin_project] -pub struct SendResponse { - res: Option, BodySize)>>, - #[pin] - body: Option>, - #[pin] - framed: Option>, +pin_project! { + /// Send HTTP/1 response + pub struct SendResponse { + res: Option, BodySize)>>, + + #[pin] + body: Option, + + #[pin] + framed: Option>, + } } impl SendResponse where B: MessageBody, + B::Error: Into, { pub fn new(framed: Framed, response: Response) -> Self { let (res, body) = response.into_parts(); @@ -38,6 +47,7 @@ impl Future for SendResponse where T: AsyncRead + AsyncWrite + Unpin, B: MessageBody + Unpin, + B::Error: Into, { type Output = Result, Error>; @@ -60,7 +70,17 @@ where .unwrap() .is_write_buf_full() { - match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { + let next = + match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { + Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), + Poll::Ready(Some(Err(err))) => { + return Poll::Ready(Err(err.into())) + } + Poll::Ready(None) => Poll::Ready(None), + Poll::Pending => Poll::Pending, + }; + + match next { Poll::Ready(item) => { // body is done when item is None body_done = item.is_none(); @@ -68,7 +88,9 @@ where let _ = this.body.take(); } let framed = this.framed.as_mut().as_pin_mut().unwrap(); - framed.write(Message::Chunk(item))?; + framed.write(Message::Chunk(item)).map_err(|err| { + Error::new_send_response().with_cause(err) + })?; } Poll::Pending => body_ready = false, } @@ -79,7 +101,10 @@ where // flush write buffer if !framed.is_write_buf_empty() { - match framed.flush(cx)? { + match framed + .flush(cx) + .map_err(|err| Error::new_send_response().with_cause(err))? + { Poll::Ready(_) => { if body_ready { continue; @@ -93,7 +118,9 @@ where // send response if let Some(res) = this.res.take() { - framed.write(res)?; + framed + .write(res) + .map_err(|err| Error::new_send_response().with_cause(err))?; continue; } diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 959c34f13..6d2f4579a 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -1,132 +1,118 @@ -use std::cell::RefCell; -use std::future::Future; -use std::marker::PhantomData; -use std::net; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, convert::TryFrom}; +use std::{ + cmp, + error::Error as StdError, + future::Future, + marker::PhantomData, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite}; -use actix_rt::time::{Instant, Sleep}; +use actix_rt::time::{sleep, Sleep}; use actix_service::Service; +use actix_utils::future::poll_fn; use bytes::{Bytes, BytesMut}; use futures_core::ready; -use h2::server::{Connection, SendResponse}; -use h2::SendStream; +use h2::{ + server::{Connection, SendResponse}, + Ping, PingPong, +}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use log::{error, trace}; +use pin_project_lite::pin_project; -use crate::body::{BodySize, MessageBody, ResponseBody}; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::message::ResponseHead; -use crate::payload::Payload; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::OnConnectData; +use crate::{ + body::{BodySize, BoxBody, MessageBody}, + config::ServiceConfig, + service::HttpFlow, + OnConnectData, Payload, Request, Response, ResponseHead, +}; const CHUNK_SIZE: usize = 16_384; -/// Dispatcher for HTTP/2 protocol. -#[pin_project::pin_project] -pub struct Dispatcher -where - T: AsyncRead + AsyncWrite + Unpin, - S: Service, - B: MessageBody, -{ - flow: Rc>>, - connection: Connection, - on_connect_data: OnConnectData, - config: ServiceConfig, - peer_addr: Option, - ka_expire: Instant, - ka_timer: Option, - _phantom: PhantomData, +pin_project! { + /// Dispatcher for HTTP/2 protocol. + pub struct Dispatcher { + flow: Rc>, + connection: Connection, + on_connect_data: OnConnectData, + config: ServiceConfig, + peer_addr: Option, + ping_pong: Option, + _phantom: PhantomData + } } impl Dispatcher where T: AsyncRead + AsyncWrite + Unpin, - S: Service, - S::Error: Into, - S::Response: Into>, - B: MessageBody, { pub(crate) fn new( - services: Rc>>, - connection: Connection, + flow: Rc>, + mut conn: Connection, on_connect_data: OnConnectData, config: ServiceConfig, - timeout: Option, peer_addr: Option, + timer: Option>>, ) -> Self { - // let keepalive = config.keep_alive_enabled(); - // let flags = if keepalive { - // Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED - // } else { - // Flags::empty() - // }; + let ping_pong = config.keep_alive().map(|dur| H2PingPong { + timer: timer + .map(|mut timer| { + // reset timer if it's received from new function. + timer.as_mut().reset(config.now() + dur); + timer + }) + .unwrap_or_else(|| Box::pin(sleep(dur))), + on_flight: false, + ping_pong: conn.ping_pong().unwrap(), + }); - // keep-alive timer - let (ka_expire, ka_timer) = if let Some(delay) = timeout { - (delay.deadline(), Some(delay)) - } else if let Some(delay) = config.keep_alive_timer() { - (delay.deadline(), Some(delay)) - } else { - (config.now(), None) - }; - - Dispatcher { - flow: services, + Self { + flow, config, peer_addr, - connection, + connection: conn, on_connect_data, - ka_expire, - ka_timer, + ping_pong, _phantom: PhantomData, } } } +struct H2PingPong { + timer: Pin>, + on_flight: bool, + ping_pong: PingPong, +} + impl Future for Dispatcher where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into + 'static, + S::Error: Into>, S::Future: 'static, - S::Response: Into> + 'static, - B: MessageBody + 'static, + S::Response: Into>, + + B: MessageBody, { - type Output = Result<(), DispatchError>; + type Output = Result<(), crate::error::DispatchError>; #[inline] fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); loop { - match ready!(Pin::new(&mut this.connection).poll_accept(cx)) { - None => return Poll::Ready(Ok(())), - - Some(Err(err)) => return Poll::Ready(Err(err.into())), - - Some(Ok((req, res))) => { - // update keep-alive expire - if this.ka_timer.is_some() { - if let Some(expire) = this.config.keep_alive_expire() { - this.ka_expire = expire; - } - } - + match Pin::new(&mut this.connection).poll_accept(cx)? { + Poll::Ready(Some((req, tx))) => { let (parts, body) = req.into_parts(); let pl = crate::h2::Payload::new(body); let pl = Payload::::H2(pl); let mut req = Request::with_payload(pl); - let head = &mut req.head_mut(); + let head = req.head_mut(); head.uri = parts.uri; head.method = parts.method; head.version = parts.version; @@ -136,236 +122,214 @@ where // merge on_connect_ext data into request extensions this.on_connect_data.merge_into(&mut req); - let svc = ServiceResponse:: { - state: ServiceResponseState::ServiceCall( - this.flow.borrow_mut().service.call(req), - Some(res), - ), - config: this.config.clone(), - buffer: None, - _phantom: PhantomData, - }; + let fut = this.flow.service.call(req); + let config = this.config.clone(); - actix_rt::spawn(svc); + // multiplex request handling with spawn task + actix_rt::spawn(async move { + // resolve service call and send response. + let res = match fut.await { + Ok(res) => handle_response(res.into(), tx, config).await, + Err(err) => { + let res: Response = err.into(); + handle_response(res, tx, config).await + } + }; + + // log error. + if let Err(err) = res { + match err { + DispatchError::SendResponse(err) => { + trace!("Error sending HTTP/2 response: {:?}", err) + } + DispatchError::SendData(err) => warn!("{:?}", err), + DispatchError::ResponseBody(err) => { + error!("Response payload stream error: {:?}", err) + } + } + } + }); } + Poll::Ready(None) => return Poll::Ready(Ok(())), + Poll::Pending => match this.ping_pong.as_mut() { + Some(ping_pong) => loop { + if ping_pong.on_flight { + // When have on flight ping pong. poll pong and and keep alive timer. + // on success pong received update keep alive timer to determine the next timing of + // ping pong. + match ping_pong.ping_pong.poll_pong(cx)? { + Poll::Ready(_) => { + ping_pong.on_flight = false; + + let dead_line = + this.config.keep_alive_expire().unwrap(); + ping_pong.timer.as_mut().reset(dead_line); + } + Poll::Pending => { + return ping_pong + .timer + .as_mut() + .poll(cx) + .map(|_| Ok(())) + } + } + } else { + // When there is no on flight ping pong. keep alive timer is used to wait for next + // timing of ping pong. Therefore at this point it serves as an interval instead. + ready!(ping_pong.timer.as_mut().poll(cx)); + + ping_pong.ping_pong.send_ping(Ping::opaque())?; + + let dead_line = this.config.keep_alive_expire().unwrap(); + ping_pong.timer.as_mut().reset(dead_line); + + ping_pong.on_flight = true; + } + }, + None => return Poll::Pending, + }, } } } } -#[pin_project::pin_project] -struct ServiceResponse { - #[pin] - state: ServiceResponseState, +enum DispatchError { + SendResponse(h2::Error), + SendData(h2::Error), + ResponseBody(Box), +} + +async fn handle_response( + res: Response, + mut tx: SendResponse, config: ServiceConfig, - buffer: Option, - _phantom: PhantomData<(I, E)>, -} - -#[pin_project::pin_project(project = ServiceResponseStateProj)] -enum ServiceResponseState { - ServiceCall(#[pin] F, Option>), - SendPayload(SendStream, #[pin] ResponseBody), -} - -impl ServiceResponse +) -> Result<(), DispatchError> where - F: Future>, - E: Into, - I: Into>, B: MessageBody, { - fn prepare_response( - &self, - head: &ResponseHead, - size: &mut BodySize, - ) -> http::Response<()> { - let mut has_date = false; - let mut skip_len = size != &BodySize::Stream; + let (res, body) = res.replace_body(()); - let mut res = http::Response::new(()); - *res.status_mut() = head.status; - *res.version_mut() = http::Version::HTTP_2; + // prepare response. + let mut size = body.size(); + let res = prepare_response(config, res.head(), &mut size); + let eof = size.is_eof(); - // Content length - match head.status { - http::StatusCode::NO_CONTENT - | http::StatusCode::CONTINUE - | http::StatusCode::PROCESSING => *size = BodySize::None, - http::StatusCode::SWITCHING_PROTOCOLS => { - skip_len = true; - *size = BodySize::Stream; + // send response head and return on eof. + let mut stream = tx + .send_response(res, eof) + .map_err(DispatchError::SendResponse)?; + + if eof { + return Ok(()); + } + + // poll response body and send chunks to client. + actix_rt::pin!(body); + + while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { + let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?; + + 'send: loop { + // reserve enough space and wait for stream ready. + stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE)); + + match poll_fn(|cx| stream.poll_capacity(cx)).await { + // No capacity left. drop body and return. + None => return Ok(()), + Some(res) => { + // Split chuck to writeable size and send to client. + let cap = res.map_err(DispatchError::SendData)?; + + let len = chunk.len(); + let bytes = chunk.split_to(cmp::min(cap, len)); + + stream + .send_data(bytes, false) + .map_err(DispatchError::SendData)?; + + // Current chuck completely sent. break send loop and poll next one. + if chunk.is_empty() { + break 'send; + } + } } + } + } + + // response body streaming finished. send end of stream and return. + stream + .send_data(Bytes::new(), true) + .map_err(DispatchError::SendData)?; + + Ok(()) +} + +fn prepare_response( + config: ServiceConfig, + head: &ResponseHead, + size: &mut BodySize, +) -> http::Response<()> { + let mut has_date = false; + let mut skip_len = size != &BodySize::Stream; + + let mut res = http::Response::new(()); + *res.status_mut() = head.status; + *res.version_mut() = http::Version::HTTP_2; + + // Content length + match head.status { + http::StatusCode::NO_CONTENT + | http::StatusCode::CONTINUE + | http::StatusCode::PROCESSING => *size = BodySize::None, + http::StatusCode::SWITCHING_PROTOCOLS => { + skip_len = true; + *size = BodySize::Stream; + } + _ => {} + } + + let _ = match size { + BodySize::None | BodySize::Stream => None, + + BodySize::Sized(0) => res + .headers_mut() + .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + + BodySize::Sized(len) => { + let mut buf = itoa::Buffer::new(); + + res.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::from_str(buf.format(*len)).unwrap(), + ) + } + }; + + // copy headers + for (key, value) in head.headers.iter() { + match *key { + // TODO: consider skipping other headers according to: + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 + // omit HTTP/1.x only headers + CONNECTION | TRANSFER_ENCODING => continue, + CONTENT_LENGTH if skip_len => continue, + DATE => has_date = true, _ => {} } - let _ = match size { - BodySize::None | BodySize::Stream => None, - BodySize::Empty => res - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), - BodySize::Sized(len) => res.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(format!("{}", len)).unwrap(), - ), - }; - - // copy headers - for (key, value) in head.headers.iter() { - match *key { - // omit HTTP/1 only headers - CONNECTION | TRANSFER_ENCODING => continue, - CONTENT_LENGTH if skip_len => continue, - DATE => has_date = true, - _ => {} - } - - res.headers_mut().append(key, value.clone()); - } - - // set date header - if !has_date { - let mut bytes = BytesMut::with_capacity(29); - self.config.set_date_header(&mut bytes); - res.headers_mut().insert( - DATE, - // SAFETY: serialized date-times are known ASCII strings - unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) }, - ); - } - - res + res.headers_mut().append(key, value.clone()); } -} -impl Future for ServiceResponse -where - F: Future>, - E: Into, - I: Into>, - B: MessageBody, -{ - type Output = (); - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.as_mut().project(); - - match this.state.project() { - ServiceResponseStateProj::ServiceCall(call, send) => { - match ready!(call.poll(cx)) { - 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 stream = match send.send_response(h2_res, size.is_eof()) { - Err(e) => { - trace!("Error sending HTTP/2 response: {:?}", e); - return Poll::Ready(()); - } - Ok(stream) => stream, - }; - - if size.is_eof() { - Poll::Ready(()) - } else { - this.state - .set(ServiceResponseState::SendPayload(stream, body)); - self.poll(cx) - } - } - - 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 HTTP/2 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) - } - } - } - } - - ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => { - loop { - loop { - match this.buffer { - Some(ref mut buffer) => { - match ready!(stream.poll_capacity(cx)) { - None => return Poll::Ready(()), - - Some(Ok(cap)) => { - let len = buffer.len(); - let bytes = buffer.split_to(cmp::min(cap, len)); - - if let Err(e) = stream.send_data(bytes, false) { - warn!("{:?}", e); - return Poll::Ready(()); - } else if !buffer.is_empty() { - let cap = cmp::min(buffer.len(), CHUNK_SIZE); - stream.reserve_capacity(cap); - } else { - this.buffer.take(); - } - } - - Some(Err(e)) => { - warn!("{:?}", e); - return Poll::Ready(()); - } - } - } - - None => match ready!(body.as_mut().poll_next(cx)) { - None => { - if let Err(e) = stream.send_data(Bytes::new(), true) - { - warn!("{:?}", e); - } - return Poll::Ready(()); - } - - Some(Ok(chunk)) => { - stream.reserve_capacity(cmp::min( - chunk.len(), - CHUNK_SIZE, - )); - *this.buffer = Some(chunk); - } - - Some(Err(e)) => { - error!("Response payload stream error: {:?}", e); - return Poll::Ready(()); - } - }, - } - } - } - } - } + // set date header + if !has_date { + let mut bytes = BytesMut::with_capacity(29); + config.set_date_header(&mut bytes); + res.headers_mut().insert( + DATE, + // SAFETY: serialized date-times are known ASCII strings + unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) }, + ); } + + res } diff --git a/actix-http/src/h2/mod.rs b/actix-http/src/h2/mod.rs index c05ee609d..25d53403e 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -1,20 +1,30 @@ -//! HTTP/2 implementation. +//! HTTP/2 protocol. use std::{ + future::Future, pin::Pin, task::{Context, Poll}, }; +use actix_codec::{AsyncRead, AsyncWrite}; +use actix_rt::time::Sleep; use bytes::Bytes; use futures_core::{ready, Stream}; -use h2::RecvStream; +use h2::{ + server::{handshake, Connection, Handshake}, + RecvStream, +}; mod dispatcher; mod service; pub use self::dispatcher::Dispatcher; pub use self::service::H2Service; -use crate::error::PayloadError; + +use crate::{ + config::ServiceConfig, + error::{DispatchError, PayloadError}, +}; /// HTTP/2 peer stream. pub struct Payload { @@ -50,3 +60,44 @@ impl Stream for Payload { } } } + +pub(crate) fn handshake_with_timeout( + io: T, + config: &ServiceConfig, +) -> HandshakeWithTimeout +where + T: AsyncRead + AsyncWrite + Unpin, +{ + HandshakeWithTimeout { + handshake: handshake(io), + timer: config.client_timer().map(Box::pin), + } +} + +pub(crate) struct HandshakeWithTimeout { + handshake: Handshake, + timer: Option>>, +} + +impl Future for HandshakeWithTimeout +where + T: AsyncRead + AsyncWrite + Unpin, +{ + type Output = Result<(Connection, Option>>), DispatchError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + match Pin::new(&mut this.handshake).poll(cx)? { + // return the timer on success handshake. It can be re-used for h2 ping-pong. + Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))), + Poll::Pending => match this.timer.as_mut() { + Some(timer) => { + ready!(timer.as_mut().poll(cx)); + Poll::Ready(Err(DispatchError::SlowRequestTimeout)) + } + None => Poll::Pending, + }, + } + } +} diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 36f7dc311..8a9061b94 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -1,31 +1,31 @@ -use std::cell::RefCell; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{net, rc::Rc}; +use std::{ + future::Future, + marker::PhantomData, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite}; use actix_rt::net::TcpStream; use actix_service::{ - fn_factory, fn_service, pipeline_factory, IntoServiceFactory, Service, - ServiceFactory, + fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, + ServiceFactoryExt as _, }; -use bytes::Bytes; -use futures_core::ready; -use futures_util::future::ok; -use h2::server::{self, Handshake}; +use actix_utils::future::ready; +use futures_core::{future::LocalBoxFuture, ready}; use log::error; -use crate::body::MessageBody; -use crate::config::ServiceConfig; -use crate::error::{DispatchError, Error}; -use crate::request::Request; -use crate::response::Response; -use crate::service::HttpFlow; -use crate::{ConnectCallback, OnConnectData}; +use crate::{ + body::{BoxBody, MessageBody}, + config::ServiceConfig, + error::DispatchError, + service::HttpFlow, + ConnectCallback, OnConnectData, Request, Response, +}; -use super::dispatcher::Dispatcher; +use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout}; /// `ServiceFactory` implementation for HTTP/2 transport pub struct H2Service { @@ -38,9 +38,10 @@ pub struct H2Service { impl H2Service where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, { /// Create new `H2Service` instance with config. @@ -66,9 +67,11 @@ where impl H2Service where S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, { /// Create plain TCP based service @@ -81,33 +84,40 @@ where Error = DispatchError, InitError = S::InitError, > { - pipeline_factory(fn_factory(|| async { - Ok::<_, S::InitError>(fn_service(|io: TcpStream| { + fn_factory(|| { + ready(Ok::<_, S::InitError>(fn_service(|io: TcpStream| { let peer_addr = io.peer_addr().ok(); - ok::<_, DispatchError>((io, peer_addr)) - })) - })) + ready(Ok::<_, DispatchError>((io, peer_addr))) + }))) + }) .and_then(self) } } #[cfg(feature = "openssl")] mod openssl { - use actix_service::{fn_factory, fn_service, ServiceFactoryExt}; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; - use actix_tls::accept::TlsError; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + openssl::{ + reexports::{Error as SslError, SslAcceptor}, + Acceptor, TlsStream, + }, + TlsError, + }; use super::*; - impl H2Service, S, B> + impl H2Service, S, B> where S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, { - /// Create OpenSSL based service + /// Create OpenSSL based service. pub fn openssl( self, acceptor: SslAcceptor, @@ -118,39 +128,43 @@ mod openssl { Error = TlsError, InitError = S::InitError, > { - pipeline_factory( - Acceptor::new(acceptor) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()), - ) - .and_then(fn_factory(|| { - ok::<_, S::InitError>(fn_service(|io: SslStream| { + Acceptor::new(acceptor) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { let peer_addr = io.get_ref().peer_addr().ok(); - ok((io, peer_addr)) - })) - })) - .and_then(self.map_err(TlsError::Service)) + (io, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) } } } #[cfg(feature = "rustls")] mod rustls { - use super::*; - use actix_service::ServiceFactoryExt; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; - use actix_tls::accept::TlsError; use std::io; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + impl H2Service, S, B> where S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, { - /// Create Rustls based service + /// Create Rustls based service. pub fn rustls( self, mut config: ServerConfig, @@ -161,32 +175,34 @@ mod rustls { Error = TlsError, InitError = S::InitError, > { - let protos = vec!["h2".to_string().into()]; - config.set_protocols(&protos); + let mut protos = vec![b"h2".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); + config.alpn_protocols = protos; - pipeline_factory( - Acceptor::new(config) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()), - ) - .and_then(fn_factory(|| { - ok::<_, S::InitError>(fn_service(|io: TlsStream| { + Acceptor::new(config) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { let peer_addr = io.get_ref().0.peer_addr().ok(); - ok((io, peer_addr)) - })) - })) - .and_then(self.map_err(TlsError::Service)) + (io, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) } } } impl ServiceFactory<(T, Option)> for H2Service where - T: AsyncRead + AsyncWrite + Unpin, + T: AsyncRead + AsyncWrite + Unpin + 'static, + S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, { type Response = (); @@ -194,62 +210,26 @@ where type Config = (); type Service = H2ServiceHandler; type InitError = S::InitError; - type Future = H2ServiceResponse; + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - H2ServiceResponse { - fut: self.srv.new_service(()), - cfg: Some(self.cfg.clone()), - on_connect_ext: self.on_connect_ext.clone(), - _phantom: PhantomData, - } - } -} + let service = self.srv.new_service(()); + let cfg = self.cfg.clone(); + let on_connect_ext = self.on_connect_ext.clone(); -#[doc(hidden)] -#[pin_project::pin_project] -pub struct H2ServiceResponse -where - S: ServiceFactory, -{ - #[pin] - fut: S::Future, - cfg: Option, - on_connect_ext: Option>>, - _phantom: PhantomData, -} - -impl Future for H2ServiceResponse -where - T: AsyncRead + AsyncWrite + Unpin, - S: ServiceFactory, - S::Error: Into + 'static, - S::Response: Into> + 'static, - >::Future: 'static, - B: MessageBody + 'static, -{ - type Output = Result, S::InitError>; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.as_mut().project(); - - this.fut.poll(cx).map_ok(|service| { - let this = self.as_mut().project(); - H2ServiceHandler::new( - this.cfg.take().unwrap(), - this.on_connect_ext.clone(), - service, - ) + Box::pin(async move { + let service = service.await?; + Ok(H2ServiceHandler::new(cfg, on_connect_ext, service)) }) } } -/// `Service` implementation for http/2 transport +/// `Service` implementation for HTTP/2 transport pub struct H2ServiceHandler where S: Service, { - flow: Rc>>, + flow: Rc>, cfg: ServiceConfig, on_connect_ext: Option>>, _phantom: PhantomData, @@ -258,7 +238,7 @@ where impl H2ServiceHandler where S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -281,7 +261,7 @@ impl Service<(T, Option)> for H2ServiceHandler, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -290,15 +270,15 @@ where type Error = DispatchError; type Future = H2ServiceHandlerResponse; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.flow.borrow_mut().service.poll_ready(cx).map_err(|e| { + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + self.flow.service.poll_ready(cx).map_err(|e| { let e = e.into(); error!("Service readiness error: {:?}", e); DispatchError::Service(e) }) } - fn call(&mut self, (io, addr): (T, Option)) -> Self::Future { + fn call(&self, (io, addr): (T, Option)) -> Self::Future { let on_connect_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); @@ -308,7 +288,7 @@ where Some(self.cfg.clone()), addr, on_connect_data, - server::handshake(io), + handshake_with_timeout(io, &self.cfg), ), } } @@ -321,11 +301,11 @@ where { Incoming(Dispatcher), Handshake( - Option>>>, + Option>>, Option, Option, OnConnectData, - Handshake, + HandshakeWithTimeout, ), } @@ -333,7 +313,7 @@ pub struct H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody + 'static, @@ -345,7 +325,7 @@ impl Future for H2ServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, B: MessageBody, @@ -362,21 +342,21 @@ where ref mut on_connect_data, ref mut handshake, ) => match ready!(Pin::new(handshake).poll(cx)) { - Ok(conn) => { + Ok((conn, timer)) => { let on_connect_data = std::mem::take(on_connect_data); self.state = State::Incoming(Dispatcher::new( srv.take().unwrap(), conn, on_connect_data, config.take().unwrap(), - None, *peer_addr, + timer, )); self.poll(cx) } Err(err) => { trace!("H2 handshake error: {}", err); - Poll::Ready(Err(err.into())) + Poll::Ready(Err(err)) } }, } diff --git a/actix-http/src/header/as_name.rs b/actix-http/src/header/as_name.rs new file mode 100644 index 000000000..04d32c41d --- /dev/null +++ b/actix-http/src/header/as_name.rs @@ -0,0 +1,51 @@ +//! Sealed [`AsHeaderName`] trait and implementations. + +use std::{borrow::Cow, str::FromStr as _}; + +use http::header::{HeaderName, InvalidHeaderName}; + +/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`]. +/// +/// [`HeaderValue`]: crate::http::HeaderValue +pub trait AsHeaderName: Sealed {} + +pub struct Seal; + +pub trait Sealed { + fn try_as_name(&self, seal: Seal) -> Result, InvalidHeaderName>; +} + +impl Sealed for HeaderName { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { + Ok(Cow::Borrowed(self)) + } +} +impl AsHeaderName for HeaderName {} + +impl Sealed for &HeaderName { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { + Ok(Cow::Borrowed(*self)) + } +} +impl AsHeaderName for &HeaderName {} + +impl Sealed for &str { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { + HeaderName::from_str(self).map(Cow::Owned) + } +} +impl AsHeaderName for &str {} + +impl Sealed for String { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { + HeaderName::from_str(self).map(Cow::Owned) + } +} +impl AsHeaderName for String {} + +impl Sealed for &String { + fn try_as_name(&self, _: Seal) -> Result, InvalidHeaderName> { + HeaderName::from_str(self).map(Cow::Owned) + } +} +impl AsHeaderName for &String {} diff --git a/actix-http/src/header/common/accept_language.rs b/actix-http/src/header/common/accept_language.rs deleted file mode 100644 index a7ad00863..000000000 --- a/actix-http/src/header/common/accept_language.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::header::{QualityItem, ACCEPT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Accept-Language` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) - /// - /// The `Accept-Language` header field can be used by user agents to - /// indicate the set of natural languages that are preferred in the - /// response. - /// - /// # ABNF - /// - /// ```text - /// Accept-Language = 1#( language-range [ weight ] ) - /// language-range = - /// ``` - /// - /// # Example values - /// * `da, en-gb;q=0.8, en;q=0.7` - /// * `en-us;q=1.0, en;q=0.5, fr` - /// - /// # Examples - /// - /// ``` - /// use language_tags::langtag; - /// use actix_http::Response; - /// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem}; - /// - /// let mut builder = Response::Ok(); - /// let mut langtag: LanguageTag = Default::default(); - /// langtag.language = Some("en".to_owned()); - /// langtag.region = Some("US".to_owned()); - /// builder.insert_header( - /// AcceptLanguage(vec![ - /// qitem(langtag), - /// ]) - /// ); - /// ``` - /// - /// ``` - /// use language_tags::langtag; - /// use actix_http::Response; - /// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem}; - /// - /// let mut builder = Response::Ok(); - /// builder.insert_header( - /// AcceptLanguage(vec![ - /// qitem(langtag!(da)), - /// QualityItem::new(langtag!(en;;;GB), q(800)), - /// QualityItem::new(langtag!(en), q(700)), - /// ]) - /// ); - /// ``` - (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem)+ - - test_accept_language { - // From the RFC - test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]); - // Own test - test_header!( - test2, vec![b"en-US, en; q=0.5, fr"], - Some(AcceptLanguage(vec![ - qitem("en-US".parse().unwrap()), - QualityItem::new("en".parse().unwrap(), q(500)), - qitem("fr".parse().unwrap()), - ]))); - } -} diff --git a/actix-http/src/header/common/cache_control.rs b/actix-http/src/header/common/cache_control.rs deleted file mode 100644 index 94ce9a750..000000000 --- a/actix-http/src/header/common/cache_control.rs +++ /dev/null @@ -1,262 +0,0 @@ -use std::fmt::{self, Write}; -use std::str::FromStr; - -use http::header; - -use crate::header::{ - fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer, -}; - -/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) -/// -/// The `Cache-Control` header field is used to specify directives for -/// caches along the request/response chain. Such cache directives are -/// unidirectional in that the presence of a directive in a request does -/// not imply that the same directive is to be given in the response. -/// -/// # ABNF -/// -/// ```text -/// Cache-Control = 1#cache-directive -/// cache-directive = token [ "=" ( token / quoted-string ) ] -/// ``` -/// -/// # Example values -/// -/// * `no-cache` -/// * `private, community="UCI"` -/// * `max-age=30` -/// -/// # Examples -/// ``` -/// use actix_http::Response; -/// use actix_http::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = Response::Ok(); -/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); -/// ``` -/// -/// ```rust -/// use actix_http::Response; -/// use actix_http::http::header::{CacheControl, CacheDirective}; -/// -/// let mut builder = Response::Ok(); -/// builder.insert_header(CacheControl(vec![ -/// CacheDirective::NoCache, -/// CacheDirective::Private, -/// CacheDirective::MaxAge(360u32), -/// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), -/// ])); -/// ``` -#[derive(PartialEq, Clone, Debug)] -pub struct CacheControl(pub Vec); - -__hyper__deref!(CacheControl => Vec); - -//TODO: this could just be the header! macro -impl Header for CacheControl { - fn name() -> header::HeaderName { - header::CACHE_CONTROL - } - - #[inline] - fn parse(msg: &T) -> Result - where - T: crate::HttpMessage, - { - let directives = from_comma_delimited(msg.headers().get_all(&Self::name()))?; - if !directives.is_empty() { - Ok(CacheControl(directives)) - } else { - Err(crate::error::ParseError::Header) - } - } -} - -impl fmt::Display for CacheControl { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt_comma_delimited(f, &self[..]) - } -} - -impl IntoHeaderValue for CacheControl { - type Error = header::InvalidHeaderValue; - - fn try_into_value(self) -> Result { - let mut writer = Writer::new(); - let _ = write!(&mut writer, "{}", self); - header::HeaderValue::from_maybe_shared(writer.take()) - } -} - -/// `CacheControl` contains a list of these directives. -#[derive(PartialEq, Clone, Debug)] -pub enum CacheDirective { - /// "no-cache" - NoCache, - /// "no-store" - NoStore, - /// "no-transform" - NoTransform, - /// "only-if-cached" - OnlyIfCached, - - // request directives - /// "max-age=delta" - MaxAge(u32), - /// "max-stale=delta" - MaxStale(u32), - /// "min-fresh=delta" - MinFresh(u32), - - // response directives - /// "must-revalidate" - MustRevalidate, - /// "public" - Public, - /// "private" - Private, - /// "proxy-revalidate" - ProxyRevalidate, - /// "s-maxage=delta" - SMaxAge(u32), - - /// Extension directives. Optionally include an argument. - Extension(String, Option), -} - -impl fmt::Display for CacheDirective { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use self::CacheDirective::*; - fmt::Display::fmt( - match *self { - NoCache => "no-cache", - NoStore => "no-store", - NoTransform => "no-transform", - OnlyIfCached => "only-if-cached", - - MaxAge(secs) => return write!(f, "max-age={}", secs), - MaxStale(secs) => return write!(f, "max-stale={}", secs), - MinFresh(secs) => return write!(f, "min-fresh={}", secs), - - MustRevalidate => "must-revalidate", - Public => "public", - Private => "private", - ProxyRevalidate => "proxy-revalidate", - SMaxAge(secs) => return write!(f, "s-maxage={}", secs), - - Extension(ref name, None) => &name[..], - Extension(ref name, Some(ref arg)) => { - return write!(f, "{}={}", name, arg); - } - }, - f, - ) - } -} - -impl FromStr for CacheDirective { - type Err = Option<::Err>; - fn from_str(s: &str) -> Result::Err>> { - use self::CacheDirective::*; - match s { - "no-cache" => Ok(NoCache), - "no-store" => Ok(NoStore), - "no-transform" => Ok(NoTransform), - "only-if-cached" => Ok(OnlyIfCached), - "must-revalidate" => Ok(MustRevalidate), - "public" => Ok(Public), - "private" => Ok(Private), - "proxy-revalidate" => Ok(ProxyRevalidate), - "" => Err(None), - _ => match s.find('=') { - Some(idx) if idx + 1 < s.len() => { - match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { - ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), - ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), - ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), - ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), - (left, right) => { - Ok(Extension(left.to_owned(), Some(right.to_owned()))) - } - } - } - Some(_) => Err(None), - None => Ok(Extension(s.to_owned(), None)), - }, - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::header::Header; - use crate::test::TestRequest; - - #[test] - fn test_parse_multiple_headers() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "no-cache, private")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::NoCache, - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_argument() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "max-age=100, private")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::MaxAge(100), - CacheDirective::Private, - ])) - ) - } - - #[test] - fn test_parse_quote_form() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "max-age=\"200\"")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![CacheDirective::MaxAge(200)])) - ) - } - - #[test] - fn test_parse_extension() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "foo, bar=baz")) - .finish(); - let cache = Header::parse(&req); - assert_eq!( - cache.ok(), - Some(CacheControl(vec![ - CacheDirective::Extension("foo".to_owned(), None), - CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), - ])) - ) - } - - #[test] - fn test_parse_bad_syntax() { - let req = TestRequest::default() - .insert_header((header::CACHE_CONTROL, "foo=")) - .finish(); - let cache: Result = Header::parse(&req); - assert_eq!(cache.ok(), None) - } -} diff --git a/actix-http/src/header/common/content_language.rs b/actix-http/src/header/common/content_language.rs deleted file mode 100644 index e9be67a1b..000000000 --- a/actix-http/src/header/common/content_language.rs +++ /dev/null @@ -1,58 +0,0 @@ -use crate::header::{QualityItem, CONTENT_LANGUAGE}; -use language_tags::LanguageTag; - -header! { - /// `Content-Language` header, defined in - /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) - /// - /// The `Content-Language` header field describes the natural language(s) - /// of the intended audience for the representation. Note that this - /// might not be equivalent to all the languages used within the - /// representation. - /// - /// # ABNF - /// - /// ```text - /// Content-Language = 1#language-tag - /// ``` - /// - /// # Example values - /// - /// * `da` - /// * `mi, en` - /// - /// # Examples - /// - /// ``` - /// use language_tags::langtag; - /// use actix_http::Response; - /// use actix_http::http::header::{ContentLanguage, qitem}; - /// - /// let mut builder = Response::Ok(); - /// builder.insert_header( - /// ContentLanguage(vec![ - /// qitem(langtag!(en)), - /// ]) - /// ); - /// ``` - /// - /// ``` - /// use language_tags::langtag; - /// use actix_http::Response; - /// use actix_http::http::header::{ContentLanguage, qitem}; - /// - /// let mut builder = Response::Ok(); - /// builder.insert_header( - /// ContentLanguage(vec![ - /// qitem(langtag!(da)), - /// qitem(langtag!(en;;;GB)), - /// ]) - /// ); - /// ``` - (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ - - test_content_language { - test_header!(test1, vec![b"da"]); - test_header!(test2, vec![b"mi, en"]); - } -} diff --git a/actix-http/src/header/common/mod.rs b/actix-http/src/header/common/mod.rs deleted file mode 100644 index 90e0a855e..000000000 --- a/actix-http/src/header/common/mod.rs +++ /dev/null @@ -1,355 +0,0 @@ -//! A Collection of Header implementations for common HTTP Headers. -//! -//! ## Mime -//! -//! Several header fields use MIME values for their contents. Keeping with the -//! strongly-typed theme, the [mime] crate -//! is used, such as `ContentType(pub Mime)`. -#![cfg_attr(rustfmt, rustfmt_skip)] - -pub use self::accept_charset::AcceptCharset; -//pub use self::accept_encoding::AcceptEncoding; -pub use self::accept::Accept; -pub use self::accept_language::AcceptLanguage; -pub use self::allow::Allow; -pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ - ContentDisposition, DispositionParam, DispositionType, -}; -pub use self::content_language::ContentLanguage; -pub use self::content_range::{ContentRange, ContentRangeSpec}; -pub use self::content_encoding::{ContentEncoding}; -pub use self::content_type::ContentType; -pub use self::date::Date; -pub use self::etag::ETag; -pub use self::expires::Expires; -pub use self::if_match::IfMatch; -pub use self::if_modified_since::IfModifiedSince; -pub use self::if_none_match::IfNoneMatch; -pub use self::if_range::IfRange; -pub use self::if_unmodified_since::IfUnmodifiedSince; -pub use self::last_modified::LastModified; -//pub use self::range::{Range, ByteRangeSpec}; - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__deref { - ($from:ty => $to:ty) => { - impl ::std::ops::Deref for $from { - type Target = $to; - - #[inline] - fn deref(&self) -> &$to { - &self.0 - } - } - - impl ::std::ops::DerefMut for $from { - #[inline] - fn deref_mut(&mut self) -> &mut $to { - &mut self.0 - } - } - }; -} - -#[doc(hidden)] -#[macro_export] -macro_rules! __hyper__tm { - ($id:ident, $tm:ident{$($tf:item)*}) => { - #[allow(unused_imports)] - #[cfg(test)] - mod $tm{ - use std::str; - use http::Method; - use mime::*; - use $crate::header::*; - use super::$id as HeaderField; - $($tf)* - } - - } -} - -#[doc(hidden)] -#[macro_export] -macro_rules! test_header { - ($id:ident, $raw:expr) => { - #[test] - fn $id() { - use super::*; - use $crate::test; - - let raw = $raw; - let a: Vec> = raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req = req.insert_header((HeaderField::name(), item)).take(); - } - let req = req.finish(); - let value = HeaderField::parse(&req); - let result = format!("{}", value.unwrap()); - let expected = String::from_utf8(raw[0].to_vec()).unwrap(); - let result_cmp: Vec = result - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - let expected_cmp: Vec = expected - .to_ascii_lowercase() - .split(' ') - .map(|x| x.to_owned()) - .collect(); - assert_eq!(result_cmp.concat(), expected_cmp.concat()); - } - }; - ($id:ident, $raw:expr, $typed:expr) => { - #[test] - fn $id() { - use $crate::test; - - let a: Vec> = $raw.iter().map(|x| x.to_vec()).collect(); - let mut req = test::TestRequest::default(); - for item in a { - req.insert_header((HeaderField::name(), item)); - } - let req = req.finish(); - let val = HeaderField::parse(&req); - let typed: Option = $typed; - // Test parsing - assert_eq!(val.ok(), typed); - // Test formatting - if typed.is_some() { - let raw = &($raw)[..]; - let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); - let mut joined = String::new(); - joined.push_str(iter.next().unwrap()); - for s in iter { - joined.push_str(", "); - joined.push_str(s); - } - assert_eq!(format!("{}", typed.unwrap()), joined); - } - } - }; -} - -#[macro_export] -macro_rules! header { - // $a:meta: Attributes associated with the header item (usually docs) - // $id:ident: Identifier of the header - // $n:expr: Lowercase name of the header - // $nn:expr: Nice name of the header - - // List header, zero or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)*) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::http::header::Header for $id { - #[inline] - fn name() -> $crate::http::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> ::std::fmt::Result { - $crate::http::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::http::header::IntoHeaderValue for $id { - type Error = $crate::http::header::InvalidHeaderValue; - - fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::http::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) - } - } - }; - // List header, one or more items - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)+) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub Vec<$item>); - __hyper__deref!($id => Vec<$item>); - impl $crate::http::header::Header for $id { - #[inline] - fn name() -> $crate::http::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name())).map($id) - } - } - impl std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - $crate::http::header::fmt_comma_delimited(f, &self.0[..]) - } - } - impl $crate::http::header::IntoHeaderValue for $id { - type Error = $crate::http::header::InvalidHeaderValue; - - fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::http::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) - } - } - }; - // Single value header - ($(#[$a:meta])*($id:ident, $name:expr) => [$value:ty]) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub struct $id(pub $value); - __hyper__deref!($id => $value); - impl $crate::http::header::Header for $id { - #[inline] - fn name() -> $crate::http::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - $crate::http::header::from_one_raw_str( - msg.headers().get(Self::name())).map($id) - } - } - impl std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - std::fmt::Display::fmt(&self.0, f) - } - } - impl $crate::http::header::IntoHeaderValue for $id { - type Error = $crate::http::header::InvalidHeaderValue; - - fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - self.0.try_into_value() - } - } - }; - // List header, one or more items with "*" option - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { - $(#[$a])* - #[derive(Clone, Debug, PartialEq)] - pub enum $id { - /// Any value is a match - Any, - /// Only the listed items are a match - Items(Vec<$item>), - } - impl $crate::http::header::Header for $id { - #[inline] - fn name() -> $crate::http::header::HeaderName { - $name - } - #[inline] - fn parse(msg: &T) -> Result - where T: $crate::HttpMessage - { - let any = msg.headers().get(Self::name()).and_then(|hdr| { - hdr.to_str().ok().and_then(|hdr| Some(hdr.trim() == "*"))}); - - if let Some(true) = any { - Ok($id::Any) - } else { - Ok($id::Items( - $crate::http::header::from_comma_delimited( - msg.headers().get_all(Self::name()))?)) - } - } - } - impl std::fmt::Display for $id { - #[inline] - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match *self { - $id::Any => f.write_str("*"), - $id::Items(ref fields) => $crate::http::header::fmt_comma_delimited( - f, &fields[..]) - } - } - } - impl $crate::http::header::IntoHeaderValue for $id { - type Error = $crate::http::header::InvalidHeaderValue; - - fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { - use std::fmt::Write; - let mut writer = $crate::http::header::Writer::new(); - let _ = write!(&mut writer, "{}", self); - $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) - } - } - }; - - // optional test module - ($(#[$a:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => ($item)* - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $n) => ($item)+ - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* ($id, $name) => [$item] - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; - ($(#[$a:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { - header! { - $(#[$a])* - ($id, $name) => {Any / ($item)+} - } - - __hyper__tm! { $id, $tm { $($tf)* }} - }; -} - -mod accept_charset; -// mod accept_encoding; -mod accept; -mod accept_language; -mod allow; -mod cache_control; -mod content_disposition; -mod content_language; -mod content_encoding; -mod content_range; -mod content_type; -mod date; -mod etag; -mod expires; -mod if_match; -mod if_modified_since; -mod if_none_match; -mod if_range; -mod if_unmodified_since; -mod last_modified; diff --git a/actix-http/src/header/into_pair.rs b/actix-http/src/header/into_pair.rs index d0d6e7324..472700548 100644 --- a/actix-http/src/header/into_pair.rs +++ b/actix-http/src/header/into_pair.rs @@ -1,4 +1,6 @@ -use std::convert::TryFrom; +//! [`IntoHeaderPair`] trait and implementations. + +use std::convert::TryFrom as _; use http::{ header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, @@ -7,7 +9,10 @@ use http::{ use super::{Header, IntoHeaderValue}; -/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s. +/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for +/// insertion into a [`HeaderMap`]. +/// +/// [`HeaderMap`]: crate::http::HeaderMap pub trait IntoHeaderPair: Sized { type Error: Into; diff --git a/actix-http/src/header/into_value.rs b/actix-http/src/header/into_value.rs index 4b1e6cbbd..bad05db64 100644 --- a/actix-http/src/header/into_value.rs +++ b/actix-http/src/header/into_value.rs @@ -1,10 +1,12 @@ -use std::convert::TryFrom; +//! [`IntoHeaderValue`] trait and implementations. + +use std::convert::TryFrom as _; use bytes::Bytes; use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use mime::Mime; -/// A trait for any object that can be Converted to a `HeaderValue` +/// An interface for types that can be converted into a [`HeaderValue`]. pub trait IntoHeaderValue: Sized { /// The type returned in the event of a conversion error. type Error: Into; @@ -126,6 +128,6 @@ impl IntoHeaderValue for Mime { #[inline] fn try_into_value(self) -> Result { - HeaderValue::try_from(format!("{}", self)) + HeaderValue::from_str(self.as_ref()) } } diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index 8f20f3e6f..dd852b021 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -1,332 +1,595 @@ -use std::{ - collections::hash_map::{self, Entry}, - convert::TryFrom, -}; +//! A multi-value [`HeaderMap`] and its iterators. + +use std::{borrow::Cow, collections::hash_map, iter, ops}; use ahash::AHashMap; -use either::Either; use http::header::{HeaderName, HeaderValue}; use smallvec::{smallvec, SmallVec}; +use crate::header::AsHeaderName; + /// A multi-map of HTTP headers. /// -/// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more values. +/// `HeaderMap` is a "multi-map" of [`HeaderName`] to one or more [`HeaderValue`]s. +/// +/// # Examples +/// ``` +/// use actix_http::http::{header, HeaderMap, HeaderValue}; +/// +/// let mut map = HeaderMap::new(); +/// +/// map.insert(header::CONTENT_TYPE, HeaderValue::from_static("text/plain")); +/// map.insert(header::ORIGIN, HeaderValue::from_static("example.com")); +/// +/// assert!(map.contains_key(header::CONTENT_TYPE)); +/// assert!(map.contains_key(header::ORIGIN)); +/// +/// let mut removed = map.remove(header::ORIGIN); +/// assert_eq!(removed.next().unwrap(), "example.com"); +/// +/// assert!(!map.contains_key(header::ORIGIN)); +/// ``` #[derive(Debug, Clone, Default)] pub struct HeaderMap { pub(crate) inner: AHashMap, } +/// A bespoke non-empty list for HeaderMap values. #[derive(Debug, Clone)] -pub(crate) enum Value { - One(HeaderValue), - Multi(SmallVec<[HeaderValue; 4]>), +pub(crate) struct Value { + inner: SmallVec<[HeaderValue; 4]>, } impl Value { - fn first(&self) -> &HeaderValue { - match self { - Value::One(ref val) => val, - Value::Multi(ref val) => &val[0], + fn one(val: HeaderValue) -> Self { + Self { + inner: smallvec![val], } } + fn first(&self) -> &HeaderValue { + &self.inner[0] + } + fn first_mut(&mut self) -> &mut HeaderValue { - match self { - Value::One(ref mut val) => val, - Value::Multi(ref mut val) => &mut val[0], - } + &mut self.inner[0] } - fn append(&mut self, val: HeaderValue) { - match self { - Value::One(_) => { - let data = std::mem::replace(self, Value::Multi(smallvec![val])); - match data { - Value::One(val) => self.append(val), - Value::Multi(_) => unreachable!(), - } - } - Value::Multi(ref mut vec) => vec.push(val), - } + fn append(&mut self, new_val: HeaderValue) { + self.inner.push(new_val) + } +} + +impl ops::Deref for Value { + type Target = SmallVec<[HeaderValue; 4]>; + + fn deref(&self) -> &Self::Target { + &self.inner } } impl HeaderMap { /// Create an empty `HeaderMap`. /// - /// The map will be created without any capacity. This function will not - /// allocate. + /// The map will be created without any capacity; this function will not allocate. + /// + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let map = HeaderMap::new(); + /// + /// assert!(map.is_empty()); + /// assert_eq!(0, map.capacity()); + /// ``` pub fn new() -> Self { - HeaderMap { - inner: AHashMap::default(), - } + HeaderMap::default() } /// Create an empty `HeaderMap` with the specified capacity. /// - /// The returned map will allocate internal storage in order to hold about - /// `capacity` elements without reallocating. However, this is a "best - /// effort" as there are usage patterns that could cause additional - /// allocations before `capacity` headers are stored in the map. + /// The map will be able to hold at least `capacity` elements without needing to reallocate. + /// If `capacity` is 0, the map will be created without allocating. /// - /// More capacity than requested may be allocated. - pub fn with_capacity(capacity: usize) -> HeaderMap { + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let map = HeaderMap::with_capacity(16); + /// + /// assert!(map.is_empty()); + /// assert!(map.capacity() >= 16); + /// ``` + pub fn with_capacity(capacity: usize) -> Self { HeaderMap { - inner: AHashMap::with_capacity_and_hasher(capacity, Default::default()), + inner: AHashMap::with_capacity(capacity), } } - /// Returns the number of keys stored in the map. + /// Create new `HeaderMap` from a `http::HeaderMap`-like drain. + pub(crate) fn from_drain(mut drain: I) -> Self + where + I: Iterator, HeaderValue)>, + { + let (first_name, first_value) = match drain.next() { + None => return HeaderMap::new(), + Some((name, val)) => { + let name = name.expect("drained first item had no name"); + (name, val) + } + }; + + let (lb, ub) = drain.size_hint(); + let capacity = ub.unwrap_or(lb); + + let mut map = HeaderMap::with_capacity(capacity); + map.append(first_name.clone(), first_value); + + let (map, _) = + drain.fold((map, first_name), |(mut map, prev_name), (name, value)| { + let name = name.unwrap_or(prev_name); + map.append(name.clone(), value); + (map, name) + }); + + map + } + + /// Returns the number of values stored in the map. /// - /// This number could be be less than or equal to actual headers stored in - /// the map. + /// See also: [`len_keys`](Self::len_keys). + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert_eq!(map.len(), 0); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// assert_eq!(map.len(), 2); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// assert_eq!(map.len(), 3); + /// ``` pub fn len(&self) -> usize { + self.inner + .iter() + .fold(0, |acc, (_, values)| acc + values.len()) + } + + /// Returns the number of _keys_ stored in the map. + /// + /// The number of values stored will be at least this number. See also: [`Self::len`]. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert_eq!(map.len_keys(), 0); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// assert_eq!(map.len_keys(), 2); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// assert_eq!(map.len_keys(), 2); + /// ``` + pub fn len_keys(&self) -> usize { self.inner.len() } /// Returns true if the map contains no elements. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert!(map.is_empty()); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(!map.is_empty()); + /// ``` pub fn is_empty(&self) -> bool { self.inner.len() == 0 } - /// Clears the map, removing all key-value pairs. Keeps the allocated memory - /// for reuse. + /// Clears the map, removing all name-value pairs. + /// + /// Keeps the allocated memory for reuse. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// assert_eq!(map.len(), 2); + /// + /// map.clear(); + /// assert!(map.is_empty()); + /// ``` pub fn clear(&mut self) { self.inner.clear(); } - /// Returns the number of headers the map can hold without reallocating. + fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> { + match key.try_as_name(super::as_name::Seal).ok()? { + Cow::Borrowed(name) => self.inner.get(name), + Cow::Owned(name) => self.inner.get(&name), + } + } + + /// Returns a reference to the _first_ value associated with a header name. /// - /// This number is an approximation as certain usage patterns could cause - /// additional allocations before the returned capacity is filled. + /// Returns `None` if there is no value associated with the key. + /// + /// Even when multiple values are associated with the key, the "first" one is returned but is + /// not guaranteed to be chosen with any particular order; though, the returned item will be + /// consistent for each call to `get` if the map has not changed. + /// + /// See also: [`get_all`](Self::get_all). + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// + /// let cookie = map.get(header::SET_COOKIE).unwrap(); + /// assert_eq!(cookie, "one=1"); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// assert_eq!(map.get(header::SET_COOKIE).unwrap(), "one=1"); + /// + /// assert_eq!(map.get(header::SET_COOKIE), map.get("set-cookie")); + /// assert_eq!(map.get(header::SET_COOKIE), map.get("Set-Cookie")); + /// + /// assert!(map.get(header::HOST).is_none()); + /// assert!(map.get("INVALID HEADER NAME").is_none()); + /// ``` + pub fn get(&self, key: impl AsHeaderName) -> Option<&HeaderValue> { + self.get_value(key).map(Value::first) + } + + /// Returns a mutable reference to the _first_ value associated a header name. + /// + /// Returns `None` if there is no value associated with the key. + /// + /// Even when multiple values are associated with the key, the "first" one is returned but is + /// not guaranteed to be chosen with any particular order; though, the returned item will be + /// consistent for each call to `get_mut` if the map has not changed. + /// + /// See also: [`get_all`](Self::get_all). + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// + /// let mut cookie = map.get_mut(header::SET_COOKIE).unwrap(); + /// assert_eq!(cookie, "one=1"); + /// + /// *cookie = HeaderValue::from_static("three=3"); + /// assert_eq!(map.get(header::SET_COOKIE).unwrap(), "three=3"); + /// + /// assert!(map.get(header::HOST).is_none()); + /// assert!(map.get("INVALID HEADER NAME").is_none()); + /// ``` + pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> { + match key.try_as_name(super::as_name::Seal).ok()? { + Cow::Borrowed(name) => self.inner.get_mut(name).map(Value::first_mut), + Cow::Owned(name) => self.inner.get_mut(&name).map(Value::first_mut), + } + } + + /// Returns an iterator over all values associated with a header name. + /// + /// The returned iterator does not incur any allocations and will yield no items if there are no + /// values associated with the key. Iteration order is guaranteed to be the same as + /// insertion order. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let mut none_iter = map.get_all(header::ORIGIN); + /// assert!(none_iter.next().is_none()); + /// + /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// + /// let mut set_cookies_iter = map.get_all(header::SET_COOKIE); + /// assert_eq!(set_cookies_iter.next().unwrap(), "one=1"); + /// assert_eq!(set_cookies_iter.next().unwrap(), "two=2"); + /// assert!(set_cookies_iter.next().is_none()); + /// ``` + pub fn get_all(&self, key: impl AsHeaderName) -> GetAll<'_> { + GetAll::new(self.get_value(key)) + } + + // TODO: get_all_mut ? + + /// Returns `true` if the map contains a value for the specified key. + /// + /// Invalid header names will simply return false. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// assert!(!map.contains_key(header::ACCEPT)); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(map.contains_key(header::ACCEPT)); + /// ``` + pub fn contains_key(&self, key: impl AsHeaderName) -> bool { + match key.try_as_name(super::as_name::Seal) { + Ok(Cow::Borrowed(name)) => self.inner.contains_key(name), + Ok(Cow::Owned(name)) => self.inner.contains_key(&name), + Err(_) => false, + } + } + + /// Inserts a name-value pair into the map. + /// + /// If the map already contained this key, the new value is associated with the key and all + /// previous values are removed and returned as a `Removed` iterator. The key is not updated; + /// this matters for types that can be `==` without being identical. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(map.contains_key(header::ACCEPT)); + /// assert_eq!(map.len(), 1); + /// + /// let mut removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/csv")); + /// assert_eq!(removed.next().unwrap(), "text/plain"); + /// assert!(removed.next().is_none()); + /// + /// assert_eq!(map.len(), 1); + /// ``` + /// + /// A convenience method is provided on the returned iterator to check if the insertion replaced + /// any values. + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); + /// assert!(removed.is_empty()); + /// + /// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html")); + /// assert!(!removed.is_empty()); + /// ``` + pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed { + let value = self.inner.insert(key, Value::one(val)); + Removed::new(value) + } + + /// Inserts a name-value pair into the map. + /// + /// If the map already contained this key, the new value is added to the list of values + /// currently associated with the key. The key is not updated; this matters for types that can + /// be `==` without being identical. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.append(header::HOST, HeaderValue::from_static("example.com")); + /// assert_eq!(map.len(), 1); + /// + /// map.append(header::ACCEPT, HeaderValue::from_static("text/csv")); + /// assert_eq!(map.len(), 2); + /// + /// map.append(header::ACCEPT, HeaderValue::from_static("text/html")); + /// assert_eq!(map.len(), 3); + /// ``` + pub fn append(&mut self, key: HeaderName, value: HeaderValue) { + match self.inner.entry(key) { + hash_map::Entry::Occupied(mut entry) => { + entry.get_mut().append(value); + } + hash_map::Entry::Vacant(entry) => { + entry.insert(Value::one(value)); + } + }; + } + + /// Removes all headers for a particular header name from the map. + /// + /// Providing an invalid header names (as a string argument) will have no effect and return + /// without error. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=2")); + /// + /// assert_eq!(map.len(), 2); + /// + /// let mut removed = map.remove(header::SET_COOKIE); + /// assert_eq!(removed.next().unwrap(), "one=1"); + /// assert_eq!(removed.next().unwrap(), "one=2"); + /// assert!(removed.next().is_none()); + /// + /// assert!(map.is_empty()); + /// ``` + /// + /// A convenience method is provided on the returned iterator to check if the `remove` call + /// actually removed any values. + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let removed = map.remove("accept"); + /// assert!(removed.is_empty()); + /// + /// map.insert(header::ACCEPT, HeaderValue::from_static("text/html")); + /// let removed = map.remove("accept"); + /// assert!(!removed.is_empty()); + /// ``` + pub fn remove(&mut self, key: impl AsHeaderName) -> Removed { + let value = match key.try_as_name(super::as_name::Seal) { + Ok(Cow::Borrowed(name)) => self.inner.remove(name), + Ok(Cow::Owned(name)) => self.inner.remove(&name), + Err(_) => None, + }; + + Removed::new(value) + } + + /// Returns the number of single-value headers the map can hold without needing to reallocate. + /// + /// Since this is a multi-value map, the actual capacity is much larger when considering + /// each header name can be associated with an arbitrary number of values. The effect is that + /// the size of `len` may be greater than `capacity` since it counts all the values. + /// Conversely, [`len_keys`](Self::len_keys) will never be larger than capacity. + /// + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let map = HeaderMap::with_capacity(16); + /// + /// assert!(map.is_empty()); + /// assert!(map.capacity() >= 16); + /// ``` pub fn capacity(&self) -> usize { self.inner.capacity() } - /// Reserves capacity for at least `additional` more headers to be inserted - /// into the `HeaderMap`. + /// Reserves capacity for at least `additional` more headers to be inserted in the map. /// - /// The header map may reserve more space to avoid frequent reallocations. - /// Like with `with_capacity`, this will be a "best effort" to avoid - /// allocations until `additional` more headers are inserted. Certain usage - /// patterns could cause additional allocations before the number is - /// reached. + /// The header map may reserve more space to avoid frequent reallocations. Additional capacity + /// only considers single-value headers. + /// + /// # Panics + /// Panics if the new allocation size overflows usize. + /// + /// # Examples + /// ``` + /// # use actix_http::http::HeaderMap; + /// let mut map = HeaderMap::with_capacity(2); + /// assert!(map.capacity() >= 2); + /// + /// map.reserve(100); + /// assert!(map.capacity() >= 102); + /// + /// assert!(map.is_empty()); + /// ``` pub fn reserve(&mut self, additional: usize) { self.inner.reserve(additional) } - /// Returns a reference to the value associated with the key. + /// An iterator over all name-value pairs. /// - /// If there are multiple values associated with the key, then the first one - /// is returned. Use `get_all` to get all values associated with a given - /// key. Returns `None` if there are no values associated with the key. - pub fn get(&self, name: N) -> Option<&HeaderValue> { - self.get2(name).map(|v| v.first()) - } - - fn get2(&self, name: N) -> Option<&Value> { - match name.as_name() { - Either::Left(name) => self.inner.get(name), - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - self.inner.get(&name) - } else { - None - } - } - } - } - - /// Returns an iterator of all values associated with a key. + /// Names will be yielded for each associated value. So, if a key has 3 associated values, it + /// will be yielded 3 times. The iteration order should be considered arbitrary. /// - /// The returned view does not incur any allocations and allows iterating the values associated - /// with the key. Returns `None` if there are no values associated with the key. Iteration order - /// is not guaranteed to be the same as insertion order. - pub fn get_all(&self, name: N) -> GetAll<'_> { - GetAll { - idx: 0, - item: self.get2(name), - } - } - - /// Returns a mutable reference to the value associated with the key. + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); /// - /// If there are multiple values associated with the key, then the first one - /// is returned. Use `entry` to get all values associated with a given - /// key. Returns `None` if there are no values associated with the key. - pub fn get_mut(&mut self, name: N) -> Option<&mut HeaderValue> { - match name.as_name() { - Either::Left(name) => self.inner.get_mut(name).map(|v| v.first_mut()), - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - self.inner.get_mut(&name).map(|v| v.first_mut()) - } else { - None - } - } - } - } - - /// Returns true if the map contains a value for the specified key. - pub fn contains_key(&self, key: N) -> bool { - match key.as_name() { - Either::Left(name) => self.inner.contains_key(name), - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - self.inner.contains_key(&name) - } else { - false - } - } - } - } - - /// An iterator visiting all key-value pairs. + /// let mut iter = map.iter(); + /// assert!(iter.next().is_none()); /// - /// The iteration order is arbitrary, but consistent across platforms for - /// the same crate version. Each key will be yielded once per associated - /// value. So, if a key has 3 associated values, it will be yielded 3 times. + /// map.append(header::HOST, HeaderValue::from_static("duck.com")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// + /// let mut iter = map.iter(); + /// assert!(iter.next().is_some()); + /// assert!(iter.next().is_some()); + /// assert!(iter.next().is_some()); + /// assert!(iter.next().is_none()); + /// + /// let pairs = map.iter().collect::>(); + /// assert!(pairs.contains(&(&header::HOST, &HeaderValue::from_static("duck.com")))); + /// assert!(pairs.contains(&(&header::SET_COOKIE, &HeaderValue::from_static("one=1")))); + /// assert!(pairs.contains(&(&header::SET_COOKIE, &HeaderValue::from_static("two=2")))); + /// ``` pub fn iter(&self) -> Iter<'_> { Iter::new(self.inner.iter()) } - /// An iterator visiting all keys. + /// An iterator over all contained header names. /// - /// The iteration order is arbitrary, but consistent across platforms for - /// the same crate version. Each key will be yielded only once even if it - /// has multiple associated values. + /// Each name will only be yielded once even if it has multiple associated values. The iteration + /// order should be considered arbitrary. + /// + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); + /// + /// let mut iter = map.keys(); + /// assert!(iter.next().is_none()); + /// + /// map.append(header::HOST, HeaderValue::from_static("duck.com")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); + /// + /// let keys = map.keys().cloned().collect::>(); + /// assert_eq!(keys.len(), 2); + /// assert!(keys.contains(&header::HOST)); + /// assert!(keys.contains(&header::SET_COOKIE)); + /// ``` pub fn keys(&self) -> Keys<'_> { Keys(self.inner.keys()) } - /// Inserts a key-value pair into the map. + /// Clears the map, returning all name-value sets as an iterator. /// - /// If the map did not previously have this key present, then `None` is - /// returned. + /// Header names will only be yielded for the first value in each set. All items that are + /// yielded without a name and after an item with a name are associated with that same name. + /// The first item will always contain a name. /// - /// If the map did have this key present, the new value is associated with - /// the key and all previous values are removed. **Note** that only a single - /// one of the previous values is returned. If there are multiple values - /// that have been previously associated with the key, then the first one is - /// returned. See `insert_mult` on `OccupiedEntry` for an API that returns - /// all values. + /// Keeps the allocated memory for reuse. + /// # Examples + /// ``` + /// # use actix_http::http::{header, HeaderMap, HeaderValue}; + /// let mut map = HeaderMap::new(); /// - /// The key is not updated, though; this matters for types that can be `==` - /// without being identical. - pub fn insert(&mut self, key: HeaderName, val: HeaderValue) { - let _ = self.inner.insert(key, Value::One(val)); - } - - /// Inserts a key-value pair into the map. + /// let mut iter = map.drain(); + /// assert!(iter.next().is_none()); + /// drop(iter); /// - /// If the map did not previously have this key present, then `false` is - /// returned. + /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); + /// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2")); /// - /// If the map did have this key present, the new value is pushed to the end - /// of the list of values currently associated with the key. The key is not - /// updated, though; this matters for types that can be `==` without being - /// identical. - pub fn append(&mut self, key: HeaderName, value: HeaderValue) { - match self.inner.entry(key) { - Entry::Occupied(mut entry) => entry.get_mut().append(value), - Entry::Vacant(entry) => { - entry.insert(Value::One(value)); - } - } - } - - /// Removes all headers for a particular header name from the map. - pub fn remove(&mut self, key: N) { - match key.as_name() { - Either::Left(name) => { - let _ = self.inner.remove(name); - } - Either::Right(s) => { - if let Ok(name) = HeaderName::try_from(s) { - let _ = self.inner.remove(&name); - } - } - } + /// let mut iter = map.drain(); + /// assert_eq!(iter.next().unwrap(), (Some(header::SET_COOKIE), HeaderValue::from_static("one=1"))); + /// assert_eq!(iter.next().unwrap(), (None, HeaderValue::from_static("two=2"))); + /// drop(iter); + /// + /// assert!(map.is_empty()); + /// ``` + pub fn drain(&mut self) -> Drain<'_> { + Drain::new(self.inner.drain()) } } -#[doc(hidden)] -pub trait AsName { - fn as_name(&self) -> Either<&HeaderName, &str>; -} - -impl AsName for HeaderName { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Left(self) - } -} - -impl<'a> AsName for &'a HeaderName { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Left(self) - } -} - -impl<'a> AsName for &'a str { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Right(self) - } -} - -impl AsName for String { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Right(self.as_str()) - } -} - -impl<'a> AsName for &'a String { - fn as_name(&self) -> Either<&HeaderName, &str> { - Either::Right(self.as_str()) - } -} - -/// Iterator for all values in a `HeaderMap` with the same name. -pub struct GetAll<'a> { - idx: usize, - item: Option<&'a Value>, -} - -impl<'a> Iterator for GetAll<'a> { - type Item = &'a HeaderValue; +/// Note that this implementation will clone a [HeaderName] for each value. Consider using +/// [`drain`](Self::drain) to control header name cloning. +impl IntoIterator for HeaderMap { + type Item = (HeaderName, HeaderValue); + type IntoIter = IntoIter; #[inline] - fn next(&mut self) -> Option<&'a HeaderValue> { - if let Some(ref val) = self.item { - match val { - Value::One(ref val) => { - self.item.take(); - Some(val) - } - Value::Multi(ref vec) => { - if self.idx < vec.len() { - let item = Some(&vec[self.idx]); - self.idx += 1; - item - } else { - self.item.take(); - None - } - } - } - } else { - None - } - } -} - -pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); - -impl<'a> Iterator for Keys<'a> { - type Item = &'a HeaderName; - - #[inline] - fn next(&mut self) -> Option<&'a HeaderName> { - self.0.next() + fn into_iter(self) -> Self::IntoIter { + IntoIter::new(self.inner.into_iter()) } } @@ -334,23 +597,143 @@ impl<'a> IntoIterator for &'a HeaderMap { type Item = (&'a HeaderName, &'a HeaderValue); type IntoIter = Iter<'a>; + #[inline] fn into_iter(self) -> Self::IntoIter { - self.iter() + Iter::new(self.inner.iter()) } } -pub struct Iter<'a> { +/// Iterator over borrowed values with the same associated name. +/// +/// See [`HeaderMap::get_all`]. +#[derive(Debug)] +pub struct GetAll<'a> { idx: usize, - current: Option<(&'a HeaderName, &'a SmallVec<[HeaderValue; 4]>)>, - iter: hash_map::Iter<'a, HeaderName, Value>, + value: Option<&'a Value>, +} + +impl<'a> GetAll<'a> { + fn new(value: Option<&'a Value>) -> Self { + Self { idx: 0, value } + } +} + +impl<'a> Iterator for GetAll<'a> { + type Item = &'a HeaderValue; + + fn next(&mut self) -> Option { + let val = self.value?; + + match val.get(self.idx) { + Some(val) => { + self.idx += 1; + Some(val) + } + None => { + // current index is none; remove value to fast-path future next calls + self.value = None; + None + } + } + } + + fn size_hint(&self) -> (usize, Option) { + match self.value { + Some(val) => (val.len(), Some(val.len())), + None => (0, Some(0)), + } + } +} + +impl ExactSizeIterator for GetAll<'_> {} + +impl iter::FusedIterator for GetAll<'_> {} + +/// Iterator over removed, owned values with the same associated name. +/// +/// Returned from methods that remove or replace items. See [`HeaderMap::insert`] +/// and [`HeaderMap::remove`]. +#[derive(Debug)] +pub struct Removed { + inner: Option>, +} + +impl Removed { + fn new(value: Option) -> Self { + let inner = value.map(|value| value.inner.into_iter()); + Self { inner } + } + + /// Returns true if iterator contains no elements, without consuming it. + /// + /// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate + /// wether any items were actually replaced or removed, respectively. + pub fn is_empty(&self) -> bool { + match self.inner { + // size hint lower bound of smallvec is the correct length + Some(ref iter) => iter.size_hint().0 == 0, + None => true, + } + } +} + +impl Iterator for Removed { + type Item = HeaderValue; + + #[inline] + fn next(&mut self) -> Option { + self.inner.as_mut()?.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + match self.inner { + Some(ref iter) => iter.size_hint(), + None => (0, None), + } + } +} + +impl ExactSizeIterator for Removed {} + +impl iter::FusedIterator for Removed {} + +/// Iterator over all names in the map. +#[derive(Debug)] +pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); + +impl<'a> Iterator for Keys<'a> { + type Item = &'a HeaderName; + + #[inline] + fn next(&mut self) -> Option { + self.0.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + self.0.size_hint() + } +} + +impl ExactSizeIterator for Keys<'_> {} + +impl iter::FusedIterator for Keys<'_> {} + +/// Iterator over borrowed name-value pairs. +#[derive(Debug)] +pub struct Iter<'a> { + inner: hash_map::Iter<'a, HeaderName, Value>, + multi_inner: Option<(&'a HeaderName, &'a SmallVec<[HeaderValue; 4]>)>, + multi_idx: usize, } impl<'a> Iter<'a> { fn new(iter: hash_map::Iter<'a, HeaderName, Value>) -> Self { Self { - iter, - idx: 0, - current: None, + inner: iter, + multi_idx: 0, + multi_inner: None, } } } @@ -358,28 +741,345 @@ impl<'a> Iter<'a> { impl<'a> Iterator for Iter<'a> { type Item = (&'a HeaderName, &'a HeaderValue); - #[inline] - fn next(&mut self) -> Option<(&'a HeaderName, &'a HeaderValue)> { - if let Some(ref mut item) = self.current { - if self.idx < item.1.len() { - let item = (item.0, &item.1[self.idx]); - self.idx += 1; - return Some(item); - } else { - self.idx = 0; - self.current.take(); - } - } - if let Some(item) = self.iter.next() { - match item.1 { - Value::One(ref value) => Some((item.0, value)), - Value::Multi(ref vec) => { - self.current = Some((item.0, vec)); - self.next() + fn next(&mut self) -> Option { + // handle in-progress multi value lists first + if let Some((name, ref mut vals)) = self.multi_inner { + match vals.get(self.multi_idx) { + Some(val) => { + self.multi_idx += 1; + return Some((name, val)); + } + None => { + // no more items in value list; reset state + self.multi_idx = 0; + self.multi_inner = None; } } - } else { - None + } + + let (name, value) = self.inner.next()?; + + // set up new inner iter and recurse into it + self.multi_inner = Some((name, &value.inner)); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // take inner lower bound + // make no attempt at an upper bound + (self.inner.size_hint().0, None) + } +} + +impl ExactSizeIterator for Iter<'_> {} + +impl iter::FusedIterator for Iter<'_> {} + +/// Iterator over drained name-value pairs. +/// +/// Iterator items are `(Option, HeaderValue)` to avoid cloning. +#[derive(Debug)] +pub struct Drain<'a> { + inner: hash_map::Drain<'a, HeaderName, Value>, + multi_inner: Option<(Option, SmallVec<[HeaderValue; 4]>)>, + multi_idx: usize, +} + +impl<'a> Drain<'a> { + fn new(iter: hash_map::Drain<'a, HeaderName, Value>) -> Self { + Self { + inner: iter, + multi_inner: None, + multi_idx: 0, } } } + +impl<'a> Iterator for Drain<'a> { + type Item = (Option, HeaderValue); + + fn next(&mut self) -> Option { + // handle in-progress multi value iterators first + if let Some((ref mut name, ref mut vals)) = self.multi_inner { + if !vals.is_empty() { + // OPTIMIZE: array removals + return Some((name.take(), vals.remove(0))); + } else { + // no more items in value iterator; reset state + self.multi_inner = None; + self.multi_idx = 0; + } + } + + let (name, value) = self.inner.next()?; + + // set up new inner iter and recurse into it + self.multi_inner = Some((Some(name), value.inner)); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // take inner lower bound + // make no attempt at an upper bound + (self.inner.size_hint().0, None) + } +} + +impl ExactSizeIterator for Drain<'_> {} + +impl iter::FusedIterator for Drain<'_> {} + +/// Iterator over owned name-value pairs. +/// +/// Implementation necessarily clones header names for each value. +#[derive(Debug)] +pub struct IntoIter { + inner: hash_map::IntoIter, + multi_inner: Option<(HeaderName, smallvec::IntoIter<[HeaderValue; 4]>)>, +} + +impl IntoIter { + fn new(inner: hash_map::IntoIter) -> Self { + Self { + inner, + multi_inner: None, + } + } +} + +impl Iterator for IntoIter { + type Item = (HeaderName, HeaderValue); + + fn next(&mut self) -> Option { + // handle in-progress multi value iterators first + if let Some((ref name, ref mut vals)) = self.multi_inner { + match vals.next() { + Some(val) => { + return Some((name.clone(), val)); + } + None => { + // no more items in value iterator; reset state + self.multi_inner = None; + } + } + } + + let (name, value) = self.inner.next()?; + + // set up new inner iter and recurse into it + self.multi_inner = Some((name, value.inner.into_iter())); + self.next() + } + + #[inline] + fn size_hint(&self) -> (usize, Option) { + // take inner lower bound + // make no attempt at an upper bound + (self.inner.size_hint().0, None) + } +} + +impl ExactSizeIterator for IntoIter {} + +impl iter::FusedIterator for IntoIter {} + +#[cfg(test)] +mod tests { + use std::iter::FusedIterator; + + use http::header; + use static_assertions::assert_impl_all; + + use super::*; + + assert_impl_all!(HeaderMap: IntoIterator); + assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator); + assert_impl_all!(Drain<'_>: Iterator, ExactSizeIterator, FusedIterator); + + #[test] + fn create() { + let map = HeaderMap::new(); + assert_eq!(map.len(), 0); + assert_eq!(map.capacity(), 0); + + let map = HeaderMap::with_capacity(16); + assert_eq!(map.len(), 0); + assert!(map.capacity() >= 16); + } + + #[test] + fn insert() { + let mut map = HeaderMap::new(); + + map.insert(header::LOCATION, HeaderValue::from_static("/test")); + assert_eq!(map.len(), 1); + } + + #[test] + fn contains() { + let mut map = HeaderMap::new(); + assert!(!map.contains_key(header::LOCATION)); + + map.insert(header::LOCATION, HeaderValue::from_static("/test")); + assert!(map.contains_key(header::LOCATION)); + assert!(map.contains_key("Location")); + assert!(map.contains_key("Location".to_owned())); + assert!(map.contains_key("location")); + } + + #[test] + fn entries_iter() { + let mut map = HeaderMap::new(); + + map.append(header::HOST, HeaderValue::from_static("duck.com")); + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut iter = map.iter(); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_none()); + + let pairs = map.iter().collect::>(); + assert!(pairs.contains(&(&header::HOST, &HeaderValue::from_static("duck.com")))); + assert!(pairs.contains(&(&header::COOKIE, &HeaderValue::from_static("one=1")))); + assert!(pairs.contains(&(&header::COOKIE, &HeaderValue::from_static("two=2")))); + } + + #[test] + fn drain_iter() { + let mut map = HeaderMap::new(); + + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut vals = vec![]; + let mut iter = map.drain(); + + let (name, val) = iter.next().unwrap(); + assert_eq!(name, Some(header::COOKIE)); + vals.push(val); + + let (name, val) = iter.next().unwrap(); + assert!(name.is_none()); + vals.push(val); + + assert!(vals.contains(&HeaderValue::from_static("one=1"))); + assert!(vals.contains(&HeaderValue::from_static("two=2"))); + + assert!(iter.next().is_none()); + drop(iter); + + assert!(map.is_empty()); + } + + #[test] + fn entries_into_iter() { + let mut map = HeaderMap::new(); + + map.append(header::HOST, HeaderValue::from_static("duck.com")); + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut iter = map.into_iter(); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_some()); + assert!(iter.next().is_none()); + } + + #[test] + fn iter_and_into_iter_same_order() { + let mut map = HeaderMap::new(); + + map.append(header::HOST, HeaderValue::from_static("duck.com")); + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut iter = map.iter(); + let mut into_iter = map.clone().into_iter(); + + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + assert_eq!(iter.next().map(owned_pair), into_iter.next()); + } + + #[test] + fn get_all_and_remove_same_order() { + let mut map = HeaderMap::new(); + + map.append(header::COOKIE, HeaderValue::from_static("one=1")); + map.append(header::COOKIE, HeaderValue::from_static("two=2")); + + let mut vals = map.get_all(header::COOKIE); + let mut removed = map.clone().remove(header::COOKIE); + + assert_eq!(vals.next(), removed.next().as_ref()); + assert_eq!(vals.next(), removed.next().as_ref()); + assert_eq!(vals.next(), removed.next().as_ref()); + } + + #[test] + fn get_all_iteration_order_matches_insertion_order() { + let mut map = HeaderMap::new(); + + let mut vals = map.get_all(header::COOKIE); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("1")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"1"); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("2")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"1"); + assert_eq!(vals.next().unwrap().as_bytes(), b"2"); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("3")); + map.append(header::COOKIE, HeaderValue::from_static("4")); + map.append(header::COOKIE, HeaderValue::from_static("5")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"1"); + assert_eq!(vals.next().unwrap().as_bytes(), b"2"); + assert_eq!(vals.next().unwrap().as_bytes(), b"3"); + assert_eq!(vals.next().unwrap().as_bytes(), b"4"); + assert_eq!(vals.next().unwrap().as_bytes(), b"5"); + assert!(vals.next().is_none()); + + let _ = map.insert(header::COOKIE, HeaderValue::from_static("6")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"6"); + assert!(vals.next().is_none()); + + let _ = map.insert(header::COOKIE, HeaderValue::from_static("7")); + let _ = map.insert(header::COOKIE, HeaderValue::from_static("8")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"8"); + assert!(vals.next().is_none()); + + map.append(header::COOKIE, HeaderValue::from_static("9")); + let mut vals = map.get_all(header::COOKIE); + assert_eq!(vals.next().unwrap().as_bytes(), b"8"); + assert_eq!(vals.next().unwrap().as_bytes(), b"9"); + assert!(vals.next().is_none()); + + // check for fused-ness + assert!(vals.next().is_none()); + } + + fn owned_pair<'a>( + (name, val): (&'a HeaderName, &'a HeaderValue), + ) -> (HeaderName, HeaderValue) { + (name.clone(), val.clone()) + } +} diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index dc97bf5ff..308cb0123 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -1,87 +1,72 @@ -//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing/conversion and other -//! header utility methods. +//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods. -use std::fmt; - -use bytes::{Bytes, BytesMut}; use percent_encoding::{AsciiSet, CONTROLS}; -pub use http::header::*; +// re-export from http except header map related items +pub use http::header::{ + HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError, +}; -use crate::error::ParseError; -use crate::httpmessage::HttpMessage; +// re-export const header names +pub use http::header::{ + ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, + ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, + ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, + ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE, + ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC, + AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING, + CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, + CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, + DATE, DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, + IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, + LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, PROXY_AUTHENTICATE, + PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, + REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT, + SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, + SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER, + TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, + WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, + X_FRAME_OPTIONS, X_XSS_PROTECTION, +}; +use crate::{error::ParseError, HttpMessage}; + +mod as_name; mod into_pair; mod into_value; +pub mod map; +mod shared; mod utils; -mod common; -pub(crate) mod map; -mod shared; - -pub use self::common::*; #[doc(hidden)] pub use self::shared::*; +pub use self::as_name::AsHeaderName; pub use self::into_pair::IntoHeaderPair; pub use self::into_value::IntoHeaderValue; -#[doc(hidden)] -pub use self::map::GetAll; pub use self::map::HeaderMap; -pub use self::utils::*; +pub use self::utils::{ + fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode, +}; -/// A trait for any object that already represents a valid header field and value. +/// An interface for types that already represent a valid header. pub trait Header: IntoHeaderValue { /// Returns the name of the header field fn name() -> HeaderName; /// Parse a header - fn parse(msg: &T) -> Result; -} - -#[doc(hidden)] -pub(crate) struct Writer { - buf: BytesMut, -} - -impl Writer { - fn new() -> Writer { - Writer { - buf: BytesMut::new(), - } - } - - fn take(&mut self) -> Bytes { - self.buf.split().freeze() - } -} - -impl fmt::Write for Writer { - #[inline] - fn write_str(&mut self, s: &str) -> fmt::Result { - self.buf.extend_from_slice(s.as_bytes()); - Ok(()) - } - - #[inline] - fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { - fmt::write(self, args) - } + fn parse(msg: &M) -> Result; } /// Convert `http::HeaderMap` to our `HeaderMap`. impl From for HeaderMap { - fn from(map: http::HeaderMap) -> HeaderMap { - let mut new_map = HeaderMap::with_capacity(map.capacity()); - for (h, v) in map.iter() { - new_map.append(h.clone(), v.clone()); - } - new_map + fn from(mut map: http::HeaderMap) -> HeaderMap { + HeaderMap::from_drain(map.drain()) } } /// This encode set is used for HTTP header values and is defined at -/// https://tools.ietf.org/html/rfc5987#section-3.2. +/// . pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS .add(b' ') .add(b'"') diff --git a/actix-http/src/header/shared/charset.rs b/actix-http/src/header/shared/charset.rs index 36bdbf7e2..1e77e1be8 100644 --- a/actix-http/src/header/shared/charset.rs +++ b/actix-http/src/header/shared/charset.rs @@ -1,14 +1,13 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; +use std::{fmt, str}; use self::Charset::*; -/// A Mime charset. +/// A MIME character set. /// /// The string representation is normalized to upper case. /// /// See . -#[derive(Clone, Debug, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] #[allow(non_camel_case_types)] pub enum Charset { /// US ASCII @@ -88,23 +87,23 @@ impl Charset { Iso_8859_8_E => "ISO-8859-8-E", Iso_8859_8_I => "ISO-8859-8-I", Gb2312 => "GB2312", - Big5 => "big5", + Big5 => "Big5", Koi8_R => "KOI8-R", Ext(ref s) => s, } } } -impl Display for Charset { +impl fmt::Display for Charset { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str(self.label()) } } -impl FromStr for Charset { +impl str::FromStr for Charset { type Err = crate::Error; - fn from_str(s: &str) -> crate::Result { + fn from_str(s: &str) -> Result { Ok(match s.to_ascii_uppercase().as_ref() { "US-ASCII" => Us_Ascii, "ISO-8859-1" => Iso_8859_1, @@ -128,7 +127,7 @@ impl FromStr for Charset { "ISO-8859-8-E" => Iso_8859_8_E, "ISO-8859-8-I" => Iso_8859_8_I, "GB2312" => Gb2312, - "big5" => Big5, + "BIG5" => Big5, "KOI8-R" => Koi8_R, s => Ext(s.to_owned()), }) diff --git a/actix-http/src/header/common/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs similarity index 63% rename from actix-http/src/header/common/content_encoding.rs rename to actix-http/src/header/shared/content_encoding.rs index b93d66101..073d90dce 100644 --- a/actix-http/src/header/common/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -1,5 +1,6 @@ -use std::{convert::Infallible, str::FromStr}; +use std::{convert::TryFrom, str::FromStr}; +use derive_more::{Display, Error}; use http::header::InvalidHeaderValue; use crate::{ @@ -8,8 +9,19 @@ use crate::{ HttpMessage, }; +/// Error returned when a content encoding is unknown. +#[derive(Debug, Display, Error)] +#[display(fmt = "unsupported content encoding")] +pub struct ContentEncodingParseError; + /// Represents a supported content encoding. -#[derive(Copy, Clone, PartialEq, Debug)] +/// +/// Includes a commonly-used subset of media types appropriate for use as HTTP content encodings. +/// See [IANA HTTP Content Coding Registry]. +/// +/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml +#[derive(Debug, Clone, Copy, PartialEq)] +#[non_exhaustive] pub enum ContentEncoding { /// Automatically select encoding based on encoding negotiation. Auto, @@ -23,6 +35,9 @@ pub enum ContentEncoding { /// Gzip algorithm. Gzip, + /// Zstd algorithm. + Zstd, + /// Indicates the identity function (i.e. no compression, nor modification). Identity, } @@ -34,27 +49,17 @@ impl ContentEncoding { matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) } - /// Convert content encoding to string + /// Convert content encoding to string. #[inline] pub fn as_str(self) -> &'static str { match self { ContentEncoding::Br => "br", ContentEncoding::Gzip => "gzip", ContentEncoding::Deflate => "deflate", + ContentEncoding::Zstd => "zstd", ContentEncoding::Identity | ContentEncoding::Auto => "identity", } } - - /// Default Q-factor (quality) value. - #[inline] - pub fn quality(self) -> f64 { - match self { - ContentEncoding::Br => 1.1, - ContentEncoding::Gzip => 1.0, - ContentEncoding::Deflate => 0.9, - ContentEncoding::Identity | ContentEncoding::Auto => 0.1, - } - } } impl Default for ContentEncoding { @@ -64,29 +69,33 @@ impl Default for ContentEncoding { } impl FromStr for ContentEncoding { - type Err = Infallible; + type Err = ContentEncodingParseError; fn from_str(val: &str) -> Result { - Ok(Self::from(val)) - } -} - -impl From<&str> for ContentEncoding { - fn from(val: &str) -> ContentEncoding { let val = val.trim(); if val.eq_ignore_ascii_case("br") { - ContentEncoding::Br + Ok(ContentEncoding::Br) } else if val.eq_ignore_ascii_case("gzip") { - ContentEncoding::Gzip + Ok(ContentEncoding::Gzip) } else if val.eq_ignore_ascii_case("deflate") { - ContentEncoding::Deflate + Ok(ContentEncoding::Deflate) + } else if val.eq_ignore_ascii_case("zstd") { + Ok(ContentEncoding::Zstd) } else { - ContentEncoding::default() + Err(ContentEncodingParseError) } } } +impl TryFrom<&str> for ContentEncoding { + type Error = ContentEncodingParseError; + + fn try_from(val: &str) -> Result { + val.parse() + } +} + impl IntoHeaderValue for ContentEncoding { type Error = InvalidHeaderValue; diff --git a/actix-http/src/header/shared/extended.rs b/actix-http/src/header/shared/extended.rs index 6bdcb7922..b2cf1d754 100644 --- a/actix-http/src/header/shared/extended.rs +++ b/actix-http/src/header/shared/extended.rs @@ -1,17 +1,17 @@ +// Originally from hyper v0.11.27 src/header/parsing.rs + use std::{fmt, str::FromStr}; use language_tags::LanguageTag; use crate::header::{Charset, HTTP_VALUE}; -// From hyper v0.11.27 src/header/parsing.rs - /// The value part of an extended parameter consisting of three parts: /// - The REQUIRED character set name (`charset`). /// - The OPTIONAL language information (`language_tag`). /// - A character sequence representing the actual value (`value`), separated by single quotes. /// -/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). #[derive(Clone, Debug, PartialEq)] pub struct ExtendedValue { /// The character set that is used to encode the `value` to a string. @@ -24,17 +24,17 @@ pub struct ExtendedValue { pub value: Vec, } -/// Parses extended header parameter values (`ext-value`), as defined in -/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). +/// Parses extended header parameter values (`ext-value`), as defined +/// in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2). /// /// Extended values are denoted by parameter names that end with `*`. /// /// ## ABNF /// -/// ```text +/// ```plain /// ext-value = charset "'" [ language ] "'" value-chars /// ; like RFC 2231's -/// ; (see [RFC2231], Section 7) +/// ; (see [RFC 2231 §7]) /// /// charset = "UTF-8" / "ISO-8859-1" / mime-charset /// @@ -43,22 +43,26 @@ pub struct ExtendedValue { /// / "!" / "#" / "$" / "%" / "&" /// / "+" / "-" / "^" / "_" / "`" /// / "{" / "}" / "~" -/// ; as in Section 2.3 of [RFC2978] +/// ; as in [RFC 2978 §2.3] /// ; except that the single quote is not included /// ; SHOULD be registered in the IANA charset registry /// -/// language = +/// language = /// /// value-chars = *( pct-encoded / attr-char ) /// /// pct-encoded = "%" HEXDIG HEXDIG -/// ; see [RFC3986], Section 2.1 +/// ; see [RFC 3986 §2.1] /// /// attr-char = ALPHA / DIGIT /// / "!" / "#" / "$" / "&" / "+" / "-" / "." /// / "^" / "_" / "`" / "|" / "~" /// ; token except ( "*" / "'" / "%" ) /// ``` +/// +/// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7 +/// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3 +/// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1 pub fn parse_extended_value( val: &str, ) -> Result { @@ -88,9 +92,9 @@ pub fn parse_extended_value( }; Ok(ExtendedValue { - value, charset, language_tag, + value, }) } diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs new file mode 100644 index 000000000..8dbdf4a62 --- /dev/null +++ b/actix-http/src/header/shared/http_date.rs @@ -0,0 +1,82 @@ +use std::{fmt, io::Write, str::FromStr, time::SystemTime}; + +use bytes::BytesMut; +use http::header::{HeaderValue, InvalidHeaderValue}; + +use crate::{ + config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue, + helpers::MutWriter, +}; + +/// A timestamp with HTTP-style formatting and parsing. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] +pub struct HttpDate(SystemTime); + +impl FromStr for HttpDate { + type Err = ParseError; + + fn from_str(s: &str) -> Result { + match httpdate::parse_http_date(s) { + Ok(sys_time) => Ok(HttpDate(sys_time)), + Err(_) => Err(ParseError::Header), + } + } +} + +impl fmt::Display for HttpDate { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let date_str = httpdate::fmt_http_date(self.0); + f.write_str(&date_str) + } +} + +impl IntoHeaderValue for HttpDate { + type Error = InvalidHeaderValue; + + fn try_into_value(self) -> Result { + let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH); + let mut wrt = MutWriter(&mut buf); + + // unwrap: date output is known to be well formed and of known length + write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap(); + + HeaderValue::from_maybe_shared(buf.split().freeze()) + } +} + +impl From for HttpDate { + fn from(sys_time: SystemTime) -> HttpDate { + HttpDate(sys_time) + } +} + +impl From for SystemTime { + fn from(HttpDate(sys_time): HttpDate) -> SystemTime { + sys_time + } +} + +#[cfg(test)] +mod tests { + use std::time::Duration; + + use super::*; + + #[test] + fn date_header() { + macro_rules! assert_parsed_date { + ($case:expr, $exp:expr) => { + assert_eq!($case.parse::().unwrap(), $exp); + }; + } + + // 784198117 = SystemTime::from(datetime!(1994-11-07 08:48:37).assume_utc()).duration_since(SystemTime::UNIX_EPOCH)); + let nov_07 = HttpDate(SystemTime::UNIX_EPOCH + Duration::from_secs(784198117)); + + assert_parsed_date!("Mon, 07 Nov 1994 08:48:37 GMT", nov_07); + assert_parsed_date!("Monday, 07-Nov-94 08:48:37 GMT", nov_07); + assert_parsed_date!("Mon Nov 7 08:48:37 1994", nov_07); + + assert!("this-is-no-date".parse::().is_err()); + } +} diff --git a/actix-http/src/header/shared/httpdate.rs b/actix-http/src/header/shared/httpdate.rs deleted file mode 100644 index 72a225589..000000000 --- a/actix-http/src/header/shared/httpdate.rs +++ /dev/null @@ -1,101 +0,0 @@ -use std::fmt::{self, Display}; -use std::io::Write; -use std::str::FromStr; -use std::time::{SystemTime, UNIX_EPOCH}; - -use bytes::buf::BufMut; -use bytes::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(OffsetDateTime); - -impl FromStr for HttpDate { - type Err = ParseError; - - fn from_str(s: &str) -> Result { - 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.format("%a, %d %b %Y %H:%M:%S GMT"), f) - } -} - -impl From for HttpDate { - fn from(dt: OffsetDateTime) -> HttpDate { - HttpDate(dt) - } -} - -impl From for HttpDate { - fn from(sys: SystemTime) -> HttpDate { - HttpDate(PrimitiveDateTime::from(sys).assume_utc()) - } -} - -impl IntoHeaderValue for HttpDate { - type Error = InvalidHeaderValue; - - fn try_into_value(self) -> Result { - let mut wrt = BytesMut::with_capacity(29).writer(); - 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 dt = date.0; - let epoch = OffsetDateTime::unix_epoch(); - - UNIX_EPOCH + (dt - epoch) - } -} - -#[cfg(test)] -mod tests { - use super::HttpDate; - 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 - ); - assert_eq!( - "Sunday, 07-Nov-94 08:48:37 GMT" - .parse::() - .unwrap(), - nov_07 - ); - assert_eq!( - "Sun Nov 7 08:48:37 1994".parse::().unwrap(), - nov_07 - ); - assert!("this-is-no-date".parse::().is_err()); - } -} diff --git a/actix-http/src/header/shared/mod.rs b/actix-http/src/header/shared/mod.rs index 72161e46b..274e13146 100644 --- a/actix-http/src/header/shared/mod.rs +++ b/actix-http/src/header/shared/mod.rs @@ -1,16 +1,14 @@ //! Originally taken from `hyper::header::shared`. mod charset; -mod encoding; -mod entity; +mod content_encoding; mod extended; -mod httpdate; +mod http_date; mod quality_item; pub use self::charset::Charset; -pub use self::encoding::Encoding; -pub use self::entity::EntityTag; +pub use self::content_encoding::ContentEncoding; pub use self::extended::{parse_extended_value, ExtendedValue}; -pub use self::httpdate::HttpDate; +pub use self::http_date::HttpDate; pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use language_tags::LanguageTag; diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 01a3b988a..a109b44ea 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -6,6 +6,8 @@ use std::{ use derive_more::{Display, Error}; +use crate::error::ParseError; + const MAX_QUALITY: u16 = 1000; const MAX_FLOAT_QUALITY: f32 = 1.0; @@ -23,9 +25,9 @@ const MAX_FLOAT_QUALITY: f32 = 1.0; /// a value between 0 and 1000 e.g. `Quality(532)` matches the quality /// `q=0.532`. /// -/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) -/// gives more information on quality values in HTTP header fields. -#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] +/// [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1) gives more +/// information on quality values in HTTP header fields. +#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)] pub struct Quality(u16); impl Quality { @@ -76,20 +78,21 @@ impl TryFrom for Quality { } } -/// Represents an item with a quality value as defined in -/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1). -#[derive(Clone, PartialEq, Debug)] +/// Represents an item with a quality value as defined +/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). +#[derive(Debug, Clone, PartialEq, Eq)] pub struct QualityItem { - /// The actual contents of the field. + /// The wrapped contents of the field. pub item: T, + /// The quality (client or server preference) for the value. pub quality: Quality, } impl QualityItem { - /// Creates a new `QualityItem` from an item and a quality. - /// The item can be of any type. - /// The quality should be a value in the range [0, 1]. + /// Constructs a new `QualityItem` from an item and a quality value. + /// + /// The item can be of any type. The quality should be a value in the range [0, 1]. pub fn new(item: T, quality: Quality) -> QualityItem { QualityItem { item, quality } } @@ -114,17 +117,18 @@ impl fmt::Display for QualityItem { } impl str::FromStr for QualityItem { - type Err = crate::error::ParseError; + type Err = ParseError; - fn from_str(qitem_str: &str) -> Result, crate::error::ParseError> { + fn from_str(qitem_str: &str) -> Result { if !qitem_str.is_ascii() { - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } // Set defaults used if parsing fails. let mut raw_item = qitem_str; let mut quality = 1f32; + // TODO: MSRV(1.52): use rsplit_once let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect(); if parts.len() == 2 { @@ -139,7 +143,7 @@ impl str::FromStr for QualityItem { if parts[0].len() < 2 { // Can't possibly be an attribute since an attribute needs at least a name followed // by an equals sign. And bare identifiers are forbidden. - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } let start = &parts[0][0..2]; @@ -148,25 +152,21 @@ impl str::FromStr for QualityItem { let q_val = &parts[0][2..]; if q_val.len() > 5 { // longer than 5 indicates an over-precise q-factor - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } - let q_value = q_val - .parse::() - .map_err(|_| crate::error::ParseError::Header)?; + let q_value = q_val.parse::().map_err(|_| ParseError::Header)?; if (0f32..=1f32).contains(&q_value) { quality = q_value; raw_item = parts[1]; } else { - return Err(crate::error::ParseError::Header); + return Err(ParseError::Header); } } } - let item = raw_item - .parse::() - .map_err(|_| crate::error::ParseError::Header)?; + let item = raw_item.parse::().map_err(|_| ParseError::Header)?; // we already checked above that the quality is within range Ok(QualityItem::new(item, Quality::from_f32(quality))) @@ -193,21 +193,70 @@ where #[cfg(test)] mod tests { - use super::super::encoding::*; use super::*; + // copy of encoding from actix-web headers + #[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt + #[derive(Clone, PartialEq, Debug)] + pub enum Encoding { + Chunked, + Brotli, + Gzip, + Deflate, + Compress, + Identity, + Trailers, + EncodingExt(String), + } + + impl fmt::Display for Encoding { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use Encoding::*; + f.write_str(match *self { + Chunked => "chunked", + Brotli => "br", + Gzip => "gzip", + Deflate => "deflate", + Compress => "compress", + Identity => "identity", + Trailers => "trailers", + EncodingExt(ref s) => s.as_ref(), + }) + } + } + + impl str::FromStr for Encoding { + type Err = crate::error::ParseError; + fn from_str(s: &str) -> Result { + use Encoding::*; + match s { + "chunked" => Ok(Chunked), + "br" => Ok(Brotli), + "deflate" => Ok(Deflate), + "gzip" => Ok(Gzip), + "compress" => Ok(Compress), + "identity" => Ok(Identity), + "trailers" => Ok(Trailers), + _ => Ok(EncodingExt(s.to_owned())), + } + } + } + #[test] fn test_quality_item_fmt_q_1() { + use Encoding::*; let x = qitem(Chunked); assert_eq!(format!("{}", x), "chunked"); } #[test] fn test_quality_item_fmt_q_0001() { + use Encoding::*; let x = QualityItem::new(Chunked, Quality(1)); assert_eq!(format!("{}", x), "chunked; q=0.001"); } #[test] fn test_quality_item_fmt_q_05() { + use Encoding::*; // Custom value let x = QualityItem { item: EncodingExt("identity".to_owned()), @@ -218,6 +267,7 @@ mod tests { #[test] fn test_quality_item_fmt_q_0() { + use Encoding::*; // Custom value let x = QualityItem { item: EncodingExt("identity".to_owned()), @@ -228,6 +278,7 @@ mod tests { #[test] fn test_quality_item_from_str1() { + use Encoding::*; let x: Result, _> = "chunked".parse(); assert_eq!( x.unwrap(), @@ -237,8 +288,10 @@ mod tests { } ); } + #[test] fn test_quality_item_from_str2() { + use Encoding::*; let x: Result, _> = "chunked; q=1".parse(); assert_eq!( x.unwrap(), @@ -248,8 +301,10 @@ mod tests { } ); } + #[test] fn test_quality_item_from_str3() { + use Encoding::*; let x: Result, _> = "gzip; q=0.5".parse(); assert_eq!( x.unwrap(), @@ -259,8 +314,10 @@ mod tests { } ); } + #[test] fn test_quality_item_from_str4() { + use Encoding::*; let x: Result, _> = "gzip; q=0.273".parse(); assert_eq!( x.unwrap(), @@ -270,16 +327,19 @@ mod tests { } ); } + #[test] fn test_quality_item_from_str5() { let x: Result, _> = "gzip; q=0.2739999".parse(); assert!(x.is_err()); } + #[test] fn test_quality_item_from_str6() { let x: Result, _> = "gzip; q=2".parse(); assert!(x.is_err()); } + #[test] fn test_quality_item_ordering() { let x: QualityItem = "gzip; q=0.5".parse().ok().unwrap(); diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index e232d462f..a23f5b751 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -1,7 +1,8 @@ +//! Header parsing utilities. + use std::{fmt, str::FromStr}; -use http::HeaderValue; - +use super::HeaderValue; use crate::{error::ParseError, header::HTTP_VALUE}; /// Reads a comma-delimited raw header into a Vec. @@ -11,9 +12,12 @@ where I: Iterator + 'a, T: FromStr, { - let mut result = Vec::new(); + let size_guess = all.size_hint().1.unwrap_or(2); + let mut result = Vec::with_capacity(size_guess); + for h in all { let s = h.to_str().map_err(|_| ParseError::Header)?; + result.extend( s.split(',') .filter_map(|x| match x.trim() { @@ -23,6 +27,7 @@ where .filter_map(|x| x.trim().parse().ok()), ) } + Ok(result) } @@ -31,10 +36,12 @@ where pub fn from_one_raw_str(val: Option<&HeaderValue>) -> Result { if let Some(line) = val { let line = line.to_str().map_err(|_| ParseError::Header)?; + if !line.is_empty() { return T::from_str(line).or(Err(ParseError::Header)); } } + Err(ParseError::Header) } @@ -45,19 +52,52 @@ where T: fmt::Display, { let mut iter = parts.iter(); + if let Some(part) = iter.next() { fmt::Display::fmt(part, f)?; } + for part in iter { f.write_str(", ")?; fmt::Display::fmt(part, f)?; } + Ok(()) } /// Percent encode a sequence of bytes with a character set defined in -/// +/// +#[inline] pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); fmt::Display::fmt(&encoded, f) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn comma_delimited_parsing() { + let headers = vec![]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![0; 0]); + + let headers = vec![ + HeaderValue::from_static("1, 2"), + HeaderValue::from_static("3,4"), + ]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![1, 2, 3, 4]); + + let headers = vec![ + HeaderValue::from_static(""), + HeaderValue::from_static(","), + HeaderValue::from_static(" "), + HeaderValue::from_static("1 ,"), + HeaderValue::from_static(""), + ]; + let res: Vec = from_comma_delimited(headers.iter()).unwrap(); + assert_eq!(res, vec![1]); + } +} diff --git a/actix-http/src/helpers.rs b/actix-http/src/helpers.rs index 13195f7db..cba94d9b8 100644 --- a/actix-http/src/helpers.rs +++ b/actix-http/src/helpers.rs @@ -1,15 +1,15 @@ use std::io; -use bytes::{BufMut, BytesMut}; +use bytes::BufMut; use http::Version; const DIGITS_START: u8 = b'0'; -pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { +pub(crate) fn write_status_line(version: Version, n: u16, buf: &mut B) { match version { - Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), - Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), - Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), + Version::HTTP_11 => buf.put_slice(b"HTTP/1.1 "), + Version::HTTP_10 => buf.put_slice(b"HTTP/1.0 "), + Version::HTTP_09 => buf.put_slice(b"HTTP/0.9 "), _ => { // other HTTP version handlers do not use this method } @@ -19,33 +19,44 @@ pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) let d10 = ((n / 10) % 10) as u8; let d1 = (n % 10) as u8; - bytes.put_u8(DIGITS_START + d100); - bytes.put_u8(DIGITS_START + d10); - bytes.put_u8(DIGITS_START + d1); + buf.put_u8(DIGITS_START + d100); + buf.put_u8(DIGITS_START + d10); + buf.put_u8(DIGITS_START + d1); // trailing space before reason - bytes.put_u8(b' '); + buf.put_u8(b' '); } -/// NOTE: bytes object has to contain enough space -pub fn write_content_length(n: u64, bytes: &mut BytesMut) { +/// Write out content length header. +/// +/// Buffer must to contain enough space or be implicitly extendable. +pub fn write_content_length(n: u64, buf: &mut B) { if n == 0 { - bytes.put_slice(b"\r\ncontent-length: 0\r\n"); + buf.put_slice(b"\r\ncontent-length: 0\r\n"); return; } - let mut buf = itoa::Buffer::new(); + let mut buffer = itoa::Buffer::new(); - bytes.put_slice(b"\r\ncontent-length: "); - bytes.put_slice(buf.format(n).as_bytes()); - bytes.put_slice(b"\r\n"); + buf.put_slice(b"\r\ncontent-length: "); + buf.put_slice(buffer.format(n).as_bytes()); + buf.put_slice(b"\r\n"); } -pub(crate) struct Writer<'a>(pub &'a mut BytesMut); +/// An `io::Write`r that only requires mutable reference and assumes that there is space available +/// in the buffer for every write operation or that it can be extended implicitly (like +/// `bytes::BytesMut`, for example). +/// +/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not +/// perform a remaining length check before writing. +pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); -impl<'a> io::Write for Writer<'a> { +impl<'a, B> io::Write for MutWriter<'a, B> +where + B: BufMut, +{ fn write(&mut self, buf: &[u8]) -> io::Result { - self.0.extend_from_slice(buf); + self.0.put_slice(buf); Ok(buf.len()) } @@ -58,6 +69,8 @@ impl<'a> io::Write for Writer<'a> { mod tests { use std::str::from_utf8; + use bytes::BytesMut; + use super::*; #[test] diff --git a/actix-http/src/httpmessage.rs b/actix-http/src/http_message.rs similarity index 82% rename from actix-http/src/httpmessage.rs rename to actix-http/src/http_message.rs index 2610b8784..ccaa320fa 100644 --- a/actix-http/src/httpmessage.rs +++ b/actix-http/src/http_message.rs @@ -1,17 +1,18 @@ -use std::cell::{Ref, RefMut}; -use std::str; +use std::{ + cell::{Ref, RefMut}, + str, +}; use encoding_rs::{Encoding, UTF_8}; use http::header; use mime::Mime; -use crate::cookie::Cookie; -use crate::error::{ContentTypeError, CookieParseError, ParseError}; -use crate::extensions::Extensions; -use crate::header::{Header, HeaderMap}; -use crate::payload::Payload; - -struct Cookies(Vec>); +use crate::{ + error::{ContentTypeError, ParseError}, + header::{Header, HeaderMap}, + payload::Payload, + Extensions, +}; /// Trait that implements general purpose operations on HTTP messages. pub trait HttpMessage: Sized { @@ -102,39 +103,6 @@ pub trait HttpMessage: Sized { Ok(false) } } - - /// Load request cookies. - #[inline] - fn cookies(&self) -> Result>>, CookieParseError> { - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.headers().get_all(header::COOKIE) { - let s = - str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; - for cookie_str in s.split(';').map(|s| s.trim()) { - if !cookie_str.is_empty() { - cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); - } - } - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } - - /// Return request cookie. - fn cookie(&self, name: &str) -> Option> { - if let Ok(cookies) = self.cookies() { - for cookie in cookies.iter() { - if cookie.name() == name { - return Some(cookie.to_owned()); - } - } - } - None - } } impl<'a, T> HttpMessage for &'a mut T diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index cc99130eb..bfb6b8c55 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,6 +1,20 @@ //! HTTP primitives for the Actix ecosystem. +//! +//! ## Crate Features +//! | Feature | Functionality | +//! | ------------------- | ------------------------------------------- | +//! | `openssl` | TLS support via [OpenSSL]. | +//! | `rustls` | TLS support via [rustls]. | +//! | `compress-brotli` | Payload compression support: Brotli. | +//! | `compress-gzip` | Payload compression support: Deflate, Gzip. | +//! | `compress-zstd` | Payload compression support: Zstd. | +//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | +//! +//! [OpenSSL]: https://crates.io/crates/openssl +//! [rustls]: https://crates.io/crates/rustls +//! [trust-dns]: https://crates.io/crates/trust-dns -#![deny(rust_2018_idioms)] +#![deny(rust_2018_idioms, nonstandard_style, clippy::uninit_assumed_init)] #![allow( clippy::type_complexity, clippy::too_many_arguments, @@ -13,28 +27,23 @@ #[macro_use] extern crate log; -#[macro_use] -mod macros; - pub mod body; mod builder; -pub mod client; mod config; -#[cfg(feature = "compress")] + +#[cfg(feature = "__compress")] pub mod encoding; mod extensions; -mod header; +pub mod header; mod helpers; -mod httpcodes; -pub mod httpmessage; +mod http_message; mod message; mod payload; mod request; mod response; +mod response_builder; mod service; -mod time_parser; -pub use cookie; pub mod error; pub mod h1; pub mod h2; @@ -43,15 +52,22 @@ pub mod ws; pub use self::builder::HttpServiceBuilder; pub use self::config::{KeepAlive, ServiceConfig}; -pub use self::error::{Error, ResponseError, Result}; +pub use self::error::Error; pub use self::extensions::Extensions; -pub use self::httpmessage::HttpMessage; +pub use self::header::ContentEncoding; +pub use self::http_message::HttpMessage; +pub use self::message::ConnectionType; pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead}; pub use self::payload::{Payload, PayloadStream}; pub use self::request::Request; -pub use self::response::{Response, ResponseBuilder}; +pub use self::response::Response; +pub use self::response_builder::ResponseBuilder; pub use self::service::HttpService; +pub use ::http::{uri, uri::Uri}; +pub use ::http::{Method, StatusCode, Version}; + +// TODO: deprecate this mish-mash of random items pub mod http { //! Various HTTP related types. @@ -61,7 +77,6 @@ pub mod http { pub use http::{uri, Error, Uri}; pub use http::{Method, StatusCode, Version}; - pub use crate::cookie::{Cookie, CookieBuilder}; pub use crate::header::HeaderMap; /// A collection of HTTP headers and helpers. @@ -87,14 +102,9 @@ type ConnectCallback = dyn Fn(&IO, &mut Extensions); /// /// # Implementation Details /// Uses Option to reduce necessary allocations when merging with request extensions. +#[derive(Default)] pub(crate) struct OnConnectData(Option); -impl Default for OnConnectData { - fn default() -> Self { - Self(None) - } -} - impl OnConnectData { /// Construct by calling the on-connect callback with the underlying transport I/O. pub(crate) fn from_io( diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index bccb4d53e..c8e1ce6db 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -1,20 +1,25 @@ -use std::cell::{Ref, RefCell, RefMut}; -use std::net; -use std::rc::Rc; +use std::{ + cell::{Ref, RefCell, RefMut}, + net, + rc::Rc, +}; use bitflags::bitflags; -use crate::extensions::Extensions; -use crate::header::HeaderMap; -use crate::http::{header, Method, StatusCode, Uri, Version}; +use crate::{ + header::{self, HeaderMap}, + Extensions, Method, StatusCode, Uri, Version, +}; /// Represents various types of connection #[derive(Copy, Clone, PartialEq, Debug)] pub enum ConnectionType { /// Close connection after response Close, + /// Keep connection alive after response KeepAlive, + /// Connection is upgraded to different type Upgrade, } @@ -41,8 +46,8 @@ pub trait Head: Default + 'static { #[derive(Debug)] pub struct RequestHead { - pub uri: Uri, pub method: Method, + pub uri: Uri, pub version: Version, pub headers: HeaderMap, pub extensions: RefCell, @@ -53,13 +58,13 @@ pub struct RequestHead { impl Default for RequestHead { fn default() -> RequestHead { RequestHead { - uri: Uri::default(), method: Method::default(), + uri: Uri::default(), version: Version::HTTP_11, headers: HeaderMap::with_capacity(16), - flags: Flags::empty(), - peer_addr: None, extensions: RefCell::new(Extensions::new()), + peer_addr: None, + flags: Flags::empty(), } } } @@ -147,15 +152,16 @@ impl RequestHead { /// Connection upgrade status pub fn upgrade(&self) -> bool { - if let Some(hdr) = self.headers().get(header::CONNECTION) { - if let Ok(s) = hdr.to_str() { - s.to_ascii_lowercase().contains("upgrade") - } else { - false - } - } else { - false - } + self.headers() + .get(header::CONNECTION) + .map(|hdr| { + if let Ok(s) = hdr.to_str() { + s.to_ascii_lowercase().contains("upgrade") + } else { + false + } + }) + .unwrap_or(false) } #[inline] @@ -186,6 +192,7 @@ impl RequestHead { } #[derive(Debug)] +#[allow(clippy::large_enum_variant)] pub enum RequestHeadType { Owned(RequestHead), Rc(Rc, Option), @@ -203,7 +210,7 @@ impl RequestHeadType { impl AsRef for RequestHeadType { fn as_ref(&self) -> &RequestHead { match self { - RequestHeadType::Owned(head) => &head, + RequestHeadType::Owned(head) => head, RequestHeadType::Rc(head, _) => head.as_ref(), } } @@ -288,14 +295,14 @@ impl ResponseHead { } } - #[inline] /// Check if keep-alive is enabled + #[inline] pub fn keep_alive(&self) -> bool { self.connection_type() == ConnectionType::KeepAlive } - #[inline] /// Check upgrade status of this message + #[inline] pub fn upgrade(&self) -> bool { self.connection_type() == ConnectionType::Upgrade } @@ -303,17 +310,15 @@ impl ResponseHead { /// Get custom reason for the response #[inline] pub fn reason(&self) -> &str { - if let Some(reason) = self.reason { - reason - } else { + self.reason.unwrap_or_else(|| { self.status .canonical_reason() .unwrap_or("") - } + }) } #[inline] - pub(crate) fn ctype(&self) -> Option { + pub(crate) fn conn_type(&self) -> Option { if self.flags.contains(Flags::CLOSE) { Some(ConnectionType::Close) } else if self.flags.contains(Flags::KEEP_ALIVE) { @@ -343,21 +348,15 @@ impl ResponseHead { } pub struct Message { + /// Rc here should not be cloned by anyone. + /// It's used to reuse allocation of T and no shared ownership is allowed. head: Rc, } impl Message { /// Get new message from the pool of objects pub fn new() -> Self { - T::with_pool(|p| p.get_message()) - } -} - -impl Clone for Message { - fn clone(&self) -> Self { - Message { - head: self.head.clone(), - } + T::with_pool(MessagePool::get_message) } } @@ -365,7 +364,7 @@ impl std::ops::Deref for Message { type Target = T; fn deref(&self) -> &Self::Target { - &self.head.as_ref() + self.head.as_ref() } } @@ -377,9 +376,7 @@ impl std::ops::DerefMut for Message { impl Drop for Message { fn drop(&mut self) { - if Rc::strong_count(&self.head) == 1 { - T::with_pool(|p| p.release(self.head.clone())) - } + T::with_pool(|p| p.release(self.head.clone())) } } @@ -392,12 +389,6 @@ impl BoxedResponseHead { pub fn new(status: StatusCode) -> Self { RESPONSE_POOL.with(|p| p.get_message(status)) } - - pub(crate) fn take(&mut self) -> Self { - BoxedResponseHead { - head: self.head.take(), - } - } } impl std::ops::Deref for BoxedResponseHead { diff --git a/actix-http/src/request.rs b/actix-http/src/request.rs index 1031d7dce..401e9745c 100644 --- a/actix-http/src/request.rs +++ b/actix-http/src/request.rs @@ -2,18 +2,20 @@ use std::{ cell::{Ref, RefMut}, - fmt, net, + fmt, net, str, }; use http::{header, Method, Uri, Version}; -use crate::extensions::Extensions; -use crate::header::HeaderMap; -use crate::httpmessage::HttpMessage; -use crate::message::{Message, RequestHead}; -use crate::payload::{Payload, PayloadStream}; +use crate::{ + extensions::Extensions, + header::HeaderMap, + message::{Message, RequestHead}, + payload::{Payload, PayloadStream}, + HttpMessage, +}; -/// Request +/// An HTTP request. pub struct Request

{ pub(crate) payload: Payload

, pub(crate) head: Message, @@ -107,7 +109,7 @@ impl

Request

{ #[inline] #[doc(hidden)] - /// Mutable reference to a http message part of the request + /// Mutable reference to a HTTP message part of the request pub fn head_mut(&mut self) -> &mut RequestHead { &mut *self.head } @@ -158,10 +160,12 @@ impl

Request

{ self.head().method == Method::CONNECT } - /// Peer socket address + /// Peer socket address. /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. + /// Peer address is the directly connected peer's socket address. If a proxy is used in front of + /// the Actix Web server, then it would be address of this proxy. + /// + /// Will only return None when called in unit tests. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -177,13 +181,17 @@ impl

fmt::Debug for Request

{ self.method(), self.path() )?; + if let Some(q) = self.uri().query().as_ref() { writeln!(f, " query: ?{:?}", q)?; } + writeln!(f, " headers:")?; - for (key, val) in self.headers() { + + for (key, val) in self.headers().iter() { writeln!(f, " {:?}: {:?}", key, val)?; } + Ok(()) } } diff --git a/actix-http/src/response.rs b/actix-http/src/response.rs index 110514e05..ad41094ae 100644 --- a/actix-http/src/response.rs +++ b/actix-http/src/response.rs @@ -1,274 +1,213 @@ -//! HTTP responses. +//! HTTP response. use std::{ cell::{Ref, RefMut}, - convert::TryInto, - fmt, - future::Future, - ops, - pin::Pin, - str, - task::{Context, Poll}, + fmt, str, }; use bytes::{Bytes, BytesMut}; -use futures_core::Stream; -use serde::Serialize; +use bytestring::ByteString; -use crate::body::{Body, BodyStream, MessageBody, ResponseBody}; -use crate::cookie::{Cookie, CookieJar}; -use crate::error::Error; -use crate::extensions::Extensions; -use crate::header::{IntoHeaderPair, IntoHeaderValue}; -use crate::http::header::{self, HeaderName, HeaderValue}; -use crate::http::{Error as HttpError, HeaderMap, StatusCode}; -use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead}; +use crate::{ + body::{BoxBody, MessageBody}, + extensions::Extensions, + header::{self, IntoHeaderValue}, + http::{HeaderMap, StatusCode}, + message::{BoxedResponseHead, ResponseHead}, + Error, ResponseBuilder, +}; -/// An HTTP Response -pub struct Response { - head: BoxedResponseHead, - body: ResponseBody, - error: Option, +/// An HTTP response. +pub struct Response { + pub(crate) head: BoxedResponseHead, + pub(crate) body: B, } -impl Response { - /// Create http response builder with specific status. +impl Response { + /// Constructs a new response with default body. + #[inline] + pub fn new(status: StatusCode) -> Self { + Response { + head: BoxedResponseHead::new(status), + body: BoxBody::new(()), + } + } + + /// Constructs a new response builder. #[inline] pub fn build(status: StatusCode) -> ResponseBuilder { ResponseBuilder::new(status) } - /// Create http response builder + // just a couple frequently used shortcuts + // this list should not grow larger than a few + + /// Constructs a new response with status 200 OK. #[inline] - pub fn build_from>(source: T) -> ResponseBuilder { - source.into() + pub fn ok() -> Self { + Response::new(StatusCode::OK) } - /// Constructs a response + /// Constructs a new response with status 400 Bad Request. #[inline] - pub fn new(status: StatusCode) -> Response { - Response { - head: BoxedResponseHead::new(status), - body: ResponseBody::Body(Body::Empty), - error: None, - } + pub fn bad_request() -> Self { + Response::new(StatusCode::BAD_REQUEST) } - /// Constructs an error response + /// Constructs a new response with status 404 Not Found. #[inline] - pub fn from_error(error: Error) -> Response { - let mut resp = error.as_response_error().error_response(); - if resp.head.status == StatusCode::INTERNAL_SERVER_ERROR { - error!("Internal Server Error: {:?}", error); - } - resp.error = Some(error); - resp + pub fn not_found() -> Self { + Response::new(StatusCode::NOT_FOUND) } - /// Convert response to response with body - pub fn into_body(self) -> Response { - let b = match self.body { - ResponseBody::Body(b) => b, - ResponseBody::Other(b) => b, - }; - Response { - head: self.head, - error: self.error, - body: ResponseBody::Other(b), - } + /// Constructs a new response with status 500 Internal Server Error. + #[inline] + pub fn internal_server_error() -> Self { + Response::new(StatusCode::INTERNAL_SERVER_ERROR) } + + // end shortcuts } impl Response { - /// Constructs a response with body + /// Constructs a new response with given body. #[inline] pub fn with_body(status: StatusCode, body: B) -> Response { Response { head: BoxedResponseHead::new(status), - body: ResponseBody::Body(body), - error: None, + body, } } + /// Returns a reference to the head of this response. #[inline] - /// Http message part of the response pub fn head(&self) -> &ResponseHead { &*self.head } + /// Returns a mutable reference to the head of this response. #[inline] - /// Mutable reference to a http message part of the response pub fn head_mut(&mut self) -> &mut ResponseHead { &mut *self.head } - /// The source `error` for this response - #[inline] - pub fn error(&self) -> Option<&Error> { - self.error.as_ref() - } - - /// Get the response status code + /// Returns the status code of this response. #[inline] pub fn status(&self) -> StatusCode { self.head.status } - /// Set the `StatusCode` for this response + /// Returns a mutable reference the status code of this response. #[inline] pub fn status_mut(&mut self) -> &mut StatusCode { &mut self.head.status } - /// Get the headers from the response + /// Returns a reference to response headers. #[inline] pub fn headers(&self) -> &HeaderMap { &self.head.headers } - /// Get a mutable reference to the headers + /// Returns a mutable reference to response headers. #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { &mut self.head.headers } - /// Get an iterator for the cookies set by this response - #[inline] - pub fn cookies(&self) -> CookieIter<'_> { - CookieIter { - iter: self.head.headers.get_all(header::SET_COOKIE), - } - } - - /// Add a cookie to this response - #[inline] - pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { - let h = &mut self.head.headers; - HeaderValue::from_str(&cookie.to_string()) - .map(|c| { - h.append(header::SET_COOKIE, c); - }) - .map_err(|e| e.into()) - } - - /// Remove all cookies with the given name from this response. Returns - /// the number of cookies removed. - #[inline] - pub fn del_cookie(&mut self, name: &str) -> usize { - let h = &mut self.head.headers; - let vals: Vec = h - .get_all(header::SET_COOKIE) - .map(|v| v.to_owned()) - .collect(); - h.remove(header::SET_COOKIE); - - let mut count: usize = 0; - for v in vals { - if let Ok(s) = v.to_str() { - if let Ok(c) = Cookie::parse_encoded(s) { - if c.name() == name { - count += 1; - continue; - } - } - } - h.append(header::SET_COOKIE, v); - } - count - } - - /// Connection upgrade status + /// Returns true if connection upgrade is enabled. #[inline] pub fn upgrade(&self) -> bool { self.head.upgrade() } - /// Keep-alive status for this connection + /// Returns true if keep-alive is enabled. pub fn keep_alive(&self) -> bool { self.head.keep_alive() } - /// Responses extensions + /// Returns a reference to the extensions of this response. #[inline] pub fn extensions(&self) -> Ref<'_, Extensions> { self.head.extensions.borrow() } - /// Mutable reference to a the response's extensions + /// Returns a mutable reference to the extensions of this response. #[inline] pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { self.head.extensions.borrow_mut() } - /// Get body of this response + /// Returns a reference to the body of this response. #[inline] - pub fn body(&self) -> &ResponseBody { + pub fn body(&self) -> &B { &self.body } - /// Set a body + /// Sets new body. pub fn set_body(self, body: B2) -> Response { Response { head: self.head, - body: ResponseBody::Body(body), - error: None, + body, } } - /// Split response and body - pub fn into_parts(self) -> (Response<()>, ResponseBody) { - ( - Response { - head: self.head, - body: ResponseBody::Body(()), - error: self.error, - }, - self.body, - ) - } - - /// Drop request's body + /// Drops body and returns new response. pub fn drop_body(self) -> Response<()> { - Response { - head: self.head, - body: ResponseBody::Body(()), - error: None, - } + self.set_body(()) } - /// Set a body and return previous body value - pub(crate) fn replace_body(self, body: B2) -> (Response, ResponseBody) { + /// Sets new body, returning new response and previous body value. + pub(crate) fn replace_body(self, body: B2) -> (Response, B) { ( Response { head: self.head, - body: ResponseBody::Body(body), - error: self.error, + body, }, self.body, ) } - /// Set a body and return previous body value + /// Returns split head and body. + /// + /// # Implementation Notes + /// Due to internal performance optimisations, the first element of the returned tuple is a + /// `Response` as well but only contains the head of the response this was called on. + pub fn into_parts(self) -> (Response<()>, B) { + self.replace_body(()) + } + + /// Returns new response with mapped body. pub fn map_body(mut self, f: F) -> Response where - F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, + F: FnOnce(&mut ResponseHead, B) -> B2, { let body = f(&mut self.head, self.body); Response { - body, head: self.head, - error: self.error, + body, } } - /// Extract response body - pub fn take_body(&mut self) -> ResponseBody { - self.body.take_body() + #[inline] + pub fn map_into_boxed_body(self) -> Response + where + B: MessageBody + 'static, + { + self.map_body(|_, body| BoxBody::new(body)) + } + + /// Returns body, consuming this response. + pub fn into_body(self) -> B { + self.body } } -impl fmt::Debug for Response { +impl fmt::Debug for Response +where + B: MessageBody, +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let res = writeln!( f, @@ -286,587 +225,110 @@ impl fmt::Debug for Response { } } -impl Future for Response { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { - Poll::Ready(Ok(Response { - head: self.head.take(), - body: self.body.take_body(), - error: self.error.take(), - })) +impl Default for Response { + #[inline] + fn default() -> Response { + Response::with_body(StatusCode::default(), B::default()) } } -pub struct CookieIter<'a> { - iter: header::GetAll<'a>, -} - -impl<'a> Iterator for CookieIter<'a> { - type Item = Cookie<'a>; - - #[inline] - fn next(&mut self) -> Option> { - for v in self.iter.by_ref() { - if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { - return Some(c); - } - } - None - } -} - -/// An HTTP response builder -/// -/// This type can be used to construct an instance of `Response` through a -/// builder-like pattern. -pub struct ResponseBuilder { - head: Option, - err: Option, - cookies: Option, -} - -impl ResponseBuilder { - #[inline] - /// Create response builder - pub fn new(status: StatusCode) -> Self { - ResponseBuilder { - head: Some(BoxedResponseHead::new(status)), - err: None, - cookies: None, - } - } - - /// Set HTTP status code of this response. - #[inline] - pub fn status(&mut self, status: StatusCode) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.status = status; - } - self - } - - /// Insert a header, replacing any that were set with an equivalent field name. - /// - /// ```rust - /// # use actix_http::Response; - /// use actix_http::http::header::ContentType; - /// - /// Response::Ok() - /// .insert_header(ContentType(mime::APPLICATION_JSON)) - /// .insert_header(("X-TEST", "value")) - /// .finish(); - /// ``` - pub fn insert_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match header.try_into_header_pair() { - Ok((key, value)) => parts.headers.insert(key, value), - Err(e) => self.err = Some(e.into()), - }; - } - - self - } - - /// Append a header, keeping any that were set with an equivalent field name. - /// - /// ```rust - /// # use actix_http::Response; - /// use actix_http::http::header::ContentType; - /// - /// Response::Ok() - /// .append_header(ContentType(mime::APPLICATION_JSON)) - /// .append_header(("X-TEST", "value1")) - /// .append_header(("X-TEST", "value2")) - /// .finish(); - /// ``` - pub fn append_header(&mut self, header: H) -> &mut Self - where - H: IntoHeaderPair, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match header.try_into_header_pair() { - Ok((key, value)) => parts.headers.append(key, value), - Err(e) => self.err = Some(e.into()), - }; - } - - self - } - - /// Replaced with [`Self::insert_header()`]. - #[deprecated = "Replaced with `insert_header((key, value))`."] - pub fn set_header(&mut self, key: K, value: V) -> &mut Self - where - K: TryInto, - K::Error: Into, - V: IntoHeaderValue, - { - if self.err.is_some() { - return self; - } - - match (key.try_into(), value.try_into_value()) { - (Ok(name), Ok(value)) => return self.insert_header((name, value)), - (Err(err), _) => self.err = Some(err.into()), - (_, Err(err)) => self.err = Some(err.into()), - } - - self - } - - /// Replaced with [`Self::append_header()`]. - #[deprecated = "Replaced with `append_header((key, value))`."] - pub fn header(&mut self, key: K, value: V) -> &mut Self - where - K: TryInto, - K::Error: Into, - V: IntoHeaderValue, - { - if self.err.is_some() { - return self; - } - - match (key.try_into(), value.try_into_value()) { - (Ok(name), Ok(value)) => return self.append_header((name, value)), - (Err(err), _) => self.err = Some(err.into()), - (_, Err(err)) => self.err = Some(err.into()), - } - - self - } - - /// Set the custom reason for the response. - #[inline] - pub fn reason(&mut self, reason: &'static str) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.reason = Some(reason); - } - self - } - - /// Set connection type to KeepAlive - #[inline] - pub fn keep_alive(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.set_connection_type(ConnectionType::KeepAlive); - } - self - } - - /// Set connection type to Upgrade - #[inline] - pub fn upgrade(&mut self, value: V) -> &mut Self - where - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.set_connection_type(ConnectionType::Upgrade); - } - - if let Ok(value) = value.try_into_value() { - self.insert_header((header::UPGRADE, value)); - } - - self - } - - /// Force close connection, even if it is marked as keep-alive - #[inline] - pub fn force_close(&mut self) -> &mut Self { - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.set_connection_type(ConnectionType::Close); - } - self - } - - /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. - #[inline] - pub fn no_chunking(&mut self, len: u64) -> &mut Self { - self.insert_header((header::CONTENT_LENGTH, len)); - - if let Some(parts) = parts(&mut self.head, &self.err) { - parts.no_chunking(true); - } - self - } - - /// Set response content type. - #[inline] - pub fn content_type(&mut self, value: V) -> &mut Self - where - V: IntoHeaderValue, - { - if let Some(parts) = parts(&mut self.head, &self.err) { - match value.try_into_value() { - Ok(value) => { - parts.headers.insert(header::CONTENT_TYPE, value); - } - Err(e) => self.err = Some(e.into()), - }; - } - self - } - - /// Set a cookie - /// - /// ```rust - /// use actix_http::{http, Request, Response}; - /// - /// fn index(req: Request) -> Response { - /// Response::Ok() - /// .cookie( - /// http::Cookie::build("name", "value") - /// .domain("www.rust-lang.org") - /// .path("/") - /// .secure(true) - /// .http_only(true) - /// .finish(), - /// ) - /// .finish() - /// } - /// ``` - pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { - if self.cookies.is_none() { - let mut jar = CookieJar::new(); - jar.add(cookie.into_owned()); - self.cookies = Some(jar) - } else { - self.cookies.as_mut().unwrap().add(cookie.into_owned()); - } - self - } - - /// Remove cookie - /// - /// ```rust - /// use actix_http::{http, Request, Response, HttpMessage}; - /// - /// fn index(req: Request) -> Response { - /// let mut builder = Response::Ok(); - /// - /// if let Some(ref cookie) = req.cookie("name") { - /// builder.del_cookie(cookie); - /// } - /// - /// builder.finish() - /// } - /// ``` - pub fn del_cookie<'a>(&mut self, cookie: &Cookie<'a>) -> &mut Self { - if self.cookies.is_none() { - self.cookies = Some(CookieJar::new()) - } - let jar = self.cookies.as_mut().unwrap(); - let cookie = cookie.clone().into_owned(); - jar.add_original(cookie.clone()); - jar.remove(cookie); - self - } - - /// This method calls provided closure with builder reference if value is `true`. - #[doc(hidden)] - #[deprecated = "Use an if statement."] - pub fn if_true(&mut self, value: bool, f: F) -> &mut Self - where - F: FnOnce(&mut ResponseBuilder), - { - if value { - f(self); - } - self - } - - /// This method calls provided closure with builder reference if value is `Some`. - #[doc(hidden)] - #[deprecated = "Use an if-let construction."] - pub fn if_some(&mut self, value: Option, f: F) -> &mut Self - where - F: FnOnce(T, &mut ResponseBuilder), - { - if let Some(val) = value { - f(val, self); - } - self - } - - /// Responses extensions - #[inline] - pub fn extensions(&self) -> Ref<'_, Extensions> { - let head = self.head.as_ref().expect("cannot reuse response builder"); - head.extensions.borrow() - } - - /// Mutable reference to a the response's extensions - #[inline] - pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { - let head = self.head.as_ref().expect("cannot reuse response builder"); - head.extensions.borrow_mut() - } - - #[inline] - /// Set a body and generate `Response`. - /// - /// `ResponseBuilder` can not be used after this call. - pub fn body>(&mut self, body: B) -> Response { - self.message_body(body.into()) - } - - /// Set a body and generate `Response`. - /// - /// `ResponseBuilder` can not be used after this call. - pub fn message_body(&mut self, body: B) -> Response { - if let Some(e) = self.err.take() { - return Response::from(Error::from(e)).into_body(); - } - - let mut response = self.head.take().expect("cannot reuse response builder"); - - if let Some(ref jar) = self.cookies { - for cookie in jar.delta() { - match HeaderValue::from_str(&cookie.to_string()) { - Ok(val) => response.headers.append(header::SET_COOKIE, val), - Err(e) => return Response::from(Error::from(e)).into_body(), - }; - } - } - - Response { - head: response, - body: ResponseBody::Body(body), - error: None, - } - } - - #[inline] - /// Set a streaming body and generate `Response`. - /// - /// `ResponseBuilder` can not be used after this call. - pub fn streaming(&mut self, stream: S) -> Response - where - S: Stream> + Unpin + 'static, - E: Into + 'static, - { - self.body(Body::from_message(BodyStream::new(stream))) - } - - /// Set a json body and generate `Response` - /// - /// `ResponseBuilder` can not be used after this call. - pub fn json(&mut self, value: T) -> Response - where - T: ops::Deref, - T::Target: Serialize, - { - match serde_json::to_string(&*value) { - Ok(body) => { - let contains = if let Some(parts) = parts(&mut self.head, &self.err) { - parts.headers.contains_key(header::CONTENT_TYPE) - } else { - true - }; - - if !contains { - self.insert_header(header::ContentType(mime::APPLICATION_JSON)); - } - - self.body(Body::from(body)) - } - Err(e) => Error::from(e).into(), - } - } - - #[inline] - /// Set an empty body and generate `Response` - /// - /// `ResponseBuilder` can not be used after this call. - pub fn finish(&mut self) -> Response { - self.body(Body::Empty) - } - - /// This method construct new `ResponseBuilder` - pub fn take(&mut self) -> ResponseBuilder { - ResponseBuilder { - head: self.head.take(), - err: self.err.take(), - cookies: self.cookies.take(), +impl>, E: Into> From> + for Response +{ + fn from(res: Result) -> Self { + match res { + Ok(val) => val.into(), + Err(err) => Response::from(err.into()), } } } -#[inline] -fn parts<'a>( - parts: &'a mut Option, - err: &Option, -) -> Option<&'a mut ResponseHead> { - if err.is_some() { - return None; - } - parts.as_mut().map(|r| &mut **r) -} - -/// Convert `Response` to a `ResponseBuilder`. Body get dropped. -impl From> for ResponseBuilder { - fn from(res: Response) -> ResponseBuilder { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - for c in res.cookies() { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - ResponseBuilder { - head: Some(res.head), - err: None, - cookies: jar, - } +impl From for Response { + fn from(mut builder: ResponseBuilder) -> Self { + builder.finish().map_into_boxed_body() } } -/// Convert `ResponseHead` to a `ResponseBuilder` -impl<'a> From<&'a ResponseHead> for ResponseBuilder { - fn from(head: &'a ResponseHead) -> ResponseBuilder { - // If this response has cookies, load them into a jar - let mut jar: Option = None; - - let cookies = CookieIter { - iter: head.headers.get_all(header::SET_COOKIE), - }; - for c in cookies { - if let Some(ref mut j) = jar { - j.add_original(c.into_owned()); - } else { - let mut j = CookieJar::new(); - j.add_original(c.into_owned()); - jar = Some(j); - } - } - - let mut msg = BoxedResponseHead::new(head.status); - msg.version = head.version; - msg.reason = head.reason; - for (k, v) in &head.headers { - msg.headers.append(k.clone(), v.clone()); - } - msg.no_chunking(!head.chunked()); - - ResponseBuilder { - head: Some(msg), - err: None, - cookies: jar, - } +impl From for Response { + fn from(val: std::convert::Infallible) -> Self { + match val {} } } -impl Future for ResponseBuilder { - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { - Poll::Ready(Ok(self.finish())) - } -} - -impl fmt::Debug for ResponseBuilder { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - let head = self.head.as_ref().unwrap(); - - let res = writeln!( - f, - "\nResponseBuilder {:?} {}{}", - head.version, - head.status, - head.reason.unwrap_or(""), - ); - let _ = writeln!(f, " headers:"); - for (key, val) in head.headers.iter() { - let _ = writeln!(f, " {:?}: {:?}", key, val); - } +impl From<&'static str> for Response<&'static str> { + fn from(val: &'static str) -> Self { + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); res } } -/// Helper converters -impl, E: Into> From> for Response { - fn from(res: Result) -> Self { - match res { - Ok(val) => val.into(), - Err(err) => err.into().into(), - } - } -} - -impl From for Response { - fn from(mut builder: ResponseBuilder) -> Self { - builder.finish() - } -} - -impl From<&'static str> for Response { - fn from(val: &'static str) -> Self { - Response::Ok() - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) - } -} - -impl From<&'static [u8]> for Response { +impl From<&'static [u8]> for Response<&'static [u8]> { fn from(val: &'static [u8]) -> Self { - Response::Ok() - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: String) -> Self { - Response::Ok() - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl<'a> From<&'a String> for Response { - fn from(val: &'a String) -> Self { - Response::Ok() - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(val) +impl From<&String> for Response { + fn from(val: &String) -> Self { + let mut res = Response::with_body(StatusCode::OK, val.clone()); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: Bytes) -> Self { - Response::Ok() - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } -impl From for Response { +impl From for Response { fn from(val: BytesMut) -> Self { - Response::Ok() - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(val) + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res + } +} + +impl From for Response { + fn from(val: ByteString) -> Self { + let mut res = Response::with_body(StatusCode::OK, val); + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + res } } #[cfg(test)] mod tests { - use serde_json::json; - use super::*; - use crate::body::Body; - use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE}; + use crate::{ + body::to_bytes, + http::header::{HeaderValue, CONTENT_TYPE, COOKIE}, + }; #[test] fn test_debug() { - let resp = Response::Ok() + let resp = Response::build(StatusCode::OK) .append_header((COOKIE, HeaderValue::from_static("cookie1=value1; "))) .append_header((COOKIE, HeaderValue::from_static("cookie2=value2; "))) .finish(); @@ -874,255 +336,73 @@ mod tests { assert!(dbg.contains("Response")); } - #[test] - fn test_response_cookies() { - use crate::httpmessage::HttpMessage; - - let req = crate::test::TestRequest::default() - .append_header((COOKIE, "cookie1=value1")) - .append_header((COOKIE, "cookie2=value2")) - .finish(); - let cookies = req.cookies().unwrap(); - - let resp = Response::Ok() - .cookie( - crate::http::Cookie::build("name", "value") - .domain("www.rust-lang.org") - .path("/test") - .http_only(true) - .max_age(time::Duration::days(1)) - .finish(), - ) - .del_cookie(&cookies[1]) - .finish(); - - let mut val: Vec<_> = resp - .headers() - .get_all(SET_COOKIE) - .map(|v| v.to_str().unwrap().to_owned()) - .collect(); - val.sort(); - assert!(val[0].starts_with("cookie1=; Max-Age=0;")); + #[actix_rt::test] + async fn test_into_response() { + let res = Response::from("test"); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - val[1], - "name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400" - ); - } - - #[test] - fn test_update_response_cookies() { - let mut r = Response::Ok() - .cookie(crate::http::Cookie::new("original", "val100")) - .finish(); - - r.add_cookie(&crate::http::Cookie::new("cookie2", "val200")) - .unwrap(); - r.add_cookie(&crate::http::Cookie::new("cookie2", "val250")) - .unwrap(); - r.add_cookie(&crate::http::Cookie::new("cookie3", "val300")) - .unwrap(); - - assert_eq!(r.cookies().count(), 4); - r.del_cookie("cookie2"); - - let mut iter = r.cookies(); - let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("cookie3", "val300")); - let v = iter.next().unwrap(); - assert_eq!((v.name(), v.value()), ("original", "val100")); - } - - #[test] - fn test_basic_builder() { - let resp = Response::Ok().insert_header(("X-TEST", "value")).finish(); - assert_eq!(resp.status(), StatusCode::OK); - } - - #[test] - fn test_upgrade() { - let resp = Response::build(StatusCode::OK) - .upgrade("websocket") - .finish(); - assert!(resp.upgrade()); - assert_eq!( - resp.headers().get(header::UPGRADE).unwrap(), - HeaderValue::from_static("websocket") - ); - } - - #[test] - fn test_force_close() { - let resp = Response::build(StatusCode::OK).force_close().finish(); - assert!(!resp.keep_alive()) - } - - #[test] - fn test_content_type() { - let resp = Response::build(StatusCode::OK) - .content_type("text/plain") - .body(Body::Empty); - assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") - } - - #[test] - fn test_json() { - let resp = Response::build(StatusCode::OK).json(vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("application/json")); - assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); - } - - #[test] - fn test_json_ct() { - let resp = Response::build(StatusCode::OK) - .insert_header((CONTENT_TYPE, "text/json")) - .json(&vec!["v1", "v2", "v3"]); - let ct = resp.headers().get(CONTENT_TYPE).unwrap(); - assert_eq!(ct, HeaderValue::from_static("text/json")); - assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); - } - - #[test] - fn test_serde_json_in_body() { - use serde_json::json; - let resp = - Response::build(StatusCode::OK).body(json!({"test-key":"test-value"})); - assert_eq!(resp.body().get_ref(), br#"{"test-key":"test-value"}"#); - } - - #[test] - fn test_into_response() { - let resp: Response = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = b"test".as_ref().into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b"test".as_ref()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = "test".to_owned().into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from("test".to_owned()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); - let resp: Response = (&"test".to_owned()).into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from("test".to_owned()); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = Bytes::from_static(b"test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); let b = BytesMut::from("test"); - let resp: Response = b.into(); - assert_eq!(resp.status(), StatusCode::OK); + let res = Response::from(b); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().get_ref(), b"test"); - } - - #[test] - fn test_into_builder() { - let mut resp: Response = "test".into(); - assert_eq!(resp.status(), StatusCode::OK); - - resp.add_cookie(&crate::http::Cookie::new("cookie1", "val100")) - .unwrap(); - - let mut builder: ResponseBuilder = resp.into(); - let resp = builder.status(StatusCode::BAD_REQUEST).finish(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - - let cookie = resp.cookies().next().unwrap(); - assert_eq!((cookie.name(), cookie.value()), ("cookie1", "val100")); - } - - #[test] - fn response_builder_header_insert_kv() { - let mut res = Response::Ok(); - res.insert_header(("Content-Type", "application/octet-stream")); - let res = res.finish(); - - assert_eq!( - res.headers().get("Content-Type"), - Some(&HeaderValue::from_static("application/octet-stream")) - ); - } - - #[test] - fn response_builder_header_insert_typed() { - let mut res = Response::Ok(); - res.insert_header(header::ContentType(mime::APPLICATION_OCTET_STREAM)); - let res = res.finish(); - - assert_eq!( - res.headers().get("Content-Type"), - Some(&HeaderValue::from_static("application/octet-stream")) - ); - } - - #[test] - fn response_builder_header_append_kv() { - let mut res = Response::Ok(); - res.append_header(("Content-Type", "application/octet-stream")); - res.append_header(("Content-Type", "application/json")); - let res = res.finish(); - - let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); - assert_eq!(headers.len(), 2); - assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); - assert!(headers.contains(&HeaderValue::from_static("application/json"))); - } - - #[test] - fn response_builder_header_append_typed() { - let mut res = Response::Ok(); - res.append_header(header::ContentType(mime::APPLICATION_OCTET_STREAM)); - res.append_header(header::ContentType(mime::APPLICATION_JSON)); - let res = res.finish(); - - let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); - assert_eq!(headers.len(), 2); - assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); - assert!(headers.contains(&HeaderValue::from_static("application/json"))); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]); } } diff --git a/actix-http/src/response_builder.rs b/actix-http/src/response_builder.rs new file mode 100644 index 000000000..0537112d5 --- /dev/null +++ b/actix-http/src/response_builder.rs @@ -0,0 +1,449 @@ +//! HTTP response builder. + +use std::{ + cell::{Ref, RefMut}, + fmt, str, +}; + +use crate::{ + body::{EitherBody, MessageBody}, + error::{Error, HttpError}, + header::{self, IntoHeaderPair, IntoHeaderValue}, + message::{BoxedResponseHead, ConnectionType, ResponseHead}, + Extensions, Response, StatusCode, +}; + +/// An HTTP response builder. +/// +/// Used to construct an instance of `Response` using a builder pattern. Response builders are often +/// created using [`Response::build`]. +/// +/// # Examples +/// ``` +/// use actix_http::{Response, ResponseBuilder, body, http::StatusCode, http::header}; +/// +/// # actix_rt::System::new().block_on(async { +/// let mut res: Response<_> = Response::build(StatusCode::OK) +/// .content_type(mime::APPLICATION_JSON) +/// .insert_header((header::SERVER, "my-app/1.0")) +/// .append_header((header::SET_COOKIE, "a=1")) +/// .append_header((header::SET_COOKIE, "b=2")) +/// .body("1234"); +/// +/// assert_eq!(res.status(), StatusCode::OK); +/// +/// assert!(res.headers().contains_key("server")); +/// assert_eq!(res.headers().get_all("set-cookie").count(), 2); +/// +/// assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), &b"1234"[..]); +/// # }) +/// ``` +pub struct ResponseBuilder { + head: Option, + err: Option, +} + +impl ResponseBuilder { + /// Create response builder + /// + /// # Examples + /// ``` + /// use actix_http::{Response, ResponseBuilder, http::StatusCode}; + /// + /// let res: Response<_> = ResponseBuilder::default().finish(); + /// assert_eq!(res.status(), StatusCode::OK); + /// ``` + #[inline] + pub fn new(status: StatusCode) -> Self { + ResponseBuilder { + head: Some(BoxedResponseHead::new(status)), + err: None, + } + } + + /// Set HTTP status code of this response. + /// + /// # Examples + /// ``` + /// use actix_http::{ResponseBuilder, http::StatusCode}; + /// + /// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish(); + /// assert_eq!(res.status(), StatusCode::NOT_FOUND); + /// ``` + #[inline] + pub fn status(&mut self, status: StatusCode) -> &mut Self { + if let Some(parts) = self.inner() { + parts.status = status; + } + self + } + + /// Insert a header, replacing any that were set with an equivalent field name. + /// + /// # Examples + /// ``` + /// use actix_http::{ResponseBuilder, http::header}; + /// + /// let res = ResponseBuilder::default() + /// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) + /// .insert_header(("X-TEST", "value")) + /// .finish(); + /// + /// assert!(res.headers().contains_key("content-type")); + /// assert!(res.headers().contains_key("x-test")); + /// ``` + pub fn insert_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + { + if let Some(parts) = self.inner() { + match header.try_into_header_pair() { + Ok((key, value)) => { + parts.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + + self + } + + /// Append a header, keeping any that were set with an equivalent field name. + /// + /// # Examples + /// ``` + /// use actix_http::{ResponseBuilder, http::header}; + /// + /// let res = ResponseBuilder::default() + /// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) + /// .append_header(("X-TEST", "value1")) + /// .append_header(("X-TEST", "value2")) + /// .finish(); + /// + /// assert_eq!(res.headers().get_all("content-type").count(), 1); + /// assert_eq!(res.headers().get_all("x-test").count(), 2); + /// ``` + pub fn append_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + { + if let Some(parts) = self.inner() { + match header.try_into_header_pair() { + Ok((key, value)) => parts.headers.append(key, value), + Err(e) => self.err = Some(e.into()), + }; + } + + self + } + + /// Set the custom reason for the response. + #[inline] + pub fn reason(&mut self, reason: &'static str) -> &mut Self { + if let Some(parts) = self.inner() { + parts.reason = Some(reason); + } + self + } + + /// Set connection type to KeepAlive + #[inline] + pub fn keep_alive(&mut self) -> &mut Self { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::KeepAlive); + } + self + } + + /// Set connection type to Upgrade + #[inline] + pub fn upgrade(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::Upgrade); + } + + if let Ok(value) = value.try_into_value() { + self.insert_header((header::UPGRADE, value)); + } + + self + } + + /// Force close connection, even if it is marked as keep-alive + #[inline] + pub fn force_close(&mut self) -> &mut Self { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::Close); + } + self + } + + /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. + #[inline] + pub fn no_chunking(&mut self, len: u64) -> &mut Self { + let mut buf = itoa::Buffer::new(); + self.insert_header((header::CONTENT_LENGTH, buf.format(len))); + + if let Some(parts) = self.inner() { + parts.no_chunking(true); + } + self + } + + /// Set response content type. + #[inline] + pub fn content_type(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + if let Some(parts) = self.inner() { + match value.try_into_value() { + Ok(value) => { + parts.headers.insert(header::CONTENT_TYPE, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref<'_, Extensions> { + let head = self.head.as_ref().expect("cannot reuse response builder"); + head.extensions.borrow() + } + + /// Mutable reference to a the response's extensions + #[inline] + pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { + let head = self.head.as_ref().expect("cannot reuse response builder"); + head.extensions.borrow_mut() + } + + /// Generate response with a wrapped body. + /// + /// This `ResponseBuilder` will be left in a useless state. + pub fn body(&mut self, body: B) -> Response> + where + B: MessageBody + 'static, + { + match self.message_body(body) { + Ok(res) => res.map_body(|_, body| EitherBody::left(body)), + Err(err) => Response::from(err).map_body(|_, body| EitherBody::right(body)), + } + } + + /// Generate response with a body. + /// + /// This `ResponseBuilder` will be left in a useless state. + pub fn message_body(&mut self, body: B) -> Result, Error> { + if let Some(err) = self.err.take() { + return Err(Error::new_http().with_cause(err)); + } + + let head = self.head.take().expect("cannot reuse response builder"); + Ok(Response { head, body }) + } + + /// Generate response with an empty body. + /// + /// This `ResponseBuilder` will be left in a useless state. + #[inline] + pub fn finish(&mut self) -> Response> { + self.body(()) + } + + /// Create an owned `ResponseBuilder`, leaving the original in a useless state. + pub fn take(&mut self) -> ResponseBuilder { + ResponseBuilder { + head: self.head.take(), + err: self.err.take(), + } + } + + /// Get access to the inner response head if there has been no error. + fn inner(&mut self) -> Option<&mut ResponseHead> { + if self.err.is_some() { + return None; + } + + self.head.as_deref_mut() + } +} + +impl Default for ResponseBuilder { + fn default() -> Self { + Self::new(StatusCode::OK) + } +} + +/// Convert `Response` to a `ResponseBuilder`. Body get dropped. +impl From> for ResponseBuilder { + fn from(res: Response) -> ResponseBuilder { + ResponseBuilder { + head: Some(res.head), + err: None, + } + } +} + +/// Convert `ResponseHead` to a `ResponseBuilder` +impl<'a> From<&'a ResponseHead> for ResponseBuilder { + fn from(head: &'a ResponseHead) -> ResponseBuilder { + let mut msg = BoxedResponseHead::new(head.status); + msg.version = head.version; + msg.reason = head.reason; + + for (k, v) in head.headers.iter() { + msg.headers.append(k.clone(), v.clone()); + } + + msg.no_chunking(!head.chunked()); + + ResponseBuilder { + head: Some(msg), + err: None, + } + } +} + +impl fmt::Debug for ResponseBuilder { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + let head = self.head.as_ref().unwrap(); + + let res = writeln!( + f, + "\nResponseBuilder {:?} {}{}", + head.version, + head.status, + head.reason.unwrap_or(""), + ); + let _ = writeln!(f, " headers:"); + for (key, val) in head.headers.iter() { + let _ = writeln!(f, " {:?}: {:?}", key, val); + } + res + } +} + +#[cfg(test)] +mod tests { + use bytes::Bytes; + + use super::*; + use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE}; + + #[test] + fn test_basic_builder() { + let resp = Response::build(StatusCode::OK) + .insert_header(("X-TEST", "value")) + .finish(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_upgrade() { + let resp = Response::build(StatusCode::OK) + .upgrade("websocket") + .finish(); + assert!(resp.upgrade()); + assert_eq!( + resp.headers().get(header::UPGRADE).unwrap(), + HeaderValue::from_static("websocket") + ); + } + + #[test] + fn test_force_close() { + let resp = Response::build(StatusCode::OK).force_close().finish(); + assert!(!resp.keep_alive()); + } + + #[test] + fn test_content_type() { + let resp = Response::build(StatusCode::OK) + .content_type("text/plain") + .body(Bytes::new()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain"); + + let resp = Response::build(StatusCode::OK) + .content_type(mime::APPLICATION_JAVASCRIPT_UTF_8) + .body(Bytes::new()); + assert_eq!( + resp.headers().get(CONTENT_TYPE).unwrap(), + "application/javascript; charset=utf-8" + ); + } + + #[test] + fn test_into_builder() { + let mut resp: Response<_> = "test".into(); + assert_eq!(resp.status(), StatusCode::OK); + + resp.headers_mut().insert( + HeaderName::from_static("cookie"), + HeaderValue::from_static("cookie1=val100"), + ); + + let mut builder: ResponseBuilder = resp.into(); + let resp = builder.status(StatusCode::BAD_REQUEST).finish(); + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + + let cookie = resp.headers().get_all("Cookie").next().unwrap(); + assert_eq!(cookie.to_str().unwrap(), "cookie1=val100"); + } + + #[test] + fn response_builder_header_insert_kv() { + let mut res = Response::build(StatusCode::OK); + res.insert_header(("Content-Type", "application/octet-stream")); + let res = res.finish(); + + assert_eq!( + res.headers().get("Content-Type"), + Some(&HeaderValue::from_static("application/octet-stream")) + ); + } + + #[test] + fn response_builder_header_insert_typed() { + let mut res = Response::build(StatusCode::OK); + res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); + let res = res.finish(); + + assert_eq!( + res.headers().get("Content-Type"), + Some(&HeaderValue::from_static("application/octet-stream")) + ); + } + + #[test] + fn response_builder_header_append_kv() { + let mut res = Response::build(StatusCode::OK); + res.append_header(("Content-Type", "application/octet-stream")); + res.append_header(("Content-Type", "application/json")); + let res = res.finish(); + + let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); + assert_eq!(headers.len(), 2); + assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); + assert!(headers.contains(&HeaderValue::from_static("application/json"))); + } + + #[test] + fn response_builder_header_append_typed() { + let mut res = Response::build(StatusCode::OK); + res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); + res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); + let res = res.finish(); + + let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); + assert_eq!(headers.len(), 2); + assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); + assert!(headers.contains(&HeaderValue::from_static("application/json"))); + } +} diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 357ac4c53..7af34ba05 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -1,24 +1,28 @@ -use std::cell::RefCell; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use std::{fmt, net, rc::Rc}; +use std::{ + fmt, + future::Future, + marker::PhantomData, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_rt::net::TcpStream; -use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; -use bytes::Bytes; -use futures_core::{ready, Future}; -use h2::server::{self, Handshake}; -use pin_project::pin_project; +use actix_service::{ + fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, +}; +use futures_core::{future::LocalBoxFuture, ready}; +use pin_project_lite::pin_project; -use crate::body::MessageBody; -use crate::builder::HttpServiceBuilder; -use crate::config::{KeepAlive, ServiceConfig}; -use crate::error::{DispatchError, Error}; -use crate::request::Request; -use crate::response::Response; -use crate::{h1, h2::Dispatcher, ConnectCallback, OnConnectData, Protocol}; +use crate::{ + body::{BoxBody, MessageBody}, + builder::HttpServiceBuilder, + config::{KeepAlive, ServiceConfig}, + error::DispatchError, + h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response, +}; /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. pub struct HttpService { @@ -33,7 +37,7 @@ pub struct HttpService { impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -48,7 +52,7 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -87,7 +91,7 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, @@ -101,9 +105,8 @@ where pub fn expect(self, expect: X1) -> HttpService where X1: ServiceFactory, - X1::Error: Into, + X1::Error: Into>, X1::InitError: fmt::Debug, - >::Future: 'static, { HttpService { expect, @@ -124,7 +127,6 @@ where U1: ServiceFactory<(Request, Framed), Config = (), Response = ()>, U1::Error: fmt::Display, U1::InitError: fmt::Debug, - )>>::Future: 'static, { HttpService { upgrade, @@ -146,23 +148,27 @@ where impl HttpService where S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, - >::Future: 'static, + U: ServiceFactory< (Request, Framed), Config = (), Response = (), >, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, - )>>::Future: 'static, { /// Create simple tcp stream service pub fn tcp( @@ -174,7 +180,7 @@ where Error = DispatchError, InitError = (), > { - pipeline_factory(|io: TcpStream| async { + fn_service(|io: TcpStream| async { let peer_addr = io.peer_addr().ok(); Ok((io, Protocol::Http1, peer_addr)) }) @@ -184,33 +190,43 @@ where #[cfg(feature = "openssl")] mod openssl { - use super::*; - use actix_service::ServiceFactoryExt; - use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream}; - use actix_tls::accept::TlsError; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + openssl::{ + reexports::{Error as SslError, SslAcceptor}, + Acceptor, TlsStream, + }, + TlsError, + }; - impl HttpService, S, B, X, U> + use super::*; + + impl HttpService, S, B, X, U> where S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, - >::Future: 'static, + U: ServiceFactory< - (Request, Framed, h1::Codec>), + (Request, Framed, h1::Codec>), Config = (), Response = (), >, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, - , h1::Codec>)>>::Future: 'static, { - /// Create openssl based service + /// Create OpenSSL based service. pub fn openssl( self, acceptor: SslAcceptor, @@ -221,25 +237,26 @@ mod openssl { Error = TlsError, InitError = (), > { - pipeline_factory( - Acceptor::new(acceptor) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()), - ) - .and_then(|io: SslStream| async { - let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { - if protos.windows(2).any(|window| window == b"h2") { - Protocol::Http2 + Acceptor::new(acceptor) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .map(|io: TlsStream| { + let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { + if protos.windows(2).any(|window| window == b"h2") { + Protocol::Http2 + } else { + Protocol::Http1 + } } else { Protocol::Http1 - } - } else { - Protocol::Http1 - }; - let peer_addr = io.get_ref().peer_addr().ok(); - Ok((io, proto, peer_addr)) - }) - .and_then(self.map_err(TlsError::Service)) + }; + + let peer_addr = io.get_ref().peer_addr().ok(); + (io, proto, peer_addr) + }) + .and_then(self.map_err(TlsError::Service)) } } } @@ -248,34 +265,40 @@ mod openssl { mod rustls { use std::io; - use actix_tls::accept::rustls::{Acceptor, ServerConfig, Session, TlsStream}; - use actix_tls::accept::TlsError; + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; use super::*; - use actix_service::ServiceFactoryExt; impl HttpService, S, B, X, U> where S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, - >::Future: 'static, + U: ServiceFactory< (Request, Framed, h1::Codec>), Config = (), Response = (), >, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, - , h1::Codec>)>>::Future: 'static, { - /// Create openssl based service + /// Create Rustls based service. pub fn rustls( self, mut config: ServerConfig, @@ -286,28 +309,29 @@ mod rustls { Error = TlsError, InitError = (), > { - let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; - config.set_protocols(&protos); + let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); + config.alpn_protocols = protos; - pipeline_factory( - Acceptor::new(config) - .map_err(TlsError::Tls) - .map_init_err(|_| panic!()), - ) - .and_then(|io: TlsStream| async { - let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() { - if protos.windows(2).any(|window| window == b"h2") { - Protocol::Http2 + Acceptor::new(config) + .map_init_err(|_| { + unreachable!("TLS acceptor service factory does not error on init") + }) + .map_err(TlsError::into_service_error) + .and_then(|io: TlsStream| async { + let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() { + if protos.windows(2).any(|window| window == b"h2") { + Protocol::Http2 + } else { + Protocol::Http1 + } } else { Protocol::Http1 - } - } else { - Protocol::Http1 - }; - let peer_addr = io.get_ref().0.peer_addr().ok(); - Ok((io, proto, peer_addr)) - }) - .and_then(self.map_err(TlsError::Service)) + }; + let peer_addr = io.get_ref().0.peer_addr().ok(); + Ok((io, proto, peer_addr)) + }) + .and_then(self.map_err(TlsError::Service)) } } } @@ -315,167 +339,94 @@ mod rustls { impl ServiceFactory<(T, Protocol, Option)> for HttpService where - T: AsyncRead + AsyncWrite + Unpin, + T: AsyncRead + AsyncWrite + Unpin + 'static, + S: ServiceFactory, - S::Error: Into + 'static, + S::Future: 'static, + S::Error: Into> + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, + B: MessageBody + 'static, + X: ServiceFactory, - X::Error: Into, + X::Future: 'static, + X::Error: Into>, X::InitError: fmt::Debug, - >::Future: 'static, + U: ServiceFactory<(Request, Framed), Config = (), Response = ()>, - U::Error: fmt::Display + Into, + U::Future: 'static, + U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, - )>>::Future: 'static, { type Response = (); type Error = DispatchError; type Config = (); type Service = HttpServiceHandler; type InitError = (); - type Future = HttpServiceResponse; + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - HttpServiceResponse { - fut: self.srv.new_service(()), - fut_ex: Some(self.expect.new_service(())), - fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), - expect: None, - upgrade: None, - on_connect_ext: self.on_connect_ext.clone(), - cfg: self.cfg.clone(), - _phantom: PhantomData, - } - } -} + let service = self.srv.new_service(()); + let expect = self.expect.new_service(()); + let upgrade = self.upgrade.as_ref().map(|s| s.new_service(())); + let on_connect_ext = self.on_connect_ext.clone(); + let cfg = self.cfg.clone(); -#[doc(hidden)] -#[pin_project] -pub struct HttpServiceResponse -where - S: ServiceFactory, - X: ServiceFactory, - U: ServiceFactory<(Request, Framed)>, -{ - #[pin] - fut: S::Future, - #[pin] - fut_ex: Option, - #[pin] - fut_upg: Option, - expect: Option, - upgrade: Option, - on_connect_ext: Option>>, - cfg: ServiceConfig, - _phantom: PhantomData, -} + Box::pin(async move { + let expect = expect + .await + .map_err(|e| log::error!("Init http expect service error: {:?}", e))?; -impl Future for HttpServiceResponse -where - T: AsyncRead + AsyncWrite + Unpin, - S: ServiceFactory, - S::Error: Into + 'static, - S::InitError: fmt::Debug, - S::Response: Into> + 'static, - >::Future: 'static, - B: MessageBody + 'static, - X: ServiceFactory, - X::Error: Into, - X::InitError: fmt::Debug, - >::Future: 'static, - U: ServiceFactory<(Request, Framed), Response = ()>, - U::Error: fmt::Display, - U::InitError: fmt::Debug, - )>>::Future: 'static, -{ - type Output = - Result, ()>; + let upgrade = match upgrade { + Some(upgrade) => { + let upgrade = upgrade.await.map_err(|e| { + log::error!("Init http upgrade service error: {:?}", e) + })?; + Some(upgrade) + } + None => None, + }; - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut this = self.as_mut().project(); + let service = service + .await + .map_err(|e| log::error!("Init http service error: {:?}", e))?; - if let Some(fut) = this.fut_ex.as_pin_mut() { - let expect = ready!(fut - .poll(cx) - .map_err(|e| log::error!("Init http service error: {:?}", e)))?; - this = self.as_mut().project(); - *this.expect = Some(expect); - this.fut_ex.set(None); - } - - if let Some(fut) = this.fut_upg.as_pin_mut() { - let upgrade = ready!(fut - .poll(cx) - .map_err(|e| log::error!("Init http service error: {:?}", e)))?; - this = self.as_mut().project(); - *this.upgrade = Some(upgrade); - this.fut_upg.set(None); - } - - let result = ready!(this - .fut - .poll(cx) - .map_err(|e| log::error!("Init http service error: {:?}", e))); - - Poll::Ready(result.map(|service| { - let this = self.as_mut().project(); - HttpServiceHandler::new( - this.cfg.clone(), + Ok(HttpServiceHandler::new( + cfg, service, - this.expect.take().unwrap(), - this.upgrade.take(), - this.on_connect_ext.clone(), - ) - })) + expect, + upgrade, + on_connect_ext, + )) + }) } } -/// `Service` implementation for http transport +/// `Service` implementation for HTTP/1 and HTTP/2 transport pub struct HttpServiceHandler where S: Service, X: Service, U: Service<(Request, Framed)>, { - flow: Rc>>, - cfg: ServiceConfig, - on_connect_ext: Option>>, + pub(super) flow: Rc>, + pub(super) cfg: ServiceConfig, + pub(super) on_connect_ext: Option>>, _phantom: PhantomData, } -/// A collection of services that describe an HTTP request flow. -pub(super) struct HttpFlow { - pub(super) service: S, - pub(super) expect: X, - pub(super) upgrade: Option, -} - -impl HttpFlow { - pub(super) fn new(service: S, expect: X, upgrade: Option) -> Rc> { - Rc::new(RefCell::new(Self { - service, - expect, - upgrade, - })) - } -} - impl HttpServiceHandler where S: Service, - S::Error: Into + 'static, - S::Future: 'static, - S::Response: Into> + 'static, - B: MessageBody + 'static, - X: Service, - X::Error: Into, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, + S::Error: Into>, + X: Service, + X::Error: Into>, + U: Service<(Request, Framed)>, + U::Error: Into>, { - fn new( + pub(super) fn new( cfg: ServiceConfig, service: S, expect: X, @@ -489,71 +440,71 @@ where _phantom: PhantomData, } } + + pub(super) fn _poll_ready( + &self, + cx: &mut Context<'_>, + ) -> Poll>> { + ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; + + ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; + + if let Some(ref upg) = self.flow.upgrade { + ready!(upg.poll_ready(cx).map_err(Into::into))?; + }; + + Poll::Ready(Ok(())) + } +} + +/// A collection of services that describe an HTTP request flow. +pub(super) struct HttpFlow { + pub(super) service: S, + pub(super) expect: X, + pub(super) upgrade: Option, +} + +impl HttpFlow { + pub(super) fn new(service: S, expect: X, upgrade: Option) -> Rc { + Rc::new(Self { + service, + expect, + upgrade, + }) + } } impl Service<(T, Protocol, Option)> for HttpServiceHandler where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, + B: MessageBody + 'static, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display + Into, + U::Error: fmt::Display + Into>, { type Response = (); type Error = DispatchError; type Future = HttpServiceHandlerResponse; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - let mut flow = self.flow.borrow_mut(); - let ready = flow - .expect - .poll_ready(cx) - .map_err(|e| { - let e = e.into(); - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service(e) - })? - .is_ready(); - - let ready = flow - .service - .poll_ready(cx) - .map_err(|e| { - let e = e.into(); - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service(e) - })? - .is_ready() - && ready; - - let ready = if let Some(ref mut upg) = flow.upgrade { - upg.poll_ready(cx) - .map_err(|e| { - let e = e.into(); - log::error!("Http service readiness error: {:?}", e); - DispatchError::Service(e) - })? - .is_ready() - && ready - } else { - ready - }; - - if ready { - Poll::Ready(Ok(())) - } else { - Poll::Pending - } + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + self._poll_ready(cx).map_err(|e| { + log::error!("HTTP service readiness error: {:?}", e); + DispatchError::Service(e) + }) } fn call( - &mut self, + &self, (io, proto, peer_addr): (T, Protocol, Option), ) -> Self::Future { let on_connect_data = @@ -561,23 +512,27 @@ where match proto { Protocol::Http2 => HttpServiceHandlerResponse { - state: State::H2Handshake(Some(( - server::handshake(io), - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - peer_addr, - ))), + state: State::H2Handshake { + handshake: Some(( + h2::handshake_with_timeout(io, &self.cfg), + self.cfg.clone(), + self.flow.clone(), + on_connect_data, + peer_addr, + )), + }, }, Protocol::Http1 => HttpServiceHandlerResponse { - state: State::H1(h1::Dispatcher::new( - io, - self.cfg.clone(), - self.flow.clone(), - on_connect_data, - peer_addr, - )), + state: State::H1 { + dispatcher: h1::Dispatcher::new( + io, + self.cfg.clone(), + self.flow.clone(), + on_connect_data, + peer_addr, + ), + }, }, proto => unimplemented!("Unsupported HTTP version: {:?}.", proto), @@ -585,60 +540,81 @@ where } } -#[pin_project(project = StateProj)] -enum State -where - S: Service, - S::Future: 'static, - S::Error: Into, - T: AsyncRead + AsyncWrite + Unpin, - B: MessageBody, - X: Service, - X::Error: Into, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - H1(#[pin] h1::Dispatcher), - H2(#[pin] Dispatcher), - H2Handshake( - Option<( - Handshake, - ServiceConfig, - Rc>>, - OnConnectData, - Option, - )>, - ), +pin_project! { + #[project = StateProj] + enum State + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, + + S: Service, + S::Future: 'static, + S::Error: Into>, + + B: MessageBody, + + X: Service, + X::Error: Into>, + + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + H1 { #[pin] dispatcher: h1::Dispatcher }, + H2 { #[pin] dispatcher: h2::Dispatcher }, + H2Handshake { + handshake: Option<( + h2::HandshakeWithTimeout, + ServiceConfig, + Rc>, + OnConnectData, + Option, + )>, + }, + } } -#[pin_project] -pub struct HttpServiceHandlerResponse -where - T: AsyncRead + AsyncWrite + Unpin, - S: Service, - S::Error: Into + 'static, - S::Future: 'static, - S::Response: Into> + 'static, - B: MessageBody + 'static, - X: Service, - X::Error: Into, - U: Service<(Request, Framed), Response = ()>, - U::Error: fmt::Display, -{ - #[pin] - state: State, +pin_project! { + pub struct HttpServiceHandlerResponse + where + T: AsyncRead, + T: AsyncWrite, + T: Unpin, + + S: Service, + S::Error: Into>, + S::Error: 'static, + S::Future: 'static, + S::Response: Into>, + S::Response: 'static, + + B: MessageBody, + + X: Service, + X::Error: Into>, + + U: Service<(Request, Framed), Response = ()>, + U::Error: fmt::Display, + { + #[pin] + state: State, + } } impl Future for HttpServiceHandlerResponse where T: AsyncRead + AsyncWrite + Unpin, + S: Service, - S::Error: Into + 'static, + S::Error: Into> + 'static, S::Future: 'static, S::Response: Into> + 'static, - B: MessageBody, + + B: MessageBody + 'static, + X: Service, - X::Error: Into, + X::Error: Into>, + U: Service<(Request, Framed), Response = ()>, U::Error: fmt::Display, { @@ -646,26 +622,29 @@ where fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { match self.as_mut().project().state.project() { - StateProj::H1(disp) => disp.poll(cx), - StateProj::H2(disp) => disp.poll(cx), - StateProj::H2Handshake(data) => { + StateProj::H1 { dispatcher } => dispatcher.poll(cx), + StateProj::H2 { dispatcher } => dispatcher.poll(cx), + StateProj::H2Handshake { handshake: data } => { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { - Ok(conn) => { - let (_, cfg, srv, on_connect_data, peer_addr) = + Ok((conn, timer)) => { + let (_, config, flow, on_connect_data, peer_addr) = data.take().unwrap(); - self.as_mut().project().state.set(State::H2(Dispatcher::new( - srv, - conn, - on_connect_data, - cfg, - None, - peer_addr, - ))); + + self.as_mut().project().state.set(State::H2 { + dispatcher: h2::Dispatcher::new( + flow, + conn, + on_connect_data, + config, + peer_addr, + timer, + ), + }); self.poll(cx) } Err(err) => { trace!("H2 handshake error: {}", err); - Poll::Ready(Err(err.into())) + Poll::Ready(Err(err)) } } } diff --git a/actix-http/src/test.rs b/actix-http/src/test.rs index 8f0a7d21a..ec781743d 100644 --- a/actix-http/src/test.rs +++ b/actix-http/src/test.rs @@ -11,13 +11,9 @@ use std::{ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use bytes::{Bytes, BytesMut}; -use http::{ - header::{self, HeaderValue}, - Method, Uri, Version, -}; +use http::{Method, Uri, Version}; use crate::{ - cookie::{Cookie, CookieJar}, header::{HeaderMap, IntoHeaderPair}, payload::Payload, Request, @@ -25,7 +21,7 @@ use crate::{ /// Test `Request` builder /// -/// ```rust,ignore +/// ```ignore /// # use http::{header, StatusCode}; /// # use actix_web::*; /// use actix_web::test::TestRequest; @@ -53,7 +49,6 @@ struct Inner { method: Method, uri: Uri, headers: HeaderMap, - cookies: CookieJar, payload: Option, } @@ -64,7 +59,6 @@ impl Default for TestRequest { uri: Uri::from_str("/").unwrap(), version: Version::HTTP_11, headers: HeaderMap::new(), - cookies: CookieJar::new(), payload: None, })) } @@ -131,12 +125,6 @@ impl TestRequest { self } - /// Set cookie for this request. - pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self { - parts(&mut self.0).cookies.add(cookie.into_owned()); - self - } - /// Set request payload. pub fn set_payload>(&mut self, data: B) -> &mut Self { let mut payload = crate::h1::Payload::empty(); @@ -165,19 +153,6 @@ impl TestRequest { head.version = inner.version; head.headers = inner.headers; - let cookie: String = inner - .cookies - .delta() - // ensure only name=value is written to cookie header - .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) - .collect::>() - .join("; "); - - if !cookie.is_empty() { - head.headers - .insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap()); - } - req } } diff --git a/actix-http/src/time_parser.rs b/actix-http/src/time_parser.rs deleted file mode 100644 index 0d06a5867..000000000 --- a/actix-http/src/time_parser.rs +++ /dev/null @@ -1,42 +0,0 @@ -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_utc(); - 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/codec.rs b/actix-http/src/ws/codec.rs index d01e8dab9..8655216fa 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -54,7 +54,7 @@ pub enum Frame { Close(Option), } -/// A `WebSocket` continuation item. +/// A WebSocket continuation item. #[derive(Debug, PartialEq)] pub enum Item { FirstText(Bytes), @@ -79,8 +79,8 @@ bitflags! { } impl Codec { - /// Create new websocket frames decoder. - pub fn new() -> Codec { + /// Create new WebSocket frames decoder. + pub const fn new() -> Codec { Codec { max_size: 65_536, flags: Flags::SERVER, diff --git a/actix-http/src/ws/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 7be7cf637..a3f766e9c 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -4,18 +4,21 @@ use std::task::{Context, Poll}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_service::{IntoService, Service}; -use actix_utils::dispatcher::{Dispatcher as InnerDispatcher, DispatcherError}; +use pin_project_lite::pin_project; use super::{Codec, Frame, Message}; -#[pin_project::pin_project] -pub struct Dispatcher -where - S: Service + 'static, - T: AsyncRead + AsyncWrite, -{ - #[pin] - inner: InnerDispatcher, +pin_project! { + pub struct Dispatcher + where + S: Service, + S: 'static, + T: AsyncRead, + T: AsyncWrite, + { + #[pin] + inner: inner::Dispatcher, + } } impl Dispatcher @@ -27,13 +30,13 @@ where { pub fn new>(io: T, service: F) -> Self { Dispatcher { - inner: InnerDispatcher::new(Framed::new(io, Codec::new()), service), + inner: inner::Dispatcher::new(Framed::new(io, Codec::new()), service), } } pub fn with>(framed: Framed, service: F) -> Self { Dispatcher { - inner: InnerDispatcher::new(framed, service), + inner: inner::Dispatcher::new(framed, service), } } } @@ -45,9 +48,396 @@ where S::Future: 'static, S::Error: 'static, { - type Output = Result<(), DispatcherError>; + type Output = Result<(), inner::DispatcherError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { self.project().inner.poll(cx) } } + +/// Framed dispatcher service and related utilities. +mod inner { + // allow dead code since this mod was ripped from actix-utils + #![allow(dead_code)] + + use core::{ + fmt, + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, + }; + + use actix_service::{IntoService, Service}; + use futures_core::stream::Stream; + use local_channel::mpsc; + use log::debug; + use pin_project_lite::pin_project; + + use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; + + use crate::{body::BoxBody, Response}; + + /// Framed transport errors + pub enum DispatcherError + where + U: Encoder + Decoder, + { + /// Inner service error. + Service(E), + + /// Frame encoding error. + Encoder(>::Error), + + /// Frame decoding error. + Decoder(::Error), + } + + impl From for DispatcherError + where + U: Encoder + Decoder, + { + fn from(err: E) -> Self { + DispatcherError::Service(err) + } + } + + impl fmt::Debug for DispatcherError + where + E: fmt::Debug, + U: Encoder + Decoder, + >::Error: fmt::Debug, + ::Error: fmt::Debug, + { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + DispatcherError::Service(ref e) => { + write!(fmt, "DispatcherError::Service({:?})", e) + } + DispatcherError::Encoder(ref e) => { + write!(fmt, "DispatcherError::Encoder({:?})", e) + } + DispatcherError::Decoder(ref e) => { + write!(fmt, "DispatcherError::Decoder({:?})", e) + } + } + } + } + + impl fmt::Display for DispatcherError + where + E: fmt::Display, + U: Encoder + Decoder, + >::Error: fmt::Debug, + ::Error: fmt::Debug, + { + fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + DispatcherError::Service(ref e) => write!(fmt, "{}", e), + DispatcherError::Encoder(ref e) => write!(fmt, "{:?}", e), + DispatcherError::Decoder(ref e) => write!(fmt, "{:?}", e), + } + } + } + + impl From> for Response + where + E: fmt::Debug + fmt::Display, + U: Encoder + Decoder, + >::Error: fmt::Debug, + ::Error: fmt::Debug, + { + fn from(err: DispatcherError) -> Self { + Response::internal_server_error().set_body(BoxBody::new(err.to_string())) + } + } + + /// Message type wrapper for signalling end of message stream. + pub enum Message { + /// Message item. + Item(T), + + /// Signal from service to flush all messages and stop processing. + Close, + } + + pin_project! { + /// A future that reads frames from a [`Framed`] object and passes them to a [`Service`]. + pub struct Dispatcher + where + S: Service<::Item, Response = I>, + S::Error: 'static, + S::Future: 'static, + T: AsyncRead, + T: AsyncWrite, + U: Encoder, + U: Decoder, + I: 'static, + >::Error: fmt::Debug, + { + service: S, + state: State, + #[pin] + framed: Framed, + rx: mpsc::Receiver, S::Error>>, + tx: mpsc::Sender, S::Error>>, + } + } + + enum State + where + S: Service<::Item>, + U: Encoder + Decoder, + { + Processing, + Error(DispatcherError), + FramedError(DispatcherError), + FlushAndStop, + Stopping, + } + + impl State + where + S: Service<::Item>, + U: Encoder + Decoder, + { + fn take_error(&mut self) -> DispatcherError { + match mem::replace(self, State::Processing) { + State::Error(err) => err, + _ => panic!(), + } + } + + fn take_framed_error(&mut self) -> DispatcherError { + match mem::replace(self, State::Processing) { + State::FramedError(err) => err, + _ => panic!(), + } + } + } + + impl Dispatcher + where + S: Service<::Item, Response = I>, + S::Error: 'static, + S::Future: 'static, + T: AsyncRead + AsyncWrite, + U: Decoder + Encoder, + I: 'static, + ::Error: fmt::Debug, + >::Error: fmt::Debug, + { + /// Create new `Dispatcher`. + pub fn new(framed: Framed, service: F) -> Self + where + F: IntoService::Item>, + { + let (tx, rx) = mpsc::channel(); + Dispatcher { + framed, + rx, + tx, + service: service.into_service(), + state: State::Processing, + } + } + + /// Construct new `Dispatcher` instance with customer `mpsc::Receiver` + pub fn with_rx( + framed: Framed, + service: F, + rx: mpsc::Receiver, S::Error>>, + ) -> Self + where + F: IntoService::Item>, + { + let tx = rx.sender(); + Dispatcher { + framed, + rx, + tx, + service: service.into_service(), + state: State::Processing, + } + } + + /// Get sender handle. + pub fn tx(&self) -> mpsc::Sender, S::Error>> { + self.tx.clone() + } + + /// Get reference to a service wrapped by `Dispatcher` instance. + pub fn service(&self) -> &S { + &self.service + } + + /// Get mutable reference to a service wrapped by `Dispatcher` instance. + pub fn service_mut(&mut self) -> &mut S { + &mut self.service + } + + /// Get reference to a framed instance wrapped by `Dispatcher` instance. + pub fn framed(&self) -> &Framed { + &self.framed + } + + /// Get mutable reference to a framed instance wrapped by `Dispatcher` instance. + pub fn framed_mut(&mut self) -> &mut Framed { + &mut self.framed + } + + /// Read from framed object. + fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool + where + S: Service<::Item, Response = I>, + S::Error: 'static, + S::Future: 'static, + T: AsyncRead + AsyncWrite, + U: Decoder + Encoder, + I: 'static, + >::Error: fmt::Debug, + { + loop { + let this = self.as_mut().project(); + match this.service.poll_ready(cx) { + Poll::Ready(Ok(_)) => { + let item = match this.framed.next_item(cx) { + Poll::Ready(Some(Ok(el))) => el, + Poll::Ready(Some(Err(err))) => { + *this.state = + State::FramedError(DispatcherError::Decoder(err)); + return true; + } + Poll::Pending => return false, + Poll::Ready(None) => { + *this.state = State::Stopping; + return true; + } + }; + + let tx = this.tx.clone(); + let fut = this.service.call(item); + actix_rt::spawn(async move { + let item = fut.await; + let _ = tx.send(item.map(Message::Item)); + }); + } + Poll::Pending => return false, + Poll::Ready(Err(err)) => { + *this.state = State::Error(DispatcherError::Service(err)); + return true; + } + } + } + } + + /// Write to framed object. + fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool + where + S: Service<::Item, Response = I>, + S::Error: 'static, + S::Future: 'static, + T: AsyncRead + AsyncWrite, + U: Decoder + Encoder, + I: 'static, + >::Error: fmt::Debug, + { + loop { + let mut this = self.as_mut().project(); + while !this.framed.is_write_buf_full() { + match Pin::new(&mut this.rx).poll_next(cx) { + Poll::Ready(Some(Ok(Message::Item(msg)))) => { + if let Err(err) = this.framed.as_mut().write(msg) { + *this.state = + State::FramedError(DispatcherError::Encoder(err)); + return true; + } + } + Poll::Ready(Some(Ok(Message::Close))) => { + *this.state = State::FlushAndStop; + return true; + } + Poll::Ready(Some(Err(err))) => { + *this.state = State::Error(DispatcherError::Service(err)); + return true; + } + Poll::Ready(None) | Poll::Pending => break, + } + } + + if !this.framed.is_write_buf_empty() { + match this.framed.flush(cx) { + Poll::Pending => break, + Poll::Ready(Ok(_)) => {} + Poll::Ready(Err(err)) => { + debug!("Error sending data: {:?}", err); + *this.state = + State::FramedError(DispatcherError::Encoder(err)); + return true; + } + } + } else { + break; + } + } + + false + } + } + + impl Future for Dispatcher + where + S: Service<::Item, Response = I>, + S::Error: 'static, + S::Future: 'static, + T: AsyncRead + AsyncWrite, + U: Decoder + Encoder, + I: 'static, + >::Error: fmt::Debug, + ::Error: fmt::Debug, + { + type Output = Result<(), DispatcherError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + loop { + let this = self.as_mut().project(); + + return match this.state { + State::Processing => { + if self.as_mut().poll_read(cx) || self.as_mut().poll_write(cx) { + continue; + } else { + Poll::Pending + } + } + State::Error(_) => { + // flush write buffer + if !this.framed.is_write_buf_empty() + && this.framed.flush(cx).is_pending() + { + return Poll::Pending; + } + Poll::Ready(Err(this.state.take_error())) + } + State::FlushAndStop => { + if !this.framed.is_write_buf_empty() { + this.framed.flush(cx).map(|res| { + if let Err(err) = res { + debug!("Error sending data: {:?}", err); + } + + Ok(()) + }) + } else { + Poll::Ready(Ok(())) + } + } + State::FramedError(_) => { + Poll::Ready(Err(this.state.take_framed_error())) + } + State::Stopping => Poll::Ready(Ok(())), + }; + } + } + } +} diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index 8c49524e6..46edf5d85 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -7,7 +7,7 @@ use crate::ws::mask::apply_mask; use crate::ws::proto::{CloseCode, CloseReason, OpCode}; use crate::ws::ProtocolError; -/// A struct representing a `WebSocket` frame. +/// A struct representing a WebSocket frame. #[derive(Debug)] pub struct Parser; @@ -16,7 +16,8 @@ impl Parser { src: &[u8], server: bool, max_size: usize, - ) -> Result)>, ProtocolError> { + ) -> Result)>, ProtocolError> + { let chunk_len = src.len(); let mut idx = 2; @@ -77,9 +78,10 @@ impl Parser { return Ok(None); } - let mask = - u32::from_le_bytes(TryFrom::try_from(&src[idx..idx + 4]).unwrap()); + let mask = TryFrom::try_from(&src[idx..idx + 4]).unwrap(); + idx += 4; + Some(mask) } else { None @@ -187,8 +189,8 @@ impl Parser { }; if mask { - let mask = rand::random::(); - dst.put_u32_le(mask); + let mask = rand::random::<[u8; 4]>(); + dst.put_slice(mask.as_ref()); dst.put_slice(payload.as_ref()); let pos = dst.len() - payload_len; apply_mask(&mut dst[pos..], mask); diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index 79e015f79..11a6ddc32 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -1,136 +1,57 @@ //! This is code from [Tungstenite project](https://github.com/snapview/tungstenite-rs) -#![allow(clippy::cast_ptr_alignment)] -use std::ptr::copy_nonoverlapping; -use std::slice; -/// Holds a slice guaranteed to be shorter than 8 bytes. -struct ShortSlice<'a> { - inner: &'a mut [u8], -} - -impl<'a> ShortSlice<'a> { - /// # Safety - /// Given slice must be shorter than 8 bytes. - unsafe fn new(slice: &'a mut [u8]) -> Self { - // Sanity check for debug builds - debug_assert!(slice.len() < 8); - ShortSlice { inner: slice } - } - - fn len(&self) -> usize { - self.inner.len() - } -} - -/// Faster version of `apply_mask()` which operates on 8-byte blocks. +/// Mask/unmask a frame. #[inline] -#[allow(clippy::cast_lossless)] -pub(crate) fn apply_mask(buf: &mut [u8], mask_u32: u32) { - // Extend the mask to 64 bits - let mut mask_u64 = ((mask_u32 as u64) << 32) | (mask_u32 as u64); - // Split the buffer into three segments - let (head, mid, tail) = align_buf(buf); +pub fn apply_mask(buf: &mut [u8], mask: [u8; 4]) { + apply_mask_fast32(buf, mask) +} - // Initial unaligned segment - let head_len = head.len(); - if head_len > 0 { - xor_short(head, mask_u64); +/// A safe unoptimized mask application. +#[inline] +fn apply_mask_fallback(buf: &mut [u8], mask: [u8; 4]) { + for (i, byte) in buf.iter_mut().enumerate() { + *byte ^= mask[i & 3]; + } +} + +/// Faster version of `apply_mask()` which operates on 4-byte blocks. +#[inline] +pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) { + let mask_u32 = u32::from_ne_bytes(mask); + + // SAFETY: + // + // buf is a valid slice borrowed mutably from bytes::BytesMut. + // + // un aligned prefix and suffix would be mask/unmask per byte. + // proper aligned middle slice goes into fast path and operates on 4-byte blocks. + let (prefix, words, suffix) = unsafe { buf.align_to_mut::() }; + apply_mask_fallback(prefix, mask); + let head = prefix.len() & 3; + let mask_u32 = if head > 0 { if cfg!(target_endian = "big") { - mask_u64 = mask_u64.rotate_left(8 * head_len as u32); + mask_u32.rotate_left(8 * head as u32) } else { - mask_u64 = mask_u64.rotate_right(8 * head_len as u32); - } - } - // Aligned segment - for v in mid { - *v ^= mask_u64; - } - // Final unaligned segment - if tail.len() > 0 { - xor_short(tail, mask_u64); - } -} - -// TODO: copy_nonoverlapping here compiles to call memcpy. While it is not so -// inefficient, it could be done better. The compiler does not understand that -// a `ShortSlice` must be smaller than a u64. -#[inline] -#[allow(clippy::needless_pass_by_value)] -fn xor_short(buf: ShortSlice<'_>, mask: u64) { - // SAFETY: we know that a `ShortSlice` fits in a u64 - unsafe { - let (ptr, len) = (buf.inner.as_mut_ptr(), buf.len()); - let mut b: u64 = 0; - #[allow(trivial_casts)] - copy_nonoverlapping(ptr, &mut b as *mut _ as *mut u8, len); - b ^= mask; - #[allow(trivial_casts)] - copy_nonoverlapping(&b as *const _ as *const u8, ptr, len); - } -} - -/// # Safety -/// Caller must ensure the buffer has the correct size and alignment. -#[inline] -unsafe fn cast_slice(buf: &mut [u8]) -> &mut [u64] { - // Assert correct size and alignment in debug builds - debug_assert!(buf.len().trailing_zeros() >= 3); - debug_assert!((buf.as_ptr() as usize).trailing_zeros() >= 3); - - slice::from_raw_parts_mut(buf.as_mut_ptr() as *mut u64, buf.len() >> 3) -} - -/// Splits a slice into three parts: -/// - an unaligned short head -/// - an aligned `u64` slice mid section -/// - an unaligned short tail -#[inline] -fn align_buf(buf: &mut [u8]) -> (ShortSlice<'_>, &mut [u64], ShortSlice<'_>) { - let start_ptr = buf.as_ptr() as usize; - let end_ptr = start_ptr + buf.len(); - - // Round *up* to next aligned boundary for start - let start_aligned = (start_ptr + 7) & !0x7; - // Round *down* to last aligned boundary for end - let end_aligned = end_ptr & !0x7; - - if end_aligned >= start_aligned { - // We have our three segments (head, mid, tail) - let (tmp, tail) = buf.split_at_mut(end_aligned - start_ptr); - let (head, mid) = tmp.split_at_mut(start_aligned - start_ptr); - - // SAFETY: we know the middle section is correctly aligned, and the outer - // sections are smaller than 8 bytes - unsafe { - ( - ShortSlice::new(head), - cast_slice(mid), - ShortSlice::new(tail), - ) + mask_u32.rotate_right(8 * head as u32) } } else { - // We didn't cross even one aligned boundary! - - // SAFETY: The outer sections are smaller than 8 bytes - unsafe { (ShortSlice::new(buf), &mut [], ShortSlice::new(&mut [])) } + mask_u32 + }; + for word in words.iter_mut() { + *word ^= mask_u32; } + apply_mask_fallback(suffix, mask_u32.to_ne_bytes()); } #[cfg(test)] mod tests { - use super::apply_mask; - - /// A safe unoptimized mask application. - fn apply_mask_fallback(buf: &mut [u8], mask: &[u8; 4]) { - for (i, byte) in buf.iter_mut().enumerate() { - *byte ^= mask[i & 3]; - } - } + use super::*; + // legacy test from old apply mask test. kept for now for back compat test. + // TODO: remove it and favor the other test. #[test] - fn test_apply_mask() { + fn test_apply_mask_legacy() { let mask = [0x6d, 0xb6, 0xb2, 0x80]; - let mask_u32 = u32::from_le_bytes(mask); let unmasked = vec![ 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, @@ -140,10 +61,10 @@ mod tests { // Check masking with proper alignment. { let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked, &mask); + apply_mask_fallback(&mut masked, mask); let mut masked_fast = unmasked.clone(); - apply_mask(&mut masked_fast, mask_u32); + apply_mask(&mut masked_fast, mask); assert_eq!(masked, masked_fast); } @@ -151,12 +72,38 @@ mod tests { // Check masking without alignment. { let mut masked = unmasked.clone(); - apply_mask_fallback(&mut masked[1..], &mask); + apply_mask_fallback(&mut masked[1..], mask); let mut masked_fast = unmasked; - apply_mask(&mut masked_fast[1..], mask_u32); + apply_mask(&mut masked_fast[1..], mask); assert_eq!(masked, masked_fast); } } + + #[test] + fn test_apply_mask() { + let mask = [0x6d, 0xb6, 0xb2, 0x80]; + let unmasked = vec![ + 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, + 0x74, 0xf9, 0x12, 0x03, + ]; + + for data_len in 0..=unmasked.len() { + let unmasked = &unmasked[0..data_len]; + // Check masking with different alignment. + for off in 0..=3 { + if unmasked.len() < off { + continue; + } + let mut masked = unmasked.to_vec(); + apply_mask_fallback(&mut masked[off..], mask); + + let mut masked_fast = unmasked.to_vec(); + apply_mask_fast32(&mut masked_fast[off..], mask); + + assert_eq!(masked, masked_fast); + } + } + } } diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index dad2646c1..cb1aa6730 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -1,6 +1,6 @@ -//! WebSocket protocol support. +//! WebSocket protocol implementation. //! -//! To setup a WebSocket, first do web socket handshake then on success convert `Payload` into a +//! To setup a WebSocket, first perform the WebSocket handshake then on success convert `Payload` into a //! `WsStream` stream and then use `WsWriter` to communicate with the peer. use std::io; @@ -8,9 +8,10 @@ use std::io; use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; -use crate::error::ResponseError; -use crate::message::RequestHead; -use crate::response::{Response, ResponseBuilder}; +use crate::body::BoxBody; +use crate::{ + header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder, +}; mod codec; mod dispatcher; @@ -24,7 +25,7 @@ pub use self::frame::Parser; pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; /// WebSocket protocol errors. -#[derive(Debug, Display, From, Error)] +#[derive(Debug, Display, Error, From)] pub enum ProtocolError { /// Received an unmasked frame from client. #[display(fmt = "Received an unmasked frame from client.")] @@ -67,16 +68,14 @@ pub enum ProtocolError { Io(io::Error), } -impl ResponseError for ProtocolError {} - /// WebSocket handshake errors -#[derive(PartialEq, Debug, Display)] +#[derive(Debug, Clone, Copy, PartialEq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. #[display(fmt = "Method not allowed.")] GetMethodRequired, - /// Upgrade header if not set to websocket. + /// Upgrade header if not set to WebSocket. #[display(fmt = "WebSocket upgrade is expected.")] NoWebsocketUpgrade, @@ -88,8 +87,8 @@ pub enum HandshakeError { #[display(fmt = "WebSocket version header is required.")] NoVersionHeader, - /// Unsupported websocket version. - #[display(fmt = "Unsupported version.")] + /// Unsupported WebSocket version. + #[display(fmt = "Unsupported WebSocket version.")] UnsupportedVersion, /// WebSocket key is not set or wrong. @@ -97,50 +96,69 @@ pub enum HandshakeError { BadWebsocketKey, } -impl ResponseError for HandshakeError { - fn error_response(&self) -> Response { - match self { - HandshakeError::GetMethodRequired => Response::MethodNotAllowed() - .insert_header((header::ALLOW, "GET")) - .finish(), +impl From for Response { + fn from(err: HandshakeError) -> Self { + match err { + HandshakeError::GetMethodRequired => { + let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); + res.headers_mut() + .insert(header::ALLOW, HeaderValue::from_static("GET")); + res + } - HandshakeError::NoWebsocketUpgrade => Response::BadRequest() - .reason("No WebSocket UPGRADE header found") - .finish(), + HandshakeError::NoWebsocketUpgrade => { + let mut res = Response::bad_request(); + res.head_mut().reason = Some("No WebSocket Upgrade header found"); + res + } - HandshakeError::NoConnectionUpgrade => Response::BadRequest() - .reason("No CONNECTION upgrade") - .finish(), + HandshakeError::NoConnectionUpgrade => { + let mut res = Response::bad_request(); + res.head_mut().reason = Some("No Connection upgrade"); + res + } - HandshakeError::NoVersionHeader => Response::BadRequest() - .reason("Websocket version header is required") - .finish(), + HandshakeError::NoVersionHeader => { + let mut res = Response::bad_request(); + res.head_mut().reason = Some("WebSocket version header is required"); + res + } - HandshakeError::UnsupportedVersion => Response::BadRequest() - .reason("Unsupported version") - .finish(), + HandshakeError::UnsupportedVersion => { + let mut res = Response::bad_request(); + res.head_mut().reason = Some("Unsupported WebSocket version"); + res + } HandshakeError::BadWebsocketKey => { - Response::BadRequest().reason("Handshake error").finish() + let mut res = Response::bad_request(); + res.head_mut().reason = Some("Handshake error"); + res } } } } -/// Verify `WebSocket` handshake request and create handshake response. +impl From<&HandshakeError> for Response { + fn from(err: &HandshakeError) -> Self { + (*err).into() + } +} + +/// Verify WebSocket handshake request and create handshake response. pub fn handshake(req: &RequestHead) -> Result { verify_handshake(req)?; Ok(handshake_response(req)) } -/// Verify `WebSocket` handshake request. +/// Verify WebSocket handshake request. pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { // WebSocket accepts only GET if req.method != Method::GET { return Err(HandshakeError::GetMethodRequired); } - // Check for "UPGRADE" to websocket header + // Check for "UPGRADE" to WebSocket header let has_hdr = if let Some(hdr) = req.headers().get(header::UPGRADE) { if let Ok(s) = hdr.to_str() { s.to_ascii_lowercase().contains("websocket") @@ -181,7 +199,7 @@ pub fn verify_handshake(req: &RequestHead) -> Result<(), HandshakeError> { Ok(()) } -/// Create websocket handshake response +/// Create WebSocket handshake response. /// /// This function returns handshake `Response`, ready to send to peer. pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { @@ -192,16 +210,20 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { Response::build(StatusCode::SWITCHING_PROTOCOLS) .upgrade("websocket") - .insert_header((header::TRANSFER_ENCODING, "chunked")) - .insert_header((header::SEC_WEBSOCKET_ACCEPT, key)) + .insert_header(( + header::SEC_WEBSOCKET_ACCEPT, + // key is known to be header value safe ascii + HeaderValue::from_bytes(&key).unwrap(), + )) .take() } #[cfg(test)] mod tests { + use crate::{header, Method}; + use super::*; use crate::test::TestRequest; - use http::{header, Method}; #[test] fn test_handshake() { @@ -314,18 +336,18 @@ mod tests { } #[test] - fn test_wserror_http_response() { - let resp: Response = HandshakeError::GetMethodRequired.error_response(); + fn test_ws_error_http_response() { + let resp: Response = HandshakeError::GetMethodRequired.into(); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let resp: Response = HandshakeError::NoWebsocketUpgrade.error_response(); + let resp: Response = HandshakeError::NoWebsocketUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::NoConnectionUpgrade.error_response(); + let resp: Response = HandshakeError::NoConnectionUpgrade.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::NoVersionHeader.error_response(); + let resp: Response = HandshakeError::NoVersionHeader.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::UnsupportedVersion.error_response(); + let resp: Response = HandshakeError::UnsupportedVersion.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let resp: Response = HandshakeError::BadWebsocketKey.error_response(); + let resp: Response = HandshakeError::BadWebsocketKey.into(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 93468d232..4227f221d 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -1,7 +1,11 @@ -use std::convert::{From, Into}; -use std::fmt; +use std::{ + convert::{From, Into}, + fmt, +}; -/// Operation codes as part of RFC6455. +/// Operation codes defined in [RFC 6455 §11.8]. +/// +/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8 #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum OpCode { /// Indicates a continuation frame of a fragmented message. @@ -28,8 +32,9 @@ pub enum OpCode { impl fmt::Display for OpCode { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - use self::OpCode::*; - match *self { + use OpCode::*; + + match self { Continue => write!(f, "CONTINUE"), Text => write!(f, "TEXT"), Binary => write!(f, "BINARY"), @@ -44,6 +49,7 @@ impl fmt::Display for OpCode { impl From for u8 { fn from(op: OpCode) -> u8 { use self::OpCode::*; + match op { Continue => 0, Text => 1, @@ -62,6 +68,7 @@ impl From for u8 { impl From for OpCode { fn from(byte: u8) -> OpCode { use self::OpCode::*; + match byte { 0 => Continue, 1 => Text, @@ -74,67 +81,69 @@ impl From for OpCode { } } -/// Status code used to indicate why an endpoint is closing the `WebSocket` -/// connection. +/// Status code used to indicate why an endpoint is closing the WebSocket connection. #[derive(Debug, Eq, PartialEq, Clone, Copy)] pub enum CloseCode { - /// Indicates a normal closure, meaning that the purpose for - /// which the connection was established has been fulfilled. + /// Indicates a normal closure, meaning that the purpose for which the connection was + /// established has been fulfilled. Normal, - /// Indicates that an endpoint is "going away", such as a server - /// going down or a browser having navigated away from a page. + + /// Indicates that an endpoint is "going away", such as a server going down or a browser having + /// navigated away from a page. Away, - /// Indicates that an endpoint is terminating the connection due - /// to a protocol error. + + /// Indicates that an endpoint is terminating the connection due to a protocol error. Protocol, - /// Indicates that an endpoint is terminating the connection - /// because it has received a type of data it cannot accept (e.g., an - /// endpoint that understands only text data MAY send this if it + + /// Indicates that an endpoint is terminating the connection because it has received a type of + /// data it cannot accept (e.g., an endpoint that understands only text data MAY send this if it /// receives a binary message). Unsupported, - /// Indicates an abnormal closure. If the abnormal closure was due to an - /// error, this close code will not be used. Instead, the `on_error` method - /// of the handler will be called with the error. However, if the connection - /// is simply dropped, without an error, this close code will be sent to the - /// handler. + + /// Indicates an abnormal closure. If the abnormal closure was due to an error, this close code + /// will not be used. Instead, the `on_error` method of the handler will be called with + /// the error. However, if the connection is simply dropped, without an error, this close code + /// will be sent to the handler. Abnormal, - /// Indicates that an endpoint is terminating the connection - /// because it has received data within a message that was not - /// consistent with the type of the message (e.g., non-UTF-8 \[RFC3629\] + + /// Indicates that an endpoint is terminating the connection because it has received data within + /// a message that was not consistent with the type of the message (e.g., non-UTF-8 \[RFC 3629\] /// data within a text message). Invalid, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that violates its policy. This - /// is a generic status code that can be returned when there is no - /// other more suitable status code (e.g., Unsupported or Size) or if there - /// is a need to hide specific details about the policy. + + /// Indicates that an endpoint is terminating the connection because it has received a message + /// that violates its policy. This is a generic status code that can be returned when there is + /// no other more suitable status code (e.g., Unsupported or Size) or if there is a need to hide + /// specific details about the policy. Policy, - /// Indicates that an endpoint is terminating the connection - /// because it has received a message that is too big for it to - /// process. + + /// Indicates that an endpoint is terminating the connection because it has received a message + /// that is too big for it to process. Size, - /// Indicates that an endpoint (client) is terminating the - /// connection because it has expected the server to negotiate one or - /// more extension, but the server didn't return them in the response - /// message of the WebSocket handshake. The list of extensions that - /// are needed should be given as the reason for closing. - /// Note that this status code is not used by the server, because it - /// can fail the WebSocket handshake instead. + + /// Indicates that an endpoint (client) is terminating the connection because it has expected + /// the server to negotiate one or more extension, but the server didn't return them in the + /// response message of the WebSocket handshake. The list of extensions that are needed should + /// be given as the reason for closing. Note that this status code is not used by the server, + /// because it can fail the WebSocket handshake instead. Extension, - /// Indicates that a server is terminating the connection because - /// it encountered an unexpected condition that prevented it from - /// fulfilling the request. + + /// Indicates that a server is terminating the connection because it encountered an unexpected + /// condition that prevented it from fulfilling the request. Error, - /// Indicates that the server is restarting. A client may choose to - /// reconnect, and if it does, it should use a randomized delay of 5-30 - /// seconds between attempts. + + /// Indicates that the server is restarting. A client may choose to reconnect, and if it does, + /// it should use a randomized delay of 5-30 seconds between attempts. Restart, - /// Indicates that the server is overloaded and the client should either - /// connect to a different IP (when multiple targets exist), or - /// reconnect to the same IP when a user has performed an action. + + /// Indicates that the server is overloaded and the client should either connect to a different + /// IP (when multiple targets exist), or reconnect to the same IP when a user has performed + /// an action. Again, + #[doc(hidden)] Tls, + #[doc(hidden)] Other(u16), } @@ -142,6 +151,7 @@ pub enum CloseCode { impl From for u16 { fn from(code: CloseCode) -> u16 { use self::CloseCode::*; + match code { Normal => 1000, Away => 1001, @@ -164,6 +174,7 @@ impl From for u16 { impl From for CloseCode { fn from(code: u16) -> CloseCode { use self::CloseCode::*; + match code { 1000 => Normal, 1001 => Away, @@ -211,17 +222,30 @@ impl> From<(CloseCode, T)> for CloseReason { } } -static WS_GUID: &str = "258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; +/// The WebSocket GUID as stated in the spec. +/// See . +static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; -// TODO: hash is always same size, we don't need String -pub fn hash_key(key: &[u8]) -> String { - use sha1::Digest; - let mut hasher = sha1::Sha1::new(); +/// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec. +/// +/// Result is a Base64 encoded byte array. `base64(sha1(input))` is always 28 bytes. +pub fn hash_key(key: &[u8]) -> [u8; 28] { + let hash = { + use sha1::Digest as _; - hasher.update(key); - hasher.update(WS_GUID.as_bytes()); + let mut hasher = sha1::Sha1::new(); - base64::encode(&hasher.finalize()) + hasher.update(key); + hasher.update(WS_GUID); + + hasher.finalize() + }; + + let mut hash_b64 = [0; 28]; + let n = base64::encode_config_slice(&hash, base64::STANDARD, &mut hash_b64); + assert_eq!(n, 28); + + hash_b64 } #[cfg(test)] @@ -289,11 +313,11 @@ mod test { #[test] fn test_hash_key() { let hash = hash_key(b"hello actix-web"); - assert_eq!(&hash, "cR1dlyUUJKp0s/Bel25u5TgvC3E="); + assert_eq!(&hash, b"cR1dlyUUJKp0s/Bel25u5TgvC3E="); } #[test] - fn closecode_from_u16() { + fn close_code_from_u16() { assert_eq!(CloseCode::from(1000u16), CloseCode::Normal); assert_eq!(CloseCode::from(1001u16), CloseCode::Away); assert_eq!(CloseCode::from(1002u16), CloseCode::Protocol); @@ -311,7 +335,7 @@ mod test { } #[test] - fn closecode_into_u16() { + fn close_code_into_u16() { assert_eq!(1000u16, Into::::into(CloseCode::Normal)); assert_eq!(1001u16, Into::::into(CloseCode::Away)); assert_eq!(1002u16, Into::::into(CloseCode::Protocol)); diff --git a/actix-http/tests/test_client.rs b/actix-http/tests/test_client.rs index 91b2412f4..4c923873f 100644 --- a/actix-http/tests/test_client.rs +++ b/actix-http/tests/test_client.rs @@ -1,8 +1,14 @@ -use actix_http::{http, HttpService, Request, Response}; +use std::convert::Infallible; + +use actix_http::{ + body::BoxBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, +}; use actix_http_test::test_server; use actix_service::ServiceFactoryExt; +use actix_utils::future; use bytes::Bytes; -use futures_util::future::{self, ok}; +use derive_more::{Display, Error}; +use futures_util::StreamExt as _; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -30,7 +36,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h1_v2() { let srv = test_server(move || { HttpService::build() - .finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) + .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -58,7 +64,7 @@ async fn test_h1_v2() { async fn test_connection_close() { let srv = test_server(move || { HttpService::build() - .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) + .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() .map(|_| ()) }) @@ -72,11 +78,11 @@ async fn test_connection_close() { async fn test_with_query_parameter() { let srv = test_server(move || { HttpService::build() - .finish(|req: Request| { + .finish(|req: Request| async move { if req.uri().query().unwrap().contains("qp=") { - ok::<_, ()>(Response::Ok().finish()) + Ok::<_, Infallible>(Response::ok()) } else { - ok::<_, ()>(Response::BadRequest().finish()) + Ok(Response::bad_request()) } }) .tcp() @@ -88,3 +94,65 @@ async fn test_with_query_parameter() { let response = request.send().await.unwrap(); assert!(response.status().is_success()); } + +#[derive(Debug, Display, Error)] +#[display(fmt = "expect failed")] +struct ExpectFailed; + +impl From for Response { + fn from(_: ExpectFailed) -> Self { + Response::new(StatusCode::EXPECTATION_FAILED) + } +} + +#[actix_rt::test] +async fn test_h1_expect() { + let srv = test_server(move || { + HttpService::build() + .expect(|req: Request| async { + if req.headers().contains_key("AUTH") { + Ok(req) + } else { + Err(ExpectFailed) + } + }) + .h1(|req: Request| async move { + let (_, mut body) = req.into_parts(); + let mut buf = Vec::new(); + while let Some(Ok(chunk)) = body.next().await { + buf.extend_from_slice(&chunk); + } + let str = std::str::from_utf8(&buf).unwrap(); + assert_eq!(str, "expect body"); + + Ok::<_, Infallible>(Response::ok()) + }) + .tcp() + }) + .await; + + // test expect without payload. + let request = srv + .request(http::Method::GET, srv.url("/")) + .insert_header(("Expect", "100-continue")); + + let response = request.send().await; + assert!(response.is_err()); + + // test expect would fail to continue + let request = srv + .request(http::Method::GET, srv.url("/")) + .insert_header(("Expect", "100-continue")); + + let response = request.send_body("expect body").await.unwrap(); + assert_eq!(response.status(), StatusCode::EXPECTATION_FAILED); + + // test expect would continue + let request = srv + .request(http::Method::GET, srv.url("/")) + .insert_header(("Expect", "100-continue")) + .insert_header(("AUTH", "996")); + + let response = request.send_body("expect body").await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-http/tests/test_h2_timer.rs b/actix-http/tests/test_h2_timer.rs new file mode 100644 index 000000000..2b9c26e4a --- /dev/null +++ b/actix-http/tests/test_h2_timer.rs @@ -0,0 +1,153 @@ +use std::io; + +use actix_http::{error::Error, HttpService, Response}; +use actix_server::Server; +use tokio::io::AsyncWriteExt; + +#[actix_rt::test] +async fn h2_ping_pong() -> io::Result<()> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + let lst = std::net::TcpListener::bind("127.0.0.1:0")?; + + let addr = lst.local_addr().unwrap(); + + let join = std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let srv = Server::build() + .disable_signals() + .workers(1) + .listen("h2_ping_pong", lst, || { + HttpService::build() + .keep_alive(3) + .h2(|_| async { Ok::<_, Error>(Response::ok()) }) + .tcp() + })? + .run(); + + tx.send(srv.handle()).unwrap(); + + srv.await + }) + }); + + let handle = rx.recv().unwrap(); + + let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); + + // use a separate thread for h2 client so it can be blocked. + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + + let (mut tx, conn) = h2::client::handshake(stream).await.unwrap(); + + tokio::spawn(async move { conn.await.unwrap() }); + + let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap(); + let res = res.await.unwrap(); + + assert_eq!(res.status().as_u16(), 200); + + sync_tx.send(()).unwrap(); + + // intentionally block the client thread so it can not answer ping pong. + std::thread::sleep(std::time::Duration::from_secs(1000)); + }) + }); + + rx.recv().unwrap(); + + let now = std::time::Instant::now(); + + // stop server gracefully. this step would take up to 30 seconds. + handle.stop(true).await; + + // join server thread. only when connection are all gone this step would finish. + join.join().unwrap()?; + + // check the time used for join server thread so it's known that the server shutdown + // is from keep alive and not server graceful shutdown timeout. + assert!(now.elapsed() < std::time::Duration::from_secs(30)); + + Ok(()) +} + +#[actix_rt::test] +async fn h2_handshake_timeout() -> io::Result<()> { + let (tx, rx) = std::sync::mpsc::sync_channel(1); + + let lst = std::net::TcpListener::bind("127.0.0.1:0")?; + + let addr = lst.local_addr().unwrap(); + + let join = std::thread::spawn(move || { + actix_rt::System::new().block_on(async move { + let srv = Server::build() + .disable_signals() + .workers(1) + .listen("h2_ping_pong", lst, || { + HttpService::build() + .keep_alive(30) + // set first request timeout to 5 seconds. + // this is the timeout used for http2 handshake. + .client_timeout(5000) + .h2(|_| async { Ok::<_, Error>(Response::ok()) }) + .tcp() + })? + .run(); + + tx.send(srv.handle()).unwrap(); + + srv.await + }) + }); + + let handle = rx.recv().unwrap(); + + let (sync_tx, rx) = std::sync::mpsc::sync_channel(1); + + // use a separate thread for tcp client so it can be blocked. + std::thread::spawn(move || { + tokio::runtime::Builder::new_current_thread() + .enable_all() + .build() + .unwrap() + .block_on(async move { + let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap(); + + // do not send the last new line intentionally. + // This should hang the server handshake + let malicious_buf = b"PRI * HTTP/2.0\r\n\r\nSM\r\n"; + stream.write_all(malicious_buf).await.unwrap(); + stream.flush().await.unwrap(); + + sync_tx.send(()).unwrap(); + + // intentionally block the client thread so it sit idle and not do handshake. + std::thread::sleep(std::time::Duration::from_secs(1000)); + + drop(stream) + }) + }); + + rx.recv().unwrap(); + + let now = std::time::Instant::now(); + + // stop server gracefully. this step would take up to 30 seconds. + handle.stop(true).await; + + // join server thread. only when connection are all gone this step would finish. + join.join().unwrap()?; + + // check the time used for join server thread so it's known that the server shutdown + // is from handshake timeout and not server graceful shutdown timeout. + assert!(now.elapsed() < std::time::Duration::from_secs(30)); + + Ok(()) +} diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index f20cfd70c..86ee17c74 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -1,17 +1,30 @@ #![cfg(feature = "openssl")] -use std::io; -use actix_http::error::{ErrorBadRequest, PayloadError}; -use actix_http::http::header::{self, HeaderName, HeaderValue}; -use actix_http::http::{Method, StatusCode, Version}; -use actix_http::httpmessage::HttpMessage; -use actix_http::{body, Error, HttpService, Request, Response}; +extern crate tls_openssl as openssl; + +use std::{convert::Infallible, io}; + +use actix_http::{ + body::{BodyStream, BoxBody, SizedStream}, + error::PayloadError, + http::{ + header::{self, HeaderValue}, + Method, StatusCode, Version, + }, + Error, HttpMessage, HttpService, Request, Response, +}; use actix_http_test::test_server; use actix_service::{fn_service, ServiceFactoryExt}; +use actix_utils::future::{err, ok, ready}; use bytes::{Bytes, BytesMut}; -use futures_util::future::{err, ok, ready}; -use futures_util::stream::{once, Stream, StreamExt}; -use open_ssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod}; +use derive_more::{Display, Error}; +use futures_core::Stream; +use futures_util::stream::{once, StreamExt as _}; +use openssl::{ + pkey::PKey, + ssl::{SslAcceptor, SslMethod}, + x509::X509, +}; async fn load_body(stream: S) -> Result where @@ -31,29 +44,26 @@ where Ok(body) } -fn ssl_acceptor() -> SslAcceptor { - // load ssl keys +fn tls_config() -> SslAcceptor { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); + let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); + 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_certificate(&cert).unwrap(); + builder.set_private_key(&key).unwrap(); + builder.set_alpn_select_callback(|_, protos| { const H2: &[u8] = b"\x02h2"; - const H11: &[u8] = b"\x08http/1.1"; if protos.windows(3).any(|window| window == H2) { Ok(b"h2") - } else if protos.windows(9).any(|window| window == H11) { - Ok(b"http/1.1") } else { - Err(AlpnError::NOACK) + Err(openssl::ssl::AlpnError::NOACK) } }); - builder - .set_alpn_protos(b"\x08http/1.1\x02h2") - .expect("Can not contrust SslAcceptor"); + builder.set_alpn_protos(b"\x02h2").unwrap(); builder.build() } @@ -62,8 +72,8 @@ fn ssl_acceptor() -> SslAcceptor { async fn test_h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, Error>(Response::Ok().finish())) - .openssl(ssl_acceptor()) + .h2(|_| ok::<_, Error>(Response::ok())) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -80,9 +90,9 @@ async fn test_h2_1() -> io::Result<()> { .finish(|req: Request| { assert!(req.peer_addr().is_some()); assert_eq!(req.version(), Version::HTTP_2); - ok::<_, Error>(Response::Ok().finish()) + ok::<_, Error>(Response::ok()) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -99,9 +109,9 @@ async fn test_h2_body() -> io::Result<()> { HttpService::build() .h2(|mut req: Request<_>| async move { let body = load_body(req.take_payload()).await?; - Ok::<_, Error>(Response::Ok().body(body)) + Ok::<_, Error>(Response::ok().set_body(body)) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -119,46 +129,39 @@ async fn test_h2_content_length() { let srv = test_server(move || { HttpService::build() .h2(|req: Request| { - let indx: usize = req.uri().path()[1..].parse().unwrap(); + let idx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ - StatusCode::NO_CONTENT, StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, + StatusCode::NO_CONTENT, StatusCode::OK, StatusCode::NOT_FOUND, ]; - ok::<_, ()>(Response::new(statuses[indx])) + ok::<_, Infallible>(Response::new(statuses[idx])) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; - let header = HeaderName::from_static("content-length"); - let value = HeaderValue::from_static("0"); + static VALUE: HeaderValue = HeaderValue::from_static("0"); { - for i in 0..4 { + let req = srv.request(Method::HEAD, srv.surl("/0")).send(); + req.await.expect_err("should timeout on recv 1xx frame"); + + let req = srv.request(Method::GET, srv.surl("/0")).send(); + req.await.expect_err("should timeout on recv 1xx frame"); + + let req = srv.request(Method::GET, srv.surl("/1")).send(); + let response = req.await.unwrap(); + assert!(response.headers().get("content-length").is_none()); + + for &i in &[2, 3] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); - - let req = srv - .request(Method::HEAD, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); - } - - for i in 4..6 { - let req = srv - .request(Method::GET, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), Some(&value)); + assert_eq!(response.headers().get("content-length"), Some(&VALUE)); } } } @@ -171,7 +174,7 @@ async fn test_h2_headers() { let mut srv = test_server(move || { let data = data.clone(); HttpService::build().h2(move |_| { - let mut builder = Response::Ok(); + let mut builder = Response::build(StatusCode::OK); for idx in 0..90 { builder.insert_header( (format!("X-TEST-{}", idx).as_str(), @@ -190,9 +193,9 @@ async fn test_h2_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); } - ok::<_, ()>(builder.body(data.clone())) + ok::<_, Infallible>(builder.body(data.clone())) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }).await; @@ -230,8 +233,8 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h2_body2() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) - .openssl(ssl_acceptor()) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -248,8 +251,8 @@ async fn test_h2_body2() { async fn test_h2_head_empty() { let mut srv = test_server(move || { HttpService::build() - .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) - .openssl(ssl_acceptor()) + .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -272,8 +275,8 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) - .openssl(ssl_acceptor()) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -295,8 +298,8 @@ async fn test_h2_head_binary() { async fn test_h2_head_binary2() { let srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) - .openssl(ssl_acceptor()) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -314,13 +317,16 @@ async fn test_h2_head_binary2() { async fn test_h2_body_length() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| { - let body = once(ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), + .h2(|_| async { + let body = once(async { + Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) + }); + + Ok::<_, Infallible>( + Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -339,13 +345,13 @@ async fn test_h2_body_chunked_explicit() { HttpService::build() .h2(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok() + ok::<_, Infallible>( + Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -367,13 +373,13 @@ async fn test_h2_response_http_error_handling() { HttpService::build() .h2(fn_service(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( - Response::Ok() + ok::<_, Infallible>( + Response::build(StatusCode::OK) .insert_header((header::CONTENT_TYPE, broken_header)) .body(STR), ) })) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -383,15 +389,30 @@ async fn test_h2_response_http_error_handling() { // read response let bytes = srv.load_body(response).await.unwrap(); - assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); + assert_eq!( + bytes, + Bytes::from_static(b"error processing HTTP: failed to parse header value") + ); +} + +#[derive(Debug, Display, Error)] +#[display(fmt = "error")] +struct BadRequest; + +impl From for Response { + fn from(err: BadRequest) -> Self { + Response::build(StatusCode::BAD_REQUEST) + .body(err.to_string()) + .map_into_boxed_body() + } } #[actix_rt::test] async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::(ErrorBadRequest("error"))) - .openssl(ssl_acceptor()) + .h2(|_| err::, _>(BadRequest)) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -413,9 +434,9 @@ async fn test_h2_on_connect() { }) .h2(|req: Request| { assert!(req.extensions().contains::()); - ok::<_, ()>(Response::Ok().finish()) + ok::<_, Infallible>(Response::ok()) }) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 2f6b31f49..873752779 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -1,21 +1,33 @@ #![cfg(feature = "rustls")] -use actix_http::error::PayloadError; -use actix_http::http::header::{self, HeaderName, HeaderValue}; -use actix_http::http::{Method, StatusCode, Version}; -use actix_http::{body, error, Error, HttpService, Request, Response}; -use actix_http_test::test_server; -use actix_service::{fn_factory_with_config, fn_service}; -use bytes::{Bytes, BytesMut}; -use futures_util::future::{self, err, ok}; -use futures_util::stream::{once, Stream, StreamExt}; -use rust_tls::{ - internal::pemfile::{certs, pkcs8_private_keys}, - NoClientAuth, ServerConfig as RustlsServerConfig, +extern crate tls_rustls as rustls; + +use std::{ + convert::{Infallible, TryFrom}, + io::{self, BufReader, Write}, + net::{SocketAddr, TcpStream as StdTcpStream}, + sync::Arc, }; -use std::fs::File; -use std::io::{self, BufReader}; +use actix_http::{ + body::{BodyStream, BoxBody, SizedStream}, + error::PayloadError, + http::{ + header::{self, HeaderName, HeaderValue}, + Method, StatusCode, Version, + }, + Error, HttpService, Request, Response, +}; +use actix_http_test::test_server; +use actix_service::{fn_factory_with_config, fn_service}; +use actix_tls::connect::rustls::webpki_roots_cert_store; +use actix_utils::future::{err, ok}; +use bytes::{Bytes, BytesMut}; +use derive_more::{Display, Error}; +use futures_core::Stream; +use futures_util::stream::{once, StreamExt as _}; +use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; +use rustls_pemfile::{certs, pkcs8_private_keys}; async fn load_body(mut stream: S) -> Result where @@ -28,23 +40,67 @@ where Ok(body) } -fn ssl_acceptor() -> RustlsServerConfig { - // load ssl keys - let mut config = RustlsServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("../tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("../tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); +fn tls_config() -> RustlsServerConfig { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + + let cert_file = &mut BufReader::new(cert_file.as_bytes()); + let key_file = &mut BufReader::new(key_file.as_bytes()); + + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + + let mut config = RustlsServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap(); + + config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec()); + config.alpn_protocols.push(H2_ALPN_PROTOCOL.to_vec()); + config } +pub fn get_negotiated_alpn_protocol( + addr: SocketAddr, + client_alpn_protocol: &[u8], +) -> Option> { + let mut config = rustls::ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(webpki_roots_cert_store()) + .with_no_client_auth(); + + config.alpn_protocols.push(client_alpn_protocol.to_vec()); + + let mut sess = rustls::ClientConnection::new( + Arc::new(config), + ServerName::try_from("localhost").unwrap(), + ) + .unwrap(); + + let mut sock = StdTcpStream::connect(addr).unwrap(); + let mut stream = rustls::Stream::new(&mut sess, &mut sock); + + // The handshake will fails because the client will not be able to verify the server + // certificate, but it doesn't matter here as we are just interested in the negotiated ALPN + // protocol + let _ = stream.flush(); + + sess.alpn_protocol().map(|proto| proto.to_vec()) +} + #[actix_rt::test] async fn test_h1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() - .h1(|_| future::ok::<_, Error>(Response::Ok().finish())) - .rustls(ssl_acceptor()) + .h1(|_| ok::<_, Error>(Response::ok())) + .rustls(tls_config()) }) .await; @@ -57,8 +113,8 @@ async fn test_h1() -> io::Result<()> { async fn test_h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() - .h2(|_| future::ok::<_, Error>(Response::Ok().finish())) - .rustls(ssl_acceptor()) + .h2(|_| ok::<_, Error>(Response::ok())) + .rustls(tls_config()) }) .await; @@ -74,9 +130,9 @@ async fn test_h1_1() -> io::Result<()> { .h1(|req: Request| { assert!(req.peer_addr().is_some()); assert_eq!(req.version(), Version::HTTP_11); - future::ok::<_, Error>(Response::Ok().finish()) + ok::<_, Error>(Response::ok()) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; @@ -92,9 +148,9 @@ async fn test_h2_1() -> io::Result<()> { .finish(|req: Request| { assert!(req.peer_addr().is_some()); assert_eq!(req.version(), Version::HTTP_2); - future::ok::<_, Error>(Response::Ok().finish()) + ok::<_, Error>(Response::ok()) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; @@ -110,9 +166,9 @@ async fn test_h2_body1() -> io::Result<()> { HttpService::build() .h2(|mut req: Request<_>| async move { let body = load_body(req.take_payload()).await?; - Ok::<_, Error>(Response::Ok().body(body)) + Ok::<_, Error>(Response::ok().set_body(body)) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; @@ -131,37 +187,44 @@ async fn test_h2_content_length() { .h2(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ - StatusCode::NO_CONTENT, StatusCode::CONTINUE, - StatusCode::SWITCHING_PROTOCOLS, - StatusCode::PROCESSING, + StatusCode::NO_CONTENT, StatusCode::OK, StatusCode::NOT_FOUND, ]; - future::ok::<_, ()>(Response::new(statuses[indx])) + ok::<_, Infallible>(Response::new(statuses[indx])) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; let header = HeaderName::from_static("content-length"); let value = HeaderValue::from_static("0"); + { - for i in 0..4 { + for &i in &[0] { + let req = srv + .request(Method::HEAD, srv.surl(&format!("/{}", i))) + .send(); + let _response = req.await.expect_err("should timeout on recv 1xx frame"); + // assert_eq!(response.headers().get(&header), None); + + let req = srv + .request(Method::GET, srv.surl(&format!("/{}", i))) + .send(); + let _response = req.await.expect_err("should timeout on recv 1xx frame"); + // assert_eq!(response.headers().get(&header), None); + } + + for &i in &[1] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); let response = req.await.unwrap(); assert_eq!(response.headers().get(&header), None); - - let req = srv - .request(Method::HEAD, srv.surl(&format!("/{}", i))) - .send(); - let response = req.await.unwrap(); - assert_eq!(response.headers().get(&header), None); } - for i in 4..6 { + for &i in &[2, 3] { let req = srv .request(Method::GET, srv.surl(&format!("/{}", i))) .send(); @@ -179,7 +242,7 @@ async fn test_h2_headers() { let mut srv = test_server(move || { let data = data.clone(); HttpService::build().h2(move |_| { - let mut config = Response::Ok(); + let mut config = Response::build(StatusCode::OK); for idx in 0..90 { config.insert_header(( format!("X-TEST-{}", idx).as_str(), @@ -198,9 +261,9 @@ async fn test_h2_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); } - future::ok::<_, ()>(config.body(data.clone())) + ok::<_, Infallible>(config.body(data.clone())) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }).await; let response = srv.sget("/").send().await.unwrap(); @@ -237,8 +300,8 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h2_body2() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| future::ok::<_, ()>(Response::Ok().body(STR))) - .rustls(ssl_acceptor()) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .rustls(tls_config()) }) .await; @@ -254,8 +317,8 @@ async fn test_h2_body2() { async fn test_h2_head_empty() { let mut srv = test_server(move || { HttpService::build() - .finish(|_| ok::<_, ()>(Response::Ok().body(STR))) - .rustls(ssl_acceptor()) + .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .rustls(tls_config()) }) .await; @@ -280,8 +343,8 @@ async fn test_h2_head_empty() { async fn test_h2_head_binary() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) - .rustls(ssl_acceptor()) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .rustls(tls_config()) }) .await; @@ -305,8 +368,8 @@ async fn test_h2_head_binary() { async fn test_h2_head_binary2() { let srv = test_server(move || { HttpService::build() - .h2(|_| ok::<_, ()>(Response::Ok().body(STR))) - .rustls(ssl_acceptor()) + .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) + .rustls(tls_config()) }) .await; @@ -327,12 +390,12 @@ async fn test_h2_body_length() { let mut srv = test_server(move || { HttpService::build() .h2(|_| { - let body = once(ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), + let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref()))); + ok::<_, Infallible>( + Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; @@ -350,13 +413,13 @@ async fn test_h2_body_chunked_explicit() { HttpService::build() .h2(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok() + ok::<_, Infallible>( + Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; @@ -376,16 +439,16 @@ async fn test_h2_response_http_error_handling() { let mut srv = test_server(move || { HttpService::build() .h2(fn_factory_with_config(|_: ()| { - ok::<_, ()>(fn_service(|_| { + ok::<_, Infallible>(fn_service(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( - Response::Ok() + ok::<_, Infallible>( + Response::build(StatusCode::OK) .insert_header((http::header::CONTENT_TYPE, broken_header)) .body(STR), ) })) })) - .rustls(ssl_acceptor()) + .rustls(tls_config()) }) .await; @@ -394,15 +457,28 @@ async fn test_h2_response_http_error_handling() { // read response let bytes = srv.load_body(response).await.unwrap(); - assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); + assert_eq!( + bytes, + Bytes::from_static(b"error processing HTTP: failed to parse header value") + ); +} + +#[derive(Debug, Display, Error)] +#[display(fmt = "error")] +struct BadRequest; + +impl From for Response { + fn from(_: BadRequest) -> Self { + Response::bad_request().set_body(BoxBody::new("error")) + } } #[actix_rt::test] async fn test_h2_service_error() { let mut srv = test_server(move || { HttpService::build() - .h2(|_| err::(error::ErrorBadRequest("error"))) - .rustls(ssl_acceptor()) + .h2(|_| err::, _>(BadRequest)) + .rustls(tls_config()) }) .await; @@ -418,8 +494,8 @@ async fn test_h2_service_error() { async fn test_h1_service_error() { let mut srv = test_server(move || { HttpService::build() - .h1(|_| err::(error::ErrorBadRequest("error"))) - .rustls(ssl_acceptor()) + .h1(|_| err::, _>(BadRequest)) + .rustls(tls_config()) }) .await; @@ -430,3 +506,85 @@ async fn test_h1_service_error() { let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(b"error")); } + +const H2_ALPN_PROTOCOL: &[u8] = b"h2"; +const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1"; +const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom"; + +#[actix_rt::test] +async fn test_alpn_h1() -> io::Result<()> { + let srv = test_server(move || { + let mut config = tls_config(); + config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); + HttpService::build() + .h1(|_| ok::<_, Error>(Response::ok())) + .rustls(config) + }) + .await; + + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL), + Some(CUSTOM_ALPN_PROTOCOL.to_vec()) + ); + + let response = srv.sget("/").send().await.unwrap(); + assert!(response.status().is_success()); + + Ok(()) +} + +#[actix_rt::test] +async fn test_alpn_h2() -> io::Result<()> { + let srv = test_server(move || { + let mut config = tls_config(); + config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); + HttpService::build() + .h2(|_| ok::<_, Error>(Response::ok())) + .rustls(config) + }) + .await; + + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL), + Some(H2_ALPN_PROTOCOL.to_vec()) + ); + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL), + Some(CUSTOM_ALPN_PROTOCOL.to_vec()) + ); + + let response = srv.sget("/").send().await.unwrap(); + assert!(response.status().is_success()); + + Ok(()) +} + +#[actix_rt::test] +async fn test_alpn_h2_1() -> io::Result<()> { + let srv = test_server(move || { + let mut config = tls_config(); + config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); + HttpService::build() + .finish(|_| ok::<_, Error>(Response::ok())) + .rustls(config) + }) + .await; + + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL), + Some(H2_ALPN_PROTOCOL.to_vec()) + ); + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), HTTP1_1_ALPN_PROTOCOL), + Some(HTTP1_1_ALPN_PROTOCOL.to_vec()) + ); + assert_eq!( + get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL), + Some(CUSTOM_ALPN_PROTOCOL.to_vec()) + ); + + let response = srv.sget("/").send().await.unwrap(); + assert!(response.status().is_success()); + + Ok(()) +} diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index b4ef74406..adf2a28ca 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -1,30 +1,37 @@ -use std::io::{Read, Write}; -use std::time::Duration; -use std::{net, thread}; +use std::{ + convert::Infallible, + io::{Read, Write}, + net, thread, + time::Duration, +}; +use actix_http::{ + body::{self, BodyStream, BoxBody, SizedStream}, + header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, + StatusCode, +}; use actix_http_test::test_server; use actix_rt::time::sleep; use actix_service::fn_service; +use actix_utils::future::{err, ok, ready}; use bytes::Bytes; -use futures_util::future::{self, err, ok, ready, FutureExt}; -use futures_util::stream::{once, StreamExt}; -use regex::Regex; - -use actix_http::httpmessage::HttpMessage; -use actix_http::{ - body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, +use derive_more::{Display, Error}; +use futures_util::{ + stream::{once, StreamExt as _}, + FutureExt as _, }; +use regex::Regex; #[actix_rt::test] async fn test_h1() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) .client_disconnect(1000) .h1(|req: Request| { assert!(req.peer_addr().is_some()); - future::ok::<_, ()>(Response::Ok().finish()) + ok::<_, Infallible>(Response::ok()) }) .tcp() }) @@ -32,11 +39,13 @@ async fn test_h1() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_2() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) .client_timeout(1000) @@ -44,7 +53,7 @@ async fn test_h1_2() { .finish(|req: Request| { assert!(req.peer_addr().is_some()); assert_eq!(req.version(), http::Version::HTTP_11); - future::ok::<_, ()>(Response::Ok().finish()) + ok::<_, Infallible>(Response::ok()) }) .tcp() }) @@ -52,20 +61,32 @@ async fn test_h1_2() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; +} + +#[derive(Debug, Display, Error)] +#[display(fmt = "expect failed")] +struct ExpectFailed; + +impl From for Response { + fn from(_: ExpectFailed) -> Self { + Response::new(StatusCode::EXPECTATION_FAILED) + } } #[actix_rt::test] async fn test_expect_continue() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { if req.head().uri.query() == Some("yes=") { ok(req) } else { - err(error::ErrorPreconditionFailed("error")) + err(ExpectFailed) } })) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .finish(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -74,29 +95,31 @@ async fn test_expect_continue() { let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 412 Precondition Failed\r\ncontent-length")); + assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length")); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); + + srv.stop().await; } #[actix_rt::test] async fn test_expect_continue_h1() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .expect(fn_service(|req: Request| { sleep(Duration::from_millis(20)).then(move |_| { if req.head().uri.query() == Some("yes=") { ok(req) } else { - err(error::ErrorPreconditionFailed("error")) + err(ExpectFailed) } }) })) - .h1(fn_service(|_| future::ok::<_, ()>(Response::Ok().finish()))) + .h1(fn_service(|_| ok::<_, Infallible>(Response::ok()))) .tcp() }) .await; @@ -105,13 +128,15 @@ async fn test_expect_continue_h1() { let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); - assert!(data.starts_with("HTTP/1.1 412 Precondition Failed\r\ncontent-length")); + assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length")); let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); + + srv.stop().await; } #[actix_rt::test] @@ -119,18 +144,20 @@ async fn test_chunked_payload() { let chunk_sizes = vec![32768, 32, 32768]; let total_size: usize = chunk_sizes.iter().sum(); - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(fn_service(|mut request: Request| { request .take_payload() .map(|res| match res { Ok(pl) => pl, - Err(e) => panic!(format!("Error reading payload: {}", e)), + Err(e) => panic!("Error reading payload: {}", e), }) .fold(0usize, |acc, chunk| ready(acc + chunk.len())) .map(|req_size| { - Ok::<_, Error>(Response::Ok().body(format!("size={}", req_size))) + Ok::<_, Error>( + Response::ok().set_body(format!("size={}", req_size)), + ) }) })) .tcp() @@ -162,20 +189,23 @@ async fn test_chunked_payload() { let re = Regex::new(r"size=(\d+)").unwrap(); let size: usize = match re.captures(&data) { Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(), - None => panic!(format!("Failed to find size in HTTP Response: {}", data)), + None => panic!("Failed to find size in HTTP Response: {}", data), }; + size }; assert_eq!(returned_size, total_size); + + srv.stop().await; } #[actix_rt::test] async fn test_slow_request() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .client_timeout(100) - .finish(|_| future::ok::<_, ()>(Response::Ok().finish())) + .finish(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -185,13 +215,15 @@ async fn test_slow_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_malformed_request() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -201,13 +233,15 @@ async fn test_http1_malformed_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 400 Bad Request")); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -222,14 +256,16 @@ async fn test_http1_keepalive() { let mut data = vec![0; 1024]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_timeout() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(1) - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -244,13 +280,15 @@ async fn test_http1_keepalive_timeout() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_close() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -265,13 +303,15 @@ async fn test_http1_keepalive_close() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http10_keepalive_default_close() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -285,13 +325,15 @@ async fn test_http10_keepalive_default_close() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http10_keepalive() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -312,14 +354,16 @@ async fn test_http10_keepalive() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] async fn test_http1_keepalive_disabled() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .keep_alive(KeepAlive::Disabled) - .h1(|_| future::ok::<_, ()>(Response::Ok().finish())) + .h1(|_| ok::<_, Infallible>(Response::ok())) .tcp() }) .await; @@ -333,6 +377,8 @@ async fn test_http1_keepalive_disabled() { let mut data = vec![0; 1024]; let res = stream.read(&mut data).unwrap(); assert_eq!(res, 0); + + srv.stop().await; } #[actix_rt::test] @@ -342,7 +388,7 @@ async fn test_content_length() { StatusCode, }; - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .h1(|req: Request| { let indx: usize = req.uri().path()[1..].parse().unwrap(); @@ -354,7 +400,7 @@ async fn test_content_length() { StatusCode::OK, StatusCode::NOT_FOUND, ]; - future::ok::<_, ()>(Response::new(statuses[indx])) + ok::<_, Infallible>(Response::new(statuses[indx])) }) .tcp() }) @@ -380,6 +426,8 @@ async fn test_content_length() { assert_eq!(response.headers().get(&header), Some(&value)); } } + + srv.stop().await; } #[actix_rt::test] @@ -390,7 +438,7 @@ async fn test_h1_headers() { let mut srv = test_server(move || { let data = data.clone(); HttpService::build().h1(move |_| { - let mut builder = Response::Ok(); + let mut builder = Response::build(StatusCode::OK); for idx in 0..90 { builder.insert_header(( format!("X-TEST-{}", idx).as_str(), @@ -409,7 +457,7 @@ async fn test_h1_headers() { TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", )); } - future::ok::<_, ()>(builder.body(data.clone())) + ok::<_, Infallible>(builder.body(data.clone())) }).tcp() }).await; @@ -419,6 +467,8 @@ async fn test_h1_headers() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from(data2)); + + srv.stop().await; } const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ @@ -447,7 +497,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ async fn test_h1_body() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -458,13 +508,15 @@ async fn test_h1_body() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_head_empty() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -483,13 +535,15 @@ async fn test_h1_head_empty() { // read response let bytes = srv.load_body(response).await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_head_binary() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -508,13 +562,15 @@ async fn test_h1_head_binary() { // read response let bytes = srv.load_body(response).await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_head_binary2() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() - .h1(|_| ok::<_, ()>(Response::Ok().body(STR))) + .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .tcp() }) .await; @@ -529,6 +585,8 @@ async fn test_h1_head_binary2() { .unwrap(); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); } + + srv.stop().await; } #[actix_rt::test] @@ -536,9 +594,9 @@ async fn test_h1_body_length() { let mut srv = test_server(|| { HttpService::build() .h1(|_| { - let body = once(ok(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), + let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref()))); + ok::<_, Infallible>( + Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) .tcp() @@ -551,6 +609,8 @@ async fn test_h1_body_length() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -559,10 +619,10 @@ async fn test_h1_body_chunked_explicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>( - Response::Ok() + ok::<_, Infallible>( + Response::build(StatusCode::OK) .insert_header((header::TRANSFER_ENCODING, "chunked")) - .streaming(body), + .body(BodyStream::new(body)), ) }) .tcp() @@ -586,6 +646,8 @@ async fn test_h1_body_chunked_explicit() { // decode assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -594,7 +656,9 @@ async fn test_h1_body_chunked_implicit() { HttpService::build() .h1(|_| { let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); - ok::<_, ()>(Response::Ok().streaming(body)) + ok::<_, Infallible>( + Response::build(StatusCode::OK).body(BodyStream::new(body)), + ) }) .tcp() }) @@ -615,6 +679,8 @@ async fn test_h1_body_chunked_implicit() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -623,8 +689,8 @@ async fn test_h1_response_http_error_handling() { HttpService::build() .h1(fn_service(|_| { let broken_header = Bytes::from_static(b"\0\0\0"); - ok::<_, ()>( - Response::Ok() + ok::<_, Infallible>( + Response::build(StatusCode::OK) .insert_header((http::header::CONTENT_TYPE, broken_header)) .body(STR), ) @@ -638,14 +704,29 @@ async fn test_h1_response_http_error_handling() { // read response let bytes = srv.load_body(response).await.unwrap(); - assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); + assert_eq!( + bytes, + Bytes::from_static(b"error processing HTTP: failed to parse header value") + ); + + srv.stop().await; +} + +#[derive(Debug, Display, Error)] +#[display(fmt = "error")] +struct BadRequest; + +impl From for Response { + fn from(_: BadRequest) -> Self { + Response::bad_request().set_body(BoxBody::new("error")) + } } #[actix_rt::test] async fn test_h1_service_error() { let mut srv = test_server(|| { HttpService::build() - .h1(|_| future::err::(error::ErrorBadRequest("error"))) + .h1(|_| err::, _>(BadRequest)) .tcp() }) .await; @@ -656,18 +737,20 @@ async fn test_h1_service_error() { // read response let bytes = srv.load_body(response).await.unwrap(); assert_eq!(bytes, Bytes::from_static(b"error")); + + srv.stop().await; } #[actix_rt::test] async fn test_h1_on_connect() { - let srv = test_server(|| { + let mut srv = test_server(|| { HttpService::build() .on_connect_ext(|_, data| { data.insert(20isize); }) .h1(|req: Request| { assert!(req.extensions().contains::()); - future::ok::<_, ()>(Response::Ok().finish()) + ok::<_, Infallible>(Response::ok()) }) .tcp() }) @@ -675,4 +758,92 @@ async fn test_h1_on_connect() { let response = srv.get("/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await; +} + +/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1. +/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1 +#[actix_rt::test] +async fn test_not_modified_spec_h1() { + // TODO: this test needing a few seconds to complete reveals some weirdness with either the + // dispatcher or the client, though similar hangs occur on other tests in this file, only + // succeeding, it seems, because of the keepalive timer + + static CL: header::HeaderName = header::CONTENT_LENGTH; + + let mut srv = test_server(|| { + HttpService::build() + .h1(|req: Request| { + let res: Response = match req.path() { + // with no content-length + "/none" => { + Response::with_body(StatusCode::NOT_MODIFIED, body::None::new()) + .map_into_boxed_body() + } + + // with no content-length + "/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") + .map_into_boxed_body(), + + // with manual content-length header and specific None body + "/cl-none" => { + let mut res = Response::with_body( + StatusCode::NOT_MODIFIED, + body::None::new(), + ); + res.headers_mut() + .insert(CL.clone(), header::HeaderValue::from_static("24")); + res.map_into_boxed_body() + } + + // with manual content-length header and ignore-able body + "/cl-body" => { + let mut res = + Response::with_body(StatusCode::NOT_MODIFIED, "1234"); + res.headers_mut() + .insert(CL.clone(), header::HeaderValue::from_static("4")); + res.map_into_boxed_body() + } + + _ => panic!("unknown route"), + }; + + ok::<_, Infallible>(res) + }) + .tcp() + }) + .await; + + let res = srv.get("/none").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!(res.headers().get(&CL), None); + assert!(srv.load_body(res).await.unwrap().is_empty()); + + let res = srv.get("/body").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!(res.headers().get(&CL), None); + assert!(srv.load_body(res).await.unwrap().is_empty()); + + let res = srv.get("/cl-none").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!( + res.headers().get(&CL), + Some(&header::HeaderValue::from_static("24")), + ); + assert!(srv.load_body(res).await.unwrap().is_empty()); + + let res = srv.get("/cl-body").send().await.unwrap(); + assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED); + assert_eq!( + res.headers().get(&CL), + Some(&header::HeaderValue::from_static("4")), + ); + // server does not prevent payload from being sent but clients may choose not to read it + // TODO: this is probably a bug, especially since CL header can differ in length from the body + assert!(!srv.load_body(res).await.unwrap().is_empty()); + + // TODO: add stream response tests + + srv.stop().await; } diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index 976fc9164..c91382013 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -1,195 +1,196 @@ -use std::cell::Cell; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::sync::{Arc, Mutex}; +use std::{ + cell::Cell, + convert::Infallible, + task::{Context, Poll}, +}; use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; +use actix_http::{ + body::{BodySize, BoxBody}, + h1, + ws::{self, CloseCode, Frame, Item, Message}, + Error, HttpService, Request, Response, +}; use actix_http_test::test_server; use actix_service::{fn_factory, Service}; -use actix_utils::dispatcher::Dispatcher; use bytes::Bytes; -use futures_util::future; -use futures_util::task::{Context, Poll}; -use futures_util::{SinkExt, StreamExt}; +use derive_more::{Display, Error, From}; +use futures_core::future::LocalBoxFuture; +use futures_util::{SinkExt as _, StreamExt as _}; -struct WsService(Arc, Cell)>>); +#[derive(Clone)] +struct WsService(Cell); -impl WsService { +impl WsService { fn new() -> Self { - WsService(Arc::new(Mutex::new((PhantomData, Cell::new(false))))) + WsService(Cell::new(false)) } - fn set_polled(&mut self) { - *self.0.lock().unwrap().1.get_mut() = true; + fn set_polled(&self) { + self.0.set(true); } fn was_polled(&self) -> bool { - self.0.lock().unwrap().1.get() + self.0.get() } } -impl Clone for WsService { - fn clone(&self) -> Self { - WsService(self.0.clone()) +#[derive(Debug, Display, Error, From)] +enum WsServiceError { + #[display(fmt = "http error")] + Http(actix_http::Error), + + #[display(fmt = "ws handshake error")] + Ws(actix_http::ws::HandshakeError), + + #[display(fmt = "io error")] + Io(std::io::Error), + + #[display(fmt = "dispatcher error")] + Dispatcher, +} + +impl From for Response { + fn from(err: WsServiceError) -> Self { + match err { + WsServiceError::Http(err) => err.into(), + WsServiceError::Ws(err) => err.into(), + WsServiceError::Io(_err) => unreachable!(), + WsServiceError::Dispatcher => Response::internal_server_error() + .set_body(BoxBody::new(format!("{}", err))), + } } } -impl Service<(Request, Framed)> for WsService +impl Service<(Request, Framed)> for WsService where T: AsyncRead + AsyncWrite + Unpin + 'static, { type Response = (); - type Error = Error; - type Future = Pin>>>; + type Error = WsServiceError; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, _ctx: &mut Context<'_>) -> Poll> { + fn poll_ready(&self, _: &mut Context<'_>) -> Poll> { self.set_polled(); Poll::Ready(Ok(())) } - fn call( - &mut self, - (req, mut framed): (Request, Framed), - ) -> Self::Future { - let fut = async move { - let res = ws::handshake(req.head()).unwrap().message_body(()); + fn call(&self, (req, mut framed): (Request, Framed)) -> Self::Future { + assert!(self.was_polled()); - framed - .send((res, body::BodySize::None).into()) + Box::pin(async move { + let res = ws::handshake(req.head())?.message_body(())?; + + framed.send((res, BodySize::None).into()).await?; + + let framed = framed.replace_codec(ws::Codec::new()); + + ws::Dispatcher::with(framed, service) .await - .unwrap(); + .map_err(|_| WsServiceError::Dispatcher)?; - Dispatcher::new(framed.replace_codec(ws::Codec::new()), service) - .await - .map_err(|_| panic!()) - }; - - Box::pin(fut) + Ok(()) + }) } } -async fn service(msg: ws::Frame) -> Result { +async fn service(msg: Frame) -> Result { let msg = match msg { - ws::Frame::Ping(msg) => ws::Message::Pong(msg), - ws::Frame::Text(text) => { - ws::Message::Text(String::from_utf8_lossy(&text).into_owned().into()) + Frame::Ping(msg) => Message::Pong(msg), + Frame::Text(text) => { + Message::Text(String::from_utf8_lossy(&text).into_owned().into()) } - ws::Frame::Binary(bin) => ws::Message::Binary(bin), - ws::Frame::Continuation(item) => ws::Message::Continuation(item), - ws::Frame::Close(reason) => ws::Message::Close(reason), - _ => panic!(), + Frame::Binary(bin) => Message::Binary(bin), + Frame::Continuation(item) => Message::Continuation(item), + Frame::Close(reason) => Message::Close(reason), + _ => return Err(ws::ProtocolError::BadOpCode.into()), }; + Ok(msg) } #[actix_rt::test] async fn test_simple() { - let ws_service = WsService::new(); - let mut srv = test_server({ - let ws_service = ws_service.clone(); - move || { - let ws_service = ws_service.clone(); - HttpService::build() - .upgrade(fn_factory(move || future::ok::<_, ()>(ws_service.clone()))) - .finish(|_| future::ok::<_, ()>(Response::NotFound())) - .tcp() - } + let mut srv = test_server(|| { + HttpService::build() + .upgrade(fn_factory(|| async { + Ok::<_, Infallible>(WsService::new()) + })) + .finish(|_| async { Ok::<_, Infallible>(Response::not_found()) }) + .tcp() }) .await; // client service let mut framed = srv.ws().await.unwrap(); - framed.send(ws::Message::Text("text".into())).await.unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Text(Bytes::from_static(b"text")) - ); + framed.send(Message::Text("text".into())).await.unwrap(); + + let item = framed.next().await.unwrap().unwrap(); + assert_eq!(item, Frame::Text(Bytes::from_static(b"text"))); + + framed.send(Message::Binary("text".into())).await.unwrap(); + + let item = framed.next().await.unwrap().unwrap(); + assert_eq!(item, Frame::Binary(Bytes::from_static(&b"text"[..]))); + + framed.send(Message::Ping("text".into())).await.unwrap(); + let item = framed.next().await.unwrap().unwrap(); + assert_eq!(item, Frame::Pong("text".to_string().into())); framed - .send(ws::Message::Binary("text".into())) + .send(Message::Continuation(Item::FirstText("text".into()))) .await .unwrap(); - let (item, mut framed) = framed.into_future().await; + let item = framed.next().await.unwrap().unwrap(); assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Binary(Bytes::from_static(&b"text"[..])) - ); - - framed.send(ws::Message::Ping("text".into())).await.unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Pong("text".to_string().into()) - ); - - framed - .send(ws::Message::Continuation(ws::Item::FirstText( - "text".into(), - ))) - .await - .unwrap(); - let (item, mut framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Continuation(ws::Item::FirstText(Bytes::from_static(b"text"))) + item, + Frame::Continuation(Item::FirstText(Bytes::from_static(b"text"))) ); assert!(framed - .send(ws::Message::Continuation(ws::Item::FirstText( - "text".into() - ))) + .send(Message::Continuation(Item::FirstText("text".into()))) .await .is_err()); assert!(framed - .send(ws::Message::Continuation(ws::Item::FirstBinary( - "text".into() - ))) + .send(Message::Continuation(Item::FirstBinary("text".into()))) .await .is_err()); framed - .send(ws::Message::Continuation(ws::Item::Continue("text".into()))) + .send(Message::Continuation(Item::Continue("text".into()))) .await .unwrap(); - let (item, mut framed) = framed.into_future().await; + let item = framed.next().await.unwrap().unwrap(); assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Continuation(ws::Item::Continue(Bytes::from_static(b"text"))) + item, + Frame::Continuation(Item::Continue(Bytes::from_static(b"text"))) ); framed - .send(ws::Message::Continuation(ws::Item::Last("text".into()))) + .send(Message::Continuation(Item::Last("text".into()))) .await .unwrap(); - let (item, mut framed) = framed.into_future().await; + let item = framed.next().await.unwrap().unwrap(); assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Continuation(ws::Item::Last(Bytes::from_static(b"text"))) + item, + Frame::Continuation(Item::Last(Bytes::from_static(b"text"))) ); assert!(framed - .send(ws::Message::Continuation(ws::Item::Continue("text".into()))) + .send(Message::Continuation(Item::Continue("text".into()))) .await .is_err()); assert!(framed - .send(ws::Message::Continuation(ws::Item::Last("text".into()))) + .send(Message::Continuation(Item::Last("text".into()))) .await .is_err()); framed - .send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) + .send(Message::Close(Some(CloseCode::Normal.into()))) .await .unwrap(); - let (item, _framed) = framed.into_future().await; - assert_eq!( - item.unwrap().unwrap(), - ws::Frame::Close(Some(ws::CloseCode::Normal.into())) - ); - - assert!(ws_service.was_polled()); + let item = framed.next().await.unwrap().unwrap(); + assert_eq!(item, Frame::Close(Some(CloseCode::Normal.into()))); } diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index e1fe9c4af..d9ded57a4 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -3,6 +3,47 @@ ## Unreleased - 2021-xx-xx +## 0.4.0-beta.9 - 2021-12-01 +* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] + +[#2463]: https://github.com/actix/actix-web/pull/2463 + + +## 0.4.0-beta.8 - 2021-11-22 +* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] +* Added `MultipartError::NoContentDisposition` variant. [#2451] +* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] +* Added `Field::name` method for getting the field name. [#2451] +* `MultipartError` now marks variants with inner errors as the source. [#2451] +* `MultipartError` is now marked as non-exhaustive. [#2451] + +[#2451]: https://github.com/actix/actix-web/pull/2451 + + +## 0.4.0-beta.7 - 2021-10-20 +* Minimum supported Rust version (MSRV) is now 1.52. + + +## 0.4.0-beta.6 - 2021-09-09 +* Minimum supported Rust version (MSRV) is now 1.51. + + +## 0.4.0-beta.5 - 2021-06-17 +* No notable changes. + + +## 0.4.0-beta.4 - 2021-04-02 +* No notable changes. + + +## 0.4.0-beta.3 - 2021-03-09 +* No notable changes. + + +## 0.4.0-beta.2 - 2021-02-10 +* No notable changes. + + ## 0.4.0-beta.1 - 2021-01-07 * Fix multipart consuming payload before header checks. [#1513] * Update `bytes` to `1.0`. [#1813] diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 44a7e8d16..04a1d75ee 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-multipart" -version = "0.4.0-beta.1" +version = "0.4.0-beta.9" authors = ["Nikolay Kim "] -description = "Multipart support for actix web framework." -readme = "README.md" +description = "Multipart form support for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-multipart/" license = "MIT OR Apache-2.0" edition = "2018" @@ -16,17 +14,21 @@ name = "actix_multipart" path = "src/lib.rs" [dependencies] -actix-web = { version = "4.0.0-beta.1", default-features = false } -actix-utils = "3.0.0-beta.1" +actix-web = { version = "4.0.0-beta.11", default-features = false } +actix-utils = "3.0.0" bytes = "1" derive_more = "0.99.5" +futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } httparse = "1.3" -futures-util = { version = "0.3.7", default-features = false } +local-waker = "0.1" log = "0.4" mime = "0.3" twoway = "0.2" [dev-dependencies] -actix-rt = "2.0.0-beta.2" -actix-http = "3.0.0-beta.1" +actix-rt = "2.2" +actix-http = "3.0.0-beta.14" +futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } +tokio = { version = "1", features = ["sync"] } +tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index edb2e0020..85c78c5f3 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -1,8 +1,17 @@ -# Multipart support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-multipart)](https://crates.io/crates/actix-multipart) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# actix-multipart -## Documentation & community resources +> Multipart form support for Actix Web. -* [API Documentation](https://docs.rs/actix-multipart/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-multipart](https://crates.io/crates/actix-multipart) -* Minimum supported Rust version: 1.40 or later +[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.9)](https://docs.rs/actix-multipart/0.4.0-beta.9) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) +
+[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.9/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.9) +[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) + +## Documentation & Resources + +- [API Documentation](https://docs.rs/actix-multipart) +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index cdbb5d395..7d0da35e0 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -2,39 +2,52 @@ use actix_web::error::{ParseError, PayloadError}; use actix_web::http::StatusCode; use actix_web::ResponseError; -use derive_more::{Display, From}; +use derive_more::{Display, Error, From}; /// A set of errors that can occur during parsing multipart streams -#[derive(Debug, Display, From)] +#[non_exhaustive] +#[derive(Debug, Display, From, Error)] pub enum MultipartError { + /// Content-Disposition header is not found or is not equal to "form-data". + /// + /// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a + /// Content-Disposition header must always be present and equal to "form-data". + #[display(fmt = "No Content-Disposition `form-data` header")] + NoContentDisposition, + /// Content-Type header is not found - #[display(fmt = "No Content-type header found")] + #[display(fmt = "No Content-Type header found")] NoContentType, + /// Can not parse Content-Type header #[display(fmt = "Can not parse Content-Type header")] ParseContentType, + /// Multipart boundary is not found #[display(fmt = "Multipart boundary is not found")] Boundary, + /// Nested multipart is not supported #[display(fmt = "Nested multipart is not supported")] Nested, + /// Multipart stream is incomplete #[display(fmt = "Multipart stream is incomplete")] Incomplete, + /// Error during field parsing #[display(fmt = "{}", _0)] Parse(ParseError), + /// Payload error #[display(fmt = "{}", _0)] Payload(PayloadError), + /// Not consumed #[display(fmt = "Multipart stream is not consumed")] NotConsumed, } -impl std::error::Error for MultipartError {} - /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { @@ -45,11 +58,10 @@ impl ResponseError for MultipartError { #[cfg(test)] mod tests { use super::*; - use actix_web::HttpResponse; #[test] fn test_multipart_error() { - let resp: HttpResponse = MultipartError::Boundary.error_response(); + let resp = MultipartError::Boundary.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index 6aaa415c4..1ad1f203d 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -1,21 +1,22 @@ //! Multipart payload support + +use actix_utils::future::{ready, Ready}; use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; -use futures_util::future::{ok, Ready}; use crate::server::Multipart; -/// Get request's payload as multipart stream +/// Get request's payload as multipart stream. /// /// Content-type: multipart/form-data; /// /// ## Server example /// -/// ```rust -/// use futures_util::stream::{Stream, StreamExt}; +/// ``` /// use actix_web::{web, HttpResponse, Error}; -/// use actix_multipart as mp; +/// use actix_multipart::Multipart; +/// use futures_util::stream::StreamExt as _; /// -/// async fn index(mut payload: mp::Multipart) -> Result { +/// async fn index(mut payload: Multipart) -> Result { /// // iterate over multipart stream /// while let Some(item) = payload.next().await { /// let mut field = item?; @@ -25,20 +26,19 @@ use crate::server::Multipart; /// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk?)); /// } /// } +/// /// Ok(HttpResponse::Ok().into()) /// } -/// # fn main() {} /// ``` impl FromRequest for Multipart { type Error = Error; type Future = Ready>; - type Config = (); #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - ok(match Multipart::boundary(req.headers()) { + ready(Ok(match Multipart::boundary(req.headers()) { Ok(boundary) => Multipart::from_boundary(boundary, payload.take()), Err(err) => Multipart::from_error(err), - }) + })) } } diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 46dd0ee9b..38a24e28f 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,4 +1,4 @@ -//! Multipart form support for Actix web. +//! Multipart form support for Actix Web. #![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index 2cde5e8cf..23397b7ee 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -1,22 +1,24 @@ -//! Multipart payload support +//! Multipart response payload support. -use std::cell::{Cell, RefCell, RefMut}; -use std::convert::TryFrom; -use std::marker::PhantomData; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{cmp, fmt}; - -use bytes::{Bytes, BytesMut}; -use futures_util::stream::{LocalBoxStream, Stream, StreamExt}; - -use actix_utils::task::LocalWaker; -use actix_web::error::{ParseError, PayloadError}; -use actix_web::http::header::{ - self, ContentDisposition, HeaderMap, HeaderName, HeaderValue, +use std::{ + cell::{Cell, RefCell, RefMut}, + cmp, + convert::TryFrom, + fmt, + marker::PhantomData, + pin::Pin, + rc::Rc, + task::{Context, Poll}, }; +use actix_web::{ + error::{ParseError, PayloadError}, + http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}, +}; +use bytes::{Bytes, BytesMut}; +use futures_core::stream::{LocalBoxStream, Stream}; +use local_waker::LocalWaker; + use crate::error::MultipartError; const MAX_HEADERS: usize = 32; @@ -30,7 +32,7 @@ const MAX_HEADERS: usize = 32; pub struct Multipart { safety: Safety, error: Option, - inner: Option>>, + inner: Option, } enum InnerMultipartItem { @@ -42,10 +44,13 @@ enum InnerMultipartItem { enum InnerState { /// Stream eof Eof, + /// Skip data until first boundary FirstBoundary, + /// Reading boundary Boundary, + /// Reading Headers, Headers, } @@ -61,7 +66,7 @@ impl Multipart { /// Create multipart instance for boundary. pub fn new(headers: &HeaderMap, stream: S) -> Multipart where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, { match Self::boundary(headers) { Ok(boundary) => Multipart::from_boundary(boundary, stream), @@ -71,39 +76,32 @@ impl Multipart { /// Extract boundary info from headers. pub(crate) fn boundary(headers: &HeaderMap) -> Result { - if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - if let Some(boundary) = ct.get_param(mime::BOUNDARY) { - Ok(boundary.as_str().to_owned()) - } else { - Err(MultipartError::Boundary) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::ParseContentType) - } - } else { - Err(MultipartError::NoContentType) - } + headers + .get(&header::CONTENT_TYPE) + .ok_or(MultipartError::NoContentType)? + .to_str() + .ok() + .and_then(|content_type| content_type.parse::().ok()) + .ok_or(MultipartError::ParseContentType)? + .get_param(mime::BOUNDARY) + .map(|boundary| boundary.as_str().to_owned()) + .ok_or(MultipartError::Boundary) } /// Create multipart instance for given boundary and stream pub(crate) fn from_boundary(boundary: String, stream: S) -> Multipart where - S: Stream> + Unpin + 'static, + S: Stream> + 'static, { Multipart { error: None, safety: Safety::new(), - inner: Some(Rc::new(RefCell::new(InnerMultipart { + inner: Some(InnerMultipart { boundary, - payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), + payload: PayloadRef::new(PayloadBuffer::new(stream)), state: InnerState::FirstBoundary, item: InnerMultipartItem::None, - }))), + }), } } @@ -120,31 +118,33 @@ impl Multipart { impl Stream for Multipart { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - if let Some(err) = self.error.take() { - Poll::Ready(Some(Err(err))) - } else if self.safety.current() { - let this = self.get_mut(); - let mut inner = this.inner.as_mut().unwrap().borrow_mut(); - if let Some(mut payload) = inner.payload.get_mut(&this.safety) { - payload.poll_stream(cx)?; + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + + match this.inner.as_mut() { + Some(inner) => { + if let Some(mut buffer) = inner.payload.get_mut(&this.safety) { + // check safety and poll read payload to buffer. + buffer.poll_stream(cx)?; + } else if !this.safety.is_clean() { + // safety violation + return Poll::Ready(Some(Err(MultipartError::NotConsumed))); + } else { + return Poll::Pending; + } + + inner.poll(&this.safety, cx) } - inner.poll(&this.safety, cx) - } else if !self.safety.is_clean() { - Poll::Ready(Some(Err(MultipartError::NotConsumed))) - } else { - Poll::Pending + None => Poll::Ready(Some(Err(this + .error + .take() + .expect("Multipart polled after finish")))), } } } impl InnerMultipart { - fn read_headers( - payload: &mut PayloadBuffer, - ) -> Result, MultipartError> { + fn read_headers(payload: &mut PayloadBuffer) -> Result, MultipartError> { match payload.read_until(b"\r\n\r\n")? { None => { if payload.eof { @@ -159,17 +159,15 @@ impl InnerMultipart { Ok(httparse::Status::Complete((_, hdrs))) => { // convert headers let mut headers = HeaderMap::with_capacity(hdrs.len()); + for h in hdrs { - if let Ok(name) = HeaderName::try_from(h.name) { - if let Ok(value) = HeaderValue::try_from(h.value) { - headers.append(name, value); - } else { - return Err(ParseError::Header.into()); - } - } else { - return Err(ParseError::Header.into()); - } + let name = + HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; + let value = HeaderValue::try_from(h.value) + .map_err(|_| ParseError::Header)?; + headers.append(name, value); } + Ok(Some(headers)) } Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), @@ -226,8 +224,7 @@ impl InnerMultipart { if chunk.len() < boundary.len() { continue; } - if &chunk[..2] == b"--" - && &chunk[2..chunk.len() - 2] == boundary.as_bytes() + if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { break; } else { @@ -273,9 +270,7 @@ impl InnerMultipart { match field.borrow_mut().poll(safety) { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(_))) => continue, - Poll::Ready(Some(Err(e))) => { - return Poll::Ready(Some(Err(e))) - } + Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), Poll::Ready(None) => true, } } @@ -311,10 +306,7 @@ impl InnerMultipart { } // read boundary InnerState::Boundary => { - match InnerMultipart::read_boundary( - &mut *payload, - &self.boundary, - )? { + match InnerMultipart::read_boundary(&mut *payload, &self.boundary)? { None => return Poll::Pending, Some(eof) => { if eof { @@ -345,31 +337,55 @@ impl InnerMultipart { return Poll::Pending; }; - // content type - let mut mt = mime::APPLICATION_OCTET_STREAM; - if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { - if let Ok(content_type) = content_type.to_str() { - if let Ok(ct) = content_type.parse::() { - mt = ct; - } - } - } + // According to RFC 7578 §4.2, a Content-Disposition header must always be present and + // set to "form-data". + + let content_disposition = headers + .get(&header::CONTENT_DISPOSITION) + .and_then(|cd| ContentDisposition::from_raw(cd).ok()) + .filter(|content_disposition| { + let is_form_data = + content_disposition.disposition == header::DispositionType::FormData; + + let has_field_name = content_disposition + .parameters + .iter() + .any(|param| matches!(param, header::DispositionParam::Name(_))); + + is_form_data && has_field_name + }); + + let cd = if let Some(content_disposition) = content_disposition { + content_disposition + } else { + return Poll::Ready(Some(Err(MultipartError::NoContentDisposition))); + }; + + let ct: mime::Mime = headers + .get(&header::CONTENT_TYPE) + .and_then(|ct| ct.to_str().ok()) + .and_then(|ct| ct.parse().ok()) + .unwrap_or(mime::APPLICATION_OCTET_STREAM); self.state = InnerState::Boundary; - // nested multipart stream - if mt.type_() == mime::MULTIPART { - Poll::Ready(Some(Err(MultipartError::Nested))) - } else { - let field = Rc::new(RefCell::new(InnerField::new( - self.payload.clone(), - self.boundary.clone(), - &headers, - )?)); - self.item = InnerMultipartItem::Field(Rc::clone(&field)); - - Poll::Ready(Some(Ok(Field::new(safety.clone(cx), headers, mt, field)))) + // nested multipart stream is not supported + if ct.type_() == mime::MULTIPART { + return Poll::Ready(Some(Err(MultipartError::Nested))); } + + let field = + InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?; + + self.item = InnerMultipartItem::Field(Rc::clone(&field)); + + Poll::Ready(Some(Ok(Field::new( + safety.clone(cx), + headers, + ct, + cd, + field, + )))) } } } @@ -384,6 +400,7 @@ impl Drop for InnerMultipart { /// A single field in a multipart stream pub struct Field { ct: mime::Mime, + cd: ContentDisposition, headers: HeaderMap, inner: Rc>, safety: Safety, @@ -394,59 +411,71 @@ impl Field { safety: Safety, headers: HeaderMap, ct: mime::Mime, + cd: ContentDisposition, inner: Rc>, ) -> Self { Field { ct, + cd, headers, inner, safety, } } - /// Get a map of headers + /// Returns a reference to the field's header map. pub fn headers(&self) -> &HeaderMap { &self.headers } - /// Get the content type of the field + /// Returns a reference to the field's content (mime) type. pub fn content_type(&self) -> &mime::Mime { &self.ct } - /// Get the content disposition of the field, if it exists - pub fn content_disposition(&self) -> Option { - // RFC 7578: 'Each part MUST contain a Content-Disposition header field - // where the disposition type is "form-data".' - if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) - { - ContentDisposition::from_raw(content_disposition).ok() - } else { - None - } + /// Returns the field's Content-Disposition. + /// + /// Per [RFC 7578 §4.2]: 'Each part MUST contain a Content-Disposition header field where the + /// disposition type is "form-data". The Content-Disposition header field MUST also contain an + /// additional parameter of "name"; the value of the "name" parameter is the original field name + /// from the form.' + /// + /// This crate validates that it exists before returning a `Field`. As such, it is safe to + /// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as + /// a convenience. + /// + /// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2 + pub fn content_disposition(&self) -> &ContentDisposition { + &self.cd + } + + /// Returns the field's name. + /// + /// See [content_disposition] regarding guarantees about + pub fn name(&self) -> &str { + self.content_disposition() + .get_name() + .expect("field name should be guaranteed to exist in multipart form-data") } } impl Stream for Field { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - if self.safety.current() { - let mut inner = self.inner.borrow_mut(); - if let Some(mut payload) = - inner.payload.as_ref().unwrap().get_mut(&self.safety) - { - payload.poll_stream(cx)?; - } - inner.poll(&self.safety) - } else if !self.safety.is_clean() { - Poll::Ready(Some(Err(MultipartError::NotConsumed))) + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + let mut inner = this.inner.borrow_mut(); + if let Some(mut buffer) = inner.payload.as_ref().unwrap().get_mut(&this.safety) { + // check safety and poll read payload to buffer. + buffer.poll_stream(cx)?; + } else if !this.safety.is_clean() { + // safety violation + return Poll::Ready(Some(Err(MultipartError::NotConsumed))); } else { - Poll::Pending + return Poll::Pending; } + + inner.poll(&this.safety) } } @@ -470,20 +499,23 @@ struct InnerField { } impl InnerField { + fn new_in_rc( + payload: PayloadRef, + boundary: String, + headers: &HeaderMap, + ) -> Result>, PayloadError> { + Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this))) + } + fn new( payload: PayloadRef, boundary: String, headers: &HeaderMap, ) -> Result { let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { - if let Ok(s) = len.to_str() { - if let Ok(len) = s.parse::() { - Some(len) - } else { - return Err(PayloadError::Incomplete(None)); - } - } else { - return Err(PayloadError::Incomplete(None)); + match len.to_str().ok().and_then(|len| len.parse::().ok()) { + Some(len) => Some(len), + None => return Err(PayloadError::Incomplete(None)), } } else { None @@ -607,8 +639,7 @@ impl InnerField { return Poll::Ready(None); } - let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) - { + let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) { if !self.eof { let res = if let Some(ref mut len) = self.length { InnerField::read_len(&mut *payload, len) @@ -628,7 +659,9 @@ impl InnerField { Ok(None) => Poll::Pending, Ok(Some(line)) => { if line.as_ref() != b"\r\n" { - log::warn!("multipart field did not read all the data or it is malformed"); + log::warn!( + "multipart field did not read all the data or it is malformed" + ); } Poll::Ready(None) } @@ -656,10 +689,7 @@ impl PayloadRef { } } - fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option> - where - 'a: 'b, - { + fn get_mut(&self, s: &Safety) -> Option> { if s.current() { Some(self.payload.borrow_mut()) } else { @@ -676,9 +706,11 @@ impl Clone for PayloadRef { } } -/// Counter. It tracks of number of clones of payloads and give access to -/// payload only to top most task panics if Safety get destroyed and it not top -/// most task. +/// Counter. It tracks of number of clones of payloads and give access to payload only to top most. +/// * When dropped, parent task is awakened. This is to support the case where Field is +/// dropped in a separate task than Multipart. +/// * Assumes that parent owners don't move to different tasks; only the top-most is allowed to. +/// * If dropped and is not top most owner, is_clean flag is set to false. #[derive(Debug)] struct Safety { task: LocalWaker, @@ -721,15 +753,16 @@ impl Safety { impl Drop for Safety { fn drop(&mut self) { - // parent task is dead if Rc::strong_count(&self.payload) != self.level { - self.clean.set(true); + // Multipart dropped leaving a Field + self.clean.set(false); } + self.task.wake(); } } -/// Payload buffer +/// Payload buffer. struct PayloadBuffer { eof: bool, buf: BytesMut, @@ -737,7 +770,7 @@ struct PayloadBuffer { } impl PayloadBuffer { - /// Create new `PayloadBuffer` instance + /// Constructs new `PayloadBuffer` instance. fn new(stream: S) -> Self where S: Stream> + 'static, @@ -745,7 +778,7 @@ impl PayloadBuffer { PayloadBuffer { eof: false, buf: BytesMut::new(), - stream: stream.boxed_local(), + stream: Box::pin(stream), } } @@ -785,7 +818,7 @@ impl PayloadBuffer { } /// Read until specified ending - pub fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { + fn read_until(&mut self, line: &[u8]) -> Result, MultipartError> { let res = twoway::find_bytes(&self.buf, line) .map(|idx| self.buf.split_to(idx + line.len()).freeze()); @@ -797,22 +830,20 @@ impl PayloadBuffer { } /// Read bytes until new line delimiter - pub fn readline(&mut self) -> Result, MultipartError> { + fn readline(&mut self) -> Result, MultipartError> { self.read_until(b"\n") } /// Read bytes until new line delimiter or eof - pub fn readline_or_eof(&mut self) -> Result, MultipartError> { + fn readline_or_eof(&mut self) -> Result, MultipartError> { match self.readline() { - Err(MultipartError::Incomplete) if self.eof => { - Ok(Some(self.buf.split().freeze())) - } + Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), line => line, } } /// Put unprocessed data back to the buffer - pub fn unprocessed(&mut self, data: Bytes) { + fn unprocessed(&mut self, data: Bytes) { let buf = BytesMut::from(data.as_ref()); let buf = std::mem::replace(&mut self.buf, buf); self.buf.extend_from_slice(&buf); @@ -824,12 +855,15 @@ mod tests { use super::*; use actix_http::h1::Payload; - use actix_utils::mpsc; use actix_web::http::header::{DispositionParam, DispositionType}; + use actix_web::rt; use actix_web::test::TestRequest; use actix_web::FromRequest; use bytes::Bytes; - use futures_util::future::lazy; + use futures_util::{future::lazy, StreamExt}; + use std::time::Duration; + use tokio::sync::mpsc; + use tokio_stream::wrappers::UnboundedReceiverStream; #[actix_rt::test] async fn test_boundary() { @@ -875,13 +909,17 @@ mod tests { } fn create_stream() -> ( - mpsc::Sender>, + mpsc::UnboundedSender>, impl Stream>, ) { - let (tx, rx) = mpsc::channel(); + let (tx, rx) = mpsc::unbounded_channel(); - (tx, rx.map(|res| res.map_err(|_| panic!()))) + ( + tx, + UnboundedReceiverStream::new(rx).map(|res| res.map_err(|_| panic!())), + ) } + // Stream that returns from a Bytes, one char at a time and Pending every other poll() struct SlowStream { bytes: Bytes, @@ -902,19 +940,18 @@ mod tests { impl Stream for SlowStream { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if !this.ready { this.ready = true; cx.waker().wake_by_ref(); return Poll::Pending; } + if this.pos == this.bytes.len() { return Poll::Ready(None); } + let res = Poll::Ready(Some(Ok(this.bytes.slice(this.pos..(this.pos + 1))))); this.pos += 1; this.ready = false; @@ -930,6 +967,7 @@ mod tests { Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ test\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ data\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", @@ -981,7 +1019,7 @@ mod tests { let mut multipart = Multipart::new(&headers, payload); match multipart.next().await { Some(Ok(mut field)) => { - let cd = field.content_disposition().unwrap(); + let cd = field.content_disposition(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); @@ -1043,7 +1081,7 @@ mod tests { let mut multipart = Multipart::new(&headers, payload); match multipart.next().await.unwrap() { Ok(mut field) => { - let cd = field.content_disposition().unwrap(); + let cd = field.content_disposition(); assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); @@ -1198,4 +1236,99 @@ mod tests { _ => unreachable!(), } } + + #[actix_rt::test] + async fn no_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = SlowStream::new(bytes); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + MultipartError::NoContentDisposition, + )); + } + + #[actix_rt::test] + async fn no_name_in_content_disposition() { + let bytes = Bytes::from( + "testasdadsad\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ + Content-Disposition: form-data; filename=\"fn.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ + test\r\n\ + --abbc761f78ff4d7cb7573b5a23f96ef0\r\n", + ); + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + header::HeaderValue::from_static( + "multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"", + ), + ); + let payload = SlowStream::new(bytes); + + let mut multipart = Multipart::new(&headers, payload); + let res = multipart.next().await.unwrap(); + assert!(res.is_err()); + assert!(matches!( + res.unwrap_err(), + MultipartError::NoContentDisposition, + )); + } + + #[actix_rt::test] + async fn test_drop_multipart_dont_hang() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_simple_request_with_header(); + sender.send(Ok(bytes)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + let mut field = multipart.next().await.unwrap().unwrap(); + + drop(multipart); + + // should fail immediately + match field.next().await { + Some(Err(MultipartError::NotConsumed)) => {} + _ => panic!(), + }; + } + + #[actix_rt::test] + async fn test_drop_field_awaken_multipart() { + let (sender, payload) = create_stream(); + let (bytes, headers) = create_simple_request_with_header(); + sender.send(Ok(bytes)).unwrap(); + drop(sender); // eof + + let mut multipart = Multipart::new(&headers, payload); + let mut field = multipart.next().await.unwrap().unwrap(); + + let task = rt::spawn(async move { + rt::time::sleep(Duration::from_secs(1)).await; + assert_eq!(field.next().await.unwrap().unwrap(), "test"); + drop(field); + }); + + // dropping field should awaken current task + let _ = multipart.next().await.unwrap().unwrap(); + task.await.unwrap(); + } } diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md new file mode 100644 index 000000000..c2858f2ba --- /dev/null +++ b/actix-router/CHANGES.md @@ -0,0 +1,132 @@ +# Changes + +## Unreleased - 2021-xx-xx +* Minimum supported Rust version (MSRV) is now 1.52. + + +## 0.5.0-beta.2 - 2021-09-09 +* Introduce `ResourceDef::join`. [#380] +* Disallow prefix routes with tail segments. [#379] +* Enforce path separators on dynamic prefixes. [#378] +* Improve malformed path error message. [#384] +* Prefix segments now always end with with a segment delimiter or end-of-input. [#2355] +* Prefix segments with trailing slashes define a trailing empty segment. [#2355] +* Support multi-pattern prefixes and joins. [#2356] +* `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356] +* Support `build_resource_path` on multi-pattern resources. [#2356] +* Minimum supported Rust version (MSRV) is now 1.51. + +[#378]: https://github.com/actix/actix-net/pull/378 +[#379]: https://github.com/actix/actix-net/pull/379 +[#380]: https://github.com/actix/actix-net/pull/380 +[#384]: https://github.com/actix/actix-net/pull/384 +[#2355]: https://github.com/actix/actix-web/pull/2355 +[#2356]: https://github.com/actix/actix-web/pull/2356 + + +## 0.5.0-beta.1 - 2021-07-20 +* Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366] +* Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373] +* Fix segment interpolation leaving `Path` in unintended state after matching. [#368] +* Fix `ResourceDef` `PartialEq` implementation. [#373] +* Re-work `IntoPatterns` trait, adding a `Patterns` enum. [#372] +* Implement `IntoPatterns` for `bytestring::ByteString`. [#372] +* Rename `Path::{len => segment_count}` to be more descriptive of it's purpose. [#370] +* Rename `ResourceDef::{resource_path => resource_path_from_iter}`. [#371] +* `ResourceDef::resource_path_from_iter` now takes an `IntoIterator`. [#373] +* Rename `ResourceDef::{resource_path_named => resource_path_from_map}`. [#371] +* Rename `ResourceDef::{is_prefix_match => find_match}`. [#373] +* Rename `ResourceDef::{match_path => capture_match_info}`. [#373] +* Rename `ResourceDef::{match_path_checked => capture_match_info_fn}`. [#373] +* Remove `ResourceDef::name_mut` and introduce `ResourceDef::set_name`. [#373] +* Rename `Router::{*_checked => *_fn}`. [#373] +* Return type of `ResourceDef::name` is now `Option<&str>`. [#373] +* Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373] + +[#368]: https://github.com/actix/actix-net/pull/368 +[#366]: https://github.com/actix/actix-net/pull/366 +[#368]: https://github.com/actix/actix-net/pull/368 +[#370]: https://github.com/actix/actix-net/pull/370 +[#371]: https://github.com/actix/actix-net/pull/371 +[#372]: https://github.com/actix/actix-net/pull/372 +[#373]: https://github.com/actix/actix-net/pull/373 + + +## 0.4.0 - 2021-06-06 +* When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357] +* Path tail patterns now match new lines (`\n`) in request URL. [#360] +* Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359] +* Methods `Path::{add, add_static}` now take `impl Into>`. [#345] + +[#345]: https://github.com/actix/actix-net/pull/345 +[#357]: https://github.com/actix/actix-net/pull/357 +[#359]: https://github.com/actix/actix-net/pull/359 +[#360]: https://github.com/actix/actix-net/pull/360 + + +## 0.3.0 - 2019-12-31 +* Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 + + +## 0.2.7 - 2021-02-06 +* Add `Router::recognize_checked` [#247] + +[#247]: https://github.com/actix/actix-net/pull/247 + + +## 0.2.6 - 2021-01-09 +* Use `bytestring` version range compatible with Bytes v1.0. [#246] + +[#246]: https://github.com/actix/actix-net/pull/246 + + +## 0.2.5 - 2020-09-20 +* Fix `from_hex()` method + + +## 0.2.4 - 2019-12-31 +* Add `ResourceDef::resource_path_named()` path generation method + + +## 0.2.3 - 2019-12-25 +* Add impl `IntoPattern` for `&String` + + +## 0.2.2 - 2019-12-25 +* Use `IntoPattern` for `RouterBuilder::path()` + + +## 0.2.1 - 2019-12-25 +* Add `IntoPattern` trait +* Add multi-pattern resources + + +## 0.2.0 - 2019-12-07 +* Update http to 0.2 +* Update regex to 1.3 +* Use bytestring instead of string + + +## 0.1.5 - 2019-05-15 +* Remove debug prints + + +## 0.1.4 - 2019-05-15 +* Fix checked resource match + + +## 0.1.3 - 2019-04-22 +* Added support for `remainder match` (i.e "/path/{tail}*") + + +## 0.1.2 - 2019-04-07 +* Export `Quoter` type +* Allow to reset `Path` instance + + +## 0.1.1 - 2019-04-03 +* Get dynamic segment by name instead of iterator. + + +## 0.1.0 - 2019-03-09 +* Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml new file mode 100644 index 000000000..b95bca505 --- /dev/null +++ b/actix-router/Cargo.toml @@ -0,0 +1,38 @@ +[package] +name = "actix-router" +version = "0.5.0-beta.2" +authors = [ + "Nikolay Kim ", + "Ali MJ Al-Nasrawy ", + "Rob Ede ", +] +description = "Resource path matching and router" +keywords = ["actix", "router", "routing"] +repository = "https://github.com/actix/actix-web.git" +license = "MIT OR Apache-2.0" +edition = "2018" + +[lib] +name = "actix_router" +path = "src/lib.rs" + +[features] +default = ["http"] + +[dependencies] +bytestring = ">=0.1.5, <2" +firestorm = "0.4" +http = { version = "0.2.3", optional = true } +log = "0.4" +regex = "1.5" +serde = "1" + +[dev-dependencies] +criterion = { version = "0.3", features = ["html_reports"] } +firestorm = { version = "0.4", features = ["enable_system_time"] } +http = "0.2.5" +serde = { version = "1", features = ["derive"] } + +[[bench]] +name = "router" +harness = false diff --git a/actix-router/LICENSE-APACHE b/actix-router/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-router/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-router/LICENSE-MIT b/actix-router/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-router/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-router/benches/router.rs b/actix-router/benches/router.rs new file mode 100644 index 000000000..a428b9f13 --- /dev/null +++ b/actix-router/benches/router.rs @@ -0,0 +1,194 @@ +//! Based on https://github.com/ibraheemdev/matchit/blob/master/benches/bench.rs + +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +macro_rules! register { + (colon) => {{ + register!(finish => ":p1", ":p2", ":p3", ":p4") + }}; + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (regex) => {{ + register!(finish => "(.*)", "(.*)", "(.*)", "(.*)") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + std::array::IntoIter::new(arr) + }}; +} + +fn call() -> impl Iterator { + let arr = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", + ]; + + std::array::IntoIter::new(arr) +} + +fn compare_routers(c: &mut Criterion) { + let mut group = c.benchmark_group("Compare Routers"); + + let mut actix = actix_router::Router::::build(); + for route in register!(brackets) { + actix.path(route, true); + } + let actix = actix.finish(); + group.bench_function("actix", |b| { + b.iter(|| { + for route in call() { + let mut path = actix_router::Path::new(route); + black_box(actix.recognize(&mut path).unwrap()); + } + }); + }); + + let regex_set = regex::RegexSet::new(register!(regex)).unwrap(); + group.bench_function("regex", |b| { + b.iter(|| { + for route in call() { + black_box(regex_set.matches(route)); + } + }); + }); + + group.finish(); +} + +criterion_group!(benches, compare_routers); +criterion_main!(benches); diff --git a/actix-router/examples/flamegraph.rs b/actix-router/examples/flamegraph.rs new file mode 100644 index 000000000..798cc22d9 --- /dev/null +++ b/actix-router/examples/flamegraph.rs @@ -0,0 +1,169 @@ +macro_rules! register { + (brackets) => {{ + register!(finish => "{p1}", "{p2}", "{p3}", "{p4}") + }}; + (finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{ + let arr = [ + concat!("/authorizations"), + concat!("/authorizations/", $p1), + concat!("/applications/", $p1, "/tokens/", $p2), + concat!("/events"), + concat!("/repos/", $p1, "/", $p2, "/events"), + concat!("/networks/", $p1, "/", $p2, "/events"), + concat!("/orgs/", $p1, "/events"), + concat!("/users/", $p1, "/received_events"), + concat!("/users/", $p1, "/received_events/public"), + concat!("/users/", $p1, "/events"), + concat!("/users/", $p1, "/events/public"), + concat!("/users/", $p1, "/events/orgs/", $p2), + concat!("/feeds"), + concat!("/notifications"), + concat!("/repos/", $p1, "/", $p2, "/notifications"), + concat!("/notifications/threads/", $p1), + concat!("/notifications/threads/", $p1, "/subscription"), + concat!("/repos/", $p1, "/", $p2, "/stargazers"), + concat!("/users/", $p1, "/starred"), + concat!("/user/starred"), + concat!("/user/starred/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/subscribers"), + concat!("/users/", $p1, "/subscriptions"), + concat!("/user/subscriptions"), + concat!("/repos/", $p1, "/", $p2, "/subscription"), + concat!("/user/subscriptions/", $p1, "/", $p2), + concat!("/users/", $p1, "/gists"), + concat!("/gists"), + concat!("/gists/", $p1), + concat!("/gists/", $p1, "/star"), + concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/refs"), + concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3), + concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3), + concat!("/issues"), + concat!("/user/issues"), + concat!("/orgs/", $p1, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3), + concat!("/repos/", $p1, "/", $p2, "/assignees"), + concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"), + concat!("/repos/", $p1, "/", $p2, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/labels/", $p3), + concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"), + concat!("/repos/", $p1, "/", $p2, "/milestones/"), + concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3), + concat!("/emojis"), + concat!("/gitignore/templates"), + concat!("/gitignore/templates/", $p1), + concat!("/meta"), + concat!("/rate_limit"), + concat!("/users/", $p1, "/orgs"), + concat!("/user/orgs"), + concat!("/orgs/", $p1), + concat!("/orgs/", $p1, "/members"), + concat!("/orgs/", $p1, "/members", $p2), + concat!("/orgs/", $p1, "/public_members"), + concat!("/orgs/", $p1, "/public_members/", $p2), + concat!("/orgs/", $p1, "/teams"), + concat!("/teams/", $p1), + concat!("/teams/", $p1, "/members"), + concat!("/teams/", $p1, "/members", $p2), + concat!("/teams/", $p1, "/repos"), + concat!("/teams/", $p1, "/repos/", $p2, "/", $p3), + concat!("/user/teams"), + concat!("/repos/", $p1, "/", $p2, "/pulls"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"), + concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"), + concat!("/user/repos"), + concat!("/users/", $p1, "/repos"), + concat!("/orgs/", $p1, "/repos"), + concat!("/repositories"), + concat!("/repos/", $p1, "/", $p2), + concat!("/repos/", $p1, "/", $p2, "/contributors"), + concat!("/repos/", $p1, "/", $p2, "/languages"), + concat!("/repos/", $p1, "/", $p2, "/teams"), + concat!("/repos/", $p1, "/", $p2, "/tags"), + concat!("/repos/", $p1, "/", $p2, "/branches"), + concat!("/repos/", $p1, "/", $p2, "/branches/", $p3), + concat!("/repos/", $p1, "/", $p2, "/collaborators"), + concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3), + concat!("/repos/", $p1, "/", $p2, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"), + concat!("/repos/", $p1, "/", $p2, "/commits"), + concat!("/repos/", $p1, "/", $p2, "/commits/", $p3), + concat!("/repos/", $p1, "/", $p2, "/readme"), + concat!("/repos/", $p1, "/", $p2, "/keys"), + concat!("/repos/", $p1, "/", $p2, "/keys", $p3), + concat!("/repos/", $p1, "/", $p2, "/downloads"), + concat!("/repos/", $p1, "/", $p2, "/downloads", $p3), + concat!("/repos/", $p1, "/", $p2, "/forks"), + concat!("/repos/", $p1, "/", $p2, "/hooks"), + concat!("/repos/", $p1, "/", $p2, "/hooks", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases"), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3), + concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"), + concat!("/repos/", $p1, "/", $p2, "/stats/contributors"), + concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"), + concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"), + concat!("/repos/", $p1, "/", $p2, "/stats/participation"), + concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"), + concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3), + concat!("/search/repositories"), + concat!("/search/code"), + concat!("/search/issues"), + concat!("/search/users"), + concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4), + concat!("/legacy/repos/search/", $p1), + concat!("/legacy/user/search/", $p1), + concat!("/legacy/user/email/", $p1), + concat!("/users/", $p1), + concat!("/user"), + concat!("/users"), + concat!("/user/emails"), + concat!("/users/", $p1, "/followers"), + concat!("/user/followers"), + concat!("/users/", $p1, "/following"), + concat!("/user/following"), + concat!("/user/following/", $p1), + concat!("/users/", $p1, "/following", $p2), + concat!("/users/", $p1, "/keys"), + concat!("/user/keys"), + concat!("/user/keys/", $p1), + ]; + + arr.to_vec() + }}; +} + +static PATHS: [&str; 5] = [ + "/authorizations", + "/user/repos", + "/repos/rust-lang/rust/stargazers", + "/orgs/rust-lang/public_members/nikomatsakis", + "/repos/rust-lang/rust/releases/1.51.0", +]; + +fn main() { + let mut router = actix_router::Router::::build(); + + for route in register!(brackets) { + router.path(route, true); + } + + let actix = router.finish(); + + if firestorm::enabled() { + firestorm::bench("target", || { + for &route in &PATHS { + let mut path = actix_router::Path::new(route); + actix.recognize(&mut path).unwrap(); + } + }) + .unwrap(); + } +} diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs new file mode 100644 index 000000000..775c48b8a --- /dev/null +++ b/actix-router/src/de.rs @@ -0,0 +1,723 @@ +use serde::de::{self, Deserializer, Error as DeError, Visitor}; +use serde::forward_to_deserialize_any; + +use crate::path::{Path, PathIter}; +use crate::ResourcePath; + +macro_rules! unsupported_type { + ($trait_fn:ident, $name:expr) => { + fn $trait_fn(self, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom(concat!( + "unsupported type: ", + $name + ))) + } + }; +} + +macro_rules! parse_single_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() != 1 { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected 1", + self.path.segment_count() + ) + .as_str(), + )) + } else { + let v = self.path[0].parse().map_err(|_| { + de::value::Error::custom(format!( + "can not parse {:?} to a {}", + &self.path[0], $tp + )) + })?; + visitor.$visit_fn(v) + } + } + }; +} + +pub struct PathDeserializer<'de, T: ResourcePath> { + path: &'de Path, +} + +impl<'de, T: ResourcePath + 'de> PathDeserializer<'de, T> { + pub fn new(path: &'de Path) -> Self { + PathDeserializer { path } + } +} + +impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T> { + type Error = de::value::Error; + + fn deserialize_map(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_map(ParamsDeserializer { + params: self.path.iter(), + current: None, + }) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_map(visitor) + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + self.deserialize_unit(visitor) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, len: usize, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.segment_count(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + len: usize, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() < len { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected {}", + self.path.segment_count(), + len + ) + .as_str(), + )) + } else { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + if self.path.is_empty() { + Err(de::value::Error::custom("expected at least one parameters")) + } else { + visitor.visit_enum(ValueEnum { + value: &self.path[0], + }) + } + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + if self.path.segment_count() != 1 { + Err(de::value::Error::custom( + format!( + "wrong number of parameters: {} expected 1", + self.path.segment_count() + ) + .as_str(), + )) + } else { + visitor.visit_str(&self.path[0]) + } + } + + fn deserialize_seq(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_seq(ParamsSeq { + params: self.path.iter(), + }) + } + + unsupported_type!(deserialize_any, "'any'"); + unsupported_type!(deserialize_bytes, "bytes"); + unsupported_type!(deserialize_option, "Option"); + unsupported_type!(deserialize_identifier, "identifier"); + unsupported_type!(deserialize_ignored_any, "ignored_any"); + + parse_single_value!(deserialize_bool, visit_bool, "bool"); + parse_single_value!(deserialize_i8, visit_i8, "i8"); + parse_single_value!(deserialize_i16, visit_i16, "i16"); + parse_single_value!(deserialize_i32, visit_i32, "i32"); + parse_single_value!(deserialize_i64, visit_i64, "i64"); + parse_single_value!(deserialize_u8, visit_u8, "u8"); + parse_single_value!(deserialize_u16, visit_u16, "u16"); + parse_single_value!(deserialize_u32, visit_u32, "u32"); + parse_single_value!(deserialize_u64, visit_u64, "u64"); + parse_single_value!(deserialize_f32, visit_f32, "f32"); + parse_single_value!(deserialize_f64, visit_f64, "f64"); + parse_single_value!(deserialize_string, visit_string, "String"); + parse_single_value!(deserialize_byte_buf, visit_string, "String"); + parse_single_value!(deserialize_char, visit_char, "char"); +} + +struct ParamsDeserializer<'de, T: ResourcePath> { + params: PathIter<'de, T>, + current: Option<(&'de str, &'de str)>, +} + +impl<'de, T: ResourcePath> de::MapAccess<'de> for ParamsDeserializer<'de, T> { + type Error = de::value::Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + self.current = self.params.next().map(|ref item| (item.0, item.1)); + match self.current { + Some((key, _)) => Ok(Some(seed.deserialize(Key { key })?)), + None => Ok(None), + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some((_, value)) = self.current.take() { + seed.deserialize(Value { value }) + } else { + Err(de::value::Error::custom("unexpected item")) + } + } +} + +struct Key<'de> { + key: &'de str, +} + +impl<'de> Deserializer<'de> for Key<'de> { + type Error = de::value::Error; + + fn deserialize_identifier(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_str(self.key) + } + + fn deserialize_any(self, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("Unexpected")) + } + + forward_to_deserialize_any! { + bool i8 i16 i32 i64 u8 u16 u32 u64 f32 f64 char str string bytes + byte_buf option unit unit_struct newtype_struct seq tuple + tuple_struct map struct enum ignored_any + } +} + +macro_rules! parse_value { + ($trait_fn:ident, $visit_fn:ident, $tp:tt) => { + fn $trait_fn(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + let v = self.value.parse().map_err(|_| { + de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp)) + })?; + visitor.$visit_fn(v) + } + }; +} + +struct Value<'de> { + value: &'de str, +} + +impl<'de> Deserializer<'de> for Value<'de> { + type Error = de::value::Error; + + parse_value!(deserialize_bool, visit_bool, "bool"); + parse_value!(deserialize_i8, visit_i8, "i8"); + parse_value!(deserialize_i16, visit_i16, "i16"); + parse_value!(deserialize_i32, visit_i32, "i16"); + parse_value!(deserialize_i64, visit_i64, "i64"); + parse_value!(deserialize_u8, visit_u8, "u8"); + parse_value!(deserialize_u16, visit_u16, "u16"); + parse_value!(deserialize_u32, visit_u32, "u32"); + parse_value!(deserialize_u64, visit_u64, "u64"); + parse_value!(deserialize_f32, visit_f32, "f32"); + parse_value!(deserialize_f64, visit_f64, "f64"); + parse_value!(deserialize_string, visit_string, "String"); + parse_value!(deserialize_byte_buf, visit_string, "String"); + parse_value!(deserialize_char, visit_char, "char"); + + fn deserialize_ignored_any(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_unit_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_unit() + } + + fn deserialize_bytes(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_bytes(self.value.as_bytes()) + } + + fn deserialize_str(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_borrowed_str(self.value) + } + + fn deserialize_option(self, visitor: V) -> Result + where + V: Visitor<'de>, + { + visitor.visit_some(self) + } + + fn deserialize_enum( + self, + _: &'static str, + _: &'static [&'static str], + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_enum(ValueEnum { value: self.value }) + } + + fn deserialize_newtype_struct( + self, + _: &'static str, + visitor: V, + ) -> Result + where + V: Visitor<'de>, + { + visitor.visit_newtype_struct(self) + } + + fn deserialize_tuple(self, _: usize, _: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple")) + } + + fn deserialize_struct( + self, + _: &'static str, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: struct")) + } + + fn deserialize_tuple_struct( + self, + _: &'static str, + _: usize, + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("unsupported type: tuple struct")) + } + + unsupported_type!(deserialize_any, "any"); + unsupported_type!(deserialize_seq, "seq"); + unsupported_type!(deserialize_map, "map"); + unsupported_type!(deserialize_identifier, "identifier"); +} + +struct ParamsSeq<'de, T: ResourcePath> { + params: PathIter<'de, T>, +} + +impl<'de, T: ResourcePath> de::SeqAccess<'de> for ParamsSeq<'de, T> { + type Error = de::value::Error; + + fn next_element_seed(&mut self, seed: U) -> Result, Self::Error> + where + U: de::DeserializeSeed<'de>, + { + match self.params.next() { + Some(item) => Ok(Some(seed.deserialize(Value { value: item.1 })?)), + None => Ok(None), + } + } +} + +struct ValueEnum<'de> { + value: &'de str, +} + +impl<'de> de::EnumAccess<'de> for ValueEnum<'de> { + type Error = de::value::Error; + type Variant = UnitVariant; + + fn variant_seed(self, seed: V) -> Result<(V::Value, Self::Variant), Self::Error> + where + V: de::DeserializeSeed<'de>, + { + Ok((seed.deserialize(Key { key: self.value })?, UnitVariant)) + } +} + +struct UnitVariant; + +impl<'de> de::VariantAccess<'de> for UnitVariant { + type Error = de::value::Error; + + fn unit_variant(self) -> Result<(), Self::Error> { + Ok(()) + } + + fn newtype_variant_seed(self, _seed: T) -> Result + where + T: de::DeserializeSeed<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn tuple_variant(self, _len: usize, _visitor: V) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } + + fn struct_variant( + self, + _: &'static [&'static str], + _: V, + ) -> Result + where + V: Visitor<'de>, + { + Err(de::value::Error::custom("not supported")) + } +} + +#[cfg(test)] +mod tests { + use serde::{de, Deserialize}; + + use super::*; + use crate::path::Path; + use crate::router::Router; + + #[derive(Deserialize)] + struct MyStruct { + key: String, + value: String, + } + + #[derive(Deserialize)] + struct Id { + _id: String, + } + + #[derive(Debug, Deserialize)] + struct Test1(String, u32); + + #[derive(Debug, Deserialize)] + struct Test2 { + key: String, + value: u32, + } + + #[derive(Debug, Deserialize, PartialEq)] + #[serde(rename_all = "lowercase")] + enum TestEnum { + Val1, + Val2, + } + + #[derive(Debug, Deserialize)] + struct Test3 { + val: TestEnum, + } + + #[test] + fn test_request_extract() { + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/user1/"); + assert!(router.recognize(&mut path).is_some()); + + let s: MyStruct = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, "user1"); + + let s: (String, String) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, "user1"); + + let mut router = Router::<()>::build(); + router.path("/{key}/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/32/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Test1 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let s: Test2 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.key, "name"); + assert_eq!(s.value, 32); + + let s: (String, u8) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(s.0, "name"); + assert_eq!(s.1, 32); + + let res: Vec = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(res[0], "name".to_owned()); + assert_eq!(res[1], "32".to_owned()); + } + + #[test] + fn test_extract_path_single() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/32/"); + assert!(router.recognize(&mut path).is_some()); + let i: i8 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, 32); + } + + #[test] + fn test_extract_enum() { + let mut router = Router::<()>::build(); + router.path("/{val}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/"); + assert!(router.recognize(&mut path).is_some()); + let i: TestEnum = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, TestEnum::Val1); + + let mut router = Router::<()>::build(); + router.path("/{val1}/{val2}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/val2/"); + assert!(router.recognize(&mut path).is_some()); + let i: (TestEnum, TestEnum) = + de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i, (TestEnum::Val1, TestEnum::Val2)); + } + + #[test] + fn test_extract_enum_value() { + let mut router = Router::<()>::build(); + router.path("/{val}/", ()); + let router = router.finish(); + + let mut path = Path::new("/val1/"); + assert!(router.recognize(&mut path).is_some()); + let i: Test3 = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + assert_eq!(i.val, TestEnum::Val1); + + let mut path = Path::new("/val3/"); + assert!(router.recognize(&mut path).is_some()); + let i: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(i.is_err()); + assert!(format!("{:?}", i).contains("unknown variant")); + } + + #[test] + fn test_extract_errors() { + let mut router = Router::<()>::build(); + router.path("/{value}/", ()); + let router = router.finish(); + + let mut path = Path::new("/name/"); + assert!(router.recognize(&mut path).is_some()); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("wrong number of parameters")); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("can not parse")); + + let s: Result<(String, String), de::value::Error> = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("wrong number of parameters")); + + let s: Result = + de::Deserialize::deserialize(PathDeserializer::new(&path)); + assert!(s.is_err()); + assert!(format!("{:?}", s).contains("can not parse")); + } + + // #[test] + // fn test_extract_path_decode() { + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); + + // macro_rules! test_single_value { + // ($value:expr, $expected:expr) => {{ + // let req = TestRequest::with_uri($value).finish(); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + // assert_eq!( + // *Path::::from_request(&req, &PathConfig::default()).unwrap(), + // $expected + // ); + // }}; + // } + + // test_single_value!("/%25/", "%"); + // test_single_value!("/%40%C2%A3%24%25%5E%26%2B%3D/", "@£$%^&+="); + // test_single_value!("/%2B/", "+"); + // test_single_value!("/%252B/", "%2B"); + // test_single_value!("/%2F/", "/"); + // test_single_value!("/%252F/", "%2F"); + // test_single_value!( + // "/http%3A%2F%2Flocalhost%3A80%2Ffoo/", + // "http://localhost:80/foo" + // ); + // test_single_value!("/%2Fvar%2Flog%2Fsyslog/", "/var/log/syslog"); + // test_single_value!( + // "/http%3A%2F%2Flocalhost%3A80%2Ffile%2F%252Fvar%252Flog%252Fsyslog/", + // "http://localhost:80/file/%2Fvar%2Flog%2Fsyslog" + // ); + + // let req = TestRequest::with_uri("/%25/7/?id=test").finish(); + + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{key}/{value}/"))); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + + // let s = Path::::from_request(&req, &PathConfig::default()).unwrap(); + // assert_eq!(s.key, "%"); + // assert_eq!(s.value, 7); + + // let s = Path::<(String, String)>::from_request(&req, &PathConfig::default()).unwrap(); + // assert_eq!(s.0, "%"); + // assert_eq!(s.1, "7"); + // } + + // #[test] + // fn test_extract_path_no_decode() { + // let mut router = Router::<()>::default(); + // router.register_resource(Resource::new(ResourceDef::new("/{value}/"))); + + // let req = TestRequest::with_uri("/%25/").finish(); + // let info = router.recognize(&req, &(), 0); + // let req = req.with_route_info(info); + // assert_eq!( + // *Path::::from_request(&req, &&PathConfig::default().disable_decoding()) + // .unwrap(), + // "%25" + // ); + // } +} diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs new file mode 100644 index 000000000..463e59e42 --- /dev/null +++ b/actix-router/src/lib.rs @@ -0,0 +1,149 @@ +//! Resource path matching and router. + +#![deny(rust_2018_idioms, nonstandard_style)] +#![doc(html_logo_url = "https://actix.rs/img/logo.png")] +#![doc(html_favicon_url = "https://actix.rs/favicon.ico")] + +mod de; +mod path; +mod resource; +mod router; + +pub use self::de::PathDeserializer; +pub use self::path::Path; +pub use self::resource::ResourceDef; +pub use self::router::{ResourceInfo, Router, RouterBuilder}; + +// TODO: this trait is necessary, document it +// see impl Resource for ServiceRequest +pub trait Resource { + fn resource_path(&mut self) -> &mut Path; +} + +pub trait ResourcePath { + fn path(&self) -> &str; +} + +impl ResourcePath for String { + fn path(&self) -> &str { + self.as_str() + } +} + +impl<'a> ResourcePath for &'a str { + fn path(&self) -> &str { + self + } +} + +impl ResourcePath for bytestring::ByteString { + fn path(&self) -> &str { + &*self + } +} + +/// One or many patterns. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub enum Patterns { + Single(String), + List(Vec), +} + +impl Patterns { + pub fn is_empty(&self) -> bool { + match self { + Patterns::Single(_) => false, + Patterns::List(pats) => pats.is_empty(), + } + } +} + +/// Helper trait for type that could be converted to one or more path pattern. +pub trait IntoPatterns { + fn patterns(&self) -> Patterns; +} + +impl IntoPatterns for String { + fn patterns(&self) -> Patterns { + Patterns::Single(self.clone()) + } +} + +impl<'a> IntoPatterns for &'a String { + fn patterns(&self) -> Patterns { + Patterns::Single((*self).clone()) + } +} + +impl<'a> IntoPatterns for &'a str { + fn patterns(&self) -> Patterns { + Patterns::Single((*self).to_owned()) + } +} + +impl IntoPatterns for bytestring::ByteString { + fn patterns(&self) -> Patterns { + Patterns::Single(self.to_string()) + } +} + +impl IntoPatterns for Patterns { + fn patterns(&self) -> Patterns { + self.clone() + } +} + +impl> IntoPatterns for Vec { + fn patterns(&self) -> Patterns { + let mut patterns = self.iter().map(|v| v.as_ref().to_owned()); + + match patterns.size_hint() { + (1, _) => Patterns::Single(patterns.next().unwrap()), + _ => Patterns::List(patterns.collect()), + } + } +} + +macro_rules! array_patterns_single (($tp:ty) => { + impl IntoPatterns for [$tp; 1] { + fn patterns(&self) -> Patterns { + Patterns::Single(self[0].to_owned()) + } + } +}); + +macro_rules! array_patterns_multiple (($tp:ty, $str_fn:expr, $($num:tt) +) => { + // for each array length specified in $num + $( + impl IntoPatterns for [$tp; $num] { + fn patterns(&self) -> Patterns { + Patterns::List(self.iter().map($str_fn).collect()) + } + } + )+ +}); + +array_patterns_single!(&str); +array_patterns_multiple!(&str, |&v| v.to_owned(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +array_patterns_single!(String); +array_patterns_multiple!(String, |v| v.clone(), 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16); + +#[cfg(feature = "http")] +mod url; + +#[cfg(feature = "http")] +pub use self::url::{Quoter, Url}; + +#[cfg(feature = "http")] +mod http_impls { + use http::Uri; + + use super::ResourcePath; + + impl ResourcePath for Uri { + fn path(&self) -> &str { + self.path() + } + } +} diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs new file mode 100644 index 000000000..9af7b0b8b --- /dev/null +++ b/actix-router/src/path.rs @@ -0,0 +1,220 @@ +use std::borrow::Cow; +use std::ops::Index; + +use firestorm::profile_method; +use serde::de; + +use crate::{de::PathDeserializer, Resource, ResourcePath}; + +#[derive(Debug, Clone)] +pub(crate) enum PathItem { + Static(Cow<'static, str>), + Segment(u16, u16), +} + +impl Default for PathItem { + fn default() -> Self { + Self::Static(Cow::Borrowed("")) + } +} + +/// Resource path match information. +/// +/// If resource path contains variable patterns, `Path` stores them. +#[derive(Debug, Clone, Default)] +pub struct Path { + path: T, + pub(crate) skip: u16, + pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>, +} + +impl Path { + pub fn new(path: T) -> Path { + Path { + path, + skip: 0, + segments: Vec::new(), + } + } + + /// Get reference to inner path instance. + #[inline] + pub fn get_ref(&self) -> &T { + &self.path + } + + /// Get mutable reference to inner path instance. + #[inline] + pub fn get_mut(&mut self) -> &mut T { + &mut self.path + } + + /// Path. + #[inline] + pub fn path(&self) -> &str { + profile_method!(path); + + let skip = self.skip as usize; + let path = self.path.path(); + if skip <= path.len() { + &path[skip..] + } else { + "" + } + } + + /// Set new path. + #[inline] + pub fn set(&mut self, path: T) { + self.skip = 0; + self.path = path; + self.segments.clear(); + } + + /// Reset state. + #[inline] + pub fn reset(&mut self) { + self.skip = 0; + self.segments.clear(); + } + + /// Skip first `n` chars in path. + #[inline] + pub fn skip(&mut self, n: u16) { + self.skip += n; + } + + pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { + profile_method!(add); + + match value { + PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), + PathItem::Segment(begin, end) => self.segments.push(( + name.into(), + PathItem::Segment(self.skip + begin, self.skip + end), + )), + } + } + + #[doc(hidden)] + pub fn add_static( + &mut self, + name: impl Into>, + value: impl Into>, + ) { + self.segments + .push((name.into(), PathItem::Static(value.into()))); + } + + /// Check if there are any matched patterns. + #[inline] + pub fn is_empty(&self) -> bool { + self.segments.is_empty() + } + + /// Returns number of interpolated segments. + #[inline] + pub fn segment_count(&self) -> usize { + self.segments.len() + } + + /// Get matched parameter by name without type conversion + pub fn get(&self, name: &str) -> Option<&str> { + profile_method!(get); + + for (seg_name, val) in self.segments.iter() { + if name == seg_name { + return match val { + PathItem::Static(ref s) => Some(s), + PathItem::Segment(s, e) => { + Some(&self.path.path()[(*s as usize)..(*e as usize)]) + } + }; + } + } + + None + } + + /// Get unprocessed part of the path + pub fn unprocessed(&self) -> &str { + &self.path.path()[(self.skip as usize)..] + } + + /// Get matched parameter by name. + /// + /// If keyed parameter is not available empty string is used as default value. + pub fn query(&self, key: &str) -> &str { + profile_method!(query); + + if let Some(s) = self.get(key) { + s + } else { + "" + } + } + + /// Return iterator to items in parameter container. + pub fn iter(&self) -> PathIter<'_, T> { + PathIter { + idx: 0, + params: self, + } + } + + /// Try to deserialize matching parameters to a specified type `U` + pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { + profile_method!(load); + de::Deserialize::deserialize(PathDeserializer::new(self)) + } +} + +#[derive(Debug)] +pub struct PathIter<'a, T> { + idx: usize, + params: &'a Path, +} + +impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> { + type Item = (&'a str, &'a str); + + #[inline] + fn next(&mut self) -> Option<(&'a str, &'a str)> { + if self.idx < self.params.segment_count() { + let idx = self.idx; + let res = match self.params.segments[idx].1 { + PathItem::Static(ref s) => s, + PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], + }; + self.idx += 1; + return Some((&self.params.segments[idx].0, res)); + } + None + } +} + +impl<'a, T: ResourcePath> Index<&'a str> for Path { + type Output = str; + + fn index(&self, name: &'a str) -> &str { + self.get(name) + .expect("Value for parameter is not available") + } +} + +impl Index for Path { + type Output = str; + + fn index(&self, idx: usize) -> &str { + match self.segments[idx].1 { + PathItem::Static(ref s) => s, + PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], + } + } +} + +impl Resource for Path { + fn resource_path(&mut self) -> &mut Self { + self + } +} diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs new file mode 100644 index 000000000..d5f738a05 --- /dev/null +++ b/actix-router/src/resource.rs @@ -0,0 +1,1820 @@ +use std::{ + borrow::{Borrow, Cow}, + collections::HashMap, + hash::{BuildHasher, Hash, Hasher}, + mem, +}; + +use firestorm::{profile_fn, profile_method, profile_section}; +use regex::{escape, Regex, RegexSet}; + +use crate::{ + path::{Path, PathItem}, + IntoPatterns, Patterns, Resource, ResourcePath, +}; + +const MAX_DYNAMIC_SEGMENTS: usize = 16; + +/// Regex flags to allow '.' in regex to match '\n' +/// +/// See the docs under: https://docs.rs/regex/1/regex/#grouping-and-flags +const REGEX_FLAGS: &str = "(?s-m)"; + +/// Describes the set of paths that match to a resource. +/// +/// `ResourceDef`s are effectively a way to transform the a custom resource pattern syntax into +/// suitable regular expressions from which to check matches with paths and capture portions of a +/// matched path into variables. Common cases are on a fast path that avoids going through the +/// regex engine. +/// +/// +/// # Pattern Format and Matching Behavior +/// +/// Resource pattern is defined as a string of zero or more _segments_ where each segment is +/// preceded by a slash `/`. +/// +/// This means that pattern string __must__ either be empty or begin with a slash (`/`). +/// This also implies that a trailing slash in pattern defines an empty segment. +/// For example, the pattern `"/user/"` has two segments: `["user", ""]` +/// +/// A key point to underhand is that `ResourceDef` matches segments, not strings. +/// It matches segments individually. +/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`, +/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`. +/// +/// This definition is consistent with the definition of absolute URL path in +/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3) +/// +/// +/// # Static Resources +/// A static resource is the most basic type of definition. Pass a pattern to +/// [new][Self::new]. Conforming paths must match the pattern exactly. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new("/home"); +/// +/// assert!(resource.is_match("/home")); +/// +/// assert!(!resource.is_match("/home/")); +/// assert!(!resource.is_match("/home/new")); +/// assert!(!resource.is_match("/homes")); +/// assert!(!resource.is_match("/search")); +/// ``` +/// +/// +/// # Dynamic Segments +/// Also known as "path parameters". Resources can define sections of a pattern that be extracted +/// from a conforming path, if it conforms to (one of) the resource pattern(s). +/// +/// The marker for a dynamic segment is curly braces wrapping an identifier. For example, +/// `/user/{id}` would match paths like `/user/123` or `/user/james` and be able to extract the user +/// IDs "123" and "james", respectively. +/// +/// However, this resource pattern (`/user/{id}`) would, not cover `/user/123/stars` (unless +/// constructed as a prefix; see next section) since the default pattern for segments matches all +/// characters until it finds a `/` character (or the end of the path). Custom segment patterns are +/// covered further down. +/// +/// Dynamic segments do not need to be delimited by `/` characters, they can be defined within a +/// path segment. For example, `/rust-is-{opinion}` can match the paths `/rust-is-cool` and +/// `/rust-is-hard`. +/// +/// For information on capturing segment values from paths or other custom resource types, +/// see [`capture_match_info`][Self::capture_match_info] +/// and [`capture_match_info_fn`][Self::capture_match_info_fn]. +/// +/// A resource definition can contain at most 16 dynamic segments. +/// +/// ## Examples +/// ``` +/// use actix_router::{Path, ResourceDef}; +/// +/// let resource = ResourceDef::prefix("/user/{id}"); +/// +/// assert!(resource.is_match("/user/123")); +/// assert!(!resource.is_match("/user")); +/// assert!(!resource.is_match("/user/")); +/// +/// let mut path = Path::new("/user/123"); +/// resource.capture_match_info(&mut path); +/// assert_eq!(path.get("id").unwrap(), "123"); +/// ``` +/// +/// +/// # Prefix Resources +/// A prefix resource is defined as pattern that can match just the start of a path, up to a +/// segment boundary. +/// +/// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior. +/// They define and therefore require an empty segment in order to match. Examples are given below. +/// +/// Empty pattern matches any path as a prefix. +/// +/// Prefix resources can contain dynamic segments. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::prefix("/home"); +/// assert!(resource.is_match("/home")); +/// assert!(resource.is_match("/home/new")); +/// assert!(!resource.is_match("/homes")); +/// +/// // prefix pattern with a trailing slash +/// let resource = ResourceDef::prefix("/user/{id}/"); +/// assert!(resource.is_match("/user/123/")); +/// assert!(resource.is_match("/user/123//stars")); +/// assert!(!resource.is_match("/user/123/stars")); +/// assert!(!resource.is_match("/user/123")); +/// ``` +/// +/// +/// # Custom Regex Segments +/// Dynamic segments can be customised to only match a specific regular expression. It can be +/// helpful to do this if resource definitions would otherwise conflict and cause one to +/// be inaccessible. +/// +/// The regex used when capturing segment values can be specified explicitly using this syntax: +/// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID +/// is numeric. +/// +/// The regex could potentially match multiple segments. If this is not wanted, then care must be +/// taken to avoid matching a slash `/`. It is guaranteed, however, that the match ends at a +/// segment boundary; the pattern `r"(/|$)` is always appended to the regex. +/// +/// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in +/// the earlier section, that segments capture a slice of the path up to the next `/` character. +/// +/// Custom regex segments can be used in static and prefix resource definition variants. +/// +/// ## Examples +/// ``` +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new(r"/user/{id:\d+}"); +/// assert!(resource.is_match("/user/123")); +/// assert!(resource.is_match("/user/314159")); +/// assert!(!resource.is_match("/user/abc")); +/// ``` +/// +/// +/// # Tail Segments +/// As a shortcut to defining a custom regex for matching _all_ remaining characters (not just those +/// up until a `/` character), there is a special pattern to match (and capture) the remaining +/// path portion. +/// +/// To do this, use the segment pattern: `{name}*`. Since a tail segment also has a name, values are +/// extracted in the same way as non-tail dynamic segments. +/// +/// ## Examples +/// ```rust +/// # use actix_router::{Path, ResourceDef}; +/// let resource = ResourceDef::new("/blob/{tail}*"); +/// assert!(resource.is_match("/blob/HEAD/Cargo.toml")); +/// assert!(resource.is_match("/blob/HEAD/README.md")); +/// +/// let mut path = Path::new("/blob/main/LICENSE"); +/// resource.capture_match_info(&mut path); +/// assert_eq!(path.get("tail").unwrap(), "main/LICENSE"); +/// ``` +/// +/// +/// # Multi-Pattern Resources +/// For resources that can map to multiple distinct paths, it may be suitable to use +/// multi-pattern resources by passing an array/vec to [`new`][Self::new]. They will be combined +/// into a regex set which is usually quicker to check matches on than checking each +/// pattern individually. +/// +/// Multi-pattern resources can contain dynamic segments just like single pattern ones. +/// However, take care to use consistent and semantically-equivalent segment names; it could affect +/// expectations in the router using these definitions and cause runtime panics. +/// +/// ## Examples +/// ```rust +/// # use actix_router::ResourceDef; +/// let resource = ResourceDef::new(["/home", "/index"]); +/// assert!(resource.is_match("/home")); +/// assert!(resource.is_match("/index")); +/// ``` +/// +/// +/// # Trailing Slashes +/// It should be noted that this library takes no steps to normalize intra-path or trailing slashes. +/// As such, all resource definitions implicitly expect a pre-processing step to normalize paths if +/// they you wish to accommodate "recoverable" path errors. Below are several examples of +/// resource-path pairs that would not be compatible. +/// +/// ## Examples +/// ```rust +/// # use actix_router::ResourceDef; +/// assert!(!ResourceDef::new("/root").is_match("/root/")); +/// assert!(!ResourceDef::new("/root/").is_match("/root")); +/// assert!(!ResourceDef::prefix("/root/").is_match("/root")); +/// ``` +#[derive(Clone, Debug)] +pub struct ResourceDef { + id: u16, + + /// Optional name of resource. + name: Option, + + /// Pattern that generated the resource definition. + patterns: Patterns, + + is_prefix: bool, + + /// Pattern type. + pat_type: PatternType, + + /// List of segments that compose the pattern, in order. + segments: Vec, +} + +#[derive(Debug, Clone, PartialEq)] +enum PatternSegment { + /// Literal slice of pattern. + Const(String), + + /// Name of dynamic segment. + Var(String), +} + +#[derive(Clone, Debug)] +#[allow(clippy::large_enum_variant)] +enum PatternType { + /// Single constant/literal segment. + Static(String), + + /// Single regular expression and list of dynamic segment names. + Dynamic(Regex, Vec<&'static str>), + + /// Regular expression set and list of component expressions plus dynamic segment names. + DynamicSet(RegexSet, Vec<(Regex, Vec<&'static str>)>), +} + +impl ResourceDef { + /// Constructs a new resource definition from patterns. + /// + /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns. + /// + /// # Panics + /// Panics if path pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::new("/user/{id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("user/1234")); + /// assert!(!resource.is_match("/foo")); + /// + /// let resource = ResourceDef::new(["/profile", "/user/{id}"]); + /// assert!(resource.is_match("/profile")); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("user/123")); + /// assert!(!resource.is_match("/foo")); + /// ``` + pub fn new(paths: T) -> Self { + profile_method!(new); + Self::new2(paths, false) + } + + /// Constructs a new resource definition using a pattern that performs prefix matching. + /// + /// More specifically, the regular expressions generated for matching are different when using + /// this method vs using `new`; they will not be appended with the `$` meta-character that + /// matches the end of an input. + /// + /// Although it will compile and run correctly, it is meaningless to construct a prefix + /// resource definition with a tail segment; use [`new`][Self::new] in this case. + /// + /// # Panics + /// Panics if path regex pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(resource.is_match("/user/123/stars")); + /// assert!(!resource.is_match("user/123")); + /// assert!(!resource.is_match("user/123/stars")); + /// assert!(!resource.is_match("/foo")); + /// ``` + pub fn prefix(paths: T) -> Self { + profile_method!(prefix); + ResourceDef::new2(paths, true) + } + + /// Constructs a new resource definition using a string pattern that performs prefix matching, + /// inserting a `/` to beginning of the pattern if absent and pattern is not empty. + /// + /// # Panics + /// Panics if path regex pattern is malformed. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// let resource = ResourceDef::root_prefix("user/{id}"); + /// + /// assert_eq!(&resource, &ResourceDef::prefix("/user/{id}")); + /// assert_eq!(&resource, &ResourceDef::root_prefix("/user/{id}")); + /// assert_ne!(&resource, &ResourceDef::new("user/{id}")); + /// assert_ne!(&resource, &ResourceDef::new("/user/{id}")); + /// + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("user/123")); + /// ``` + pub fn root_prefix(path: &str) -> Self { + profile_method!(root_prefix); + ResourceDef::prefix(insert_slash(path).into_owned()) + } + + /// Returns a numeric resource ID. + /// + /// If not explicitly set using [`set_id`][Self::set_id], this will return `0`. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// assert_eq!(resource.id(), 0); + /// + /// resource.set_id(42); + /// assert_eq!(resource.id(), 42); + /// ``` + pub fn id(&self) -> u16 { + self.id + } + + /// Set numeric resource ID. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// resource.set_id(42); + /// assert_eq!(resource.id(), 42); + /// ``` + pub fn set_id(&mut self, id: u16) { + self.id = id; + } + + /// Returns resource definition name, if set. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// assert!(resource.name().is_none()); + /// + /// resource.set_name("root"); + /// assert_eq!(resource.name().unwrap(), "root"); + pub fn name(&self) -> Option<&str> { + self.name.as_deref() + } + + /// Assigns a new name to the resource. + /// + /// # Panics + /// Panics if `name` is an empty string. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// resource.set_name("root"); + /// assert_eq!(resource.name().unwrap(), "root"); + /// ``` + pub fn set_name(&mut self, name: impl Into) { + let name = name.into(); + + assert!(!name.is_empty(), "resource name should not be empty"); + + self.name = Some(name) + } + + /// Returns `true` if pattern type is prefix. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// assert!(ResourceDef::prefix("/user").is_prefix()); + /// assert!(!ResourceDef::new("/user").is_prefix()); + /// ``` + pub fn is_prefix(&self) -> bool { + self.is_prefix + } + + /// Returns the pattern string that generated the resource definition. + /// + /// If definition is constructed with multiple patterns, the first pattern is returned. To get + /// all patterns, use [`patterns_iter`][Self::pattern_iter]. If resource has 0 patterns, + /// returns `None`. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/user/{id}"); + /// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); + /// + /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); + /// assert_eq!(resource.pattern(), Some("/profile")); + pub fn pattern(&self) -> Option<&str> { + match &self.patterns { + Patterns::Single(pattern) => Some(pattern.as_str()), + Patterns::List(patterns) => patterns.first().map(AsRef::as_ref), + } + } + + /// Returns iterator of pattern strings that generated the resource definition. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut resource = ResourceDef::new("/root"); + /// let mut iter = resource.pattern_iter(); + /// assert_eq!(iter.next().unwrap(), "/root"); + /// assert!(iter.next().is_none()); + /// + /// let mut resource = ResourceDef::new(["/root", "/backup"]); + /// let mut iter = resource.pattern_iter(); + /// assert_eq!(iter.next().unwrap(), "/root"); + /// assert_eq!(iter.next().unwrap(), "/backup"); + /// assert!(iter.next().is_none()); + pub fn pattern_iter(&self) -> impl Iterator { + struct PatternIter<'a> { + patterns: &'a Patterns, + list_idx: usize, + done: bool, + } + + impl<'a> Iterator for PatternIter<'a> { + type Item = &'a str; + + fn next(&mut self) -> Option { + match &self.patterns { + Patterns::Single(pattern) => { + if self.done { + return None; + } + + self.done = true; + Some(pattern.as_str()) + } + Patterns::List(patterns) if patterns.is_empty() => None, + Patterns::List(patterns) => match patterns.get(self.list_idx) { + Some(pattern) => { + self.list_idx += 1; + Some(pattern.as_str()) + } + None => { + // fast path future call + self.done = true; + None + } + }, + } + } + + fn size_hint(&self) -> (usize, Option) { + match &self.patterns { + Patterns::Single(_) => (1, Some(1)), + Patterns::List(patterns) => (patterns.len(), Some(patterns.len())), + } + } + } + + PatternIter { + patterns: &self.patterns, + list_idx: 0, + done: false, + } + } + + /// Joins two resources. + /// + /// Resulting resource is prefix if `other` is prefix. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let joined = ResourceDef::prefix("/root").join(&ResourceDef::prefix("/seg")); + /// assert_eq!(joined, ResourceDef::prefix("/root/seg")); + /// ``` + pub fn join(&self, other: &ResourceDef) -> ResourceDef { + let patterns = self + .pattern_iter() + .flat_map(move |this| other.pattern_iter().map(move |other| (this, other))) + .map(|(this, other)| [this, other].join("")) + .collect::>(); + + match patterns.len() { + 1 => ResourceDef::new2(&patterns[0], other.is_prefix()), + _ => ResourceDef::new2(patterns, other.is_prefix()), + } + } + + /// Returns `true` if `path` matches this resource. + /// + /// The behavior of this method depends on how the `ResourceDef` was constructed. For example, + /// static resources will not be able to match as many paths as dynamic and prefix resources. + /// See [`ResourceDef`] struct docs for details on resource definition types. + /// + /// This method will always agree with [`find_match`][Self::find_match] on whether the path + /// matches or not. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// // static resource + /// let resource = ResourceDef::new("/user"); + /// assert!(resource.is_match("/user")); + /// assert!(!resource.is_match("/users")); + /// assert!(!resource.is_match("/user/123")); + /// assert!(!resource.is_match("/foo")); + /// + /// // dynamic resource + /// let resource = ResourceDef::new("/user/{user_id}"); + /// assert!(resource.is_match("/user/123")); + /// assert!(!resource.is_match("/user/123/stars")); + /// + /// // prefix resource + /// let resource = ResourceDef::prefix("/root"); + /// assert!(resource.is_match("/root")); + /// assert!(resource.is_match("/root/leaf")); + /// assert!(!resource.is_match("/roots")); + /// + /// // more examples are shown in the `ResourceDef` struct docs + /// ``` + #[inline] + pub fn is_match(&self, path: &str) -> bool { + profile_method!(is_match); + + // this function could be expressed as: + // `self.find_match(path).is_some()` + // but this skips some checks and uses potentially faster regex methods + + match &self.pat_type { + PatternType::Static(pattern) => self.static_match(pattern, path).is_some(), + PatternType::Dynamic(re, _) => re.is_match(path), + PatternType::DynamicSet(re, _) => re.is_match(path), + } + } + + /// Tries to match `path` to this resource, returning the position in the path where the + /// match ends. + /// + /// This method will always agree with [`is_match`][Self::is_match] on whether the path matches + /// or not. + /// + /// # Examples + /// ``` + /// use actix_router::ResourceDef; + /// + /// // static resource + /// let resource = ResourceDef::new("/user"); + /// assert_eq!(resource.find_match("/user"), Some(5)); + /// assert!(resource.find_match("/user/").is_none()); + /// assert!(resource.find_match("/user/123").is_none()); + /// assert!(resource.find_match("/foo").is_none()); + /// + /// // constant prefix resource + /// let resource = ResourceDef::prefix("/user"); + /// assert_eq!(resource.find_match("/user"), Some(5)); + /// assert_eq!(resource.find_match("/user/"), Some(5)); + /// assert_eq!(resource.find_match("/user/123"), Some(5)); + /// + /// // dynamic prefix resource + /// let resource = ResourceDef::prefix("/user/{id}"); + /// assert_eq!(resource.find_match("/user/123"), Some(9)); + /// assert_eq!(resource.find_match("/user/1234/"), Some(10)); + /// assert_eq!(resource.find_match("/user/12345/stars"), Some(11)); + /// assert!(resource.find_match("/user/").is_none()); + /// + /// // multi-pattern resource + /// let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + /// assert_eq!(resource.find_match("/user/123"), Some(9)); + /// assert_eq!(resource.find_match("/profile/1234"), Some(13)); + /// ``` + pub fn find_match(&self, path: &str) -> Option { + profile_method!(find_match); + + match &self.pat_type { + PatternType::Static(pattern) => self.static_match(pattern, path), + + PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), + + PatternType::DynamicSet(re, params) => { + let idx = re.matches(path).into_iter().next()?; + let (ref pattern, _) = params[idx]; + Some(pattern.captures(path)?[1].len()) + } + } + } + + /// Collects dynamic segment values into `path`. + /// + /// Returns `true` if `path` matches this resource. + /// + /// # Examples + /// ``` + /// use actix_router::{Path, ResourceDef}; + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// let mut path = Path::new("/user/123/stars"); + /// assert!(resource.capture_match_info(&mut path)); + /// assert_eq!(path.get("id").unwrap(), "123"); + /// assert_eq!(path.unprocessed(), "/stars"); + /// + /// let resource = ResourceDef::new("/blob/{path}*"); + /// let mut path = Path::new("/blob/HEAD/Cargo.toml"); + /// assert!(resource.capture_match_info(&mut path)); + /// assert_eq!(path.get("path").unwrap(), "HEAD/Cargo.toml"); + /// assert_eq!(path.unprocessed(), ""); + /// ``` + pub fn capture_match_info(&self, path: &mut Path) -> bool { + profile_method!(capture_match_info); + self.capture_match_info_fn(path, |_, _| true, ()) + } + + /// Collects dynamic segment values into `resource` after matching paths and executing + /// check function. + /// + /// The check function is given a reference to the passed resource and optional arbitrary data. + /// This is useful if you want to conditionally match on some non-path related aspect of the + /// resource type. + /// + /// Returns `true` if resource path matches this resource definition _and_ satisfies the + /// given check function. + /// + /// # Examples + /// ``` + /// use actix_router::{Path, ResourceDef}; + /// + /// fn try_match(resource: &ResourceDef, path: &mut Path<&str>) -> bool { + /// let admin_allowed = std::env::var("ADMIN_ALLOWED").ok(); + /// + /// resource.capture_match_info_fn( + /// path, + /// // when env var is not set, reject when path contains "admin" + /// |res, admin_allowed| !res.path().contains("admin"), + /// &admin_allowed + /// ) + /// } + /// + /// let resource = ResourceDef::prefix("/user/{id}"); + /// + /// // path matches; segment values are collected into path + /// let mut path = Path::new("/user/james/stars"); + /// assert!(try_match(&resource, &mut path)); + /// assert_eq!(path.get("id").unwrap(), "james"); + /// assert_eq!(path.unprocessed(), "/stars"); + /// + /// // path matches but fails check function; no segments are collected + /// let mut path = Path::new("/user/admin/stars"); + /// assert!(!try_match(&resource, &mut path)); + /// assert_eq!(path.unprocessed(), "/user/admin/stars"); + /// ``` + pub fn capture_match_info_fn( + &self, + resource: &mut R, + check_fn: F, + user_data: U, + ) -> bool + where + R: Resource, + T: ResourcePath, + F: FnOnce(&R, U) -> bool, + { + profile_method!(capture_match_info_fn); + + let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default(); + let path = resource.resource_path(); + let path_str = path.path(); + + let (matched_len, matched_vars) = match &self.pat_type { + PatternType::Static(pattern) => { + profile_section!(pattern_static_or_prefix); + + match self.static_match(pattern, path_str) { + Some(len) => (len, None), + None => return false, + } + } + + PatternType::Dynamic(re, names) => { + profile_section!(pattern_dynamic); + + let captures = { + profile_section!(pattern_dynamic_regex_exec); + + match re.captures(path.path()) { + Some(captures) => captures, + _ => return false, + } + }; + + { + profile_section!(pattern_dynamic_extract_captures); + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!( + "Dynamic path match but not all segments found: {}", + name + ); + return false; + } + } + }; + + (captures[1].len(), Some(names)) + } + + PatternType::DynamicSet(re, params) => { + profile_section!(pattern_dynamic_set); + + let path = path.path(); + let (pattern, names) = match re.matches(path).into_iter().next() { + Some(idx) => ¶ms[idx], + _ => return false, + }; + + let captures = match pattern.captures(path.path()) { + Some(captures) => captures, + _ => return false, + }; + + for (no, name) in names.iter().enumerate() { + if let Some(m) = captures.name(name) { + segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); + } else { + log::error!("Dynamic path match but not all segments found: {}", name); + return false; + } + } + + (captures[1].len(), Some(names)) + } + }; + + if !check_fn(resource, user_data) { + return false; + } + + // Modify `path` to skip matched part and store matched segments + let path = resource.resource_path(); + + if let Some(vars) = matched_vars { + for i in 0..vars.len() { + path.add(vars[i], mem::take(&mut segments[i])); + } + } + + path.skip(matched_len as u16); + + true + } + + /// Assembles resource path using a closure that maps variable segment names to values. + fn build_resource_path(&self, path: &mut String, mut vars: F) -> bool + where + F: FnMut(&str) -> Option, + I: AsRef, + { + for segment in &self.segments { + match segment { + PatternSegment::Const(val) => path.push_str(val), + PatternSegment::Var(name) => match vars(name) { + Some(val) => path.push_str(val.as_ref()), + _ => return false, + }, + } + } + + true + } + + /// Assembles full resource path from iterator of dynamic segment values. + /// + /// Returns `true` on success. + /// + /// For multi-pattern resources, the first pattern is used under the assumption that it would be + /// equivalent to any other choice. + /// + /// # Examples + /// ``` + /// # use actix_router::ResourceDef; + /// let mut s = String::new(); + /// let resource = ResourceDef::new("/user/{id}/post/{title}"); + /// + /// assert!(resource.resource_path_from_iter(&mut s, &["123", "my-post"])); + /// assert_eq!(s, "/user/123/post/my-post"); + /// ``` + pub fn resource_path_from_iter(&self, path: &mut String, values: I) -> bool + where + I: IntoIterator, + I::Item: AsRef, + { + profile_method!(resource_path_from_iter); + let mut iter = values.into_iter(); + self.build_resource_path(path, |_| iter.next()) + } + + /// Assembles resource path from map of dynamic segment values. + /// + /// Returns `true` on success. + /// + /// For multi-pattern resources, the first pattern is used under the assumption that it would be + /// equivalent to any other choice. + /// + /// # Examples + /// ``` + /// # use std::collections::HashMap; + /// # use actix_router::ResourceDef; + /// let mut s = String::new(); + /// let resource = ResourceDef::new("/user/{id}/post/{title}"); + /// + /// let mut map = HashMap::new(); + /// map.insert("id", "123"); + /// map.insert("title", "my-post"); + /// + /// assert!(resource.resource_path_from_map(&mut s, &map)); + /// assert_eq!(s, "/user/123/post/my-post"); + /// ``` + pub fn resource_path_from_map( + &self, + path: &mut String, + values: &HashMap, + ) -> bool + where + K: Borrow + Eq + Hash, + V: AsRef, + S: BuildHasher, + { + profile_method!(resource_path_from_map); + self.build_resource_path(path, |name| values.get(name).map(AsRef::::as_ref)) + } + + /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`. + fn static_match(&self, pattern: &str, path: &str) -> Option { + let rem = path.strip_prefix(pattern)?; + + match self.is_prefix { + // resource is not a prefix so an exact match is needed + false if rem.is_empty() => Some(pattern.len()), + + // resource is a prefix so rem should start with a path delimiter + true if rem.is_empty() || rem.starts_with('/') => Some(pattern.len()), + + // otherwise, no match + _ => None, + } + } + + fn new2(paths: T, is_prefix: bool) -> Self { + profile_method!(new2); + + let patterns = paths.patterns(); + let (pat_type, segments) = match &patterns { + Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), + + // since zero length pattern sets are possible + // just return a useless `ResourceDef` + Patterns::List(patterns) if patterns.is_empty() => ( + PatternType::DynamicSet(RegexSet::empty(), Vec::new()), + Vec::new(), + ), + + Patterns::List(patterns) => { + let mut re_set = Vec::with_capacity(patterns.len()); + let mut pattern_data = Vec::new(); + let mut segments = None; + + for pattern in patterns { + match ResourceDef::parse(pattern, is_prefix, true) { + (PatternType::Dynamic(re, names), segs) => { + re_set.push(re.as_str().to_owned()); + pattern_data.push((re, names)); + segments.get_or_insert(segs); + } + _ => unreachable!(), + } + } + + let pattern_re_set = RegexSet::new(re_set).unwrap(); + let segments = segments.unwrap_or_else(Vec::new); + + ( + PatternType::DynamicSet(pattern_re_set, pattern_data), + segments, + ) + } + }; + + ResourceDef { + id: 0, + name: None, + patterns, + is_prefix, + pat_type, + segments, + } + } + + /// Parses a dynamic segment definition from a pattern. + /// + /// The returned tuple includes: + /// - the segment descriptor, either `Var` or `Tail` + /// - the segment's regex to check values against + /// - the remaining, unprocessed string slice + /// - whether the parsed parameter represents a tail pattern + /// + /// # Panics + /// Panics if given patterns does not contain a dynamic segment. + fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) { + profile_method!(parse_param); + + const DEFAULT_PATTERN: &str = "[^/]+"; + const DEFAULT_PATTERN_TAIL: &str = ".*"; + + let mut params_nesting = 0usize; + let close_idx = pattern + .find(|c| match c { + '{' => { + params_nesting += 1; + false + } + '}' => { + params_nesting -= 1; + params_nesting == 0 + } + _ => false, + }) + .unwrap_or_else(|| { + panic!( + r#"pattern "{}" contains malformed dynamic segment"#, + pattern + ) + }); + + let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); + + // remove outer curly brackets + param = ¶m[1..param.len() - 1]; + + let tail = unprocessed == "*"; + + let (name, pattern) = match param.find(':') { + Some(idx) => { + assert!(!tail, "custom regex is not supported for tail match"); + + let (name, pattern) = param.split_at(idx); + (name, &pattern[1..]) + } + None => ( + param, + if tail { + unprocessed = &unprocessed[1..]; + DEFAULT_PATTERN_TAIL + } else { + DEFAULT_PATTERN + }, + ), + }; + + let segment = PatternSegment::Var(name.to_string()); + let regex = format!(r"(?P<{}>{})", &name, &pattern); + + (segment, regex, unprocessed, tail) + } + + /// Parse `pattern` using `is_prefix` and `force_dynamic` flags. + /// + /// Parameters: + /// - `is_prefix`: Use `true` if `pattern` should be treated as a prefix; i.e., a conforming + /// path will be a match even if it has parts remaining to process + /// - `force_dynamic`: Use `true` to disallow the return of static and prefix segments. + /// + /// The returned tuple includes: + /// - the pattern type detected, either `Static`, `Prefix`, or `Dynamic` + /// - a list of segment descriptors from the pattern + fn parse( + pattern: &str, + is_prefix: bool, + force_dynamic: bool, + ) -> (PatternType, Vec) { + profile_method!(parse); + + if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') { + // pattern is static + return ( + PatternType::Static(pattern.to_owned()), + vec![PatternSegment::Const(pattern.to_owned())], + ); + } + + let mut unprocessed = pattern; + let mut segments = Vec::new(); + let mut re = format!("{}^", REGEX_FLAGS); + let mut dyn_segment_count = 0; + let mut has_tail_segment = false; + + while let Some(idx) = unprocessed.find('{') { + let (prefix, rem) = unprocessed.split_at(idx); + + segments.push(PatternSegment::Const(prefix.to_owned())); + re.push_str(&escape(prefix)); + + let (param_pattern, re_part, rem, tail) = Self::parse_param(rem); + + if tail { + has_tail_segment = true; + } + + segments.push(param_pattern); + re.push_str(&re_part); + + unprocessed = rem; + dyn_segment_count += 1; + } + + if is_prefix && has_tail_segment { + // tail segments in prefixes have no defined semantics + + #[cfg(not(test))] + log::warn!( + "Prefix resources should not have tail segments. \ + Use `ResourceDef::new` constructor. \ + This may become a panic in the future." + ); + + // panic in tests to make this case detectable + #[cfg(test)] + panic!("prefix resource definitions should not have tail segments"); + } + + if unprocessed.ends_with('*') { + // unnamed tail segment + + #[cfg(not(test))] + log::warn!( + "Tail segments must have names. \ + Consider `.../{{tail}}*`. \ + This may become a panic in the future." + ); + + // panic in tests to make this case detectable + #[cfg(test)] + panic!("tail segments must have names"); + } else if !has_tail_segment && !unprocessed.is_empty() { + // prevent `Const("")` element from being added after last dynamic segment + + segments.push(PatternSegment::Const(unprocessed.to_owned())); + re.push_str(&escape(unprocessed)); + } + + assert!( + dyn_segment_count <= MAX_DYNAMIC_SEGMENTS, + "Only {} dynamic segments are allowed, provided: {}", + MAX_DYNAMIC_SEGMENTS, + dyn_segment_count + ); + + // Store the pattern in capture group #1 to have context info outside it + let mut re = format!("({})", re); + + // Ensure the match ends at a segment boundary + if !has_tail_segment { + if is_prefix { + re.push_str(r"(/|$)"); + } else { + re.push('$'); + } + } + + let re = match Regex::new(&re) { + Ok(re) => re, + Err(err) => panic!("Wrong path pattern: \"{}\" {}", pattern, err), + }; + + // `Bok::leak(Box::new(name))` is an intentional memory leak. In typical applications the + // routing table is only constructed once (per worker) so leak is bounded. If you are + // constructing `ResourceDef`s more than once in your application's lifecycle you would + // expect a linear increase in leaked memory over time. + let names = re + .capture_names() + .filter_map(|name| name.map(|name| Box::leak(Box::new(name.to_owned())).as_str())) + .collect(); + + (PatternType::Dynamic(re, names), segments) + } +} + +impl Eq for ResourceDef {} + +impl PartialEq for ResourceDef { + fn eq(&self, other: &ResourceDef) -> bool { + self.patterns == other.patterns && self.is_prefix == other.is_prefix + } +} + +impl Hash for ResourceDef { + fn hash(&self, state: &mut H) { + self.patterns.hash(state); + } +} + +impl<'a> From<&'a str> for ResourceDef { + fn from(path: &'a str) -> ResourceDef { + ResourceDef::new(path) + } +} + +impl From for ResourceDef { + fn from(path: String) -> ResourceDef { + ResourceDef::new(path) + } +} + +pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> { + profile_fn!(insert_slash); + + if !path.is_empty() && !path.starts_with('/') { + let mut new_path = String::with_capacity(path.len() + 1); + new_path.push('/'); + new_path.push_str(path); + Cow::Owned(new_path) + } else { + Cow::Borrowed(path) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn equivalence() { + assert_eq!( + ResourceDef::root_prefix("/root"), + ResourceDef::prefix("/root") + ); + assert_eq!( + ResourceDef::root_prefix("root"), + ResourceDef::prefix("/root") + ); + assert_eq!( + ResourceDef::root_prefix("/{id}"), + ResourceDef::prefix("/{id}") + ); + assert_eq!( + ResourceDef::root_prefix("{id}"), + ResourceDef::prefix("/{id}") + ); + + assert_eq!(ResourceDef::new("/"), ResourceDef::new(["/"])); + assert_eq!(ResourceDef::new("/"), ResourceDef::new(vec!["/"])); + + assert_ne!(ResourceDef::new(""), ResourceDef::prefix("")); + assert_ne!(ResourceDef::new("/"), ResourceDef::prefix("/")); + assert_ne!(ResourceDef::new("/{id}"), ResourceDef::prefix("/{id}")); + } + + #[test] + fn parse_static() { + let re = ResourceDef::new(""); + + assert!(!re.is_prefix()); + + assert!(re.is_match("")); + assert!(!re.is_match("/")); + assert_eq!(re.find_match(""), Some(0)); + assert_eq!(re.find_match("/"), None); + + let re = ResourceDef::new("/"); + assert!(re.is_match("/")); + assert!(!re.is_match("")); + assert!(!re.is_match("/foo")); + + let re = ResourceDef::new("/name"); + assert!(re.is_match("/name")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name/")); + assert!(!re.is_match("/name~")); + + let mut path = Path::new("/name"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match("/name1"), None); + assert_eq!(re.find_match("/name/"), None); + assert_eq!(re.find_match("/name~"), None); + + let re = ResourceDef::new("/name/"); + assert!(re.is_match("/name/")); + assert!(!re.is_match("/name")); + assert!(!re.is_match("/name/gs")); + + let re = ResourceDef::new("/user/profile"); + assert!(re.is_match("/user/profile")); + assert!(!re.is_match("/user/profile/profile")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + } + + #[test] + fn parse_param() { + let re = ResourceDef::new("/user/{id}"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/user/1245125"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + assert_eq!(path.unprocessed(), ""); + + let re = ResourceDef::new("/v{version}/resource/{id}"); + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adage32"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adage32"); + assert_eq!(path.unprocessed(), ""); + + let re = ResourceDef::new("/{id:[[:digit:]]{6}}"); + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + let mut path = Path::new("/012345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + assert_eq!(path.unprocessed(), ""); + } + + #[allow(clippy::cognitive_complexity)] + #[test] + fn dynamic_set() { + let re = ResourceDef::new(vec![ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + "/static", + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let mut path = Path::new("/user/profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/user/1245125"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "1245125"); + assert_eq!(path.unprocessed(), ""); + + assert!(re.is_match("/v1/resource/320120")); + assert!(!re.is_match("/v/resource/1")); + assert!(!re.is_match("/resource")); + + let mut path = Path::new("/v151/resource/adage32"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("version").unwrap(), "151"); + assert_eq!(path.get("id").unwrap(), "adage32"); + + assert!(re.is_match("/012345")); + assert!(!re.is_match("/012")); + assert!(!re.is_match("/01234567")); + assert!(!re.is_match("/XXXXXX")); + + assert!(re.is_match("/static")); + assert!(!re.is_match("/a/static")); + assert!(!re.is_match("/static/a")); + + let mut path = Path::new("/012345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "012345"); + + let re = ResourceDef::new([ + "/user/{id}", + "/v{version}/resource/{id}", + "/{id:[[:digit:]]{6}}", + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + + let re = ResourceDef::new([ + "/user/{id}".to_string(), + "/v{version}/resource/{id}".to_string(), + "/{id:[[:digit:]]{6}}".to_string(), + ]); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(!re.is_match("/user/2345/")); + assert!(!re.is_match("/user/2345/sdg")); + } + + #[test] + fn dynamic_set_prefix() { + let re = ResourceDef::prefix(vec!["/u/{id}", "/{id:[[:digit:]]{3}}"]); + + assert_eq!(re.find_match("/u/abc"), Some(6)); + assert_eq!(re.find_match("/u/abc/123"), Some(6)); + assert_eq!(re.find_match("/s/user/profile"), None); + + assert_eq!(re.find_match("/123"), Some(4)); + assert_eq!(re.find_match("/123/456"), Some(4)); + assert_eq!(re.find_match("/12345"), None); + + let mut path = Path::new("/151/res"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "151"); + assert_eq!(path.unprocessed(), "/res"); + } + + #[test] + fn parse_tail() { + let re = ResourceDef::new("/user/-{id}*"); + + let mut path = Path::new("/user/-profile"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "profile"); + + let mut path = Path::new("/user/-2345"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + + let mut path = Path::new("/user/-2345/"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345/"); + + let mut path = Path::new("/user/-2345/sdg"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345/sdg"); + } + + #[test] + fn static_tail() { + let re = ResourceDef::new("/user{tail}*"); + assert!(re.is_match("/users")); + assert!(re.is_match("/user-foo")); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(re.is_match("/user/2345/")); + assert!(re.is_match("/user/2345/sdg")); + assert!(!re.is_match("/foo/profile")); + + let re = ResourceDef::new("/user/{tail}*"); + assert!(re.is_match("/user/profile")); + assert!(re.is_match("/user/2345")); + assert!(re.is_match("/user/2345/")); + assert!(re.is_match("/user/2345/sdg")); + assert!(!re.is_match("/foo/profile")); + } + + #[test] + fn dynamic_tail() { + let re = ResourceDef::new("/user/{id}/{tail}*"); + assert!(!re.is_match("/user/2345")); + let mut path = Path::new("/user/2345/sdg"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + assert_eq!(path.get("tail").unwrap(), "sdg"); + assert_eq!(path.unprocessed(), ""); + } + + #[test] + fn newline_patterns_and_paths() { + let re = ResourceDef::new("/user/a\nb"); + assert!(re.is_match("/user/a\nb")); + assert!(!re.is_match("/user/a\nb/profile")); + + let re = ResourceDef::new("/a{x}b/test/a{y}b"); + let mut path = Path::new("/a\nb/test/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("x").unwrap(), "\n"); + assert_eq!(path.get("y").unwrap(), "\n"); + + let re = ResourceDef::new("/user/{tail}*"); + assert!(re.is_match("/user/a\nb/")); + + let re = ResourceDef::new("/user/{id}*"); + let mut path = Path::new("/user/a\nb/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); + + let re = ResourceDef::new("/user/{id:.*}"); + let mut path = Path::new("/user/a\nb/a\nb"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "a\nb/a\nb"); + } + + #[cfg(feature = "http")] + #[test] + fn parse_urlencoded_param() { + use std::convert::TryFrom; + + let re = ResourceDef::new("/user/{id}/test"); + + let mut path = Path::new("/user/2345/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "2345"); + + let mut path = Path::new("/user/qwe%25/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + + let uri = http::Uri::try_from("/user/qwe%25/test").unwrap(); + let mut path = Path::new(uri); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + } + + #[test] + fn prefix_static() { + let re = ResourceDef::prefix("/name"); + + assert!(re.is_prefix()); + + assert!(re.is_match("/name")); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/test/test")); + assert!(!re.is_match("/name1")); + assert!(!re.is_match("/name~")); + + let mut path = Path::new("/name"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), ""); + + let mut path = Path::new("/name/test"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "/test"); + + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match("/name/"), Some(5)); + assert_eq!(re.find_match("/name/test/test"), Some(5)); + assert_eq!(re.find_match("/name1"), None); + assert_eq!(re.find_match("/name~"), None); + + let re = ResourceDef::prefix("/name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name//gs")); + assert!(!re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/name/gs"); + assert!(!re.capture_match_info(&mut path)); + + let mut path = Path::new("/name//gs"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(path.unprocessed(), "/gs"); + + let re = ResourceDef::root_prefix("name/"); + assert!(re.is_match("/name/")); + assert!(re.is_match("/name//gs")); + assert!(!re.is_match("/name/gs")); + assert!(!re.is_match("/name")); + + let mut path = Path::new("/name/gs"); + assert!(!re.capture_match_info(&mut path)); + } + + #[test] + fn prefix_dynamic() { + let re = ResourceDef::prefix("/{name}"); + + assert!(re.is_prefix()); + + assert!(re.is_match("/name/")); + assert!(re.is_match("/name/gs")); + assert!(re.is_match("/name")); + + assert_eq!(re.find_match("/name/"), Some(5)); + assert_eq!(re.find_match("/name/gs"), Some(5)); + assert_eq!(re.find_match("/name"), Some(5)); + assert_eq!(re.find_match(""), None); + + let mut path = Path::new("/test2/"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + assert_eq!(path.unprocessed(), "/"); + + let mut path = Path::new("/test2/subpath1/subpath2/index.html"); + assert!(re.capture_match_info(&mut path)); + assert_eq!(&path["name"], "test2"); + assert_eq!(&path[0], "test2"); + assert_eq!(path.unprocessed(), "/subpath1/subpath2/index.html"); + + let resource = ResourceDef::prefix("/user"); + // input string shorter than prefix + assert!(resource.find_match("/foo").is_none()); + } + + #[test] + fn prefix_empty() { + let re = ResourceDef::prefix(""); + + assert!(re.is_prefix()); + + assert!(re.is_match("")); + assert!(re.is_match("/")); + assert!(re.is_match("/name/test/test")); + } + + #[test] + fn build_path_list() { + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/test"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); + assert_eq!(s, "/user/user1/test"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}/test"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/test"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2"); + + let mut s = String::new(); + let resource = ResourceDef::new("/user/{item1}/{item2}/"); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter())); + assert_eq!(s, "/user/item/item2/"); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter())); + assert_eq!(s, "/user/item/item2/"); + } + + #[test] + fn multi_pattern_build_path() { + let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); + assert_eq!(s, "/user/123"); + } + + #[test] + fn multi_pattern_capture_segment_values() { + let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); + + let mut path = Path::new("/user/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + + let mut path = Path::new("/profile/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + + let resource = ResourceDef::new(["/user/{id}", "/profile/{uid}"]); + + let mut path = Path::new("/user/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_some()); + assert!(path.get("uid").is_none()); + + let mut path = Path::new("/profile/123"); + assert!(resource.capture_match_info(&mut path)); + assert!(path.get("id").is_none()); + assert!(path.get("uid").is_some()); + } + + #[test] + fn dynamic_prefix_proper_segmentation() { + let resource = ResourceDef::prefix(r"/id/{id:\d{3}}"); + + assert!(resource.is_match("/id/123")); + assert!(resource.is_match("/id/123/foo")); + assert!(!resource.is_match("/id/1234")); + assert!(!resource.is_match("/id/123a")); + + assert_eq!(resource.find_match("/id/123"), Some(7)); + assert_eq!(resource.find_match("/id/123/foo"), Some(7)); + assert_eq!(resource.find_match("/id/1234"), None); + assert_eq!(resource.find_match("/id/123a"), None); + } + + #[test] + fn build_path_map() { + let resource = ResourceDef::new("/user/{item1}/{item2}/"); + + let mut map = HashMap::new(); + map.insert("item1", "item"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_map(&mut s, &map)); + + map.insert("item2", "item2"); + + let mut s = String::new(); + assert!(resource.resource_path_from_map(&mut s, &map)); + assert_eq!(s, "/user/item/item2/"); + } + + #[test] + fn build_path_tail() { + let resource = ResourceDef::new("/user/{item1}*"); + + let mut s = String::new(); + assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter())); + + let mut s = String::new(); + assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter())); + assert_eq!(s, "/user/user1"); + + let mut s = String::new(); + let mut map = HashMap::new(); + map.insert("item1", "item"); + assert!(resource.resource_path_from_map(&mut s, &map)); + assert_eq!(s, "/user/item"); + } + + #[test] + fn prefix_trailing_slash() { + // The prefix "/abc/" matches two segments: ["user", ""] + + // These are not prefixes + let re = ResourceDef::prefix("/abc/"); + assert_eq!(re.find_match("/abc/def"), None); + assert_eq!(re.find_match("/abc//def"), Some(5)); + + let re = ResourceDef::prefix("/{id}/"); + assert_eq!(re.find_match("/abc/def"), None); + assert_eq!(re.find_match("/abc//def"), Some(5)); + } + + #[test] + fn join() { + // test joined defs match the same paths as each component separately + + fn seq_find_match(re1: &ResourceDef, re2: &ResourceDef, path: &str) -> Option { + let len1 = re1.find_match(path)?; + let len2 = re2.find_match(&path[len1..])?; + Some(len1 + len2) + } + + macro_rules! join_test { + ($pat1:expr, $pat2:expr => $($test:expr),+) => {{ + let pat1 = $pat1; + let pat2 = $pat2; + $({ + let _path = $test; + let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::new(pat2)); + let _seq = seq_find_match(&re1, &re2, _path); + let _join = re1.join(&re2).find_match(_path); + assert_eq!( + _seq, _join, + "patterns: prefix {:?}, {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", + pat1, pat2, _path, _seq, _join + ); + assert!(!re1.join(&re2).is_prefix()); + + let (re1, re2) = (ResourceDef::prefix(pat1), ResourceDef::prefix(pat2)); + let _seq = seq_find_match(&re1, &re2, _path); + let _join = re1.join(&re2).find_match(_path); + assert_eq!( + _seq, _join, + "patterns: prefix {:?}, prefix {:?}; mismatch on \"{}\"; seq={:?}; join={:?}", + pat1, pat2, _path, _seq, _join + ); + assert!(re1.join(&re2).is_prefix()); + })+ + }} + } + + join_test!("", "" => "", "/hello", "/"); + join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123"); + join_test!("", "/user" => "", "/user", "foo", "/user11", "user", "user/123"); + join_test!("/user", "/xx" => "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); + + join_test!(["/ver/{v}", "/v{v}"], ["/req/{req}", "/{req}"] => "/v1/abc", + "/ver/1/abc", "/v1/req/abc", "/ver/1/req/abc", "/v1/abc/def", + "/ver1/req/abc/def", "", "/", "/v1/"); + } + + #[test] + fn match_methods_agree() { + macro_rules! match_methods_agree { + ($pat:expr => $($test:expr),+) => {{ + match_methods_agree!(finish $pat, ResourceDef::new($pat), $($test),+); + }}; + (prefix $pat:expr => $($test:expr),+) => {{ + match_methods_agree!(finish $pat, ResourceDef::prefix($pat), $($test),+); + }}; + (finish $pat:expr, $re:expr, $($test:expr),+) => {{ + let re = $re; + $({ + let _is = re.is_match($test); + let _find = re.find_match($test).is_some(); + assert_eq!( + _is, _find, + "pattern: {:?}; mismatch on \"{}\"; is={}; find={}", + $pat, $test, _is, _find + ); + })+ + }} + } + + match_methods_agree!("" => "", "/", "/foo"); + match_methods_agree!("/" => "", "/", "/foo"); + match_methods_agree!("/user" => "user", "/user", "/users", "/user/123", "/foo"); + match_methods_agree!("/v{v}" => "v", "/v", "/v1", "/v222", "/foo"); + match_methods_agree!(["/v{v}", "/version/{v}"] => "/v", "/v1", "/version", "/version/1", "/foo"); + + match_methods_agree!("/path{tail}*" => "/path", "/path1", "/path/123"); + match_methods_agree!("/path/{tail}*" => "/path", "/path1", "/path/123"); + + match_methods_agree!(prefix "" => "", "/", "/foo"); + match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo"); + match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234"); + match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1"); + } + + #[test] + #[should_panic] + fn duplicate_segment_name() { + ResourceDef::new("/user/{id}/post/{id}"); + } + + #[test] + #[should_panic] + fn invalid_dynamic_segment_delimiter() { + ResourceDef::new("/user/{username"); + } + + #[test] + #[should_panic] + fn invalid_dynamic_segment_name() { + ResourceDef::new("/user/{}"); + } + + #[test] + #[should_panic] + fn invalid_too_many_dynamic_segments() { + // valid + ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}"); + + // panics + ResourceDef::new( + "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}", + ); + } + + #[test] + #[should_panic] + fn invalid_custom_regex_for_tail() { + ResourceDef::new(r"/{tail:\d+}*"); + } + + #[test] + #[should_panic] + fn invalid_unnamed_tail_segment() { + ResourceDef::new("/*"); + } + + #[test] + #[should_panic] + fn prefix_plus_tail_match_is_allowed() { + ResourceDef::prefix("/user/{id}*"); + } +} diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs new file mode 100644 index 000000000..fad1a440b --- /dev/null +++ b/actix-router/src/router.rs @@ -0,0 +1,282 @@ +use firestorm::profile_method; + +use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath}; + +#[derive(Debug, Copy, Clone, PartialEq)] +pub struct ResourceId(pub u16); + +/// Information about current resource +#[derive(Debug, Clone)] +pub struct ResourceInfo { + #[allow(dead_code)] + resource: ResourceId, +} + +/// Resource router. +// T is the resource itself +// U is any other data needed for routing like method guards +pub struct Router { + routes: Vec<(ResourceDef, T, Option)>, +} + +impl Router { + pub fn build() -> RouterBuilder { + RouterBuilder { + resources: Vec::new(), + } + } + + pub fn recognize(&self, resource: &mut R) -> Option<(&T, ResourceId)> + where + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize); + + for item in self.routes.iter() { + if item.0.capture_match_info(resource.resource_path()) { + return Some((&item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_mut(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)> + where + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_mut); + + for item in self.routes.iter_mut() { + if item.0.capture_match_info(resource.resource_path()) { + return Some((&mut item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_fn(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)> + where + F: Fn(&R, &Option) -> bool, + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_checked); + + for item in self.routes.iter() { + if item.0.capture_match_info_fn(resource, &check, &item.2) { + return Some((&item.1, ResourceId(item.0.id()))); + } + } + + None + } + + pub fn recognize_mut_fn( + &mut self, + resource: &mut R, + check: F, + ) -> Option<(&mut T, ResourceId)> + where + F: Fn(&R, &Option) -> bool, + R: Resource

, + P: ResourcePath, + { + profile_method!(recognize_mut_checked); + + for item in self.routes.iter_mut() { + if item.0.capture_match_info_fn(resource, &check, &item.2) { + return Some((&mut item.1, ResourceId(item.0.id()))); + } + } + + None + } +} + +pub struct RouterBuilder { + resources: Vec<(ResourceDef, T, Option)>, +} + +impl RouterBuilder { + /// Register resource for specified path. + pub fn path( + &mut self, + path: P, + resource: T, + ) -> &mut (ResourceDef, T, Option) { + profile_method!(path); + + self.resources + .push((ResourceDef::new(path), resource, None)); + self.resources.last_mut().unwrap() + } + + /// Register resource for specified path prefix. + pub fn prefix(&mut self, prefix: &str, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(prefix); + + self.resources + .push((ResourceDef::prefix(prefix), resource, None)); + self.resources.last_mut().unwrap() + } + + /// Register resource for ResourceDef + pub fn rdef(&mut self, rdef: ResourceDef, resource: T) -> &mut (ResourceDef, T, Option) { + profile_method!(rdef); + + self.resources.push((rdef, resource, None)); + self.resources.last_mut().unwrap() + } + + /// Finish configuration and create router instance. + pub fn finish(self) -> Router { + Router { + routes: self.resources, + } + } +} + +#[cfg(test)] +mod tests { + use crate::path::Path; + use crate::router::{ResourceId, Router}; + + #[allow(clippy::cognitive_complexity)] + #[test] + fn test_recognizer_1() { + let mut router = Router::::build(); + router.path("/name", 10).0.set_id(0); + router.path("/name/{val}", 11).0.set_id(1); + router.path("/name/{val}/index.html", 12).0.set_id(2); + router.path("/file/{file}.{ext}", 13).0.set_id(3); + router.path("/v{val}/{val2}/index.html", 14).0.set_id(4); + router.path("/v/{tail:.*}", 15).0.set_id(5); + router.path("/test2/{test}.html", 16).0.set_id(6); + router.path("/{test}/index.html", 17).0.set_id(7); + let mut router = router.finish(); + + let mut path = Path::new("/unknown"); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/name"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + assert_eq!(info, ResourceId(0)); + assert!(path.is_empty()); + + let mut path = Path::new("/name/value"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(info, ResourceId(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + let mut path = Path::new("/name/value2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 12); + assert_eq!(info, ResourceId(2)); + assert_eq!(path.get("val").unwrap(), "value2"); + + let mut path = Path::new("/file/file.gz"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 13); + assert_eq!(info, ResourceId(3)); + assert_eq!(path.get("file").unwrap(), "file"); + assert_eq!(path.get("ext").unwrap(), "gz"); + + let mut path = Path::new("/vtest/ttt/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 14); + assert_eq!(info, ResourceId(4)); + assert_eq!(path.get("val").unwrap(), "test"); + assert_eq!(path.get("val2").unwrap(), "ttt"); + + let mut path = Path::new("/v/blah-blah/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 15); + assert_eq!(info, ResourceId(5)); + assert_eq!(path.get("tail").unwrap(), "blah-blah/index.html"); + + let mut path = Path::new("/test2/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 16); + assert_eq!(info, ResourceId(6)); + assert_eq!(path.get("test").unwrap(), "index"); + + let mut path = Path::new("/bbb/index.html"); + let (h, info) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 17); + assert_eq!(info, ResourceId(7)); + assert_eq!(path.get("test").unwrap(), "bbb"); + } + + #[test] + fn test_recognizer_2() { + let mut router = Router::::build(); + router.path("/index.json", 10); + router.path("/{source}.json", 11); + let mut router = router.finish(); + + let mut path = Path::new("/index.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test.json"); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + } + + #[test] + fn test_recognizer_with_prefix() { + let mut router = Router::::build(); + router.path("/name", 10).0.set_id(0); + router.path("/name/{val}", 11).0.set_id(1); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(5); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test/name"); + path.skip(5); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test/name/value"); + path.skip(5); + let (h, id) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(id, ResourceId(1)); + assert_eq!(path.get("val").unwrap(), "value"); + assert_eq!(&path["val"], "value"); + + // same patterns + let mut router = Router::::build(); + router.path("/name", 10); + router.path("/name/{val}", 11); + let mut router = router.finish(); + + let mut path = Path::new("/name"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 10); + + let mut path = Path::new("/test2/name-test"); + path.skip(6); + assert!(router.recognize_mut(&mut path).is_none()); + + let mut path = Path::new("/test2/name/ttt"); + path.skip(6); + let (h, _) = router.recognize_mut(&mut path).unwrap(); + assert_eq!(*h, 11); + assert_eq!(&path["val"], "ttt"); + } +} diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs new file mode 100644 index 000000000..e08a7171a --- /dev/null +++ b/actix-router/src/url.rs @@ -0,0 +1,288 @@ +use crate::ResourcePath; + +#[allow(dead_code)] +const GEN_DELIMS: &[u8] = b":/?#[]@"; +#[allow(dead_code)] +const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,"; +#[allow(dead_code)] +const SUB_DELIMS: &[u8] = b"!$'()*,+?=;"; +#[allow(dead_code)] +const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;"; +#[allow(dead_code)] +const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ + 1234567890 + -._~"; +const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz + ABCDEFGHIJKLMNOPQRSTUVWXYZ + 1234567890 + -._~ + !$'()*,"; +const QS: &[u8] = b"+&=;b"; + +#[inline] +fn bit_at(array: &[u8], ch: u8) -> bool { + array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0 +} + +#[inline] +fn set_bit(array: &mut [u8], ch: u8) { + array[(ch >> 3) as usize] |= 1 << (ch & 7) +} + +thread_local! { + static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+"); +} + +#[derive(Default, Clone, Debug)] +pub struct Url { + uri: http::Uri, + path: Option, +} + +impl Url { + pub fn new(uri: http::Uri) -> Url { + let path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + + Url { uri, path } + } + + pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url { + Url { + path: quoter.requote(uri.path().as_bytes()), + uri, + } + } + + pub fn uri(&self) -> &http::Uri { + &self.uri + } + + pub fn path(&self) -> &str { + if let Some(ref s) = self.path { + s + } else { + self.uri.path() + } + } + + #[inline] + pub fn update(&mut self, uri: &http::Uri) { + self.uri = uri.clone(); + self.path = DEFAULT_QUOTER.with(|q| q.requote(uri.path().as_bytes())); + } + + #[inline] + pub fn update_with_quoter(&mut self, uri: &http::Uri, quoter: &Quoter) { + self.uri = uri.clone(); + self.path = quoter.requote(uri.path().as_bytes()); + } +} + +impl ResourcePath for Url { + #[inline] + fn path(&self) -> &str { + self.path() + } +} + +pub struct Quoter { + safe_table: [u8; 16], + protected_table: [u8; 16], +} + +impl Quoter { + pub fn new(safe: &[u8], protected: &[u8]) -> Quoter { + let mut q = Quoter { + safe_table: [0; 16], + protected_table: [0; 16], + }; + + // prepare safe table + for i in 0..128 { + if ALLOWED.contains(&i) { + set_bit(&mut q.safe_table, i); + } + if QS.contains(&i) { + set_bit(&mut q.safe_table, i); + } + } + + for ch in safe { + set_bit(&mut q.safe_table, *ch) + } + + // prepare protected table + for ch in protected { + set_bit(&mut q.safe_table, *ch); + set_bit(&mut q.protected_table, *ch); + } + + q + } + + pub fn requote(&self, val: &[u8]) -> Option { + let mut has_pct = 0; + let mut pct = [b'%', 0, 0]; + let mut idx = 0; + let mut cloned: Option> = None; + + let len = val.len(); + while idx < len { + let ch = val[idx]; + + if has_pct != 0 { + pct[has_pct] = val[idx]; + has_pct += 1; + if has_pct == 3 { + has_pct = 0; + let buf = cloned.as_mut().unwrap(); + + if let Some(ch) = restore_ch(pct[1], pct[2]) { + if ch < 128 { + if bit_at(&self.protected_table, ch) { + buf.extend_from_slice(&pct); + idx += 1; + continue; + } + + if bit_at(&self.safe_table, ch) { + buf.push(ch); + idx += 1; + continue; + } + } + buf.push(ch); + } else { + buf.extend_from_slice(&pct[..]); + } + } + } else if ch == b'%' { + has_pct = 1; + if cloned.is_none() { + let mut c = Vec::with_capacity(len); + c.extend_from_slice(&val[..idx]); + cloned = Some(c); + } + } else if let Some(ref mut cloned) = cloned { + cloned.push(ch) + } + idx += 1; + } + + cloned.map(|data| String::from_utf8_lossy(&data).into_owned()) + } +} + +#[inline] +fn from_hex(v: u8) -> Option { + if (b'0'..=b'9').contains(&v) { + Some(v - 0x30) // ord('0') == 0x30 + } else if (b'A'..=b'F').contains(&v) { + Some(v - 0x41 + 10) // ord('A') == 0x41 + } else if (b'a'..=b'f').contains(&v) { + Some(v - 0x61 + 10) // ord('a') == 0x61 + } else { + None + } +} + +#[inline] +fn restore_ch(d1: u8, d2: u8) -> Option { + from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2)) +} + +#[cfg(test)] +mod tests { + use http::Uri; + use std::convert::TryFrom; + + use super::*; + use crate::{Path, ResourceDef}; + + const PROTECTED: &[u8] = b"%/+"; + + fn match_url(pattern: &'static str, url: impl AsRef) -> Path { + let re = ResourceDef::new(pattern); + let uri = Uri::try_from(url.as_ref()).unwrap(); + let mut path = Path::new(Url::new(uri)); + assert!(re.capture_match_info(&mut path)); + path + } + + fn percent_encode(data: &[u8]) -> String { + data.iter().map(|c| format!("%{:02X}", c)).collect() + } + + #[test] + fn test_parse_url() { + let re = "/user/{id}/test"; + + let path = match_url(re, "/user/2345/test"); + assert_eq!(path.get("id").unwrap(), "2345"); + + // "%25" should never be decoded into '%' to guarantee the output is a valid + // percent-encoded format + let path = match_url(re, "/user/qwe%25/test"); + assert_eq!(path.get("id").unwrap(), "qwe%25"); + + let path = match_url(re, "/user/qwe%25rty/test"); + assert_eq!(path.get("id").unwrap(), "qwe%25rty"); + } + + #[test] + fn test_protected_chars() { + let encoded = percent_encode(PROTECTED); + let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); + assert_eq!(path.get("id").unwrap(), &encoded); + } + + #[test] + fn test_non_protecteed_ascii() { + let nonprotected_ascii = ('\u{0}'..='\u{7F}') + .filter(|&c| c.is_ascii() && !PROTECTED.contains(&(c as u8))) + .collect::(); + let encoded = percent_encode(nonprotected_ascii.as_bytes()); + let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded)); + assert_eq!(path.get("id").unwrap(), &nonprotected_ascii); + } + + #[test] + fn test_valid_utf8_multibyte() { + let test = ('\u{FF00}'..='\u{FFFF}').collect::(); + let encoded = percent_encode(test.as_bytes()); + let path = match_url("/a/{id}/b", format!("/a/{}/b", &encoded)); + assert_eq!(path.get("id").unwrap(), &test); + } + + #[test] + fn test_invalid_utf8() { + let invalid_utf8 = percent_encode((0x80..=0xff).collect::>().as_slice()); + let uri = Uri::try_from(format!("/{}", invalid_utf8)).unwrap(); + let path = Path::new(Url::new(uri)); + + // We should always get a valid utf8 string + assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok()); + } + + #[test] + fn test_from_hex() { + let hex = b"0123456789abcdefABCDEF"; + + for i in 0..256 { + let c = i as u8; + if hex.contains(&c) { + assert!(from_hex(c).is_some()) + } else { + assert!(from_hex(c).is_none()) + } + } + + let expected = [ + 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15, + ]; + for i in 0..hex.len() { + assert_eq!(from_hex(hex[i]).unwrap(), expected[i]); + } + } +} diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md new file mode 100644 index 000000000..b739011f0 --- /dev/null +++ b/actix-test/CHANGES.md @@ -0,0 +1,38 @@ +# Changes + +## Unreleased - 2021-xx-xx + + +## 0.1.0-beta.7 - 2021-11-22 +* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] + +[#2408]: https://github.com/actix/actix-web/pull/2408 + + +## 0.1.0-beta.6 - 2021-11-15 +* No significant changes from `0.1.0-beta.5`. + + +## 0.1.0-beta.5 - 2021-10-20 +* Updated rustls to v0.20. [#2414] +* Minimum supported Rust version (MSRV) is now 1.52. + +[#2414]: https://github.com/actix/actix-web/pull/2414 + + +## 0.1.0-beta.4 - 2021-09-09 +* Minimum supported Rust version (MSRV) is now 1.51. + + +## 0.1.0-beta.3 - 2021-06-20 +* No significant changes from `0.1.0-beta.2`. + + +## 0.1.0-beta.2 - 2021-04-17 +* No significant changes from `0.1.0-beta.1`. + + +## 0.1.0-beta.1 - 2021-04-02 +* Move integration testing structs from `actix-web`. [#2112] + +[#2112]: https://github.com/actix/actix-web/pull/2112 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml new file mode 100644 index 000000000..dcaa3e9a3 --- /dev/null +++ b/actix-test/Cargo.toml @@ -0,0 +1,48 @@ +[package] +name = "actix-test" +version = "0.1.0-beta.7" +authors = [ + "Nikolay Kim ", + "Rob Ede ", +] +description = "Integration testing tools for Actix Web applications" +keywords = ["http", "web", "framework", "async", "futures"] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" +categories = [ + "network-programming", + "asynchronous", + "web-programming::http-server", + "web-programming::websocket", +] +license = "MIT OR Apache-2.0" +edition = "2018" + +[features] +default = [] + +# rustls +rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] + +# openssl +openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] + +[dependencies] +actix-codec = "0.4.1" +actix-http = "3.0.0-beta.14" +actix-http-test = "3.0.0-beta.7" +actix-service = "2.0.0" +actix-utils = "3.0.0" +actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] } +actix-rt = "2.1" +awc = { version = "3.0.0-beta.11", default-features = false, features = ["cookies"] } + +futures-core = { version = "0.3.7", default-features = false, features = ["std"] } +futures-util = { version = "0.3.7", default-features = false, features = [] } +log = "0.4" +serde = { version = "1", features = ["derive"] } +serde_json = "1" +serde_urlencoded = "0.7" +tls-openssl = { package = "openssl", version = "0.10.9", optional = true } +tls-rustls = { package = "rustls", version = "0.20.0", optional = true } +tokio = { version = "1.2", features = ["sync"] } diff --git a/actix-test/LICENSE-APACHE b/actix-test/LICENSE-APACHE new file mode 120000 index 000000000..965b606f3 --- /dev/null +++ b/actix-test/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-test/LICENSE-MIT b/actix-test/LICENSE-MIT new file mode 120000 index 000000000..76219eb72 --- /dev/null +++ b/actix-test/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs new file mode 100644 index 000000000..1decd6e98 --- /dev/null +++ b/actix-test/src/lib.rs @@ -0,0 +1,576 @@ +//! Integration testing tools for Actix Web applications. +//! +//! The main integration testing tool is [`TestServer`]. It spawns a real HTTP server on an +//! unused port and provides methods that use a real HTTP client. Therefore, it is much closer to +//! real-world cases than using `init_service`, which skips HTTP encoding and decoding. +//! +//! # Examples +//! ``` +//! use actix_web::{get, web, test, App, HttpResponse, Error, Responder}; +//! +//! #[get("/")] +//! async fn my_handler() -> Result { +//! Ok(HttpResponse::Ok()) +//! } +//! +//! #[actix_rt::test] +//! async fn test_example() { +//! let srv = actix_test::start(|| +//! App::new().service(my_handler) +//! ); +//! +//! let req = srv.get("/"); +//! let res = req.send().await.unwrap(); +//! +//! assert!(res.status().is_success()); +//! } +//! ``` + +#[cfg(feature = "openssl")] +extern crate tls_openssl as openssl; +#[cfg(feature = "rustls")] +extern crate tls_rustls as rustls; + +use std::{fmt, net, thread, time::Duration}; + +use actix_codec::{AsyncRead, AsyncWrite, Framed}; +pub use actix_http::test::TestBuffer; +use actix_http::{ + http::{HeaderMap, Method}, + ws, HttpService, Request, Response, +}; +use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; +use actix_web::{ + body::MessageBody, + dev::{AppConfig, Server, ServerHandle, Service}, + rt::{self, System}, + web, Error, +}; +use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; +use futures_core::Stream; + +pub use actix_http_test::unused_addr; +pub use actix_web::test::{ + call_service, default_service, init_service, load_stream, ok_service, read_body, + read_body_json, read_response, read_response_json, TestRequest, +}; +use tokio::sync::mpsc; + +/// Start default [`TestServer`]. +/// +/// # Examples +/// ``` +/// use actix_web::{get, web, test, App, HttpResponse, Error, Responder}; +/// +/// #[get("/")] +/// async fn my_handler() -> Result { +/// Ok(HttpResponse::Ok()) +/// } +/// +/// #[actix_web::test] +/// async fn test_example() { +/// let srv = actix_test::start(|| +/// App::new().service(my_handler) +/// ); +/// +/// let req = srv.get("/"); +/// let res = req.send().await.unwrap(); +/// +/// assert!(res.status().is_success()); +/// } +/// ``` +pub fn start(factory: F) -> TestServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoServiceFactory, + S: ServiceFactory + 'static, + S::Error: Into + 'static, + S::InitError: fmt::Debug, + S::Response: Into> + 'static, + >::Future: 'static, + B: MessageBody + 'static, +{ + start_with(TestServerConfig::default(), factory) +} + +/// Start test server with custom configuration +/// +/// Check [`TestServerConfig`] docs for configuration options. +/// +/// # Examples +/// ``` +/// use actix_web::{get, web, test, App, HttpResponse, Error, Responder}; +/// +/// #[get("/")] +/// async fn my_handler() -> Result { +/// Ok(HttpResponse::Ok()) +/// } +/// +/// #[actix_web::test] +/// async fn test_example() { +/// let srv = actix_test::start_with(actix_test::config().h1(), || +/// App::new().service(my_handler) +/// ); +/// +/// let req = srv.get("/"); +/// let res = req.send().await.unwrap(); +/// +/// assert!(res.status().is_success()); +/// } +/// ``` +pub fn start_with(cfg: TestServerConfig, factory: F) -> TestServer +where + F: Fn() -> I + Send + Clone + 'static, + I: IntoServiceFactory, + S: ServiceFactory + 'static, + S::Error: Into + 'static, + S::InitError: fmt::Debug, + S::Response: Into> + 'static, + >::Future: 'static, + B: MessageBody + 'static, +{ + // for sending handles and server info back from the spawned thread + let (started_tx, started_rx) = std::sync::mpsc::channel(); + + // for signaling the shutdown of spawned server and system + let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1); + + let tls = match cfg.stream { + StreamType::Tcp => false, + #[cfg(feature = "openssl")] + StreamType::Openssl(_) => true, + #[cfg(feature = "rustls")] + StreamType::Rustls(_) => true, + }; + + // run server in separate orphaned thread + thread::spawn(move || { + rt::System::new().block_on(async move { + let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); + let local_addr = tcp.local_addr().unwrap(); + let factory = factory.clone(); + let srv_cfg = cfg.clone(); + let timeout = cfg.client_timeout; + + let builder = Server::build().workers(1).disable_signals().system_exit(); + + let srv = match srv_cfg.stream { + StreamType::Tcp => match srv_cfg.tp { + HttpVer::Http1 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .tcp() + }), + HttpVer::Http2 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .tcp() + }), + HttpVer::Both => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .tcp() + }), + }, + #[cfg(feature = "openssl")] + StreamType::Openssl(acceptor) => match cfg.tp { + HttpVer::Http1 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .openssl(acceptor.clone()) + }), + HttpVer::Http2 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .openssl(acceptor.clone()) + }), + HttpVer::Both => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .openssl(acceptor.clone()) + }), + }, + #[cfg(feature = "rustls")] + StreamType::Rustls(config) => match cfg.tp { + HttpVer::Http1 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .rustls(config.clone()) + }), + HttpVer::Http2 => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .rustls(config.clone()) + }), + HttpVer::Both => builder.listen("test", tcp, move || { + let app_cfg = AppConfig::__priv_test_new( + false, + local_addr.to_string(), + local_addr, + ); + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + HttpService::build() + .client_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .rustls(config.clone()) + }), + }, + } + .expect("test server could not be created"); + + let srv = srv.run(); + started_tx + .send((System::current(), srv.handle(), local_addr)) + .unwrap(); + + // drive server loop + srv.await.unwrap(); + + // notify TestServer that server and system have shut down + // all thread managed resources should be dropped at this point + }); + + let _ = thread_stop_tx.send(()); + }); + + let (system, server, addr) = started_rx.recv().unwrap(); + + let client = { + let connector = { + #[cfg(feature = "openssl")] + { + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; + + 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)); + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) + .ssl(builder.build()) + } + #[cfg(not(feature = "openssl"))] + { + Connector::new() + .conn_lifetime(Duration::from_secs(0)) + .timeout(Duration::from_millis(30000)) + } + }; + + Client::builder().connector(connector).finish() + }; + + TestServer { + server, + thread_stop_rx, + client, + system, + addr, + tls, + } +} + +#[derive(Debug, Clone)] +enum HttpVer { + Http1, + Http2, + Both, +} + +#[derive(Clone)] +enum StreamType { + Tcp, + #[cfg(feature = "openssl")] + Openssl(openssl::ssl::SslAcceptor), + #[cfg(feature = "rustls")] + Rustls(rustls::ServerConfig), +} + +/// Create default test server config. +pub fn config() -> TestServerConfig { + TestServerConfig::default() +} + +#[derive(Clone)] +pub struct TestServerConfig { + tp: HttpVer, + stream: StreamType, + client_timeout: u64, +} + +impl Default for TestServerConfig { + fn default() -> Self { + TestServerConfig::new() + } +} + +impl TestServerConfig { + /// Create default server configuration + pub(crate) fn new() -> TestServerConfig { + TestServerConfig { + tp: HttpVer::Both, + stream: StreamType::Tcp, + client_timeout: 5000, + } + } + + /// Accept HTTP/1.1 only. + pub fn h1(mut self) -> Self { + self.tp = HttpVer::Http1; + self + } + + /// Accept HTTP/2 only. + pub fn h2(mut self) -> Self { + self.tp = HttpVer::Http2; + self + } + + /// Accept secure connections via OpenSSL. + #[cfg(feature = "openssl")] + pub fn openssl(mut self, acceptor: openssl::ssl::SslAcceptor) -> Self { + self.stream = StreamType::Openssl(acceptor); + self + } + + /// Accept secure connections via Rustls. + #[cfg(feature = "rustls")] + pub fn rustls(mut self, config: rustls::ServerConfig) -> Self { + self.stream = StreamType::Rustls(config); + self + } + + /// Set client timeout in milliseconds for first request. + pub fn client_timeout(mut self, val: u64) -> Self { + self.client_timeout = val; + self + } +} + +/// A basic HTTP server controller that simplifies the process of writing integration tests for +/// Actix Web applications. +/// +/// See [`start`] for usage example. +pub struct TestServer { + server: ServerHandle, + thread_stop_rx: mpsc::Receiver<()>, + client: awc::Client, + system: rt::System, + addr: net::SocketAddr, + tls: bool, +} + +impl TestServer { + /// Construct test server url + pub fn addr(&self) -> net::SocketAddr { + self.addr + } + + /// Construct test server url + pub fn url(&self, uri: &str) -> String { + let scheme = if self.tls { "https" } else { "http" }; + + if uri.starts_with('/') { + format!("{}://localhost:{}{}", scheme, self.addr.port(), uri) + } else { + format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri) + } + } + + /// Create `GET` request. + pub fn get(&self, path: impl AsRef) -> ClientRequest { + self.client.get(self.url(path.as_ref()).as_str()) + } + + /// Create `POST` request. + pub fn post(&self, path: impl AsRef) -> ClientRequest { + self.client.post(self.url(path.as_ref()).as_str()) + } + + /// Create `HEAD` request. + pub fn head(&self, path: impl AsRef) -> ClientRequest { + self.client.head(self.url(path.as_ref()).as_str()) + } + + /// Create `PUT` request. + pub fn put(&self, path: impl AsRef) -> ClientRequest { + self.client.put(self.url(path.as_ref()).as_str()) + } + + /// Create `PATCH` request. + pub fn patch(&self, path: impl AsRef) -> ClientRequest { + self.client.patch(self.url(path.as_ref()).as_str()) + } + + /// Create `DELETE` request. + pub fn delete(&self, path: impl AsRef) -> ClientRequest { + self.client.delete(self.url(path.as_ref()).as_str()) + } + + /// Create `OPTIONS` request. + pub fn options(&self, path: impl AsRef) -> ClientRequest { + self.client.options(self.url(path.as_ref()).as_str()) + } + + /// Connect request with given method and path. + pub fn request(&self, method: Method, path: impl AsRef) -> ClientRequest { + self.client.request(method, path.as_ref()) + } + + pub async fn load_body( + &mut self, + mut response: ClientResponse, + ) -> Result + where + S: Stream> + Unpin + 'static, + { + response.body().limit(10_485_760).await + } + + /// Connect to WebSocket server at a given path. + pub async fn ws_at( + &mut self, + path: &str, + ) -> Result, awc::error::WsClientError> { + let url = self.url(path); + let connect = self.client.ws(url).connect(); + connect.await.map(|(_, framed)| framed) + } + + /// Connect to a WebSocket server. + pub async fn ws( + &mut self, + ) -> Result, awc::error::WsClientError> { + self.ws_at("/").await + } + + /// Get default HeaderMap of Client. + /// + /// Returns Some(&mut HeaderMap) when Client object is unique + /// (No other clone of client exists at the same time). + pub fn client_headers(&mut self) -> Option<&mut HeaderMap> { + self.client.headers() + } + + /// Stop HTTP server. + /// + /// Waits for spawned `Server` and `System` to shutdown (force) shutdown. + pub async fn stop(mut self) { + // signal server to stop + self.server.stop(false).await; + + // also signal system to stop + // though this is handled by `ServerBuilder::exit_system` too + self.system.stop(); + + // wait for thread to be stopped but don't care about result + let _ = self.thread_stop_rx.recv().await; + } +} + +impl Drop for TestServer { + fn drop(&mut self) { + // calls in this Drop impl should be enough to shut down the server, system, and thread + // without needing to await anything + + // signal server to stop + let _ = self.server.stop(true); + + // signal system to stop + self.system.stop(); + } +} diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index 8791853b2..146b54fd9 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -2,10 +2,37 @@ ## Unreleased - 2021-xx-xx * Add `ws:WsResponseBuilder` for building web socket session response. [#1920] +* Minimum supported Rust version (MSRV) is now 1.52. [#1920]: https://github.com/actix/actix-web/pull/1920 +## 4.0.0-beta.7 - 2021-09-09 +* Minimum supported Rust version (MSRV) is now 1.51. + + +## 4.0.0-beta.6 - 2021-06-26 +* Update `actix` to `0.12`. [#2277] + +[#2277]: https://github.com/actix/actix-web/pull/2277 + + +## 4.0.0-beta.5 - 2021-06-17 +* No notable changes. + + +## 4.0.0-beta.4 - 2021-04-02 +* No notable changes. + + +## 4.0.0-beta.3 - 2021-03-09 +* No notable changes. + + +## 4.0.0-beta.2 - 2021-02-10 +* No notable changes. + + ## 4.0.0-beta.1 - 2021-01-07 * Update `pin-project` to `1.0`. * Update `bytes` to `1.0`. [#1813] diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index 0f90edb07..28b5b29ea 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,13 +1,11 @@ [package] name = "actix-web-actors" -version = "4.0.0-beta.1" +version = "4.0.0-beta.7" authors = ["Nikolay Kim "] -description = "Actix actors support for actix web framework." -readme = "README.md" +description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/actix-web-actors/" +repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2018" @@ -16,18 +14,21 @@ name = "actix_web_actors" path = "src/lib.rs" [dependencies] -actix = "0.11.0-beta.1" -actix-codec = "0.4.0-beta.1" -actix-http = "3.0.0-beta.1" -actix-web = { version = "4.0.0-beta.1", default-features = false } +actix = { version = "0.12.0", default-features = false } +actix-codec = "0.4.1" +actix-http = "3.0.0-beta.14" +actix-web = { version = "4.0.0-beta.11", default-features = false } bytes = "1" bytestring = "1" futures-core = { version = "0.3.7", default-features = false } -pin-project = "1.0.0" +pin-project-lite = "0.2" tokio = { version = "1", features = ["sync"] } [dev-dependencies] -actix-rt = "2.0.0-beta.2" -env_logger = "0.7" +actix-rt = "2.2" +actix-test = "0.1.0-beta.7" + +awc = { version = "3.0.0-beta.11", default-features = false } +env_logger = "0.9" futures-util = { version = "0.3.7", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index fb8c3a621..2c29dedf2 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -1,8 +1,17 @@ -Actix actors support for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-web-actors)](https://crates.io/crates/actix-web-actors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +# actix-web-actors -## Documentation & community resources +> Actix actors support for Actix Web. -* [API Documentation](https://docs.rs/actix-web-actors/) -* [Chat on gitter](https://gitter.im/actix/actix) -* Cargo package: [actix-web-actors](https://crates.io/crates/actix-web-actors) -* Minimum supported Rust version: 1.40 or later +[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +![License](https://img.shields.io/crates/l/actix-web-actors.svg) +
+[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7) +[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) + +## Documentation & Resources + +- [API Documentation](https://docs.rs/actix-web-actors) +- Minimum Supported Rust Version (MSRV): 1.52 diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index 2dd93c727..d7459aea4 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -3,9 +3,7 @@ use std::future::Future; use std::pin::Pin; use std::task::{Context, Poll}; -use actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope, -}; +use actix::dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope}; use actix::fut::ActorFuture; use actix::{ Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, @@ -15,7 +13,7 @@ use bytes::Bytes; use futures_core::Stream; use tokio::sync::oneshot::Sender; -/// Execution context for http actors +/// Execution context for HTTP actors pub struct HttpContext
where A: Actor>, @@ -46,7 +44,7 @@ where #[inline] fn spawn(&mut self, fut: F) -> SpawnHandle where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.spawn(fut) } @@ -54,7 +52,7 @@ where #[inline] fn wait(&mut self, fut: F) where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.wait(fut) } @@ -165,10 +163,7 @@ where { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { if self.fut.alive() { let _ = Pin::new(&mut self.fut).poll(cx); } @@ -233,14 +228,14 @@ mod tests { #[actix_rt::test] async fn test_default_resource() { - let mut srv = + let srv = init_service(App::new().service(web::resource("/test").to(|| { HttpResponse::Ok().streaming(HttpContext::create(MyActor { count: 0 })) }))) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 0421f05fb..7a4823d91 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -1,4 +1,4 @@ -//! Actix actors integration for Actix web framework +//! Actix actors support for Actix Web. #![deny(rust_2018_idioms)] #![allow(clippy::borrow_interior_mutable_const)] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 503fc4d84..51280fe66 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -7,26 +7,30 @@ use std::task::{Context, Poll}; use std::{collections::VecDeque, convert::TryFrom}; use actix::dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, - ToEnvelope, + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope, }; use actix::fut::ActorFuture; use actix::{ - Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, - Message as ActixMessage, SpawnHandle, + Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, + SpawnHandle, }; use actix_codec::{Decoder, Encoder}; -use actix_http::ws::{hash_key, Codec}; pub use actix_http::ws::{ CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, }; -use actix_web::dev::HttpResponseBuilder; -use actix_web::error::{Error, PayloadError}; -use actix_web::http::{header, Method, StatusCode}; -use actix_web::{HttpRequest, HttpResponse}; +use actix_http::{ + http::HeaderValue, + ws::{hash_key, Codec}, +}; +use actix_web::{ + error::{Error, PayloadError}, + http::{header, Method, StatusCode}, + HttpRequest, HttpResponse, HttpResponseBuilder, +}; use bytes::{Bytes, BytesMut}; use bytestring::ByteString; use futures_core::Stream; +use pin_project_lite::pin_project; use tokio::sync::oneshot::Sender; /// Builder for Websocket Session response. @@ -50,8 +54,7 @@ use tokio::sync::oneshot::Sender; /// ``` pub struct WsResponseBuilder<'a, A, T> where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { actor: A, @@ -64,8 +67,7 @@ where impl<'a, A, T> WsResponseBuilder<'a, A, T> where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { pub fn new(actor: A, req: &'a HttpRequest, stream: T) -> Self { @@ -166,8 +168,7 @@ where match self.codec { Some(codec) => { - let out_stream = - WebsocketContext::with_codec(self.actor, self.stream, codec); + let out_stream = WebsocketContext::with_codec(self.actor, self.stream, codec); Ok(res.streaming(out_stream)) } None => { @@ -210,8 +211,7 @@ where /// Perform WebSocket handshake and start actor. pub fn start(actor: A, req: &HttpRequest, stream: T) -> Result where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { let mut res = handshake(req)?; @@ -228,15 +228,14 @@ where /// /// If successful, returns a pair where the first item is an address for the /// created actor and the second item is the response that should be returned -/// from the websocket request. +/// from the WebSocket request. pub fn start_with_addr( actor: A, req: &HttpRequest, stream: T, ) -> Result<(Addr, HttpResponse), Error> where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { let mut res = handshake(req)?; @@ -244,7 +243,7 @@ where Ok((addr, res.streaming(out_stream))) } -/// Do websocket handshake and start ws actor. +/// Do WebSocket handshake and start ws actor. /// /// `protocols` is a sequence of known protocols. pub fn start_with_protocols( @@ -254,15 +253,14 @@ pub fn start_with_protocols( stream: T, ) -> Result where - A: Actor> - + StreamHandler>, + A: Actor> + StreamHandler>, T: Stream> + 'static, { let mut res = handshake_with_protocols(req, protocols)?; Ok(res.streaming(WebsocketContext::create(actor, stream))) } -/// Prepare `WebSocket` handshake response. +/// Prepare WebSocket handshake response. /// /// This function returns handshake `HttpResponse`, ready to send to peer. /// It does not perform any IO. @@ -270,7 +268,7 @@ pub fn handshake(req: &HttpRequest) -> Result(&mut self, fut: F) -> SpawnHandle where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.spawn(fut) } fn wait(&mut self, fut: F) where - F: ActorFuture + 'static, + F: ActorFuture + 'static, { self.inner.wait(fut) } @@ -479,10 +481,7 @@ where } /// Create a new Websocket context - pub fn with_factory( - stream: S, - f: F, - ) -> impl Stream> + pub fn with_factory(stream: S, f: F) -> impl Stream> where F: FnOnce(&mut Self) -> A + 'static, A: StreamHandler>, @@ -601,10 +600,7 @@ where { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if this.fut.alive() { @@ -642,13 +638,14 @@ where } } -#[pin_project::pin_project] -struct WsStream { - #[pin] - stream: S, - decoder: Codec, - buf: BytesMut, - closed: bool, +pin_project! { + struct WsStream { + #[pin] + stream: S, + decoder: Codec, + buf: BytesMut, + closed: bool, + } } impl WsStream @@ -671,15 +668,11 @@ where { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let mut this = self.as_mut().project(); if !*this.closed { loop { - this = self.as_mut().project(); match Pin::new(&mut this.stream).poll_next(cx) { Poll::Ready(Some(Ok(chunk))) => { this.buf.extend_from_slice(&chunk[..]); @@ -690,9 +683,10 @@ where } Poll::Pending => break, Poll::Ready(Some(Err(e))) => { - return Poll::Ready(Some(Err(ProtocolError::Io( - io::Error::new(io::ErrorKind::Other, format!("{}", e)), - )))); + return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new( + io::ErrorKind::Other, + format!("{}", e), + ))))); } } } diff --git a/actix-web-actors/tests/test_ws.rs b/actix-web-actors/tests/test_ws.rs index 41194ed0a..b66935623 100644 --- a/actix-web-actors/tests/test_ws.rs +++ b/actix-web-actors/tests/test_ws.rs @@ -1,6 +1,6 @@ use actix::prelude::*; use actix_http::ws::Codec; -use actix_web::{test, web, App, HttpRequest}; +use actix_web::{web, App, HttpRequest}; use actix_web_actors::*; use bytes::Bytes; use futures_util::{SinkExt, StreamExt}; @@ -12,11 +12,7 @@ impl Actor for Ws { } impl StreamHandler> for Ws { - fn handle( - &mut self, - msg: Result, - ctx: &mut Self::Context, - ) { + fn handle(&mut self, msg: Result, ctx: &mut Self::Context) { match msg.unwrap() { ws::Message::Ping(msg) => ctx.pong(&msg), ws::Message::Text(text) => ctx.text(text), @@ -30,7 +26,7 @@ impl StreamHandler> for Ws { const MAX_FRAME_SIZE: usize = 10_000; const DEFAULT_FRAME_SIZE: usize = 10; -async fn common_test_code(mut srv: test::TestServer, frame_size: usize) { +async fn common_test_code(mut srv: actix_test::TestServer, frame_size: usize) { // client service let mut framed = srv.ws().await.unwrap(); framed.send(ws::Message::Text("text".into())).await.unwrap(); @@ -61,7 +57,7 @@ async fn common_test_code(mut srv: test::TestServer, frame_size: usize) { #[actix_rt::test] async fn test_builder() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream).start() @@ -74,7 +70,7 @@ async fn test_builder() { #[actix_rt::test] async fn test_builder_with_frame_size() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream) @@ -88,9 +84,8 @@ async fn test_builder_with_frame_size() { } #[actix_rt::test] -#[should_panic] async fn test_builder_with_frame_size_exceeded() { - let mut srv = test::start(|| { + let mut srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream) @@ -103,21 +98,16 @@ async fn test_builder_with_frame_size_exceeded() { // client service let mut framed = srv.ws().await.unwrap(); - // Create a request with a frame size larger than expected. This should - // panic with '`Err` value: Overflow'. + // create a request with a frame size larger than expected let bytes = Bytes::from(vec![0; MAX_FRAME_SIZE + 1]); - framed - .send(ws::Message::Binary(bytes.clone())) - .await - .unwrap(); + framed.send(ws::Message::Binary(bytes)).await.unwrap(); - // try unwrapping to panic. - framed.next().await.unwrap().unwrap(); + assert!(framed.next().await.is_none()); } #[actix_rt::test] async fn test_builder_with_codec() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream) @@ -132,7 +122,7 @@ async fn test_builder_with_codec() { #[actix_rt::test] async fn test_builder_with_protocols() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream) @@ -147,7 +137,7 @@ async fn test_builder_with_protocols() { #[actix_rt::test] async fn test_builder_full() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream) @@ -164,7 +154,7 @@ async fn test_builder_full() { #[actix_rt::test] async fn test_builder_with_codec_and_frame_size() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( |req: HttpRequest, stream: web::Payload| async move { ws::WsResponseBuilder::new(Ws, &req, stream) @@ -180,11 +170,9 @@ async fn test_builder_with_codec_and_frame_size() { #[actix_rt::test] async fn test_simple() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to( - |req: HttpRequest, stream: web::Payload| async move { - ws::start(Ws, &req, stream) - }, + |req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) }, )) }); diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index a7675d9dd..3811ef030 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -3,6 +3,38 @@ ## Unreleased - 2021-xx-xx +## 0.5.0-beta.5 - 2021-10-20 +* Improve error recovery potential when macro input is invalid. [#2410] +* Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] +* Minimum supported Rust version (MSRV) is now 1.52. + +[#2410]: https://github.com/actix/actix-web/pull/2410 +[#2409]: https://github.com/actix/actix-web/pull/2409 + + +## 0.5.0-beta.4 - 2021-09-09 +* In routing macros, paths are now validated at compile time. [#2350] +* Minimum supported Rust version (MSRV) is now 1.51. + +[#2350]: https://github.com/actix/actix-web/pull/2350 + + +## 0.5.0-beta.3 - 2021-06-17 +* No notable changes. + + +## 0.5.0-beta.2 - 2021-03-09 +* Preserve doc comments when using route macros. [#2022] +* Add `name` attribute to `route` macro. [#1934] + +[#2022]: https://github.com/actix/actix-web/pull/2022 +[#1934]: https://github.com/actix/actix-web/pull/1934 + + +## 0.5.0-beta.1 - 2021-02-10 +* Use new call signature for `System::new`. + + ## 0.4.0 - 2020-09-20 * Added compile success and failure testing. [#1677] * Add `route` macro for supporting multiple HTTP methods guards. [#1674] diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index 00875cf1b..8497f0b23 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,12 +1,13 @@ [package] name = "actix-web-codegen" -version = "0.4.0" -description = "Actix web proc macros" -readme = "README.md" +version = "0.5.0-beta.5" +description = "Routing and runtime macros for Actix Web" homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web" -documentation = "https://docs.rs/actix-web-codegen" -authors = ["Nikolay Kim "] +repository = "https://github.com/actix/actix-web.git" +authors = [ + "Nikolay Kim ", + "Rob Ede ", +] license = "MIT OR Apache-2.0" edition = "2018" @@ -17,10 +18,15 @@ proc-macro = true quote = "1" syn = { version = "1", features = ["full", "parsing"] } proc-macro2 = "1" +actix-router = "0.5.0-beta.2" [dev-dependencies] -actix-rt = "2.0.0-beta.2" -actix-web = "4.0.0-beta.1" -futures-util = { version = "0.3.7", default-features = false } +actix-rt = "2.2" +actix-macros = "0.2.3" +actix-test = "0.1.0-beta.7" +actix-utils = "3.0.0" +actix-web = "4.0.0-beta.11" + +futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } trybuild = "1" rustversion = "1" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 887502075..2ffd5b31c 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,22 +1,23 @@ # actix-web-codegen -> Helper and convenience macros for Actix Web +> Routing and runtime macros for Actix Web. -[![crates.io](https://meritbadge.herokuapp.com/actix-web-codegen)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg)](https://docs.rs/actix-web-codegen/0.4.0/actix_web_codegen/) -[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) -[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) -[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) -[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.5)](https://docs.rs/actix-web-codegen/0.5.0-beta.5) +[![Version](https://img.shields.io/badge/rustc-1.52+-ab6000.svg)](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html) +![License](https://img.shields.io/crates/l/actix-web-codegen.svg) +
+[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.5) +[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/actix-web-codegen) -- [Chat on Gitter](https://gitter.im/actix/actix-web) -- Cargo package: [actix-web-codegen](https://crates.io/crates/actix-web-codegen) -- Minimum supported Rust version: 1.46 or later. +- Minimum Supported Rust Version (MSRV): 1.52 ## Compile Testing + Uses the [`trybuild`] crate. All compile fail tests should include a stderr file generated by `trybuild`. See the [workflow section](https://github.com/dtolnay/trybuild#workflow) of the trybuild docs for info on how to do this. [`trybuild`]: https://github.com/dtolnay/trybuild diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 50e5be712..cebf9e5fb 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -1,6 +1,6 @@ -//! Macros for reducing boilerplate code in Actix Web applications. +//! Routing and runtime macros for Actix Web. //! -//! ## Actix Web Re-exports +//! # Actix Web Re-exports //! Actix Web re-exports a version of this crate in it's entirety so you usually don't have to //! specify a dependency on this crate explicitly. Sometimes, however, updates are made to this //! crate before the actix-web dependency is updated. Therefore, code examples here will show @@ -10,7 +10,7 @@ //! # Runtime Setup //! Used for setting up the actix async runtime. See [macro@main] macro docs. //! -//! ```rust +//! ``` //! #[actix_web_codegen::main] // or `#[actix_web::main]` in Actix Web apps //! async fn main() { //! async { println!("Hello world"); }.await @@ -23,7 +23,7 @@ //! //! See docs for: [GET], [POST], [PATCH], [PUT], [DELETE], [HEAD], [CONNECT], [OPTIONS], [TRACE] //! -//! ```rust +//! ``` //! # use actix_web::HttpResponse; //! # use actix_web_codegen::get; //! #[get("/test")] @@ -36,7 +36,7 @@ //! Similar to the single method handler macro but takes one or more arguments for the HTTP methods //! it should respond to. See [macro@route] macro docs. //! -//! ```rust +//! ``` //! # use actix_web::HttpResponse; //! # use actix_web_codegen::route; //! #[route("/test", method="GET", method="HEAD")] @@ -59,18 +59,20 @@ #![recursion_limit = "512"] use proc_macro::TokenStream; +use quote::quote; mod route; /// Creates resource handler, allowing multiple HTTP method guards. /// /// # Syntax -/// ```text +/// ```plain /// #[route("path", method="HTTP_METHOD"[, attributes])] /// ``` /// /// # Attributes /// - `"path"` - Raw literal string with path for which to register handler. +/// - `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used. /// - `method="HTTP_METHOD"` - Registers HTTP method to provide guard for. Upper-case string, "GET", "POST" for example. /// - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard` /// - `wrap="Middleware"` - Registers a resource middleware. @@ -81,7 +83,7 @@ mod route; /// /// # Example /// -/// ```rust +/// ``` /// # use actix_web::HttpResponse; /// # use actix_web_codegen::route; /// #[route("/test", method="GET", method="HEAD")] @@ -110,12 +112,13 @@ concat!(" Creates route handler with `actix_web::guard::", stringify!($variant), "`. # Syntax -```text +```plain #[", stringify!($method), r#"("path"[, attributes])] ``` # Attributes - `"path"` - Raw literal string with path for which to register handler. +- `name="resource_name"` - Specifies resource name for the handler. If not set, the function name of handler is used. - `guard="function_name"` - Registers function as guard using `actix_web::guard::fn_guard`. - `wrap="Middleware"` - Registers a resource middleware. @@ -125,7 +128,7 @@ code, e.g `my_guard` or `my_module::my_guard`. # Example -```rust +``` # use actix_web::HttpResponse; # use actix_web_codegen::"#, stringify!($method), "; #[", stringify!($method), r#"("/")] @@ -155,42 +158,41 @@ method_macro! { } /// Marks async main function as the actix system entry-point. -/// -/// # Actix Web Re-export -/// This macro can be applied with `#[actix_web::main]` when used in Actix Web applications. -/// -/// # Usage -/// ```rust -/// #[actix_web_codegen::main] + +/// # Examples +/// ``` +/// #[actix_web::main] /// async fn main() { /// async { println!("Hello world"); }.await /// } /// ``` #[proc_macro_attribute] pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { - use quote::quote; - - let mut input = syn::parse_macro_input!(item as syn::ItemFn); - let attrs = &input.attrs; - let vis = &input.vis; - let sig = &mut input.sig; - let body = &input.block; - let name = &sig.ident; - - if sig.asyncness.is_none() { - return syn::Error::new_spanned(sig.fn_token, "only async fn is supported") - .to_compile_error() - .into(); - } - - sig.asyncness = None; - - (quote! { - #(#attrs)* - #vis #sig { - actix_web::rt::System::new(stringify!(#name)) - .block_on(async move { #body }) - } + let mut output: TokenStream = (quote! { + #[::actix_web::rt::main(system = "::actix_web::rt::System")] }) - .into() + .into(); + + output.extend(item); + output +} + +/// Marks async test functions to use the actix system entry-point. +/// +/// # Examples +/// ``` +/// #[actix_web::test] +/// async fn test() { +/// assert_eq!(async { "Hello world" }.await, "Hello world"); +/// } +/// ``` +#[proc_macro_attribute] +pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { + let mut output: TokenStream = (quote! { + #[::actix_web::rt::test(system = "::actix_web::rt::System")] + }) + .into(); + + output.extend(item); + output } diff --git a/actix-web-codegen/src/route.rs b/actix-web-codegen/src/route.rs index ddbd42454..eac1948a7 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -3,10 +3,11 @@ extern crate proc_macro; use std::collections::HashSet; use std::convert::TryFrom; +use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{format_ident, quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, NestedMeta}; +use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta}; enum ResourceType { Async, @@ -78,6 +79,7 @@ impl TryFrom<&syn::LitStr> for MethodType { struct Args { path: syn::LitStr, + resource_name: Option, guards: Vec, wrappers: Vec, methods: HashSet, @@ -86,6 +88,7 @@ struct Args { impl Args { fn new(args: AttributeArgs, method: Option) -> syn::Result { let mut path = None; + let mut resource_name = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); let mut methods = HashSet::new(); @@ -99,6 +102,7 @@ impl Args { match arg { NestedMeta::Lit(syn::Lit::Str(lit)) => match path { None => { + let _ = ResourceDef::new(lit.value()); path = Some(lit); } _ => { @@ -109,7 +113,16 @@ impl Args { } }, NestedMeta::Meta(syn::Meta::NameValue(nv)) => { - if nv.path.is_ident("guard") { + if nv.path.is_ident("name") { + if let syn::Lit::Str(lit) = nv.lit { + resource_name = Some(lit); + } else { + return Err(syn::Error::new_spanned( + nv.lit, + "Attribute name expects literal string!", + )); + } + } else if nv.path.is_ident("guard") { if let syn::Lit::Str(lit) = nv.lit { guards.push(Ident::new(&lit.value(), Span::call_site())); } else { @@ -164,6 +177,7 @@ impl Args { } Ok(Args { path: path.unwrap(), + resource_name, guards, wrappers, methods, @@ -176,6 +190,9 @@ pub struct Route { args: Args, ast: syn::ItemFn, resource_type: ResourceType, + + /// The doc comment attributes to copy to generated struct, if any. + doc_attributes: Vec, } fn guess_resource_type(typ: &syn::Type) -> ResourceType { @@ -203,7 +220,7 @@ fn guess_resource_type(typ: &syn::Type) -> ResourceType { impl Route { pub fn new( args: AttributeArgs, - input: TokenStream, + ast: syn::ItemFn, method: Option, ) -> syn::Result { if args.is_empty() { @@ -212,15 +229,23 @@ impl Route { format!( r#"invalid service definition, expected #[{}("")]"#, method - .map(|it| it.as_str()) - .unwrap_or("route") + .map_or("route", |it| it.as_str()) .to_ascii_lowercase() ), )); } - let ast: syn::ItemFn = syn::parse(input)?; + let name = ast.sig.ident.clone(); + // Try and pull out the doc comments so that we can reapply them to the generated struct. + // Note that multi line doc comments are converted to multiple doc attributes. + let doc_attributes = ast + .attrs + .iter() + .filter(|attr| attr.path.is_ident("doc")) + .cloned() + .collect(); + let args = Args::new(args, method)?; if args.methods.is_empty() { return Err(syn::Error::new( @@ -248,6 +273,7 @@ impl Route { args, ast, resource_type, + doc_attributes, }) } } @@ -260,13 +286,17 @@ impl ToTokens for Route { args: Args { path, + resource_name, guards, wrappers, methods, }, resource_type, + doc_attributes, } = self; - let resource_name = name.to_string(); + let resource_name = resource_name + .as_ref() + .map_or_else(|| name.to_string(), LitStr::value); let method_guards = { let mut others = methods.iter(); // unwrapping since length is checked to be at least one @@ -287,6 +317,7 @@ impl ToTokens for Route { }; let stream = quote! { + #(#doc_attributes)* #[allow(non_camel_case_types, missing_docs)] pub struct #name; @@ -315,8 +346,28 @@ pub(crate) fn with_method( input: TokenStream, ) -> TokenStream { let args = parse_macro_input!(args as syn::AttributeArgs); - match Route::new(args, input, method) { + + let ast = match syn::parse::(input.clone()) { + Ok(ast) => ast, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; + + match Route::new(args, ast, method) { Ok(route) => route.into_token_stream().into(), - Err(err) => err.to_compile_error().into(), + // on macro related error, make IDEs happy; see fn docs + Err(err) => input_and_compile_error(input, err), } } + +/// Converts the error to a token stream and appends it to the original input. +/// +/// Returning the original input in addition to the error is good for IDEs which can gracefully +/// recover and show more precise errors within the macro body. +/// +/// See for more info. +fn input_and_compile_error(mut item: TokenStream, err: syn::Error) -> TokenStream { + let compile_err = TokenStream::from(err.to_compile_error()); + item.extend(compile_err); + item +} diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/test_macro.rs index 389d09c82..769cf2bc3 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/test_macro.rs @@ -1,14 +1,17 @@ use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; -use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; -use actix_web::http::header::{HeaderName, HeaderValue}; -use actix_web::{http, test, web::Path, App, Error, HttpResponse, Responder}; -use actix_web_codegen::{ - connect, delete, get, head, options, patch, post, put, route, trace, +use actix_utils::future::{ok, Ready}; +use actix_web::{ + dev::{Service, ServiceRequest, ServiceResponse, Transform}, + http::{ + self, + header::{HeaderName, HeaderValue}, + StatusCode, + }, + web, App, Error, HttpResponse, Responder, }; -use futures_util::future; +use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace}; +use futures_core::future::LocalBoxFuture; // Make sure that we can name function as 'config' #[get("/config")] @@ -58,26 +61,26 @@ async fn trace_test() -> impl Responder { #[get("/test")] fn auto_async() -> impl Future> { - future::ok(HttpResponse::Ok().finish()) + ok(HttpResponse::Ok().finish()) } #[get("/test")] fn auto_sync() -> impl Future> { - future::ok(HttpResponse::Ok().finish()) + ok(HttpResponse::Ok().finish()) } #[put("/test/{param}")] -async fn put_param_test(_: Path) -> impl Responder { +async fn put_param_test(_: web::Path) -> impl Responder { HttpResponse::Created() } #[delete("/test/{param}")] -async fn delete_param_test(_: Path) -> impl Responder { +async fn delete_param_test(_: web::Path) -> impl Responder { HttpResponse::NoContent() } #[get("/test/{param}")] -async fn get_param_test(_: Path) -> impl Responder { +async fn get_param_test(_: web::Path) -> impl Responder { HttpResponse::Ok() } @@ -86,6 +89,13 @@ async fn route_test() -> impl Responder { HttpResponse::Ok() } +#[get("/custom_resource_name", name = "custom")] +async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder { + assert!(req.url_for_static("custom").is_ok()); + assert!(req.url_for_static("custom_resource_name_test").is_err()); + HttpResponse::Ok() +} + pub struct ChangeStatusCode; impl Transform for ChangeStatusCode @@ -98,10 +108,10 @@ where type Error = Error; type Transform = ChangeStatusCodeMiddleware; type InitError = (); - type Future = future::Ready>; + type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { - future::ok(ChangeStatusCodeMiddleware { service }) + ok(ChangeStatusCodeMiddleware { service }) } } @@ -117,14 +127,11 @@ where { type Response = ServiceResponse; type Error = Error; - #[allow(clippy::type_complexity)] - type Future = Pin>>>; + type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_web::dev::forward_ready!(service); - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { let fut = self.service.call(req); Box::pin(async move { @@ -139,13 +146,14 @@ where } #[get("/test/wrap", wrap = "ChangeStatusCode")] -async fn get_wrap(_: Path) -> impl Responder { +async fn get_wrap(_: web::Path) -> impl Responder { + // panic!("actually never gets called because path failed to extract"); HttpResponse::Ok() } #[actix_rt::test] async fn test_params() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new() .service(get_param_test) .service(put_param_test) @@ -167,7 +175,7 @@ async fn test_params() { #[actix_rt::test] async fn test_body() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new() .service(post_test) .service(put_test) @@ -178,6 +186,7 @@ async fn test_body() { .service(patch_test) .service(test_handler) .service(route_test) + .service(custom_resource_name_test) }); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); @@ -232,22 +241,30 @@ async fn test_body() { let request = srv.request(http::Method::PATCH, srv.url("/multi")); let response = request.send().await.unwrap(); assert!(!response.status().is_success()); + + let request = srv.request(http::Method::GET, srv.url("/custom_resource_name")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); } #[actix_rt::test] async fn test_auto_async() { - let srv = test::start(|| App::new().service(auto_async)); + let srv = actix_test::start(|| App::new().service(auto_async)); let request = srv.request(http::Method::GET, srv.url("/test")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); } -#[actix_rt::test] +#[actix_web::test] async fn test_wrap() { - let srv = test::start(|| App::new().service(get_wrap)); + let srv = actix_test::start(|| App::new().service(get_wrap)); let request = srv.request(http::Method::GET, srv.url("/test/wrap")); - let response = request.send().await.unwrap(); + let mut response = request.send().await.unwrap(); + assert_eq!(response.status(), StatusCode::NOT_FOUND); assert!(response.headers().contains_key("custom-header")); + let body = response.body().await.unwrap(); + let body = String::from_utf8(body.to_vec()).unwrap(); + assert!(body.contains("wrong number of parameters")); } diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index d2d8a38f5..dd70cb7ca 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,3 +1,4 @@ +#[rustversion::stable(1.52)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); @@ -9,12 +10,9 @@ fn compile_macros() { t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); + t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); + + t.pass("tests/trybuild/docstring-ok.rs"); + + t.pass("tests/trybuild/test-runtime.rs"); } - -// #[rustversion::not(nightly)] -// fn skip_on_nightly(t: &trybuild::TestCases) { -// -// } - -// #[rustversion::nightly] -// fn skip_on_nightly(_t: &trybuild::TestCases) {} diff --git a/actix-web-codegen/tests/trybuild/docstring-ok.rs b/actix-web-codegen/tests/trybuild/docstring-ok.rs new file mode 100644 index 000000000..4cf310be5 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/docstring-ok.rs @@ -0,0 +1,17 @@ +use actix_web::{Responder, HttpResponse, App}; +use actix_web_codegen::*; + +/// doc comments shouldn't break anything +#[get("/")] +async fn index() -> impl Responder { + HttpResponse::Ok() +} + +#[actix_web::main] +async fn main() { + let srv = actix_test::start(|| App::new().service(index)); + + let request = srv.get("/"); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs index 9a38050f7..9322b4895 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.rs @@ -7,9 +7,9 @@ async fn index() -> String { #[actix_web::main] async fn main() { - use actix_web::{App, test}; + use actix_web::App; - let srv = test::start(|| App::new().service(index)); + let srv = actix_test::start(|| App::new().service(index)); let request = srv.get("/"); let response = request.send().await.unwrap(); diff --git a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr index f3eda68af..90cff1b1c 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -4,8 +4,8 @@ error: HTTP method defined more than once: `GET` 3 | #[route("/", method="GET", method="GET")] | ^^^^^ -error[E0425]: cannot find value `index` in this scope - --> $DIR/route-duplicate-method-fail.rs:12:49 +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> $DIR/route-duplicate-method-fail.rs:12:55 | -12 | let srv = test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope +12 | let srv = actix_test::start(|| App::new().service(index)); + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs new file mode 100644 index 000000000..1258a6f2f --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.rs @@ -0,0 +1,33 @@ +use actix_web_codegen::get; + +#[get("/{")] +async fn zero() -> &'static str { + "malformed resource def" +} + +#[get("/{foo")] +async fn one() -> &'static str { + "malformed resource def" +} + +#[get("/{}")] +async fn two() -> &'static str { + "malformed resource def" +} + +#[get("/*")] +async fn three() -> &'static str { + "malformed resource def" +} + +#[get("/{tail:\\d+}*")] +async fn four() -> &'static str { + "malformed resource def" +} + +#[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")] +async fn five() -> &'static str { + "malformed resource def" +} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr new file mode 100644 index 000000000..93c510109 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr @@ -0,0 +1,42 @@ +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:3:1 + | +3 | #[get("/{")] + | ^^^^^^^^^^^^ + | + = help: message: pattern "{" contains malformed dynamic segment + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:8:1 + | +8 | #[get("/{foo")] + | ^^^^^^^^^^^^^^^ + | + = help: message: pattern "{foo" contains malformed dynamic segment + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:13:1 + | +13 | #[get("/{}")] + | ^^^^^^^^^^^^^ + | + = help: message: Wrong path pattern: "/{}" regex parse error: + ((?s-m)^/(?P<>[^/]+))$ + ^ + error: empty capture group name + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:23:1 + | +23 | #[get("/{tail:\\d+}*")] + | ^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: custom regex is not supported for tail match + +error: custom attribute panicked + --> $DIR/route-malformed-path-fail.rs:28:1 + | +28 | #[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")] + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = help: message: Only 16 dynamic segments are allowed, provided: 17 diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs index ce87a55a4..cd43c0669 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.rs @@ -7,9 +7,9 @@ async fn index() -> String { #[actix_web::main] async fn main() { - use actix_web::{App, test}; + use actix_web::App; - let srv = test::start(|| App::new().service(index)); + let srv = actix_test::start(|| App::new().service(index)); let request = srv.get("/"); let response = request.send().await.unwrap(); diff --git a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr index 0518a61ed..c36b090c0 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -6,8 +6,8 @@ error: The #[route(..)] macro requires at least one `method` attribute | = note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info) -error[E0425]: cannot find value `index` in this scope - --> $DIR/route-missing-method-fail.rs:12:49 +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> $DIR/route-missing-method-fail.rs:12:55 | -12 | let srv = test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope +12 | let srv = actix_test::start(|| App::new().service(index)); + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/route-ok.rs b/actix-web-codegen/tests/trybuild/route-ok.rs index c4f679604..e1082e88e 100644 --- a/actix-web-codegen/tests/trybuild/route-ok.rs +++ b/actix-web-codegen/tests/trybuild/route-ok.rs @@ -7,9 +7,9 @@ async fn index() -> String { #[actix_web::main] async fn main() { - use actix_web::{App, test}; + use actix_web::App; - let srv = test::start(|| App::new().service(index)); + let srv = actix_test::start(|| App::new().service(index)); let request = srv.get("/"); let response = request.send().await.unwrap(); diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs index 28cd1344c..1a50e01bc 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs @@ -7,9 +7,9 @@ async fn index() -> String { #[actix_web::main] async fn main() { - use actix_web::{App, test}; + use actix_web::App; - let srv = test::start(|| App::new().service(index)); + let srv = actix_test::start(|| App::new().service(index)); let request = srv.get("/"); let response = request.send().await.unwrap(); diff --git a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr index 9d87f310b..dda366067 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr @@ -4,8 +4,8 @@ error: Unexpected HTTP method: `UNEXPECTED` 3 | #[route("/", method="UNEXPECTED")] | ^^^^^^^^^^^^ -error[E0425]: cannot find value `index` in this scope - --> $DIR/route-unexpected-method-fail.rs:12:49 +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> $DIR/route-unexpected-method-fail.rs:12:55 | -12 | let srv = test::start(|| App::new().service(index)); - | ^^^^^ not found in this scope +12 | let srv = actix_test::start(|| App::new().service(index)); + | ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` diff --git a/actix-web-codegen/tests/trybuild/simple.rs b/actix-web-codegen/tests/trybuild/simple.rs index 761b04905..8170edbfa 100644 --- a/actix-web-codegen/tests/trybuild/simple.rs +++ b/actix-web-codegen/tests/trybuild/simple.rs @@ -1,4 +1,4 @@ -use actix_web::{Responder, HttpResponse, App, test}; +use actix_web::{Responder, HttpResponse, App}; use actix_web_codegen::*; #[get("/config")] @@ -8,7 +8,7 @@ async fn config() -> impl Responder { #[actix_web::main] async fn main() { - let srv = test::start(|| App::new().service(config)); + let srv = actix_test::start(|| App::new().service(config)); let request = srv.get("/config"); let response = request.send().await.unwrap(); diff --git a/actix-web-codegen/tests/trybuild/test-runtime.rs b/actix-web-codegen/tests/trybuild/test-runtime.rs new file mode 100644 index 000000000..0b901b258 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/test-runtime.rs @@ -0,0 +1,6 @@ +#[actix_web::test] +async fn my_test() { + assert!(async { 1 }.await, 1); +} + +fn main() {} diff --git a/awc/CHANGES.md b/awc/CHANGES.md index c5c1185be..ab3362b72 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,9 +1,97 @@ # Changes ## Unreleased - 2021-xx-xx + + +## 3.0.0-beta.12 - 2021-11-30 +* Update `actix-tls` to `3.0.0-rc.1`. [#2474] + +[#2474]: https://github.com/actix/actix-web/pull/2474 + + +## 3.0.0-beta.11 - 2021-11-22 +* No significant changes from `3.0.0-beta.10`. + + +## 3.0.0-beta.10 - 2021-11-15 +* No significant changes from `3.0.0-beta.9`. + + +## 3.0.0-beta.9 - 2021-10-20 +* Updated rustls to v0.20. [#2414] + +[#2414]: https://github.com/actix/actix-web/pull/2414 + + +## 3.0.0-beta.8 - 2021-09-09 +### Changed +* Send headers within the redirect requests. [#2310] + +[#2310]: https://github.com/actix/actix-web/pull/2310 + + +## 3.0.0-beta.7 - 2021-06-26 +### Changed +* Change compression algorithm features flags. [#2250] + +[#2250]: https://github.com/actix/actix-web/pull/2250 + + +## 3.0.0-beta.6 - 2021-06-17 +* No significant changes since 3.0.0-beta.5. + + +## 3.0.0-beta.5 - 2021-04-17 +### Removed +* Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] + +[#2148]: https://github.com/actix/actix-web/pull/2148 + + +## 3.0.0-beta.4 - 2021-04-02 +### Added +* Add `Client::headers` to get default mut reference of `HeaderMap` of client object. [#2114] + +### Changed +* `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] +* Fix http/https encoding when enabling `compress` feature. [#2116] +* Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header + methods now take `IntoHeaderPair` tuples. [#2094] + +[#2081]: https://github.com/actix/actix-web/pull/2081 +[#2094]: https://github.com/actix/actix-web/pull/2094 +[#2114]: https://github.com/actix/actix-web/pull/2114 +[#2116]: https://github.com/actix/actix-web/pull/2116 + + +## 3.0.0-beta.3 - 2021-03-08 +### Added +* `ClientResponse::timeout` for set the timeout of collecting response body. [#1931] +* `ClientBuilder::local_address` for bind to a local ip address for this client. [#2024] + +### Changed +* Feature `cookies` is now optional and enabled by default. [#1981] +* `ClientBuilder::connector` method would take `actix_http::client::Connector` type. [#2008] +* Basic auth password now takes blank passwords as an empty string instead of Option. [#2050] + +### Removed +* `ClientBuilder::default` function [#2008] + +[#1931]: https://github.com/actix/actix-web/pull/1931 +[#1981]: https://github.com/actix/actix-web/pull/1981 +[#2008]: https://github.com/actix/actix-web/pull/2008 +[#2024]: https://github.com/actix/actix-web/pull/2024 +[#2050]: https://github.com/actix/actix-web/pull/2050 + + +## 3.0.0-beta.2 - 2021-02-10 ### Added * `ClientRequest::insert_header` method which allows using typed headers. [#1869] * `ClientRequest::append_header` method which allows using typed headers. [#1869] +* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] + +### Changed +* Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905] ### Removed * `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] @@ -12,6 +100,8 @@ * `ClientRequest::header`; use `ClientRequest::append_header`. [#1869] [#1869]: https://github.com/actix/actix-web/pull/1869 +[#1905]: https://github.com/actix/actix-web/pull/1905 +[#1969]: https://github.com/actix/actix-web/pull/1969 ## 3.0.0-beta.1 - 2021-01-07 diff --git a/awc/Cargo.toml b/awc/Cargo.toml index 90f33c9ba..fc60f5edb 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,19 +1,20 @@ [package] name = "awc" -version = "3.0.0-beta.1" -authors = ["Nikolay Kim "] +version = "3.0.0-beta.12" +authors = [ + "Nikolay Kim ", + "fakeshadow <24548779@qq.com>", +] description = "Async HTTP and WebSocket client library built on the Actix ecosystem" -readme = "README.md" keywords = ["actix", "http", "framework", "async", "web"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -documentation = "https://docs.rs/awc/" categories = [ "network-programming", "asynchronous", "web-programming::http-client", "web-programming::websocket", ] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web.git" license = "MIT OR Apache-2.0" edition = "2018" @@ -22,55 +23,92 @@ name = "awc" path = "src/lib.rs" [package.metadata.docs.rs] -features = ["openssl", "rustls", "compress"] +# features that docs.rs will build with +features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] [features] -default = ["compress"] +default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] # openssl -openssl = ["open-ssl", "actix-http/openssl"] +openssl = ["tls-openssl", "actix-tls/openssl"] # rustls -rustls = ["rust-tls", "actix-http/rustls"] +rustls = ["tls-rustls", "actix-tls/rustls"] -# content-encoding support -compress = ["actix-http/compress"] +# Brotli algorithm content-encoding support +compress-brotli = ["actix-http/compress-brotli", "__compress"] +# Gzip and deflate algorithms content-encoding support +compress-gzip = ["actix-http/compress-gzip", "__compress"] +# Zstd algorithm content-encoding support +compress-zstd = ["actix-http/compress-zstd", "__compress"] + +# cookie parsing and cookie jar +cookies = ["cookie"] + +# trust-dns as dns resolver +trust-dns = ["trust-dns-resolver"] + +# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Don't rely on these whatsoever. They may disappear at anytime. +__compress = [] + +# Enable dangerous feature for testing and local network usage: +# - HTTP/2 over TCP(No Tls). +# DO NOT enable this over any internet use case. +dangerous-h2c = [] [dependencies] -actix-codec = "0.4.0-beta.1" -actix-service = "2.0.0-beta.3" -actix-http = "3.0.0-beta.1" -actix-rt = "2.0.0-beta.2" +actix-codec = "0.4.1" +actix-service = "2.0.0" +actix-http = "3.0.0-beta.14" +actix-rt = { version = "2.1", default-features = false } +actix-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] } +actix-utils = "3.0.0" +ahash = "0.7" base64 = "0.13" bytes = "1" -cfg-if = "1.0" +cfg-if = "1" derive_more = "0.99.5" futures-core = { version = "0.3.7", default-features = false } +futures-util = { version = "0.3.7", default-features = false } +h2 = "0.3" +http = "0.2.5" +itoa = "0.4" log =" 0.4" mime = "0.3" percent-encoding = "2.1" +pin-project-lite = "0.2" rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -open-ssl = { version = "0.10", package = "openssl", optional = true } -rust-tls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } +tokio = { version = "1", features = ["sync"] } + +cookie = { version = "0.15", features = ["percent-encode"], optional = true } + +tls-openssl = { package = "openssl", version = "0.10.9", optional = true } +tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } + +trust-dns-resolver = { version = "0.20.0", optional = true } [dev-dependencies] -# TODO: actix is temporary added as dev dep for actix-macro reason. -# Can be removed when it does not impact tests. -actix = "0.11.0-beta.1" -actix-web = { version = "4.0.0-beta.1", features = ["openssl"] } -actix-http = { version = "3.0.0-beta.1", features = ["openssl"] } -actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] } -actix-utils = "3.0.0-beta.1" -actix-server = "2.0.0-beta.2" -actix-tls = { version = "3.0.0-beta.2", features = ["openssl", "rustls"] } +actix-web = { version = "4.0.0-beta.11", features = ["openssl"] } +actix-http = { version = "3.0.0-beta.14", features = ["openssl"] } +actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] } +actix-utils = "3.0.0" +actix-server = "2.0.0-beta.9" +actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] } +actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] } brotli2 = "0.3.2" +env_logger = "0.9" flate2 = "1.0.13" futures-util = { version = "0.3.7", default-features = false } -env_logger = "0.7" +static_assertions = "1.1" rcgen = "0.8" -webpki = "0.21" +rustls-pemfile = "0.2" + +[[example]] +name = "client" +required-features = ["rustls"] diff --git a/awc/README.md b/awc/README.md index 972a80140..b0faedc68 100644 --- a/awc/README.md +++ b/awc/README.md @@ -3,30 +3,30 @@ > Async HTTP and WebSocket client library. [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=2.0.3)](https://docs.rs/awc/2.0.3) -![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/2.0.3/status.svg)](https://deps.rs/crate/awc/2.0.3) -[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.12)](https://docs.rs/awc/3.0.0-beta.12) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) +[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.12/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.12) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) ## Documentation & Resources - [API Documentation](https://docs.rs/awc) -- [Example Project](https://github.com/actix/examples/tree/HEAD/awc_https) -- [Chat on Gitter](https://gitter.im/actix/actix-web) -- Minimum Supported Rust Version (MSRV): 1.46.0 +- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) +- Minimum Supported Rust Version (MSRV): 1.52 ## Example + ```rust use actix_rt::System; use awc::Client; fn main() { - System::new("test").block_on(async { + System::new().block_on(async { let client = Client::default(); let res = client .get("http://www.rust-lang.org") // <- Create request builder - .header("User-Agent", "Actix-web") + .insert_header(("User-Agent", "Actix-web")) .send() // <- Send http request .await; diff --git a/examples/client.rs b/awc/examples/client.rs similarity index 53% rename from examples/client.rs rename to awc/examples/client.rs index b9574590d..653cb226f 100644 --- a/examples/client.rs +++ b/awc/examples/client.rs @@ -1,18 +1,20 @@ -use actix_http::Error; +use std::error::Error as StdError; #[actix_web::main] -async fn main() -> Result<(), Error> { - std::env::set_var("RUST_LOG", "actix_http=trace"); +async fn main() -> Result<(), Box> { + std::env::set_var("RUST_LOG", "client=trace,awc=trace,actix_http=trace"); env_logger::init(); let client = awc::Client::new(); // Create request builder, configure request and send - let mut response = client + let request = client .get("https://www.rust-lang.org/") - .append_header(("User-Agent", "Actix-web")) - .send() - .await?; + .append_header(("User-Agent", "Actix-web")); + + println!("Request: {:?}", request); + + let mut response = request.send().await?; // server http response println!("Response: {:?}", response); diff --git a/awc/src/any_body.rs b/awc/src/any_body.rs new file mode 100644 index 000000000..cb9038ff3 --- /dev/null +++ b/awc/src/any_body.rs @@ -0,0 +1,266 @@ +use std::{ + borrow::Cow, + fmt, mem, + pin::Pin, + task::{Context, Poll}, +}; + +use bytes::{Bytes, BytesMut}; +use futures_core::Stream; +use pin_project_lite::pin_project; + +use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream}; + +use crate::BoxError; + +pin_project! { + /// Represents various types of HTTP message body. + #[derive(Clone)] + #[project = AnyBodyProj] + pub enum AnyBody { + /// Empty response. `Content-Length` header is not set. + None, + + /// Complete, in-memory response body. + Bytes { body: Bytes }, + + /// Generic / Other message body. + Body { #[pin] body: B }, + } +} + +impl AnyBody { + /// Constructs a "body" representing an empty response. + pub fn none() -> Self { + Self::None + } + + /// Constructs a new, 0-length body. + pub fn empty() -> Self { + Self::Bytes { body: Bytes::new() } + } + + /// Create boxed body from generic message body. + pub fn new_boxed(body: B) -> Self + where + B: MessageBody + 'static, + { + Self::Body { + body: BoxBody::new(body), + } + } + + /// Constructs new `AnyBody` instance from a slice of bytes by copying it. + /// + /// If your bytes container is owned, it may be cheaper to use a `From` impl. + pub fn copy_from_slice(s: &[u8]) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(s), + } + } + + #[doc(hidden)] + #[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")] + pub fn from_slice(s: &[u8]) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(s), + } + } +} + +impl AnyBody { + /// Create body from generic message body. + pub fn new(body: B) -> Self { + Self::Body { body } + } +} + +impl AnyBody +where + B: MessageBody + 'static, +{ + pub fn into_boxed(self) -> AnyBody { + match self { + Self::None => AnyBody::None, + Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes }, + Self::Body { body } => AnyBody::new_boxed(body), + } + } +} + +impl MessageBody for AnyBody +where + B: MessageBody, +{ + type Error = crate::BoxError; + + fn size(&self) -> BodySize { + match self { + AnyBody::None => BodySize::None, + AnyBody::Bytes { ref body } => BodySize::Sized(body.len() as u64), + AnyBody::Body { ref body } => body.size(), + } + } + + fn poll_next( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll>> { + match self.project() { + AnyBodyProj::None => Poll::Ready(None), + AnyBodyProj::Bytes { body } => { + let len = body.len(); + if len == 0 { + Poll::Ready(None) + } else { + Poll::Ready(Some(Ok(mem::take(body)))) + } + } + + AnyBodyProj::Body { body } => body.poll_next(cx).map_err(|err| err.into()), + } + } +} + +impl PartialEq for AnyBody { + fn eq(&self, other: &AnyBody) -> bool { + match self { + AnyBody::None => matches!(*other, AnyBody::None), + AnyBody::Bytes { body } => match other { + AnyBody::Bytes { body: b2 } => body == b2, + _ => false, + }, + AnyBody::Body { .. } => false, + } + } +} + +impl fmt::Debug for AnyBody { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match *self { + AnyBody::None => write!(f, "AnyBody::None"), + AnyBody::Bytes { ref body } => write!(f, "AnyBody::Bytes({:?})", body), + AnyBody::Body { ref body } => write!(f, "AnyBody::Message({:?})", body), + } + } +} + +impl From<&'static str> for AnyBody { + fn from(string: &'static str) -> Self { + Self::Bytes { + body: Bytes::from_static(string.as_ref()), + } + } +} + +impl From<&'static [u8]> for AnyBody { + fn from(bytes: &'static [u8]) -> Self { + Self::Bytes { + body: Bytes::from_static(bytes), + } + } +} + +impl From> for AnyBody { + fn from(vec: Vec) -> Self { + Self::Bytes { + body: Bytes::from(vec), + } + } +} + +impl From for AnyBody { + fn from(string: String) -> Self { + Self::Bytes { + body: Bytes::from(string), + } + } +} + +impl From<&'_ String> for AnyBody { + fn from(string: &String) -> Self { + Self::Bytes { + body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)), + } + } +} + +impl From> for AnyBody { + fn from(string: Cow<'_, str>) -> Self { + match string { + Cow::Owned(s) => Self::from(s), + Cow::Borrowed(s) => Self::Bytes { + body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)), + }, + } + } +} + +impl From for AnyBody { + fn from(bytes: Bytes) -> Self { + Self::Bytes { body: bytes } + } +} + +impl From for AnyBody { + fn from(bytes: BytesMut) -> Self { + Self::Bytes { + body: bytes.freeze(), + } + } +} + +impl From> for AnyBody +where + S: Stream> + 'static, + E: Into + 'static, +{ + fn from(stream: SizedStream) -> Self { + AnyBody::new_boxed(stream) + } +} + +impl From> for AnyBody +where + S: Stream> + 'static, + E: Into + 'static, +{ + fn from(stream: BodyStream) -> Self { + AnyBody::new_boxed(stream) + } +} + +#[cfg(test)] +mod tests { + use std::marker::PhantomPinned; + + use static_assertions::{assert_impl_all, assert_not_impl_all}; + + use super::*; + + struct PinType(PhantomPinned); + + impl MessageBody for PinType { + type Error = crate::BoxError; + + fn size(&self) -> BodySize { + unimplemented!() + } + + fn poll_next( + self: Pin<&mut Self>, + _cx: &mut Context<'_>, + ) -> Poll>> { + unimplemented!() + } + } + + assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody>: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Send, Sync, Unpin); + assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin); + assert_impl_all!(AnyBody: MessageBody); + + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); + assert_not_impl_all!(AnyBody: Send, Sync, Unpin); +} diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 351a493af..70a28c419 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -1,63 +1,89 @@ -use std::cell::RefCell; -use std::convert::TryFrom; -use std::fmt; -use std::rc::Rc; -use std::time::Duration; +use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; -use actix_http::client::{Connect as HttpConnect, ConnectError, Connection, Connector}; -use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName}; -use actix_service::Service; +use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri}; +use actix_rt::net::{ActixStream, TcpStream}; +use actix_service::{boxed, Service}; -use crate::connect::{Connect, ConnectorWrapper}; -use crate::{Client, ClientConfig}; +use crate::{ + client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection}, + connect::DefaultConnector, + error::SendRequestError, + middleware::{NestTransform, Redirect, Transform}, + Client, ClientConfig, ConnectRequest, ConnectResponse, +}; /// An HTTP Client builder /// /// This type can be used to construct an instance of `Client` through a /// builder-like pattern. -pub struct ClientBuilder { +pub struct ClientBuilder { default_headers: bool, - 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 { - fn default() -> Self { - Self::new() - } + connector: Connector, + middleware: M, + local_address: Option, + max_redirects: u8, } impl ClientBuilder { - pub fn new() -> Self { + #[allow(clippy::new_ret_no_self)] + pub fn new() -> ClientBuilder< + impl Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone, + (), + > { ClientBuilder { + middleware: (), default_headers: true, - allow_redirects: true, - max_redirects: 10, headers: HeaderMap::new(), timeout: Some(Duration::from_secs(5)), - connector: None, + local_address: None, + connector: Connector::new(), max_http_version: None, stream_window_size: None, conn_window_size: None, + max_redirects: 10, } } +} +impl ClientBuilder +where + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + + 'static, + Io: ActixStream + fmt::Debug + 'static, +{ /// Use custom connector service. - pub fn connector(mut self, connector: T) -> Self + pub fn connector(self, connector: Connector) -> ClientBuilder where - T: Service + 'static, - T::Response: Connection, - ::Future: 'static, - T::Future: 'static, + S1: Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone + + 'static, + Io1: ActixStream + fmt::Debug + 'static, { - self.connector = Some(RefCell::new(Box::new(ConnectorWrapper(connector)))); - self + ClientBuilder { + middleware: self.middleware, + default_headers: self.default_headers, + headers: self.headers, + timeout: self.timeout, + local_address: self.local_address, + connector, + max_http_version: self.max_http_version, + stream_window_size: self.stream_window_size, + conn_window_size: self.conn_window_size, + max_redirects: self.max_redirects, + } } /// Set request timeout @@ -75,18 +101,33 @@ impl ClientBuilder { self } + /// Set local IP Address the connector would use for establishing connection. + pub fn local_address(mut self, addr: IpAddr) -> Self { + self.local_address = Some(addr); + self + } + + /// Maximum supported HTTP major version. + /// + /// Supported versions are HTTP/1.1 and HTTP/2. + pub fn max_http_version(mut self, val: http::Version) -> Self { + self.max_http_version = Some(val); + self + } + /// Do not follow redirects. /// /// Redirects are allowed by default. pub fn disable_redirects(mut self) -> Self { - self.allow_redirects = false; + self.max_redirects = 0; 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); + /// Set max number of redirects. + /// + /// Max redirects is set to 10 by default. + pub fn max_redirects(mut self, num: u8) -> Self { + self.max_redirects = num; self } @@ -108,14 +149,6 @@ impl ClientBuilder { self } - /// Set max number of redirects. - /// - /// Max redirects is set to 10 by default. - pub fn max_redirects(mut self, num: usize) -> Self { - self.max_redirects = num; - self - } - /// Do not add default request headers. /// By default `Date` and `User-Agent` headers are set. pub fn no_default_headers(mut self) -> Self { @@ -145,9 +178,9 @@ impl ClientBuilder { } /// Set client wide HTTP basic authorization header - pub fn basic_auth(self, username: U, password: Option<&str>) -> Self + pub fn basic_auth(self, username: N, password: Option<&str>) -> Self where - U: fmt::Display, + N: fmt::Display, { let auth = match password { Some(password) => format!("{}:{}", username, password), @@ -167,31 +200,78 @@ impl ClientBuilder { self.header(header::AUTHORIZATION, format!("Bearer {}", token)) } - /// Finish build process and create `Client` instance. - pub fn finish(self) -> Client { - 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 { + /// Registers middleware, in the form of a middleware component (type), + /// that runs during inbound and/or outbound processing in the request + /// life-cycle (request -> response), modifying request/response as + /// necessary, across all requests managed by the Client. + pub fn wrap( + self, + mw: M1, + ) -> ClientBuilder> + where + M: Transform, + M1: Transform, + { + ClientBuilder { + middleware: NestTransform::new(self.middleware, mw), + default_headers: self.default_headers, + max_http_version: self.max_http_version, + stream_window_size: self.stream_window_size, + conn_window_size: self.conn_window_size, headers: self.headers, timeout: self.timeout, - connector, + connector: self.connector, + local_address: self.local_address, + max_redirects: self.max_redirects, + } + } + + /// Finish build process and create `Client` instance. + pub fn finish(self) -> Client + where + M: Transform>, ConnectRequest> + 'static, + M::Transform: + Service, + { + let redirect_time = self.max_redirects; + + if redirect_time > 0 { + self.wrap(Redirect::new().max_redirect_times(redirect_time)) + ._finish() + } else { + self._finish() + } + } + + fn _finish(self) -> Client + where + M: Transform>, ConnectRequest> + 'static, + M::Transform: + Service, + { + let mut connector = self.connector; + + if let Some(val) = self.max_http_version { + connector = connector.max_http_version(val); }; - Client(Rc::new(config)) + 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) + }; + if let Some(val) = self.local_address { + connector = connector.local_address(val); + } + + let connector = DefaultConnector::new(connector.finish()); + let connector = boxed::rc_service(self.middleware.new_transform(connector)); + + Client(ClientConfig { + headers: Rc::new(self.headers), + timeout: self.timeout, + connector, + }) } } diff --git a/actix-http/src/client/config.rs b/awc/src/client/config.rs similarity index 80% rename from actix-http/src/client/config.rs rename to awc/src/client/config.rs index 369732964..530c1e03b 100644 --- a/actix-http/src/client/config.rs +++ b/awc/src/client/config.rs @@ -1,4 +1,4 @@ -use std::time::Duration; +use std::{net::IpAddr, time::Duration}; const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB @@ -7,24 +7,28 @@ const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB #[derive(Clone)] pub(crate) struct ConnectorConfig { pub(crate) timeout: Duration, + pub(crate) handshake_timeout: Duration, pub(crate) conn_lifetime: Duration, pub(crate) conn_keep_alive: Duration, pub(crate) disconnect_timeout: Option, pub(crate) limit: usize, pub(crate) conn_window_size: u32, pub(crate) stream_window_size: u32, + pub(crate) local_address: Option, } impl Default for ConnectorConfig { fn default() -> Self { Self { - timeout: Duration::from_secs(1), + timeout: Duration::from_secs(5), + handshake_timeout: Duration::from_secs(5), conn_lifetime: Duration::from_secs(75), conn_keep_alive: Duration::from_secs(15), disconnect_timeout: Some(Duration::from_millis(3000)), limit: 100, conn_window_size: DEFAULT_H2_CONN_WINDOW, stream_window_size: DEFAULT_H2_STREAM_WINDOW, + local_address: None, } } } diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs new file mode 100644 index 000000000..0e1f0bfec --- /dev/null +++ b/awc/src/client/connection.rs @@ -0,0 +1,452 @@ +use std::{ + io, + ops::{Deref, DerefMut}, + pin::Pin, + task::{Context, Poll}, + time, +}; + +use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; +use actix_rt::task::JoinHandle; +use bytes::Bytes; +use futures_core::future::LocalBoxFuture; +use h2::client::SendRequest; + +use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead}; + +use crate::BoxError; + +use super::error::SendRequestError; +use super::pool::Acquired; +use super::{h1proto, h2proto}; + +/// Trait alias for types impl [tokio::io::AsyncRead] and [tokio::io::AsyncWrite]. +pub trait ConnectionIo: AsyncRead + AsyncWrite + Unpin + 'static {} + +impl ConnectionIo for T {} + +/// HTTP client connection +pub struct H1Connection { + io: Option, + created: time::Instant, + acquired: Acquired, +} + +impl H1Connection { + /// close or release the connection to pool based on flag input + pub(super) fn on_release(&mut self, keep_alive: bool) { + if keep_alive { + self.release(); + } else { + self.close(); + } + } + + /// Close connection + fn close(&mut self) { + let io = self.io.take().unwrap(); + self.acquired.close(ConnectionInnerType::H1(io)); + } + + /// Release this connection to the connection pool + fn release(&mut self) { + let io = self.io.take().unwrap(); + self.acquired + .release(ConnectionInnerType::H1(io), self.created); + } + + fn io_pin_mut(self: Pin<&mut Self>) -> Pin<&mut Io> { + Pin::new(self.get_mut().io.as_mut().unwrap()) + } +} + +impl AsyncRead for H1Connection { + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + self.io_pin_mut().poll_read(cx, buf) + } +} + +impl AsyncWrite for H1Connection { + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + self.io_pin_mut().poll_write(cx, buf) + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + self.io_pin_mut().poll_flush(cx) + } + + fn poll_shutdown( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + ) -> Poll> { + self.io_pin_mut().poll_shutdown(cx) + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + self.io_pin_mut().poll_write_vectored(cx, bufs) + } + + fn is_write_vectored(&self) -> bool { + self.io.as_ref().unwrap().is_write_vectored() + } +} + +/// HTTP2 client connection +pub struct H2Connection { + io: Option, + created: time::Instant, + acquired: Acquired, +} + +impl Deref for H2Connection { + type Target = SendRequest; + + fn deref(&self) -> &Self::Target { + &self.io.as_ref().unwrap().sender + } +} + +impl DerefMut for H2Connection { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.io.as_mut().unwrap().sender + } +} + +impl H2Connection { + /// close or release the connection to pool based on flag input + pub(super) fn on_release(&mut self, close: bool) { + if close { + self.close(); + } else { + self.release(); + } + } + + /// Close connection + fn close(&mut self) { + let io = self.io.take().unwrap(); + self.acquired.close(ConnectionInnerType::H2(io)); + } + + /// Release this connection to the connection pool + fn release(&mut self) { + let io = self.io.take().unwrap(); + self.acquired + .release(ConnectionInnerType::H2(io), self.created); + } +} + +/// `H2ConnectionInner` has two parts: `SendRequest` and `Connection`. +/// +/// `Connection` is spawned as an async task on runtime and `H2ConnectionInner` holds a handle +/// for this task. Therefore, it can wake up and quit the task when SendRequest is dropped. +pub(super) struct H2ConnectionInner { + handle: JoinHandle<()>, + sender: SendRequest, +} + +impl H2ConnectionInner { + pub(super) fn new( + sender: SendRequest, + connection: h2::client::Connection, + ) -> Self { + let handle = actix_rt::spawn(async move { + let _ = connection.await; + }); + + Self { handle, sender } + } +} + +/// Cancel spawned connection task on drop. +impl Drop for H2ConnectionInner { + fn drop(&mut self) { + // TODO: this can end up sending extraneous requests; see if there is a better way to handle + if self + .sender + .send_request(http::Request::new(()), true) + .is_err() + { + self.handle.abort(); + } + } +} + +/// Unified connection type cover HTTP/1 Plain/TLS and HTTP/2 protocols. +#[allow(dead_code)] +pub enum Connection> +where + A: ConnectionIo, + B: ConnectionIo, +{ + Tcp(ConnectionType
), + Tls(ConnectionType), +} + +/// Unified connection type cover Http1/2 protocols +pub enum ConnectionType { + H1(H1Connection), + H2(H2Connection), +} + +/// Helper type for storing connection types in pool. +pub(super) enum ConnectionInnerType { + H1(Io), + H2(H2ConnectionInner), +} + +impl ConnectionType { + pub(super) fn from_pool( + inner: ConnectionInnerType, + created: time::Instant, + acquired: Acquired, + ) -> Self { + match inner { + ConnectionInnerType::H1(io) => Self::from_h1(io, created, acquired), + ConnectionInnerType::H2(io) => Self::from_h2(io, created, acquired), + } + } + + pub(super) fn from_h1(io: Io, created: time::Instant, acquired: Acquired) -> Self { + Self::H1(H1Connection { + io: Some(io), + created, + acquired, + }) + } + + pub(super) fn from_h2( + io: H2ConnectionInner, + created: time::Instant, + acquired: Acquired, + ) -> Self { + Self::H2(H2Connection { + io: Some(io), + created, + acquired, + }) + } +} + +impl Connection +where + A: ConnectionIo, + B: ConnectionIo, +{ + /// Send a request through connection. + pub fn send_request( + self, + head: H, + body: RB, + ) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> + where + H: Into + 'static, + RB: MessageBody + 'static, + RB::Error: Into, + { + Box::pin(async move { + match self { + Connection::Tcp(ConnectionType::H1(conn)) => { + h1proto::send_request(conn, head.into(), body).await + } + Connection::Tls(ConnectionType::H1(conn)) => { + h1proto::send_request(conn, head.into(), body).await + } + Connection::Tls(ConnectionType::H2(conn)) => { + h2proto::send_request(conn, head.into(), body).await + } + _ => unreachable!("Plain Tcp connection can be used only in Http1 protocol"), + } + }) + } + + /// Send request, returns Response and Framed tunnel. + pub fn open_tunnel + 'static>( + self, + head: H, + ) -> LocalBoxFuture< + 'static, + Result<(ResponseHead, Framed, ClientCodec>), SendRequestError>, + > { + Box::pin(async move { + match self { + Connection::Tcp(ConnectionType::H1(ref _conn)) => { + let (head, framed) = h1proto::open_tunnel(self, head.into()).await?; + Ok((head, framed)) + } + Connection::Tls(ConnectionType::H1(ref _conn)) => { + let (head, framed) = h1proto::open_tunnel(self, head.into()).await?; + Ok((head, framed)) + } + Connection::Tls(ConnectionType::H2(mut conn)) => { + conn.release(); + Err(SendRequestError::TunnelNotSupported) + } + Connection::Tcp(ConnectionType::H2(_)) => { + unreachable!("Plain Tcp connection can be used only in Http1 protocol") + } + } + }) + } +} + +impl AsyncRead for Connection +where + A: ConnectionIo, + B: ConnectionIo, +{ + fn poll_read( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &mut ReadBuf<'_>, + ) -> Poll> { + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_read(cx, buf), + _ => unreachable!("H2Connection can not impl AsyncRead trait"), + } + } +} + +const H2_UNREACHABLE_WRITE: &str = "H2Connection can not impl AsyncWrite trait"; + +impl AsyncWrite for Connection +where + A: ConnectionIo, + B: ConnectionIo, +{ + fn poll_write( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + buf: &[u8], + ) -> Poll> { + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_write(cx, buf), + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } + + fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } + + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), + Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_shutdown(cx), + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } + + fn poll_write_vectored( + self: Pin<&mut Self>, + cx: &mut Context<'_>, + bufs: &[io::IoSlice<'_>], + ) -> Poll> { + match self.get_mut() { + Connection::Tcp(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_write_vectored(cx, bufs) + } + Connection::Tls(ConnectionType::H1(conn)) => { + Pin::new(conn).poll_write_vectored(cx, bufs) + } + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } + + fn is_write_vectored(&self) -> bool { + match *self { + Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), + Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(), + _ => unreachable!(H2_UNREACHABLE_WRITE), + } + } +} + +#[cfg(test)] +mod test { + use std::{ + future::Future, + net, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, + }; + + use actix_rt::{ + net::TcpStream, + time::{interval, Interval}, + }; + + use super::*; + + #[actix_rt::test] + async fn test_h2_connection_drop() { + let addr = "127.0.0.1:0".parse::().unwrap(); + let listener = net::TcpListener::bind(addr).unwrap(); + let local = listener.local_addr().unwrap(); + + std::thread::spawn(move || while listener.accept().is_ok() {}); + + let tcp = TcpStream::connect(local).await.unwrap(); + let (sender, connection) = h2::client::handshake(tcp).await.unwrap(); + let conn = H2ConnectionInner::new(sender.clone(), connection); + + assert!(sender.clone().ready().await.is_ok()); + assert!(h2::client::SendRequest::clone(&conn.sender) + .ready() + .await + .is_ok()); + + drop(conn); + + struct DropCheck { + sender: h2::client::SendRequest, + interval: Interval, + start_from: Instant, + } + + impl Future for DropCheck { + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + match futures_core::ready!(this.sender.poll_ready(cx)) { + Ok(()) => { + if this.start_from.elapsed() > Duration::from_secs(10) { + panic!("connection should be gone and can not be ready"); + } else { + let _ = this.interval.poll_tick(cx); + Poll::Pending + } + } + Err(_) => Poll::Ready(()), + } + } + } + + DropCheck { + sender, + interval: interval(Duration::from_millis(100)), + start_from: Instant::now(), + } + .await; + } +} diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs new file mode 100644 index 000000000..40b3c4d32 --- /dev/null +++ b/awc/src/client/connector.rs @@ -0,0 +1,868 @@ +use std::{ + fmt, + future::Future, + net::IpAddr, + pin::Pin, + rc::Rc, + task::{Context, Poll}, + time::Duration, +}; + +use actix_http::Protocol; +use actix_rt::{ + net::{ActixStream, TcpStream}, + time::{sleep, Sleep}, +}; +use actix_service::Service; +use actix_tls::connect::{ + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, + Connector as TcpConnector, Resolver, +}; +use futures_core::{future::LocalBoxFuture, ready}; +use http::Uri; +use pin_project_lite::pin_project; + +use super::config::ConnectorConfig; +use super::connection::{Connection, ConnectionIo}; +use super::error::ConnectError; +use super::pool::ConnectionPool; +use super::Connect; + +enum OurTlsConnector { + #[allow(dead_code)] // only dead when no TLS feature is enabled + None, + + #[cfg(feature = "openssl")] + Openssl(actix_tls::connect::openssl::reexports::SslConnector), + + #[cfg(feature = "rustls")] + Rustls(std::sync::Arc), +} + +/// Manages HTTP client network connectivity. +/// +/// The `Connector` type uses a builder-like combinator pattern for service +/// construction that finishes by calling the `.finish()` method. +/// +/// ```ignore +/// use std::time::Duration; +/// use actix_http::client::Connector; +/// +/// let connector = Connector::new() +/// .timeout(Duration::from_secs(5)) +/// .finish(); +/// ``` +pub struct Connector { + connector: T, + config: ConnectorConfig, + + #[allow(dead_code)] // only dead when no TLS feature is enabled + ssl: OurTlsConnector, +} + +impl Connector<()> { + #[allow(clippy::new_ret_no_self, clippy::let_unit_value)] + pub fn new() -> Connector< + impl Service< + ConnectInfo, + Response = TcpConnection, + Error = actix_tls::connect::ConnectError, + > + Clone, + > { + Connector { + connector: TcpConnector::new(resolver::resolver()).service(), + config: ConnectorConfig::default(), + ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), + } + } + + /// Provides an empty TLS connector when no TLS feature is enabled. + #[cfg(not(any(feature = "openssl", feature = "rustls")))] + fn build_ssl(_: Vec>) -> OurTlsConnector { + OurTlsConnector::None + } + + /// Build TLS connector with rustls, based on supplied ALPN protocols + /// + /// Note that if both `openssl` and `rustls` features are enabled, rustls will be used. + #[cfg(feature = "rustls")] + fn build_ssl(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::rustls::{reexports::ClientConfig, webpki_roots_cert_store}; + + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(webpki_roots_cert_store()) + .with_no_client_auth(); + + config.alpn_protocols = protocols; + + OurTlsConnector::Rustls(std::sync::Arc::new(config)) + } + + /// Build TLS connector with openssl, based on supplied ALPN protocols + #[cfg(all(feature = "openssl", not(feature = "rustls")))] + fn build_ssl(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::openssl::reexports::{SslConnector, SslMethod}; + use bytes::{BufMut, BytesMut}; + + let mut alpn = BytesMut::with_capacity(20); + for proto in &protocols { + alpn.put_u8(proto.len() as u8); + alpn.put(proto.as_slice()); + } + + let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); + if let Err(err) = ssl.set_alpn_protos(&alpn) { + log::error!("Can not set ALPN protocol: {:?}", err); + } + + OurTlsConnector::Openssl(ssl.build()) + } +} + +impl Connector { + /// Use custom connector. + pub fn connector(self, connector: S1) -> Connector + where + Io1: ActixStream + fmt::Debug + 'static, + S1: Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone, + { + Connector { + connector, + config: self.config, + ssl: self.ssl, + } + } +} + +impl Connector +where + // Note: + // Input Io type is bound to ActixStream trait but internally in client module they + // are bound to ConnectionIo trait alias. And latter is the trait exposed to public + // in the form of Box type. + // + // This remap is to hide ActixStream's trait methods. They are not meant to be called + // from user code. + IO: ActixStream + fmt::Debug + 'static, + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + + 'static, +{ + /// Tcp connection timeout, i.e. max time to connect to remote host including dns name + /// resolution. Set to 5 second by default. + pub fn timeout(mut self, timeout: Duration) -> Self { + self.config.timeout = timeout; + self + } + + /// Tls handshake timeout, i.e. max time to do tls handshake with remote host after tcp + /// connection established. Set to 5 second by default. + pub fn handshake_timeout(mut self, timeout: Duration) -> Self { + self.config.handshake_timeout = timeout; + self + } + + #[cfg(feature = "openssl")] + /// Use custom `SslConnector` instance. + pub fn ssl( + mut self, + connector: actix_tls::connect::openssl::reexports::SslConnector, + ) -> Self { + self.ssl = OurTlsConnector::Openssl(connector); + self + } + + #[cfg(feature = "rustls")] + /// Use custom `ClientConfig` instance. + pub fn rustls( + mut self, + connector: std::sync::Arc, + ) -> Self { + self.ssl = OurTlsConnector::Rustls(connector); + self + } + + /// Maximum supported HTTP major version. + /// + /// Supported versions are HTTP/1.1 and 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 only supports 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.config.limit = limit; + self + } + + /// Set keep-alive period for opened connection. + /// + /// Keep-alive period is the period between connection usage. If + /// the delay between repeated usages of the same connection + /// 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.config.conn_keep_alive = dur; + self + } + + /// Set max lifetime period for connection. + /// + /// Connection lifetime is max lifetime of any opened connection + /// 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.config.conn_lifetime = dur; + self + } + + /// Set server connection disconnect timeout in milliseconds. + /// + /// Defines a timeout for disconnect connection. If a disconnect procedure does not complete + /// within this time, the socket get dropped. This timeout affects only secure connections. + /// + /// To disable timeout set value to 0. + /// + /// By default disconnect timeout is set to 3000 milliseconds. + pub fn disconnect_timeout(mut self, dur: Duration) -> Self { + self.config.disconnect_timeout = Some(dur); + self + } + + /// Set local IP Address the connector would use for establishing connection. + pub fn local_address(mut self, addr: IpAddr) -> Self { + self.config.local_address = Some(addr); + self + } + + /// Finish configuration process and create connector service. + /// The Connector builder always concludes by calling `finish()` last in + /// its combinator chain. + pub fn finish(self) -> ConnectorService { + let local_address = self.config.local_address; + let timeout = self.config.timeout; + + let tcp_service_inner = + TcpConnectorInnerService::new(self.connector, timeout, local_address); + + #[allow(clippy::redundant_clone)] + let tcp_service = TcpConnectorService { + service: tcp_service_inner.clone(), + }; + + let tls_service = match self.ssl { + OurTlsConnector::None => { + #[cfg(not(feature = "dangerous-h2c"))] + { + None + } + + #[cfg(feature = "dangerous-h2c")] + { + use std::io; + + use actix_tls::connect::Connection; + use actix_utils::future::{ready, Ready}; + + impl IntoConnectionIo for TcpConnection> { + fn into_connection_io(self) -> (Box, Protocol) { + let io = self.into_parts().0; + (io, Protocol::Http2) + } + } + + /// With the `dangerous-h2c` feature enabled, this connector uses a no-op TLS + /// connection service that passes through plain TCP as a TLS connection. + /// + /// The protocol version of this fake TLS connection is set to be HTTP/2. + #[derive(Clone)] + struct NoOpTlsConnectorService; + + impl Service> for NoOpTlsConnectorService + where + IO: ActixStream + 'static, + { + type Response = Connection>; + type Error = io::Error; + type Future = Ready>; + + actix_service::always_ready!(); + + fn call(&self, connection: Connection) -> Self::Future { + let (io, connection) = connection.replace_io(()); + let (_, connection) = connection.replace_io(Box::new(io) as _); + + ready(Ok(connection)) + } + } + + let handshake_timeout = self.config.handshake_timeout; + + let tls_service = TlsConnectorService { + tcp_service: tcp_service_inner, + tls_service: NoOpTlsConnectorService, + timeout: handshake_timeout, + }; + + Some(actix_service::boxed::rc_service(tls_service)) + } + } + + #[cfg(feature = "openssl")] + OurTlsConnector::Openssl(tls) => { + const H2: &[u8] = b"h2"; + + use actix_tls::connect::openssl::{reexports::AsyncSslStream, TlsConnector}; + + impl IntoConnectionIo for TcpConnection> { + fn into_connection_io(self) -> (Box, Protocol) { + let sock = self.into_parts().0; + let h2 = sock + .ssl() + .selected_alpn_protocol() + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + if h2 { + (Box::new(sock), Protocol::Http2) + } else { + (Box::new(sock), Protocol::Http1) + } + } + } + + let handshake_timeout = self.config.handshake_timeout; + + let tls_service = TlsConnectorService { + tcp_service: tcp_service_inner, + tls_service: TlsConnector::service(tls), + timeout: handshake_timeout, + }; + + Some(actix_service::boxed::rc_service(tls_service)) + } + + #[cfg(feature = "rustls")] + OurTlsConnector::Rustls(tls) => { + const H2: &[u8] = b"h2"; + + use actix_tls::connect::rustls::{reexports::AsyncTlsStream, TlsConnector}; + + impl IntoConnectionIo for TcpConnection> { + fn into_connection_io(self) -> (Box, Protocol) { + let sock = self.into_parts().0; + let h2 = sock + .get_ref() + .1 + .alpn_protocol() + .map_or(false, |protos| protos.windows(2).any(|w| w == H2)); + if h2 { + (Box::new(sock), Protocol::Http2) + } else { + (Box::new(sock), Protocol::Http1) + } + } + } + + let handshake_timeout = self.config.handshake_timeout; + + let tls_service = TlsConnectorService { + tcp_service: tcp_service_inner, + tls_service: TlsConnector::service(tls), + timeout: handshake_timeout, + }; + + Some(actix_service::boxed::rc_service(tls_service)) + } + }; + + let tcp_config = self.config.no_disconnect_timeout(); + + let tcp_pool = ConnectionPool::new(tcp_service, tcp_config); + + let tls_config = self.config; + let tls_pool = + tls_service.map(move |tls_service| ConnectionPool::new(tls_service, tls_config)); + + ConnectorServicePriv { tcp_pool, tls_pool } + } +} + +/// tcp service for map `TcpConnection` type to `(Io, Protocol)` +#[derive(Clone)] +pub struct TcpConnectorService { + service: S, +} + +impl Service for TcpConnectorService +where + S: Service, Error = ConnectError> + + Clone + + 'static, +{ + type Response = (Io, Protocol); + type Error = ConnectError; + type Future = TcpConnectorFuture; + + actix_service::forward_ready!(service); + + fn call(&self, req: Connect) -> Self::Future { + TcpConnectorFuture { + fut: self.service.call(req), + } + } +} + +pin_project! { + #[project = TcpConnectorFutureProj] + pub struct TcpConnectorFuture { + #[pin] + fut: Fut, + } +} + +impl Future for TcpConnectorFuture +where + Fut: Future, ConnectError>>, +{ + type Output = Result<(Io, Protocol), ConnectError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + self.project() + .fut + .poll(cx) + .map_ok(|res| (res.into_parts().0, Protocol::Http1)) + } +} + +/// service for establish tcp connection and do client tls handshake. +/// operation is canceled when timeout limit reached. +struct TlsConnectorService { + /// TCP connection is canceled on `TcpConnectorInnerService`'s timeout setting. + tcp_service: Tcp, + + /// TLS connection is canceled on `TlsConnectorService`'s timeout setting. + tls_service: Tls, + + timeout: Duration, +} + +impl Service for TlsConnectorService +where + Tcp: Service, Error = ConnectError> + + Clone + + 'static, + Tls: Service, Error = std::io::Error> + Clone + 'static, + Tls::Response: IntoConnectionIo, + IO: ConnectionIo, +{ + type Response = (Box, Protocol); + type Error = ConnectError; + type Future = TlsConnectorFuture; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + ready!(self.tcp_service.poll_ready(cx))?; + ready!(self.tls_service.poll_ready(cx))?; + Poll::Ready(Ok(())) + } + + fn call(&self, req: Connect) -> Self::Future { + let fut = self.tcp_service.call(req); + let tls_service = self.tls_service.clone(); + let timeout = self.timeout; + + TlsConnectorFuture::TcpConnect { + fut, + tls_service: Some(tls_service), + timeout, + } + } +} + +pin_project! { + #[project = TlsConnectorProj] + #[allow(clippy::large_enum_variant)] + enum TlsConnectorFuture { + TcpConnect { + #[pin] + fut: Fut1, + tls_service: Option, + timeout: Duration, + }, + TlsConnect { + #[pin] + fut: Fut2, + #[pin] + timeout: Sleep, + }, + } + +} +/// helper trait for generic over different TlsStream types between tls crates. +trait IntoConnectionIo { + fn into_connection_io(self) -> (Box, Protocol); +} + +impl Future for TlsConnectorFuture +where + S: Service, Response = Res, Error = std::io::Error, Future = Fut2>, + S::Response: IntoConnectionIo, + Fut1: Future, ConnectError>>, + Fut2: Future>, + Io: ConnectionIo, +{ + type Output = Result<(Box, Protocol), ConnectError>; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.as_mut().project() { + TlsConnectorProj::TcpConnect { + fut, + tls_service, + timeout, + } => { + let res = ready!(fut.poll(cx))?; + let fut = tls_service + .take() + .expect("TlsConnectorFuture polled after complete") + .call(res); + let timeout = sleep(*timeout); + self.set(TlsConnectorFuture::TlsConnect { fut, timeout }); + self.poll(cx) + } + TlsConnectorProj::TlsConnect { fut, timeout } => match fut.poll(cx)? { + Poll::Ready(res) => Poll::Ready(Ok(res.into_connection_io())), + Poll::Pending => timeout.poll(cx).map(|_| Err(ConnectError::Timeout)), + }, + } + } +} + +/// service for establish tcp connection. +/// operation is canceled when timeout limit reached. +#[derive(Clone)] +pub struct TcpConnectorInnerService { + service: S, + timeout: Duration, + local_address: Option, +} + +impl TcpConnectorInnerService { + fn new(service: S, timeout: Duration, local_address: Option) -> Self { + Self { + service, + timeout, + local_address, + } + } +} + +impl Service for TcpConnectorInnerService +where + S: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + + 'static, +{ + type Response = S::Response; + type Error = ConnectError; + type Future = TcpConnectorInnerFuture; + + actix_service::forward_ready!(service); + + fn call(&self, req: Connect) -> Self::Future { + let mut req = ConnectInfo::new(req.uri).set_addr(req.addr); + + if let Some(local_addr) = self.local_address { + req = req.set_local_addr(local_addr); + } + + TcpConnectorInnerFuture { + fut: self.service.call(req), + timeout: sleep(self.timeout), + } + } +} + +pin_project! { + #[project = TcpConnectorInnerFutureProj] + pub struct TcpConnectorInnerFuture { + #[pin] + fut: Fut, + #[pin] + timeout: Sleep, + } +} + +impl Future for TcpConnectorInnerFuture +where + Fut: Future, TcpConnectError>>, +{ + type Output = Result, ConnectError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.project(); + match this.fut.poll(cx) { + Poll::Ready(res) => Poll::Ready(res.map_err(ConnectError::from)), + Poll::Pending => this.timeout.poll(cx).map(|_| Err(ConnectError::Timeout)), + } + } +} + +/// Connector service for pooled Plain/Tls Tcp connections. +pub type ConnectorService = ConnectorServicePriv< + TcpConnectorService>, + Rc< + dyn Service< + Connect, + Response = (Box, Protocol), + Error = ConnectError, + Future = LocalBoxFuture< + 'static, + Result<(Box, Protocol), ConnectError>, + >, + >, + >, + IO, + Box, +>; + +pub struct ConnectorServicePriv +where + S1: Service, + S2: Service, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + tcp_pool: ConnectionPool, + tls_pool: Option>, +} + +impl Service for ConnectorServicePriv +where + S1: Service + Clone + 'static, + S2: Service + Clone + 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + type Response = Connection; + type Error = ConnectError; + type Future = ConnectorServiceFuture; + + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { + ready!(self.tcp_pool.poll_ready(cx))?; + if let Some(ref tls_pool) = self.tls_pool { + ready!(tls_pool.poll_ready(cx))?; + } + Poll::Ready(Ok(())) + } + + fn call(&self, req: Connect) -> Self::Future { + match req.uri.scheme_str() { + Some("https") | Some("wss") => match self.tls_pool { + None => ConnectorServiceFuture::SslIsNotSupported, + Some(ref pool) => ConnectorServiceFuture::Tls { + fut: pool.call(req), + }, + }, + _ => ConnectorServiceFuture::Tcp { + fut: self.tcp_pool.call(req), + }, + } + } +} + +pin_project! { + #[project = ConnectorServiceFutureProj] + pub enum ConnectorServiceFuture + where + S1: Service, + S1: Clone, + S1: 'static, + S2: Service, + S2: Clone, + S2: 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, + { + Tcp { + #[pin] + fut: as Service>::Future + }, + Tls { + #[pin] + fut: as Service>::Future + }, + SslIsNotSupported + } +} + +impl Future for ConnectorServiceFuture +where + S1: Service + Clone + 'static, + S2: Service + Clone + 'static, + Io1: ConnectionIo, + Io2: ConnectionIo, +{ + type Output = Result, ConnectError>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.project() { + ConnectorServiceFutureProj::Tcp { fut } => fut.poll(cx).map_ok(Connection::Tcp), + ConnectorServiceFutureProj::Tls { fut } => fut.poll(cx).map_ok(Connection::Tls), + ConnectorServiceFutureProj::SslIsNotSupported => { + Poll::Ready(Err(ConnectError::SslIsNotSupported)) + } + } + } +} + +#[cfg(not(feature = "trust-dns"))] +mod resolver { + use super::*; + + pub(super) fn resolver() -> Resolver { + Resolver::default() + } +} + +#[cfg(feature = "trust-dns")] +mod resolver { + use std::{cell::RefCell, net::SocketAddr}; + + use actix_tls::connect::Resolve; + use futures_core::future::LocalBoxFuture; + use trust_dns_resolver::{ + config::{ResolverConfig, ResolverOpts}, + system_conf::read_system_conf, + TokioAsyncResolver, + }; + + use super::*; + + pub(super) fn resolver() -> Resolver { + // new type for impl Resolve trait for TokioAsyncResolver. + struct TrustDnsResolver(TokioAsyncResolver); + + impl Resolve for TrustDnsResolver { + fn lookup<'a>( + &'a self, + host: &'a str, + port: u16, + ) -> LocalBoxFuture<'a, Result, Box>> + { + Box::pin(async move { + let res = self + .0 + .lookup_ip(host) + .await? + .iter() + .map(|ip| SocketAddr::new(ip, port)) + .collect(); + Ok(res) + }) + } + } + + // resolver struct is cached in thread local so new clients can reuse the existing instance + thread_local! { + static TRUST_DNS_RESOLVER: RefCell> = RefCell::new(None); + } + + // get from thread local or construct a new trust-dns resolver. + TRUST_DNS_RESOLVER.with(|local| { + let resolver = local.borrow().as_ref().map(Clone::clone); + + match resolver { + Some(resolver) => resolver, + + None => { + let (cfg, opts) = match read_system_conf() { + Ok((cfg, opts)) => (cfg, opts), + Err(e) => { + log::error!("TRust-DNS can not load system config: {}", e); + (ResolverConfig::default(), ResolverOpts::default()) + } + }; + + let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap(); + + // box trust dns resolver and put it in thread local. + let resolver = Resolver::custom(TrustDnsResolver(resolver)); + *local.borrow_mut() = Some(resolver.clone()); + + resolver + } + } + }) + } +} + +#[cfg(feature = "dangerous-h2c")] +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use actix_http::{HttpService, Request, Response, Version}; + use actix_http_test::test_server; + use actix_service::ServiceFactoryExt as _; + + use super::*; + use crate::Client; + + #[actix_rt::test] + async fn h2c_connector() { + let mut srv = test_server(|| { + HttpService::build() + .h2(|_req: Request| async { Ok::<_, Infallible>(Response::ok()) }) + .tcp() + .map_err(|_| ()) + }) + .await; + + let connector = Connector { + connector: TcpConnector::new(resolver::resolver()).service(), + config: ConnectorConfig::default(), + ssl: OurTlsConnector::None, + }; + + let client = Client::builder().connector(connector).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); + + srv.stop().await; + } +} diff --git a/actix-http/src/client/error.rs b/awc/src/client/error.rs similarity index 72% rename from actix-http/src/client/error.rs rename to awc/src/client/error.rs index a5f1b2e8e..d351b5d5e 100644 --- a/actix-http/src/client/error.rs +++ b/awc/src/client/error.rs @@ -1,16 +1,17 @@ -use std::io; +use std::{fmt, io}; -use actix_tls::connect::resolver::ResolveError; use derive_more::{Display, From}; -#[cfg(feature = "openssl")] -use actix_tls::accept::openssl::SslError; +use actix_http::{error::ParseError, http::Error as HttpError}; -use crate::error::{Error, ParseError, ResponseError}; -use crate::http::{Error as HttpError, StatusCode}; +#[cfg(feature = "openssl")] +use actix_tls::accept::openssl::reexports::Error as OpensslError; + +use crate::BoxError; /// A set of errors that can occur while connecting to an HTTP host #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum ConnectError { /// SSL feature is not enabled #[display(fmt = "SSL is not supported")] @@ -19,14 +20,14 @@ pub enum ConnectError { /// SSL error #[cfg(feature = "openssl")] #[display(fmt = "{}", _0)] - SslError(SslError), + SslError(OpensslError), /// Failed to resolve the hostname #[display(fmt = "Failed resolving hostname: {}", _0)] - Resolver(ResolveError), + Resolver(Box), /// No dns records - #[display(fmt = "No dns records found for the input")] + #[display(fmt = "No DNS records found for the input")] NoRecords, /// Http2 error @@ -65,14 +66,18 @@ impl From for ConnectError { } #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum InvalidUrl { - #[display(fmt = "Missing url scheme")] + #[display(fmt = "Missing URL scheme")] MissingScheme, - #[display(fmt = "Unknown url scheme")] + + #[display(fmt = "Unknown URL scheme")] UnknownScheme, + #[display(fmt = "Missing host name")] MissingHost, - #[display(fmt = "Url parse error: {}", _0)] + + #[display(fmt = "URL parse error: {}", _0)] HttpError(http::Error), } @@ -80,66 +85,73 @@ impl std::error::Error for InvalidUrl {} /// A set of errors that can occur during request sending and response reading #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum SendRequestError { /// Invalid URL #[display(fmt = "Invalid URL: {}", _0)] Url(InvalidUrl), + /// Failed to connect to host #[display(fmt = "Failed to connect to host: {}", _0)] Connect(ConnectError), + /// Error sending request Send(io::Error), + /// Error parsing response Response(ParseError), + /// Http error #[display(fmt = "{}", _0)] Http(HttpError), + /// Http2 error #[display(fmt = "{}", _0)] H2(h2::Error), + /// Response took too long #[display(fmt = "Timeout while waiting for response")] Timeout, - /// Tunnels are not supported for http2 connection + + /// Tunnels are not supported for HTTP/2 connection #[display(fmt = "Tunnels are not supported for http2 connection")] TunnelNotSupported, + /// Error sending request body - Body(Error), + Body(BoxError), + + /// Other errors that can occur after submitting a request. + #[display(fmt = "{:?}: {}", _1, _0)] + Custom(BoxError, Box), } impl std::error::Error for SendRequestError {} -/// Convert `SendRequestError` to a server `Response` -impl ResponseError for SendRequestError { - fn status_code(&self) -> StatusCode { - match *self { - SendRequestError::Connect(ConnectError::Timeout) => { - StatusCode::GATEWAY_TIMEOUT - } - SendRequestError::Connect(_) => StatusCode::BAD_REQUEST, - _ => StatusCode::INTERNAL_SERVER_ERROR, - } - } -} - /// A set of errors that can occur during freezing a request #[derive(Debug, Display, From)] +#[non_exhaustive] pub enum FreezeRequestError { /// Invalid URL #[display(fmt = "Invalid URL: {}", _0)] Url(InvalidUrl), - /// Http error + + /// HTTP error #[display(fmt = "{}", _0)] Http(HttpError), + + /// Other errors that can occur after submitting a request. + #[display(fmt = "{:?}: {}", _1, _0)] + Custom(BoxError, Box), } impl std::error::Error for FreezeRequestError {} impl From for SendRequestError { - fn from(e: FreezeRequestError) -> Self { - match e { - FreezeRequestError::Url(e) => e.into(), - FreezeRequestError::Http(e) => e.into(), + fn from(err: FreezeRequestError) -> Self { + match err { + FreezeRequestError::Url(err) => err.into(), + FreezeRequestError::Http(err) => err.into(), + FreezeRequestError::Custom(err, msg) => SendRequestError::Custom(err, msg), } } } diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs new file mode 100644 index 000000000..b26a97eeb --- /dev/null +++ b/awc/src/client/h1proto.rs @@ -0,0 +1,230 @@ +use std::{ + io::Write, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_codec::Framed; +use actix_http::{ + body::{BodySize, MessageBody}, + error::PayloadError, + h1, + http::{ + header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, + StatusCode, + }, + Payload, RequestHeadType, ResponseHead, +}; +use actix_utils::future::poll_fn; +use bytes::buf::BufMut; +use bytes::{Bytes, BytesMut}; +use futures_core::{ready, Stream}; +use futures_util::SinkExt as _; +use pin_project_lite::pin_project; + +use crate::BoxError; + +use super::connection::{ConnectionIo, H1Connection}; +use super::error::{ConnectError, SendRequestError}; + +pub(crate) async fn send_request( + io: H1Connection, + mut head: RequestHeadType, + body: B, +) -> Result<(ResponseHead, Payload), SendRequestError> +where + Io: ConnectionIo, + B: MessageBody, + B::Error: Into, +{ + // set request host header + if !head.as_ref().headers.contains_key(HOST) + && !head.extra_headers().iter().any(|h| h.contains_key(HOST)) + { + if let Some(host) = head.as_ref().uri.host() { + let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); + + match head.as_ref().uri.port_u16() { + None | Some(80) | Some(443) => write!(wrt, "{}", host)?, + Some(port) => write!(wrt, "{}:{}", host, port)?, + }; + + match wrt.get_mut().split().freeze().try_into_value() { + Ok(value) => match head { + RequestHeadType::Owned(ref mut head) => { + head.headers.insert(HOST, value); + } + RequestHeadType::Rc(_, ref mut extra_headers) => { + let headers = extra_headers.get_or_insert(HeaderMap::new()); + headers.insert(HOST, value); + } + }, + Err(e) => log::error!("Can not set HOST header {}", e), + } + } + } + + // create Framed and prepare sending request + let mut framed = Framed::new(io, h1::ClientCodec::default()); + + // Check EXPECT header and enable expect handle flag accordingly. + // See https://datatracker.ietf.org/doc/html/rfc7231#section-5.1.1 + let is_expect = if head.as_ref().headers.contains_key(EXPECT) { + match body.size() { + BodySize::None | BodySize::Sized(0) => { + let keep_alive = framed.codec_ref().keepalive(); + framed.io_mut().on_release(keep_alive); + + // TODO: use a new variant or a new type better describing error violate + // `Requirements for clients` session of above RFC + return Err(SendRequestError::Connect(ConnectError::Disconnected)); + } + _ => true, + } + } else { + false + }; + + framed.send((head, body.size()).into()).await?; + + let mut pin_framed = Pin::new(&mut framed); + + // special handle for EXPECT request. + let (do_send, mut res_head) = if is_expect { + let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) + .await + .ok_or(ConnectError::Disconnected)??; + + // return response head in case status code is not continue + // and current head would be used as final response head. + (head.status == StatusCode::CONTINUE, Some(head)) + } else { + (true, None) + }; + + if do_send { + // send request body + match body.size() { + BodySize::None | BodySize::Sized(0) => {} + _ => send_body(body, pin_framed.as_mut()).await?, + }; + + // read response and init read body + let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx)) + .await + .ok_or(ConnectError::Disconnected)??; + + res_head = Some(head); + } + + let head = res_head.unwrap(); + + match pin_framed.codec_ref().message_type() { + h1::MessageType::None => { + let keep_alive = pin_framed.codec_ref().keepalive(); + pin_framed.io_mut().on_release(keep_alive); + + Ok((head, Payload::None)) + } + _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))), + } +} + +pub(crate) async fn open_tunnel( + io: Io, + head: RequestHeadType, +) -> Result<(ResponseHead, Framed), SendRequestError> +where + Io: ConnectionIo, +{ + // create Framed and send request. + let mut framed = Framed::new(io, h1::ClientCodec::default()); + framed.send((head, BodySize::None).into()).await?; + + // read response head. + let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx)) + .await + .ok_or(ConnectError::Disconnected)??; + + Ok((head, framed)) +} + +/// send request body to the peer +pub(crate) async fn send_body( + body: B, + mut framed: Pin<&mut Framed>, +) -> Result<(), SendRequestError> +where + Io: ConnectionIo, + B: MessageBody, + B::Error: Into, +{ + actix_rt::pin!(body); + + let mut eof = false; + while !eof { + while !eof && !framed.as_ref().is_write_buf_full() { + match poll_fn(|cx| body.as_mut().poll_next(cx)).await { + Some(Ok(chunk)) => { + framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?; + } + Some(Err(err)) => return Err(SendRequestError::Body(err.into())), + None => { + eof = true; + framed.as_mut().write(h1::Message::Chunk(None))?; + } + } + } + + if !framed.as_ref().is_write_buf_empty() { + poll_fn(|cx| match framed.as_mut().flush(cx) { + Poll::Ready(Ok(_)) => Poll::Ready(Ok(())), + Poll::Ready(Err(err)) => Poll::Ready(Err(err)), + Poll::Pending => { + if !framed.as_ref().is_write_buf_full() { + Poll::Ready(Ok(())) + } else { + Poll::Pending + } + } + }) + .await?; + } + } + + framed.get_mut().flush().await?; + Ok(()) +} + +pin_project! { + pub(crate) struct PlStream { + #[pin] + framed: Framed, h1::ClientPayloadCodec>, + } +} + +impl PlStream { + fn new(framed: Framed, h1::ClientCodec>) -> Self { + let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); + + PlStream { framed } + } +} + +impl Stream for PlStream { + type Item = Result; + + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let mut this = self.project(); + + match ready!(this.framed.as_mut().next_item(cx)?) { + Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))), + Some(None) => { + let keep_alive = this.framed.codec_ref().keepalive(); + this.framed.io_mut().on_release(keep_alive); + Poll::Ready(None) + } + None => Poll::Ready(None), + } + } +} diff --git a/actix-http/src/client/h2proto.rs b/awc/src/client/h2proto.rs similarity index 63% rename from actix-http/src/client/h2proto.rs rename to awc/src/client/h2proto.rs index 76915f214..9ced5776b 100644 --- a/actix-http/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -1,46 +1,44 @@ -use std::convert::TryFrom; use std::future::Future; -use std::time; -use actix_codec::{AsyncRead, AsyncWrite}; +use actix_utils::future::poll_fn; use bytes::Bytes; -use futures_util::future::poll_fn; -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}; +use log::trace; -use crate::body::{BodySize, MessageBody}; -use crate::header::HeaderMap; -use crate::message::{RequestHeadType, ResponseHead}; -use crate::payload::Payload; +use actix_http::{ + body::{BodySize, MessageBody}, + header::HeaderMap, + Payload, RequestHeadType, ResponseHead, +}; -use super::config::ConnectorConfig; -use super::connection::{ConnectionType, IoConnection}; -use super::error::SendRequestError; -use super::pool::Acquired; +use crate::BoxError; -pub(crate) async fn send_request( - mut io: SendRequest, +use super::{ + config::ConnectorConfig, + connection::{ConnectionIo, H2Connection}, + error::SendRequestError, +}; + +pub(crate) async fn send_request( + mut io: H2Connection, head: RequestHeadType, body: B, - created: time::Instant, - pool: Option>, ) -> Result<(ResponseHead, Payload), SendRequestError> where - T: AsyncRead + AsyncWrite + Unpin + 'static, + Io: ConnectionIo, B: MessageBody, + B::Error: Into, { trace!("Sending client request: {:?} {:?}", head, body.size()); + let head_req = head.as_ref().method == Method::HEAD; let length = body.size(); - let eof = matches!( - length, - BodySize::None | BodySize::Empty | BodySize::Sized(0) - ); + let eof = matches!(length, BodySize::None | BodySize::Sized(0)); let mut req = Request::new(()); *req.uri_mut() = head.as_ref().uri.clone(); @@ -53,17 +51,24 @@ where // Content length let _ = match length { BodySize::None => None, + + BodySize::Sized(0) => req + .headers_mut() + .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), + + BodySize::Sized(len) => { + let mut buf = itoa::Buffer::new(); + + req.headers_mut().insert( + CONTENT_LENGTH, + HeaderValue::from_str(buf.format(len)).unwrap(), + ) + } + BodySize::Stream => { skip_len = false; None } - BodySize::Empty => req - .headers_mut() - .insert(CONTENT_LENGTH, HeaderValue::from_static("0")), - BodySize::Sized(len) => req.headers_mut().insert( - CONTENT_LENGTH, - HeaderValue::try_from(format!("{}", len)).unwrap(), - ), }; // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. @@ -86,7 +91,10 @@ where // copy headers for (key, value) in headers { match *key { - CONNECTION | TRANSFER_ENCODING => continue, // http2 specific + // TODO: consider skipping other headers according to: + // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2 + // omit HTTP/1.x only headers + CONNECTION | TRANSFER_ENCODING => continue, CONTENT_LENGTH if skip_len => continue, // DATE => has_date = true, _ => {} @@ -96,13 +104,13 @@ where let res = poll_fn(|cx| io.poll_ready(cx)).await; if let Err(e) = res { - release(io, pool, created, e.is_io()); + io.on_release(e.is_io()); return Err(SendRequestError::from(e)); } let resp = match io.send_request(req, eof) { Ok((fut, send)) => { - release(io, pool, created, false); + io.on_release(false); if !eof { send_body(body, send).await?; @@ -110,7 +118,7 @@ where fut.await.map_err(SendRequestError::from)? } Err(e) => { - release(io, pool, created, e.is_io()); + io.on_release(e.is_io()); return Err(e.into()); } }; @@ -124,12 +132,15 @@ where Ok((head, payload)) } -async fn send_body( - body: B, - mut send: SendStream, -) -> Result<(), SendRequestError> { +async fn send_body(body: B, mut send: SendStream) -> Result<(), SendRequestError> +where + B: MessageBody, + B::Error: Into, +{ let mut buf = None; - pin_mut!(body); + + actix_rt::pin!(body); + loop { if buf.is_none() { match poll_fn(|cx| body.as_mut().poll_next(cx)).await { @@ -137,10 +148,10 @@ async fn send_body( send.reserve_capacity(b.len()); buf = Some(b); } - Some(Err(e)) => return Err(e.into()), + Some(Err(err)) => return Err(SendRequestError::Body(err.into())), None => { - if let Err(e) = send.send_data(Bytes::new(), true) { - return Err(e.into()); + if let Err(err) = send.send_data(Bytes::new(), true) { + return Err(err.into()); } send.reserve_capacity(0); return Ok(()); @@ -157,43 +168,23 @@ async fn send_body( if let Err(e) = send.send_data(bytes, false) { return Err(e.into()); - } else { - if !b.is_empty() { - send.reserve_capacity(b.len()); - } else { - buf = None; - } - continue; } + if !b.is_empty() { + send.reserve_capacity(b.len()); + } else { + buf = None; + } + continue; } Some(Err(e)) => return Err(e.into()), } } } -/// release SendRequest object -fn release( - io: SendRequest, - pool: Option>, - created: time::Instant, - close: bool, -) { - if let Some(mut pool) = pool { - if close { - pool.close(IoConnection::new(ConnectionType::H2(io), created, None)); - } else { - pool.release(IoConnection::new(ConnectionType::H2(io), created, None)); - } - } -} - -pub(crate) fn handshake( +pub(crate) fn handshake( io: Io, config: &ConnectorConfig, -) -> impl Future, Connection), h2::Error>> -where - Io: AsyncRead + AsyncWrite + Unpin + 'static, -{ +) -> impl Future, Connection), h2::Error>> { let mut builder = Builder::new(); builder .initial_window_size(config.stream_window_size) diff --git a/actix-http/src/client/mod.rs b/awc/src/client/mod.rs similarity index 55% rename from actix-http/src/client/mod.rs rename to awc/src/client/mod.rs index dd1e9b25a..0d5c899bc 100644 --- a/actix-http/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -1,4 +1,5 @@ -//! Http client api +//! HTTP client. + use http::Uri; mod config; @@ -9,10 +10,13 @@ mod h1proto; mod h2proto; mod pool; -pub use self::connection::Connection; -pub use self::connector::Connector; +pub use actix_tls::connect::{ + ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection, +}; + +pub use self::connection::{Connection, ConnectionIo}; +pub use self::connector::{Connector, ConnectorService}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; -pub use self::pool::Protocol; #[derive(Clone)] pub struct Connect { diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs new file mode 100644 index 000000000..9d130412b --- /dev/null +++ b/awc/src/client/pool.rs @@ -0,0 +1,658 @@ +//! Client connection pooling keyed on the authority part of the connection URI. + +use std::{ + cell::RefCell, + collections::VecDeque, + future::Future, + io, + ops::Deref, + pin::Pin, + rc::Rc, + sync::Arc, + task::{Context, Poll}, + time::{Duration, Instant}, +}; + +use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; +use actix_http::Protocol; +use actix_rt::time::{sleep, Sleep}; +use actix_service::Service; +use ahash::AHashMap; +use futures_core::future::LocalBoxFuture; +use futures_util::FutureExt; +use http::uri::Authority; +use pin_project_lite::pin_project; +use tokio::sync::{OwnedSemaphorePermit, Semaphore}; + +use super::config::ConnectorConfig; +use super::connection::{ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner}; +use super::error::ConnectError; +use super::h2proto::handshake; +use super::Connect; + +#[derive(Hash, Eq, PartialEq, Clone, Debug)] +pub struct Key { + authority: Authority, +} + +impl From for Key { + fn from(authority: Authority) -> Key { + Key { authority } + } +} + +#[doc(hidden)] +/// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key. +pub struct ConnectionPool +where + Io: AsyncWrite + Unpin + 'static, +{ + connector: S, + inner: ConnectionPoolInner, +} + +/// wrapper type for check the ref count of Rc. +pub struct ConnectionPoolInner(Rc>) +where + Io: AsyncWrite + Unpin + 'static; + +impl ConnectionPoolInner +where + Io: AsyncWrite + Unpin + 'static, +{ + fn new(config: ConnectorConfig) -> Self { + let permits = Arc::new(Semaphore::new(config.limit)); + let available = RefCell::new(AHashMap::default()); + + Self(Rc::new(ConnectionPoolInnerPriv { + config, + available, + permits, + })) + } + + /// spawn a async for graceful shutdown h1 Io type with a timeout. + fn close(&self, conn: ConnectionInnerType) { + if let Some(timeout) = self.config.disconnect_timeout { + if let ConnectionInnerType::H1(io) = conn { + actix_rt::spawn(CloseConnection::new(io, timeout)); + } + } + } +} + +impl Clone for ConnectionPoolInner +where + Io: AsyncWrite + Unpin + 'static, +{ + fn clone(&self) -> Self { + Self(Rc::clone(&self.0)) + } +} + +impl Deref for ConnectionPoolInner +where + Io: AsyncWrite + Unpin + 'static, +{ + type Target = ConnectionPoolInnerPriv; + + fn deref(&self) -> &Self::Target { + &*self.0 + } +} + +impl Drop for ConnectionPoolInner +where + Io: AsyncWrite + Unpin + 'static, +{ + fn drop(&mut self) { + // When strong count is one it means the pool is dropped + // remove and drop all Io types. + if Rc::strong_count(&self.0) == 1 { + self.permits.close(); + std::mem::take(&mut *self.available.borrow_mut()) + .into_iter() + .for_each(|(_, conns)| { + conns.into_iter().for_each(|pooled| self.close(pooled.conn)) + }); + } + } +} + +pub struct ConnectionPoolInnerPriv +where + Io: AsyncWrite + Unpin + 'static, +{ + config: ConnectorConfig, + available: RefCell>>>, + permits: Arc, +} + +impl ConnectionPool +where + Io: AsyncWrite + Unpin + 'static, +{ + /// Construct a new connection pool. + /// + /// [`super::config::ConnectorConfig`]'s `limit` is used as the max permits allowed for + /// in-flight connections. + /// + /// The pool can only have equal to `limit` amount of requests spawning/using Io type + /// concurrently. + /// + /// Any requests beyond limit would be wait in fifo order and get notified in async manner + /// by [`tokio::sync::Semaphore`] + pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self { + let inner = ConnectionPoolInner::new(config); + + Self { connector, inner } + } +} + +impl Service for ConnectionPool +where + S: Service + Clone + 'static, + Io: ConnectionIo, +{ + type Response = ConnectionType; + type Error = ConnectError; + type Future = LocalBoxFuture<'static, Result>; + + actix_service::forward_ready!(connector); + + fn call(&self, req: Connect) -> Self::Future { + let connector = self.connector.clone(); + let inner = self.inner.clone(); + + Box::pin(async move { + let key = if let Some(authority) = req.uri.authority() { + authority.clone().into() + } else { + return Err(ConnectError::Unresolved); + }; + + // acquire an owned permit and carry it with connection + let permit = inner.permits.clone().acquire_owned().await.map_err(|_| { + ConnectError::Io(io::Error::new( + io::ErrorKind::Other, + "failed to acquire semaphore on client connection pool", + )) + })?; + + let conn = { + let mut conn = None; + + // check if there is idle connection for given key. + let mut map = inner.available.borrow_mut(); + + if let Some(conns) = map.get_mut(&key) { + let now = Instant::now(); + + while let Some(mut c) = conns.pop_front() { + let config = &inner.config; + let idle_dur = now - c.used; + let age = now - c.created; + let conn_ineligible = + idle_dur > config.conn_keep_alive || age > config.conn_lifetime; + + if conn_ineligible { + // drop connections that are too old + inner.close(c.conn); + } else { + // check if the connection is still usable + if let ConnectionInnerType::H1(ref mut io) = c.conn { + let check = ConnectionCheckFuture { io }; + match check.now_or_never().expect("ConnectionCheckFuture must never yield with Poll::Pending.") { + ConnectionState::Tainted => { + inner.close(c.conn); + continue; + } + ConnectionState::Skip => continue, + ConnectionState::Live => conn = Some(c), + } + } else { + conn = Some(c); + } + + break; + } + } + }; + + conn + }; + + // construct acquired. It's used to put Io type back to pool/ close the Io type. + // permit is carried with the whole lifecycle of Acquired. + let acquired = Acquired { key, inner, permit }; + + // match the connection and spawn new one if did not get anything. + match conn { + Some(conn) => Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired)), + None => { + let (io, proto) = connector.call(req).await?; + + // TODO: remove when http3 is added in support. + assert!(proto != Protocol::Http3); + + if proto == Protocol::Http1 { + Ok(ConnectionType::from_h1(io, Instant::now(), acquired)) + } else { + let config = &acquired.inner.config; + let (sender, connection) = handshake(io, config).await?; + let inner = H2ConnectionInner::new(sender, connection); + Ok(ConnectionType::from_h2(inner, Instant::now(), acquired)) + } + } + } + }) + } +} + +/// Type for check the connection and determine if it's usable. +struct ConnectionCheckFuture<'a, Io> { + io: &'a mut Io, +} + +enum ConnectionState { + /// IO is pending and a new request would wake it. + Live, + + /// IO unexpectedly has unread data and should be dropped. + Tainted, + + /// IO should be skipped but not dropped. + Skip, +} + +impl Future for ConnectionCheckFuture<'_, Io> +where + Io: AsyncRead + Unpin, +{ + type Output = ConnectionState; + + // this future is only used to get access to Context. + // It should never return Poll::Pending. + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + let mut buf = [0; 2]; + let mut read_buf = ReadBuf::new(&mut buf); + + let state = match Pin::new(&mut this.io).poll_read(cx, &mut read_buf) { + Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => ConnectionState::Tainted, + + Poll::Pending => ConnectionState::Live, + _ => ConnectionState::Skip, + }; + + Poll::Ready(state) + } +} + +struct PooledConnection { + conn: ConnectionInnerType, + used: Instant, + created: Instant, +} + +pin_project! { + #[project = CloseConnectionProj] + struct CloseConnection { + io: Io, + #[pin] + timeout: Sleep, + } +} + +impl CloseConnection +where + Io: AsyncWrite + Unpin, +{ + fn new(io: Io, timeout: Duration) -> Self { + CloseConnection { + io, + timeout: sleep(timeout), + } + } +} + +impl Future for CloseConnection +where + Io: AsyncWrite + Unpin, +{ + type Output = (); + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> { + let this = self.project(); + + match this.timeout.poll(cx) { + Poll::Ready(_) => Poll::Ready(()), + Poll::Pending => Pin::new(this.io).poll_shutdown(cx).map(|_| ()), + } + } +} + +pub struct Acquired +where + Io: AsyncWrite + Unpin + 'static, +{ + /// authority key for identify connection. + key: Key, + /// handle to connection pool. + inner: ConnectionPoolInner, + /// permit for limit concurrent in-flight connection for a Client object. + permit: OwnedSemaphorePermit, +} + +impl Acquired { + /// Close the IO. + pub(super) fn close(&self, conn: ConnectionInnerType) { + self.inner.close(conn); + } + + /// Release IO back into pool. + pub(super) fn release(&self, conn: ConnectionInnerType, created: Instant) { + let Acquired { key, inner, .. } = self; + + inner + .available + .borrow_mut() + .entry(key.clone()) + .or_insert_with(VecDeque::new) + .push_back(PooledConnection { + conn, + created, + used: Instant::now(), + }); + + let _ = &self.permit; + } +} + +#[cfg(test)] +mod test { + use std::{cell::Cell, io}; + + use http::Uri; + + use super::*; + use crate::client::connection::ConnectionType; + + /// A stream type that always returns pending on async read. + /// + /// Mocks an idle TCP stream that is ready to be used for client connections. + struct TestStream(Rc>); + + impl Drop for TestStream { + fn drop(&mut self) { + self.0.set(self.0.get() - 1); + } + } + + impl AsyncRead for TestStream { + fn poll_read( + self: Pin<&mut Self>, + _: &mut Context<'_>, + _: &mut ReadBuf<'_>, + ) -> Poll> { + Poll::Pending + } + } + + impl AsyncWrite for TestStream { + fn poll_write( + self: Pin<&mut Self>, + _: &mut Context<'_>, + _: &[u8], + ) -> Poll> { + unimplemented!() + } + + fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + unimplemented!() + } + + fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll> { + Poll::Ready(Ok(())) + } + } + + #[derive(Clone)] + struct TestPoolConnector { + generated: Rc>, + } + + impl Service for TestPoolConnector { + type Response = (TestStream, Protocol); + type Error = ConnectError; + type Future = LocalBoxFuture<'static, Result>; + + actix_service::always_ready!(); + + fn call(&self, _: Connect) -> Self::Future { + self.generated.set(self.generated.get() + 1); + let generated = self.generated.clone(); + Box::pin(async { Ok((TestStream(generated), Protocol::Http1)) }) + } + } + + fn release(conn: ConnectionType) + where + T: AsyncRead + AsyncWrite + Unpin + 'static, + { + match conn { + ConnectionType::H1(mut conn) => conn.on_release(true), + ConnectionType::H2(mut conn) => conn.on_release(false), + } + } + + #[actix_rt::test] + async fn test_pool_limit() { + let connector = TestPoolConnector { + generated: Rc::new(Cell::new(0)), + }; + + let config = ConnectorConfig { + limit: 1, + ..Default::default() + }; + + let pool = super::ConnectionPool::new(connector, config); + + let req = Connect { + uri: Uri::from_static("http://localhost"), + addr: None, + }; + + let conn = pool.call(req.clone()).await.unwrap(); + + let waiting = Rc::new(Cell::new(true)); + + let waiting_clone = waiting.clone(); + actix_rt::spawn(async move { + actix_rt::time::sleep(Duration::from_millis(100)).await; + waiting_clone.set(false); + drop(conn); + }); + + assert!(waiting.get()); + + let now = Instant::now(); + let conn = pool.call(req).await.unwrap(); + + release(conn); + assert!(!waiting.get()); + assert!(now.elapsed() >= Duration::from_millis(100)); + } + + #[actix_rt::test] + async fn test_pool_keep_alive() { + let generated = Rc::new(Cell::new(0)); + let generated_clone = generated.clone(); + + let connector = TestPoolConnector { generated }; + + let config = ConnectorConfig { + conn_keep_alive: Duration::from_secs(1), + ..Default::default() + }; + + let pool = super::ConnectionPool::new(connector, config); + + let req = Connect { + uri: Uri::from_static("http://localhost"), + addr: None, + }; + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + actix_rt::time::sleep(Duration::from_millis(1500)).await; + actix_rt::task::yield_now().await; + + let conn = pool.call(req).await.unwrap(); + // Note: spawned recycle connection is not ran yet. + // This is tokio current thread runtime specific behavior. + assert_eq!(2, generated_clone.get()); + + // yield task so the old connection is properly dropped. + actix_rt::task::yield_now().await; + assert_eq!(1, generated_clone.get()); + + release(conn); + } + + #[actix_rt::test] + async fn test_pool_lifetime() { + let generated = Rc::new(Cell::new(0)); + let generated_clone = generated.clone(); + + let connector = TestPoolConnector { generated }; + + let config = ConnectorConfig { + conn_lifetime: Duration::from_secs(1), + ..Default::default() + }; + + let pool = super::ConnectionPool::new(connector, config); + + let req = Connect { + uri: Uri::from_static("http://localhost"), + addr: None, + }; + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + actix_rt::time::sleep(Duration::from_millis(1500)).await; + actix_rt::task::yield_now().await; + + let conn = pool.call(req).await.unwrap(); + // Note: spawned recycle connection is not ran yet. + // This is tokio current thread runtime specific behavior. + assert_eq!(2, generated_clone.get()); + + // yield task so the old connection is properly dropped. + actix_rt::task::yield_now().await; + assert_eq!(1, generated_clone.get()); + + release(conn); + } + + #[actix_rt::test] + async fn test_pool_authority_key() { + let generated = Rc::new(Cell::new(0)); + let generated_clone = generated.clone(); + + let connector = TestPoolConnector { generated }; + + let config = ConnectorConfig::default(); + + let pool = super::ConnectionPool::new(connector, config); + + let req = Connect { + uri: Uri::from_static("https://crates.io"), + addr: None, + }; + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + let conn = pool.call(req).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + let req = Connect { + uri: Uri::from_static("https://google.com"), + addr: None, + }; + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(2, generated_clone.get()); + release(conn); + let conn = pool.call(req).await.unwrap(); + assert_eq!(2, generated_clone.get()); + release(conn); + } + + #[actix_rt::test] + async fn test_pool_drop() { + let generated = Rc::new(Cell::new(0)); + let generated_clone = generated.clone(); + + let connector = TestPoolConnector { generated }; + + let config = ConnectorConfig::default(); + + let pool = Rc::new(super::ConnectionPool::new(connector, config)); + + let req = Connect { + uri: Uri::from_static("https://crates.io"), + addr: None, + }; + + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(1, generated_clone.get()); + release(conn); + + let req = Connect { + uri: Uri::from_static("https://google.com"), + addr: None, + }; + let conn = pool.call(req.clone()).await.unwrap(); + assert_eq!(2, generated_clone.get()); + release(conn); + + let clone1 = pool.clone(); + let clone2 = clone1.clone(); + + drop(clone2); + for _ in 0..2 { + actix_rt::task::yield_now().await; + } + assert_eq!(2, generated_clone.get()); + + drop(clone1); + for _ in 0..2 { + actix_rt::task::yield_now().await; + } + assert_eq!(2, generated_clone.get()); + + drop(pool); + for _ in 0..2 { + actix_rt::task::yield_now().await; + } + assert_eq!(0, generated_clone.get()); + } +} diff --git a/awc/src/connect.rs b/awc/src/connect.rs index 8ee239f76..19870b069 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -1,252 +1,170 @@ -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::{fmt, io, net}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; -use actix_http::body::Body; -use actix_http::client::{ - Connect as ClientConnect, ConnectError, Connection, SendRequestError, +use std::{ + future::Future, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, }; -use actix_http::h1::ClientCodec; -use actix_http::http::HeaderMap; -use actix_http::{RequestHead, RequestHeadType, ResponseHead}; + +use actix_codec::Framed; +use actix_http::{h1::ClientCodec, Payload, RequestHead, RequestHeadType, ResponseHead}; use actix_service::Service; +use futures_core::{future::LocalBoxFuture, ready}; -use crate::response::ClientResponse; +use crate::{ + any_body::AnyBody, + client::{ + Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, + }, + response::ClientResponse, +}; -pub(crate) struct ConnectorWrapper(pub T); +pub type BoxConnectorService = Rc< + dyn Service< + ConnectRequest, + Response = ConnectResponse, + Error = SendRequestError, + Future = LocalBoxFuture<'static, Result>, + >, +>; -pub(crate) trait Connect { - fn send_request( - &mut self, - head: RequestHead, - body: Body, - addr: Option, - ) -> Pin>>>; +pub type BoxedSocket = Box; - fn send_request_extra( - &mut self, - head: Rc, - extra_headers: Option, - body: Body, - addr: Option, - ) -> Pin>>>; - - /// Send request, returns Response and Framed - fn open_tunnel( - &mut self, - head: RequestHead, - addr: Option, - ) -> Pin< - Box< - dyn Future< - Output = Result< - (ResponseHead, Framed), - SendRequestError, - >, - >, - >, - >; - - /// Send request and extra headers, returns Response and Framed - fn open_tunnel_extra( - &mut self, - head: Rc, - extra_headers: Option, - addr: Option, - ) -> Pin< - Box< - dyn Future< - Output = Result< - (ResponseHead, Framed), - SendRequestError, - >, - >, - >, - >; +pub enum ConnectRequest { + Client(RequestHeadType, AnyBody, Option), + Tunnel(RequestHead, Option), } -impl Connect for ConnectorWrapper +pub enum ConnectResponse { + Client(ClientResponse), + Tunnel(ResponseHead, Framed), +} + +impl ConnectResponse { + pub fn into_client_response(self) -> ClientResponse { + match self { + ConnectResponse::Client(res) => res, + _ => panic!( + "ClientResponse only reachable with ConnectResponse::ClientResponse variant" + ), + } + } + + pub fn into_tunnel_response(self) -> (ResponseHead, Framed) { + match self { + ConnectResponse::Tunnel(head, framed) => (head, framed), + _ => panic!( + "TunnelResponse only reachable with ConnectResponse::TunnelResponse variant" + ), + } + } +} + +pub struct DefaultConnector { + connector: S, +} + +impl DefaultConnector { + pub(crate) fn new(connector: S) -> Self { + Self { connector } + } +} + +impl Service for DefaultConnector where - T: Service, - T::Response: Connection, - ::Io: 'static, - ::Future: 'static, - ::TunnelFuture: 'static, - T::Future: 'static, + S: Service>, + Io: ConnectionIo, { - fn send_request( - &mut self, - head: RequestHead, - body: Body, - addr: Option, - ) -> Pin>>> { + type Response = ConnectResponse; + type Error = SendRequestError; + type Future = ConnectRequestFuture; + + actix_service::forward_ready!(connector); + + fn call(&self, req: ConnectRequest) -> Self::Future { // connect to the host - let fut = self.0.call(ClientConnect { - uri: head.uri.clone(), - addr, - }); + let fut = match req { + ConnectRequest::Client(ref head, .., addr) => self.connector.call(ClientConnect { + uri: head.as_ref().uri.clone(), + addr, + }), + ConnectRequest::Tunnel(ref head, addr) => self.connector.call(ClientConnect { + uri: head.uri.clone(), + addr, + }), + }; - Box::pin(async move { - let connection = fut.await?; - - // send request - connection - .send_request(RequestHeadType::from(head), body) - .await - .map(|(head, payload)| ClientResponse::new(head, payload)) - }) + ConnectRequestFuture::Connection { + fut, + req: Some(req), + } } +} - fn send_request_extra( - &mut self, - head: Rc, - extra_headers: Option, - body: Body, - addr: Option, - ) -> Pin>>> { - // connect to the host - let fut = self.0.call(ClientConnect { - uri: head.uri.clone(), - addr, - }); - - Box::pin(async move { - let connection = fut.await?; - - // send request - let (head, payload) = connection - .send_request(RequestHeadType::Rc(head, extra_headers), body) - .await?; - - Ok(ClientResponse::new(head, payload)) - }) - } - - fn open_tunnel( - &mut self, - head: RequestHead, - addr: Option, - ) -> Pin< - Box< - dyn Future< - Output = Result< - (ResponseHead, Framed), - SendRequestError, - >, +pin_project_lite::pin_project! { + #[project = ConnectRequestProj] + pub enum ConnectRequestFuture + where + Io: ConnectionIo + { + Connection { + #[pin] + fut: Fut, + req: Option + }, + Client { + fut: LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> + }, + Tunnel { + fut: LocalBoxFuture< + 'static, + Result<(ResponseHead, Framed, ClientCodec>), SendRequestError>, >, - >, - > { - // connect to the host - let fut = self.0.call(ClientConnect { - uri: head.uri.clone(), - addr, - }); - - Box::pin(async move { - let connection = fut.await?; - - // send request - let (head, framed) = - connection.open_tunnel(RequestHeadType::from(head)).await?; - - let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); - Ok((head, framed)) - }) - } - - fn open_tunnel_extra( - &mut self, - head: Rc, - extra_headers: Option, - addr: Option, - ) -> Pin< - Box< - dyn Future< - Output = Result< - (ResponseHead, Framed), - SendRequestError, - >, - >, - >, - > { - // connect to the host - let fut = self.0.call(ClientConnect { - uri: head.uri.clone(), - addr, - }); - - Box::pin(async move { - let connection = fut.await?; - - // send request - let (head, framed) = connection - .open_tunnel(RequestHeadType::Rc(head, extra_headers)) - .await?; - - let framed = framed.into_map_io(|io| BoxedSocket(Box::new(Socket(io)))); - Ok((head, framed)) - }) + } } } -trait AsyncSocket { - fn as_read(&self) -> &(dyn AsyncRead + Unpin); - fn as_read_mut(&mut self) -> &mut (dyn AsyncRead + Unpin); - fn as_write(&mut self) -> &mut (dyn AsyncWrite + Unpin); -} +impl Future for ConnectRequestFuture +where + Fut: Future, ConnectError>>, + Io: ConnectionIo, +{ + type Output = Result; -struct Socket(T); - -impl AsyncSocket for Socket { - fn as_read(&self) -> &(dyn AsyncRead + Unpin) { - &self.0 - } - fn as_read_mut(&mut self) -> &mut (dyn AsyncRead + Unpin) { - &mut self.0 - } - fn as_write(&mut self) -> &mut (dyn AsyncWrite + Unpin) { - &mut self.0 - } -} - -pub struct BoxedSocket(Box); - -impl fmt::Debug for BoxedSocket { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "BoxedSocket") - } -} - -impl AsyncRead for BoxedSocket { - fn poll_read( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &mut ReadBuf<'_>, - ) -> Poll> { - Pin::new(self.get_mut().0.as_read_mut()).poll_read(cx, buf) - } -} - -impl AsyncWrite for BoxedSocket { - fn poll_write( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - buf: &[u8], - ) -> Poll> { - Pin::new(self.get_mut().0.as_write()).poll_write(cx, buf) - } - - fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { - Pin::new(self.get_mut().0.as_write()).poll_flush(cx) - } - - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(self.get_mut().0.as_write()).poll_shutdown(cx) + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.as_mut().project() { + ConnectRequestProj::Connection { fut, req } => { + let connection = ready!(fut.poll(cx))?; + let req = req.take().unwrap(); + match req { + ConnectRequest::Client(head, body, ..) => { + // send request + let fut = ConnectRequestFuture::Client { + fut: connection.send_request(head, body), + }; + self.set(fut); + } + ConnectRequest::Tunnel(head, ..) => { + // send request + let fut = ConnectRequestFuture::Tunnel { + fut: connection.open_tunnel(RequestHeadType::from(head)), + }; + self.set(fut); + } + } + self.poll(cx) + } + ConnectRequestProj::Client { fut } => { + let (head, payload) = ready!(fut.as_mut().poll(cx))?; + Poll::Ready(Ok(ConnectResponse::Client(ClientResponse::new( + head, payload, + )))) + } + ConnectRequestProj::Tunnel { fut } => { + let (head, framed) = ready!(fut.as_mut().poll(cx))?; + let framed = framed.into_map_io(|io| Box::new(io) as _); + Poll::Ready(Ok(ConnectResponse::Tunnel(head, framed))) + } + } } } diff --git a/awc/src/error.rs b/awc/src/error.rs index d008166d9..726e1a506 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -1,17 +1,17 @@ -//! Http client errors -pub use actix_http::client::{ - ConnectError, FreezeRequestError, InvalidUrl, SendRequestError, -}; -pub use actix_http::error::PayloadError; -pub use actix_http::http::Error as HttpError; -pub use actix_http::ws::HandshakeError as WsHandshakeError; -pub use actix_http::ws::ProtocolError as WsProtocolError; +//! HTTP client errors -use actix_http::ResponseError; +pub use actix_http::{ + error::PayloadError, + http::{header::HeaderValue, Error as HttpError, StatusCode}, + ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError}, +}; + +use derive_more::{Display, From}; use serde_json::error::Error as JsonError; -use actix_http::http::{header::HeaderValue, StatusCode}; -use derive_more::{Display, From}; +pub use crate::client::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; + +// TODO: address display, error, and from impls /// Websocket client error #[derive(Debug, Display, From)] @@ -19,24 +19,31 @@ pub enum WsClientError { /// Invalid response status #[display(fmt = "Invalid response status")] InvalidResponseStatus(StatusCode), + /// Invalid upgrade header #[display(fmt = "Invalid upgrade header")] InvalidUpgradeHeader, + /// Invalid connection header #[display(fmt = "Invalid connection header")] InvalidConnectionHeader(HeaderValue), - /// Missing CONNECTION header - #[display(fmt = "Missing CONNECTION header")] + + /// Missing Connection header + #[display(fmt = "Missing Connection header")] MissingConnectionHeader, - /// Missing SEC-WEBSOCKET-ACCEPT header - #[display(fmt = "Missing SEC-WEBSOCKET-ACCEPT header")] + + /// Missing Sec-Websocket-Accept header + #[display(fmt = "Missing Sec-Websocket-Accept header")] MissingWebSocketAcceptHeader, + /// Invalid challenge response #[display(fmt = "Invalid challenge response")] - InvalidChallengeResponse(String, HeaderValue), + InvalidChallengeResponse([u8; 28], HeaderValue), + /// Protocol error #[display(fmt = "{}", _0)] Protocol(WsProtocolError), + /// Send request error #[display(fmt = "{}", _0)] SendRequest(SendRequestError), @@ -71,6 +78,3 @@ pub enum JsonPayloadError { } impl std::error::Error for JsonPayloadError {} - -/// Return `InternalServerError` for `JsonPayloadError` -impl ResponseError for JsonPayloadError {} diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 878f404c6..472397359 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,21 +1,21 @@ -use std::convert::TryFrom; -use std::net; -use std::rc::Rc; -use std::time::Duration; +use std::{convert::TryFrom, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; -use actix_http::body::Body; -use actix_http::http::header::IntoHeaderValue; -use actix_http::http::{Error as HttpError, HeaderMap, HeaderName, Method, Uri}; -use actix_http::{Error, RequestHead}; +use actix_http::{ + http::{header::IntoHeaderValue, Error as HttpError, HeaderMap, HeaderName, Method, Uri}, + RequestHead, +}; -use crate::sender::{RequestSender, SendClientRequest}; -use crate::ClientConfig; +use crate::{ + any_body::AnyBody, + sender::{RequestSender, SendClientRequest}, + BoxError, ClientConfig, +}; -/// `FrozenClientRequest` struct represents clonable client request. +/// `FrozenClientRequest` struct represents cloneable client request. /// It could be used to send same request multiple times. #[derive(Clone)] pub struct FrozenClientRequest { @@ -23,7 +23,7 @@ pub struct FrozenClientRequest { pub(crate) addr: Option, pub(crate) response_decompress: bool, pub(crate) timeout: Option, - pub(crate) config: Rc, + pub(crate) config: ClientConfig, } impl FrozenClientRequest { @@ -45,13 +45,13 @@ impl FrozenClientRequest { /// Send a body. pub fn send_body(&self, body: B) -> SendClientRequest where - B: Into, + B: Into, { RequestSender::Rc(self.head.clone(), None).send_body( self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, body, ) } @@ -62,7 +62,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, value, ) } @@ -73,7 +73,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, value, ) } @@ -82,13 +82,13 @@ impl FrozenClientRequest { pub fn send_stream(&self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into + 'static, { RequestSender::Rc(self.head.clone(), None).send_stream( self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, stream, ) } @@ -99,7 +99,7 @@ impl FrozenClientRequest { self.addr, self.response_decompress, self.timeout, - self.config.as_ref(), + &self.config, ) } @@ -145,7 +145,9 @@ impl FrozenSendBuilder { { match HeaderName::try_from(key) { Ok(key) => match value.try_into_value() { - Ok(value) => self.extra_headers.insert(key, value), + Ok(value) => { + self.extra_headers.insert(key, value); + } Err(e) => self.err = Some(e.into()), }, Err(e) => self.err = Some(e.into()), @@ -156,7 +158,7 @@ impl FrozenSendBuilder { /// Complete request construction and send a body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: Into, { if let Some(e) = self.err { return e.into(); @@ -166,7 +168,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, body, ) } @@ -181,7 +183,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, value, ) } @@ -196,7 +198,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, value, ) } @@ -205,7 +207,7 @@ impl FrozenSendBuilder { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into + 'static, { if let Some(e) = self.err { return e.into(); @@ -215,7 +217,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, stream, ) } @@ -230,7 +232,7 @@ impl FrozenSendBuilder { self.req.addr, self.req.response_decompress, self.req.timeout, - self.req.config.as_ref(), + &self.req.config, ) } } diff --git a/awc/src/lib.rs b/awc/src/lib.rs index f750a3df2..2f4183120 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -1,7 +1,6 @@ //! `awc` is a HTTP and WebSocket client library built on the Actix ecosystem. //! -//! ## Making a GET request -//! +//! # Making a GET request //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -16,10 +15,8 @@ //! # } //! ``` //! -//! ## Making POST requests -//! -//! ### Raw body contents -//! +//! # Making POST requests +//! ## Raw body contents //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -31,8 +28,7 @@ //! # } //! ``` //! -//! ### Forms -//! +//! ## Forms //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -46,8 +42,7 @@ //! # } //! ``` //! -//! ### JSON -//! +//! ## JSON //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), awc::error::SendRequestError> { @@ -64,8 +59,24 @@ //! # } //! ``` //! -//! ## WebSocket support +//! # Response Compression +//! All [official][iana-encodings] and common content encoding codecs are supported, optionally. //! +//! The `Accept-Encoding` header will automatically be populated with enabled codecs and added to +//! outgoing requests, allowing servers to select their `Content-Encoding` accordingly. +//! +//! Feature flags enable these codecs according to the table below. By default, all `compress-*` +//! features are enabled. +//! +//! | Feature | Codecs | +//! | ----------------- | ------------- | +//! | `compress-brotli` | brotli | +//! | `compress-gzip` | gzip, deflate | +//! | `compress-zstd` | zstd | +//! +//! [iana-encodings]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml#content-coding +//! +//! # WebSocket support //! ```no_run //! # #[actix_rt::main] //! # async fn main() -> Result<(), Box> { @@ -93,72 +104,78 @@ #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] -use std::cell::RefCell; -use std::convert::TryFrom; -use std::rc::Rc; -use std::time::Duration; - -pub use actix_http::{client::Connector, cookie, http}; - -use actix_http::http::{Error as HttpError, HeaderMap, Method, Uri}; -use actix_http::RequestHead; - +mod any_body; mod builder; +mod client; mod connect; pub mod error; mod frozen; +pub mod middleware; mod request; mod response; mod sender; pub mod test; pub mod ws; +pub use actix_http::http; +#[cfg(feature = "cookies")] +pub use cookie; + pub use self::builder::ClientBuilder; -pub use self::connect::BoxedSocket; +pub use self::client::Connector; +pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; pub use self::request::ClientRequest; pub use self::response::{ClientResponse, JsonBody, MessageBody}; pub use self::sender::SendClientRequest; -use self::connect::{Connect, ConnectorWrapper}; +use std::{convert::TryFrom, rc::Rc, time::Duration}; + +use actix_http::{ + http::{Error as HttpError, HeaderMap, Method, Uri}, + RequestHead, +}; +use actix_rt::net::TcpStream; +use actix_service::Service; + +use self::client::{ConnectInfo, TcpConnectError, TcpConnection}; + +pub(crate) type BoxError = Box; /// An asynchronous HTTP and WebSocket client. /// -/// ## Examples +/// You should take care to create, at most, one `Client` per thread. Otherwise, expect higher CPU +/// and memory usage. /// -/// ```rust +/// # Examples +/// ``` /// use awc::Client; /// /// #[actix_rt::main] /// async fn main() { /// let mut client = Client::default(); /// -/// let res = client.get("http://www.rust-lang.org") // <- Create request builder -/// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send http request -/// .await; // <- send request and wait for response +/// let res = client.get("http://www.rust-lang.org") +/// .insert_header(("User-Agent", "my-app/1.2")) +/// .send() +/// .await; /// /// println!("Response: {:?}", res); /// } /// ``` #[derive(Clone)] -pub struct Client(Rc); +pub struct Client(ClientConfig); +#[derive(Clone)] pub(crate) struct ClientConfig { - pub(crate) connector: RefCell>, - pub(crate) headers: HeaderMap, + pub(crate) connector: BoxConnectorService, + pub(crate) headers: Rc, pub(crate) timeout: Option, } impl Default for Client { fn default() -> Self { - Client(Rc::new(ClientConfig { - connector: RefCell::new(Box::new(ConnectorWrapper( - Connector::new().finish(), - ))), - headers: HeaderMap::new(), - timeout: Some(Duration::from_secs(5)), - })) + ClientBuilder::new().finish() } } @@ -170,7 +187,13 @@ impl Client { /// Create `Client` builder. /// This function is equivalent of `ClientBuilder::new()`. - pub fn builder() -> ClientBuilder { + pub fn builder() -> ClientBuilder< + impl Service< + ConnectInfo, + Response = TcpConnection, + Error = TcpConnectError, + > + Clone, + > { ClientBuilder::new() } @@ -280,4 +303,12 @@ impl Client { } req } + + /// Get default HeaderMap of Client. + /// + /// Returns Some(&mut HeaderMap) when Client object is unique + /// (No other clone of client exists at the same time). + pub fn headers(&mut self) -> Option<&mut HeaderMap> { + Rc::get_mut(&mut self.0.headers) + } } diff --git a/awc/src/middleware/mod.rs b/awc/src/middleware/mod.rs new file mode 100644 index 000000000..330e3b7fe --- /dev/null +++ b/awc/src/middleware/mod.rs @@ -0,0 +1,71 @@ +mod redirect; + +pub use self::redirect::Redirect; + +use std::marker::PhantomData; + +use actix_service::Service; + +/// Trait for transform a type to another one. +/// Both the input and output type should impl [actix_service::Service] trait. +pub trait Transform { + type Transform: Service; + + /// Creates and returns a new Transform component. + fn new_transform(self, service: S) -> Self::Transform; +} + +#[doc(hidden)] +/// Helper struct for constructing Nested types that would call `Transform::new_transform` +/// in a chain. +/// +/// The child field would be called first and the output `Service` type is +/// passed to parent as input type. +pub struct NestTransform +where + T1: Transform, + T2: Transform, +{ + child: T1, + parent: T2, + _service: PhantomData<(S, Req)>, +} + +impl NestTransform +where + T1: Transform, + T2: Transform, +{ + pub(crate) fn new(child: T1, parent: T2) -> Self { + NestTransform { + child, + parent, + _service: PhantomData, + } + } +} + +impl Transform for NestTransform +where + T1: Transform, + T2: Transform, +{ + type Transform = T2::Transform; + + fn new_transform(self, service: S) -> Self::Transform { + let service = self.child.new_transform(service); + self.parent.new_transform(service) + } +} + +/// Dummy impl for kick start `NestTransform` type in `ClientBuilder` type +impl Transform for () +where + S: Service, +{ + type Transform = S; + + fn new_transform(self, service: S) -> Self::Transform { + service + } +} diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs new file mode 100644 index 000000000..89cff22cd --- /dev/null +++ b/awc/src/middleware/redirect.rs @@ -0,0 +1,588 @@ +use std::{ + convert::TryFrom, + future::Future, + net::SocketAddr, + pin::Pin, + rc::Rc, + task::{Context, Poll}, +}; + +use actix_http::{ + http::{header, Method, StatusCode, Uri}, + RequestHead, RequestHeadType, +}; +use actix_service::Service; +use bytes::Bytes; +use futures_core::ready; + +use super::Transform; +use crate::{ + any_body::AnyBody, + client::{InvalidUrl, SendRequestError}, + connect::{ConnectRequest, ConnectResponse}, + ClientResponse, +}; + +pub struct Redirect { + max_redirect_times: u8, +} + +impl Default for Redirect { + fn default() -> Self { + Self::new() + } +} + +impl Redirect { + pub fn new() -> Self { + Self { + max_redirect_times: 10, + } + } + + pub fn max_redirect_times(mut self, times: u8) -> Self { + self.max_redirect_times = times; + self + } +} + +impl Transform for Redirect +where + S: Service + 'static, +{ + type Transform = RedirectService; + + fn new_transform(self, service: S) -> Self::Transform { + RedirectService { + max_redirect_times: self.max_redirect_times, + connector: Rc::new(service), + } + } +} + +pub struct RedirectService { + max_redirect_times: u8, + connector: Rc, +} + +impl Service for RedirectService +where + S: Service + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = RedirectServiceFuture; + + actix_service::forward_ready!(connector); + + fn call(&self, req: ConnectRequest) -> Self::Future { + match req { + ConnectRequest::Tunnel(head, addr) => { + let fut = self.connector.call(ConnectRequest::Tunnel(head, addr)); + RedirectServiceFuture::Tunnel { fut } + } + ConnectRequest::Client(head, body, addr) => { + let connector = self.connector.clone(); + let max_redirect_times = self.max_redirect_times; + + // backup the uri and method for reuse schema and authority. + let (uri, method, headers) = match head { + RequestHeadType::Owned(ref head) => { + (head.uri.clone(), head.method.clone(), head.headers.clone()) + } + RequestHeadType::Rc(ref head, ..) => { + (head.uri.clone(), head.method.clone(), head.headers.clone()) + } + }; + + let body_opt = match body { + AnyBody::Bytes { ref body } => Some(body.clone()), + _ => None, + }; + + let fut = connector.call(ConnectRequest::Client(head, body, addr)); + + RedirectServiceFuture::Client { + fut, + max_redirect_times, + uri: Some(uri), + method: Some(method), + headers: Some(headers), + body: body_opt, + addr, + connector: Some(connector), + } + } + } + } +} + +pin_project_lite::pin_project! { + #[project = RedirectServiceProj] + pub enum RedirectServiceFuture + where + S: Service, + S: 'static + { + Tunnel { #[pin] fut: S::Future }, + Client { + #[pin] + fut: S::Future, + max_redirect_times: u8, + uri: Option, + method: Option, + headers: Option, + body: Option, + addr: Option, + connector: Option>, + } + } +} + +impl Future for RedirectServiceFuture +where + S: Service + 'static, +{ + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + match self.as_mut().project() { + RedirectServiceProj::Tunnel { fut } => fut.poll(cx), + RedirectServiceProj::Client { + fut, + max_redirect_times, + uri, + method, + headers, + body, + addr, + connector, + } => match ready!(fut.poll(cx))? { + ConnectResponse::Client(res) => match res.head().status { + StatusCode::MOVED_PERMANENTLY + | StatusCode::FOUND + | StatusCode::SEE_OTHER + | StatusCode::TEMPORARY_REDIRECT + | StatusCode::PERMANENT_REDIRECT + if *max_redirect_times > 0 => + { + let is_redirect = res.head().status == StatusCode::TEMPORARY_REDIRECT + || res.head().status == StatusCode::PERMANENT_REDIRECT; + + let prev_uri = uri.take().unwrap(); + + // rebuild uri from the location header value. + let next_uri = build_next_uri(&res, &prev_uri)?; + + // take ownership of states that could be reused + let addr = addr.take(); + let connector = connector.take(); + + // reset method + let method = if is_redirect { + method.take().unwrap() + } else { + let method = method.take().unwrap(); + match method { + Method::GET | Method::HEAD => method, + _ => Method::GET, + } + }; + + let mut body = body.take(); + let body_new = if is_redirect { + // try to reuse body + match body { + Some(ref bytes) => AnyBody::Bytes { + body: bytes.clone(), + }, + // TODO: should this be AnyBody::Empty or AnyBody::None. + _ => AnyBody::empty(), + } + } else { + body = None; + // remove body + AnyBody::None + }; + + let mut headers = headers.take().unwrap(); + + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + + // use a new request head. + let mut head = RequestHead::default(); + head.uri = next_uri.clone(); + head.method = method.clone(); + head.headers = headers.clone(); + + let head = RequestHeadType::Owned(head); + + let mut max_redirect_times = *max_redirect_times; + max_redirect_times -= 1; + + let fut = connector + .as_ref() + .unwrap() + .call(ConnectRequest::Client(head, body_new, addr)); + + self.set(RedirectServiceFuture::Client { + fut, + max_redirect_times, + uri: Some(next_uri), + method: Some(method), + headers: Some(headers), + body, + addr, + connector, + }); + + self.poll(cx) + } + _ => Poll::Ready(Ok(ConnectResponse::Client(res))), + }, + _ => unreachable!("ConnectRequest::Tunnel is not handled by Redirect"), + }, + } + } +} + +fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result { + let uri = res + .headers() + .get(header::LOCATION) + .map(|value| { + // try to parse the location to a full uri + let uri = Uri::try_from(value.as_bytes()) + .map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; + if uri.scheme().is_none() || uri.authority().is_none() { + let uri = Uri::builder() + .scheme(prev_uri.scheme().cloned().unwrap()) + .authority(prev_uri.authority().cloned().unwrap()) + .path_and_query(value.as_bytes()) + .build()?; + Ok::<_, SendRequestError>(uri) + } else { + Ok(uri) + } + }) + // TODO: this error type is wrong. + .ok_or(SendRequestError::Url(InvalidUrl::MissingScheme))??; + + Ok(uri) +} + +fn remove_sensitive_headers(headers: &mut header::HeaderMap, prev_uri: &Uri, next_uri: &Uri) { + if next_uri.host() != prev_uri.host() + || next_uri.port() != prev_uri.port() + || next_uri.scheme() != prev_uri.scheme() + { + headers.remove(header::COOKIE); + headers.remove(header::AUTHORIZATION); + headers.remove(header::PROXY_AUTHORIZATION); + } +} + +#[cfg(test)] +mod tests { + use actix_web::{web, App, Error, HttpRequest, HttpResponse}; + + use super::*; + use crate::http::HeaderValue; + use crate::ClientBuilder; + use std::str::FromStr; + + #[actix_rt::test] + async fn test_basic_redirect() { + let client = ClientBuilder::new() + .disable_redirects() + .wrap(Redirect::new().max_redirect_times(10)) + .finish(); + + let srv = actix_test::start(|| { + App::new() + .service(web::resource("/test").route(web::to(|| async { + Ok::<_, Error>(HttpResponse::BadRequest()) + }))) + .service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Found() + .append_header(("location", "/test")) + .finish(), + ) + }))) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status().as_u16(), 400); + } + + #[actix_rt::test] + async fn test_redirect_limit() { + let client = ClientBuilder::new() + .disable_redirects() + .wrap(Redirect::new().max_redirect_times(1)) + .connector(crate::Connector::new()) + .finish(); + + let srv = actix_test::start(|| { + App::new() + .service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Found() + .append_header(("location", "/test")) + .finish(), + ) + }))) + .service(web::resource("/test").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Found() + .append_header(("location", "/test2")) + .finish(), + ) + }))) + .service(web::resource("/test2").route(web::to(|| async { + Ok::<_, Error>(HttpResponse::BadRequest()) + }))) + }); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status().as_u16(), 302); + } + + #[actix_rt::test] + async fn test_redirect_status_kind_307_308() { + let srv = actix_test::start(|| { + async fn root() -> HttpResponse { + HttpResponse::TemporaryRedirect() + .append_header(("location", "/test")) + .finish() + } + + async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { + if req.method() == Method::POST && !body.is_empty() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let res = srv.post("/").send_body("Hello").await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_status_kind_301_302_303() { + let srv = actix_test::start(|| { + async fn root() -> HttpResponse { + HttpResponse::Found() + .append_header(("location", "/test")) + .finish() + } + + async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { + if (req.method() == Method::GET || req.method() == Method::HEAD) + && body.is_empty() + { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let res = srv.post("/").send_body("Hello").await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + let res = srv.post("/").send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_headers() { + let srv = actix_test::start(|| { + async fn root(req: HttpRequest) -> HttpResponse { + if req + .headers() + .get("custom") + .unwrap_or(&HeaderValue::from_str("").unwrap()) + == "value" + { + HttpResponse::Found() + .append_header(("location", "/test")) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test(req: HttpRequest) -> HttpResponse { + if req + .headers() + .get("custom") + .unwrap_or(&HeaderValue::from_str("").unwrap()) + == "value" + { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test").route(web::to(test))) + }); + + let client = ClientBuilder::new() + .header("custom", "value") + .disable_redirects() + .finish(); + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 302); + + let client = ClientBuilder::new().header("custom", "value").finish(); + let res = client.get(srv.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + let client = ClientBuilder::new().finish(); + let res = client + .get(srv.url("/")) + .insert_header(("custom", "value")) + .send() + .await + .unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_redirect_cross_origin_headers() { + // defining two services to have two different origins + let srv2 = actix_test::start(|| { + async fn root(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_none() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new().service(web::resource("/").route(web::to(root))) + }); + let srv2_port: u16 = srv2.addr().port(); + + let srv1 = actix_test::start(move || { + async fn root(req: HttpRequest) -> HttpResponse { + let port = *req.app_data::().unwrap(); + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Found() + .append_header(( + "location", + format!("http://localhost:{}/", port).as_str(), + )) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test1(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Found() + .append_header(("location", "/test2")) + .finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + async fn test2(req: HttpRequest) -> HttpResponse { + if req.headers().get(header::AUTHORIZATION).is_some() { + HttpResponse::Ok().finish() + } else { + HttpResponse::InternalServerError().finish() + } + } + + App::new() + .app_data(srv2_port) + .service(web::resource("/").route(web::to(root))) + .service(web::resource("/test1").route(web::to(test1))) + .service(web::resource("/test2").route(web::to(test2))) + }); + + // send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header + let client = ClientBuilder::new() + .header(header::AUTHORIZATION, "auth_key_value") + .finish(); + let res = client.get(srv1.url("/")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + + // send a request to same origin, http://srv1/test1 then http://srv1/test2. So it should NOT remove any header + let res = client.get(srv1.url("/test1")).send().await.unwrap(); + assert_eq!(res.status().as_u16(), 200); + } + + #[actix_rt::test] + async fn test_remove_sensitive_headers() { + fn gen_headers() -> header::HeaderMap { + let mut headers = header::HeaderMap::new(); + headers.insert(header::USER_AGENT, HeaderValue::from_str("value").unwrap()); + headers.insert( + header::AUTHORIZATION, + HeaderValue::from_str("value").unwrap(), + ); + headers.insert( + header::PROXY_AUTHORIZATION, + HeaderValue::from_str("value").unwrap(), + ); + headers.insert(header::COOKIE, HeaderValue::from_str("value").unwrap()); + headers + } + + // Same origin + let prev_uri = Uri::from_str("https://host/path1").unwrap(); + let next_uri = Uri::from_str("https://host/path2").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 4); + + // different schema + let prev_uri = Uri::from_str("http://host/").unwrap(); + let next_uri = Uri::from_str("https://host/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different host + let prev_uri = Uri::from_str("https://host1/").unwrap(); + let next_uri = Uri::from_str("https://host2/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different port + let prev_uri = Uri::from_str("https://host:12/").unwrap(); + let next_uri = Uri::from_str("https://host:23/").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + + // different everything! + let prev_uri = Uri::from_str("http://host1:12/path1").unwrap(); + let next_uri = Uri::from_str("https://host2:23/path2").unwrap(); + let mut headers = gen_headers(); + remove_sensitive_headers(&mut headers, &prev_uri, &next_uri); + assert_eq!(headers.len(), 1); + } +} diff --git a/awc/src/request.rs b/awc/src/request.rs index b9a333b18..d26b703f6 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,51 +1,43 @@ -use std::convert::TryFrom; -use std::rc::Rc; -use std::time::Duration; -use std::{fmt, net}; +use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration}; use bytes::Bytes; use futures_core::Stream; use serde::Serialize; -use actix_http::body::Body; -use actix_http::cookie::{Cookie, CookieJar}; -use actix_http::http::header::{self, IntoHeaderPair}; -use actix_http::http::{ - uri, ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, - Version, +use actix_http::{ + http::{ + header::{self, IntoHeaderPair}, + ConnectionType, Error as HttpError, HeaderMap, HeaderValue, Method, Uri, Version, + }, + RequestHead, }; -use actix_http::{Error, RequestHead}; -use crate::error::{FreezeRequestError, InvalidUrl}; -use crate::frozen::FrozenClientRequest; -use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest}; -use crate::ClientConfig; +use crate::{ + any_body::AnyBody, + error::{FreezeRequestError, InvalidUrl}, + frozen::FrozenClientRequest, + sender::{PrepForSendingError, RequestSender, SendClientRequest}, + BoxError, ClientConfig, +}; -cfg_if::cfg_if! { - if #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] { - const HTTPS_ENCODING: &str = "br, gzip, deflate"; - } else if #[cfg(feature = "compress")] { - const HTTPS_ENCODING: &str = "br"; - } else { - const HTTPS_ENCODING: &str = "identity"; - } -} +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; /// An HTTP Client request builder /// /// This type can be used to construct an instance of `ClientRequest` through a /// builder-like pattern. /// -/// ```rust +/// ``` /// #[actix_rt::main] /// async fn main() { /// let response = awc::Client::new() /// .get("http://www.rust-lang.org") // <- Create request builder /// .insert_header(("User-Agent", "Actix-web")) -/// .send() // <- Send http request +/// .send() // <- Send HTTP request /// .await; /// -/// response.and_then(|response| { // <- server http response +/// response.and_then(|response| { // <- server HTTP response /// println!("Response: {:?}", response); /// Ok(()) /// }); @@ -55,15 +47,17 @@ pub struct ClientRequest { pub(crate) head: RequestHead, err: Option, addr: Option, - cookies: Option, response_decompress: bool, timeout: Option, - config: Rc, + config: ClientConfig, + + #[cfg(feature = "cookies")] + cookies: Option, } impl ClientRequest { /// Create new client request builder. - pub(crate) fn new(method: Method, uri: U, config: Rc) -> Self + pub(crate) fn new(method: Method, uri: U, config: ClientConfig) -> Self where Uri: TryFrom, >::Error: Into, @@ -73,6 +67,7 @@ impl ClientRequest { head: RequestHead::default(), err: None, addr: None, + #[cfg(feature = "cookies")] cookies: None, timeout: None, response_decompress: true, @@ -121,10 +116,10 @@ impl ClientRequest { &self.head.method } - #[doc(hidden)] /// Set HTTP version of this request. /// /// By default requests's HTTP version depends on network stream + #[doc(hidden)] #[inline] pub fn version(mut self, version: Version) -> Self { self.head.version = version; @@ -159,7 +154,9 @@ impl ClientRequest { H: IntoHeaderPair, { match header.try_into_header_pair() { - Ok((key, value)) => self.head.headers.insert(key, value), + Ok((key, value)) => { + self.head.headers.insert(key, value); + } Err(e) => self.err = Some(e.into()), }; @@ -185,16 +182,16 @@ impl ClientRequest { /// Append a header, keeping any that were set with an equivalent field name. /// - /// ```rust + /// ``` /// # #[actix_rt::main] /// # async fn main() { /// # use awc::Client; - /// use awc::http::header::ContentType; + /// use awc::http::header::CONTENT_TYPE; /// /// Client::new() /// .get("http://www.rust-lang.org") /// .insert_header(("X-TEST", "value")) - /// .insert_header(ContentType(mime::APPLICATION_JSON)); + /// .insert_header((CONTENT_TYPE, mime::APPLICATION_JSON)); /// # } /// ``` pub fn append_header(mut self, header: H) -> Self @@ -217,7 +214,7 @@ impl ClientRequest { } /// Force close connection instead of returning it back to connections pool. - /// This setting affect only http/1 connections. + /// This setting affect only HTTP/1 connections. #[inline] pub fn force_close(mut self) -> Self { self.head.set_connection_type(ConnectionType::Close); @@ -232,7 +229,9 @@ impl ClientRequest { >::Error: Into, { match HeaderValue::try_from(value) { - Ok(value) => self.head.headers.insert(header::CONTENT_TYPE, value), + Ok(value) => { + self.head.headers.insert(header::CONTENT_TYPE, value); + } Err(e) => self.err = Some(e.into()), } self @@ -241,40 +240,35 @@ impl ClientRequest { /// Set content length #[inline] pub fn content_length(self, len: u64) -> Self { - self.append_header((header::CONTENT_LENGTH, len)) + let mut buf = itoa::Buffer::new(); + self.insert_header((header::CONTENT_LENGTH, buf.format(len))) } - /// Set HTTP basic authorization header - pub fn basic_auth(self, username: U, password: Option<&str>) -> Self - where - U: fmt::Display, - { - let auth = match password { - Some(password) => format!("{}:{}", username, password), - None => format!("{}:", username), - }; - self.append_header(( + /// Set HTTP basic authorization header. + /// + /// If no password is needed, just provide an empty string. + pub fn basic_auth(self, username: impl fmt::Display, password: impl fmt::Display) -> Self { + let auth = format!("{}:{}", username, password); + + self.insert_header(( header::AUTHORIZATION, format!("Basic {}", base64::encode(&auth)), )) } /// Set HTTP bearer authentication header - pub fn bearer_auth(self, token: T) -> Self - where - T: fmt::Display, - { - self.append_header((header::AUTHORIZATION, format!("Bearer {}", token))) + pub fn bearer_auth(self, token: impl fmt::Display) -> Self { + self.insert_header((header::AUTHORIZATION, format!("Bearer {}", token))) } /// Set a cookie /// - /// ```rust + /// ``` /// #[actix_rt::main] /// async fn main() { /// let resp = awc::Client::new().get("https://www.rust-lang.org") /// .cookie( - /// awc::http::Cookie::build("name", "value") + /// awc::cookie::Cookie::build("name", "value") /// .domain("www.rust-lang.org") /// .path("/") /// .secure(true) @@ -287,6 +281,7 @@ impl ClientRequest { /// println!("Response: {:?}", resp); /// } /// ``` + #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); @@ -313,34 +308,6 @@ impl ClientRequest { self } - /// This method calls provided closure with builder reference if value is `true`. - #[doc(hidden)] - #[deprecated = "Use an if statement."] - pub fn if_true(self, value: bool, f: F) -> Self - where - F: FnOnce(ClientRequest) -> ClientRequest, - { - if value { - f(self) - } else { - self - } - } - - /// This method calls provided closure with builder reference if value is `Some`. - #[doc(hidden)] - #[deprecated = "Use an if-let construction."] - pub fn if_some(self, value: Option, f: F) -> Self - where - F: FnOnce(T, ClientRequest) -> ClientRequest, - { - if let Some(val) = value { - f(val, self) - } else { - self - } - } - /// Sets the query part of the request pub fn query( mut self, @@ -384,7 +351,7 @@ impl ClientRequest { /// Complete request construction and send body. pub fn send_body(self, body: B) -> SendClientRequest where - B: Into, + B: Into, { let slf = match self.prep_for_sending() { Ok(slf) => slf, @@ -395,7 +362,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, body, ) } @@ -411,7 +378,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, value, ) } @@ -429,7 +396,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, value, ) } @@ -438,7 +405,7 @@ impl ClientRequest { pub fn send_stream(self, stream: S) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into + 'static, { let slf = match self.prep_for_sending() { Ok(slf) => slf, @@ -449,7 +416,7 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, stream, ) } @@ -465,11 +432,12 @@ impl ClientRequest { slf.addr, slf.response_decompress, slf.timeout, - slf.config.as_ref(), + &slf.config, ) } - fn prep_for_sending(mut self) -> Result { + // allow unused mut when cookies feature is disabled + fn prep_for_sending(#[allow(unused_mut)] mut self) -> Result { if let Some(e) = self.err { return Err(e.into()); } @@ -490,11 +458,12 @@ impl ClientRequest { } // set cookies + #[cfg(feature = "cookies")] if let Some(ref mut jar) = self.cookies { let cookie: String = jar .delta() // ensure only name=value is written to cookie header - .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .map(|c| c.stripped().encoded().to_string()) .collect::>() .join("; "); @@ -507,26 +476,44 @@ impl ClientRequest { let mut slf = self; + // Set Accept-Encoding HTTP header depending on enabled feature. + // If decompress is not ask, then we are not able to find which encoding is + // supported, so we cannot guess Accept-Encoding HTTP header. if slf.response_decompress { - let https = slf - .head - .uri - .scheme() - .map(|s| s == &uri::Scheme::HTTPS) - .unwrap_or(true); + // Set Accept-Encoding with compression algorithm awc is built with. + #[allow(clippy::vec_init_then_push)] + #[cfg(feature = "__compress")] + let accept_encoding = { + let mut encoding = vec![]; - if https { - slf = - slf.insert_header_if_none((header::ACCEPT_ENCODING, HTTPS_ENCODING)) - } else { - #[cfg(any(feature = "flate2-zlib", feature = "flate2-rust"))] + #[cfg(feature = "compress-brotli")] { - slf = slf.insert_header_if_none(( - header::ACCEPT_ENCODING, - "gzip, deflate", - )) + encoding.push("br"); } + + #[cfg(feature = "compress-gzip")] + { + encoding.push("gzip"); + encoding.push("deflate"); + } + + #[cfg(feature = "compress-zstd")] + encoding.push("zstd"); + + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled" + ); + + encoding.join(", ") }; + + // Otherwise tell the server, we do not support any compression algorithm. + // So we clearly indicate that we do want identity encoding. + #[cfg(not(feature = "__compress"))] + let accept_encoding = "identity"; + + slf = slf.insert_header_if_none((header::ACCEPT_ENCODING, accept_encoding)); } Ok(slf) @@ -552,6 +539,8 @@ impl fmt::Debug for ClientRequest { mod tests { use std::time::SystemTime; + use actix_http::http::header::HttpDate; + use super::*; use crate::Client; @@ -568,7 +557,7 @@ mod tests { let req = Client::new() .put("/") .version(Version::HTTP_2) - .insert_header(header::Date(SystemTime::now().into())) + .insert_header((header::DATE, HttpDate::from(SystemTime::now()))) .content_type("plain/text") .append_header((header::SERVER, "awc")); @@ -637,9 +626,7 @@ mod tests { #[actix_rt::test] async fn client_basic_auth() { - let req = Client::new() - .get("/") - .basic_auth("username", Some("password")); + let req = Client::new().get("/").basic_auth("username", "password"); assert_eq!( req.head .headers @@ -650,7 +637,7 @@ mod tests { "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" ); - let req = Client::new().get("/").basic_auth("username", None); + let req = Client::new().get("/").basic_auth("username", ""); assert_eq!( req.head .headers diff --git a/awc/src/response.rs b/awc/src/response.rs index c3e7d71ce..a966edd08 100644 --- a/awc/src/response.rs +++ b/awc/src/response.rs @@ -1,26 +1,64 @@ -use std::cell::{Ref, RefMut}; -use std::fmt; -use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + cell::{Ref, RefMut}, + fmt, + future::Future, + io, + marker::PhantomData, + pin::Pin, + task::{Context, Poll}, + time::{Duration, Instant}, +}; +use actix_http::{ + error::PayloadError, + http::{header, HeaderMap, StatusCode, Version}, + Extensions, HttpMessage, Payload, PayloadStream, ResponseHead, +}; +use actix_rt::time::{sleep, Sleep}; use bytes::{Bytes, BytesMut}; use futures_core::{ready, Stream}; - -use actix_http::cookie::Cookie; -use actix_http::error::{CookieParseError, PayloadError}; -use actix_http::http::header::{CONTENT_LENGTH, SET_COOKIE}; -use actix_http::http::{HeaderMap, StatusCode, Version}; -use actix_http::{Extensions, HttpMessage, Payload, PayloadStream, ResponseHead}; use serde::de::DeserializeOwned; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, ParseError as CookieParseError}; use crate::error::JsonPayloadError; /// Client Response pub struct ClientResponse { pub(crate) head: ResponseHead, pub(crate) payload: Payload, + pub(crate) timeout: ResponseTimeout, +} + +/// helper enum with reusable sleep passed from `SendClientResponse`. +/// See `ClientResponse::_timeout` for reason. +pub(crate) enum ResponseTimeout { + Disabled(Option>>), + Enabled(Pin>), +} + +impl Default for ResponseTimeout { + fn default() -> Self { + Self::Disabled(None) + } +} + +impl ResponseTimeout { + fn poll_timeout(&mut self, cx: &mut Context<'_>) -> Result<(), PayloadError> { + match *self { + Self::Enabled(ref mut timeout) => { + if timeout.as_mut().poll(cx).is_ready() { + Err(PayloadError::Io(io::Error::new( + io::ErrorKind::TimedOut, + "Response Payload IO timed out", + ))) + } else { + Ok(()) + } + } + Self::Disabled(_) => Ok(()), + } + } } impl HttpMessage for ClientResponse { @@ -30,6 +68,10 @@ impl HttpMessage for ClientResponse { &self.head.headers } + fn take_payload(&mut self) -> Payload { + std::mem::replace(&mut self.payload, Payload::None) + } + fn extensions(&self) -> Ref<'_, Extensions> { self.head.extensions() } @@ -37,35 +79,16 @@ impl HttpMessage for ClientResponse { fn extensions_mut(&self) -> RefMut<'_, Extensions> { self.head.extensions_mut() } - - fn take_payload(&mut self) -> Payload { - std::mem::replace(&mut self.payload, Payload::None) - } - - /// Load request cookies. - #[inline] - fn cookies(&self) -> Result>>, CookieParseError> { - struct Cookies(Vec>); - - if self.extensions().get::().is_none() { - let mut cookies = Vec::new(); - for hdr in self.headers().get_all(&SET_COOKIE) { - let s = std::str::from_utf8(hdr.as_bytes()) - .map_err(CookieParseError::from)?; - cookies.push(Cookie::parse_encoded(s)?.into_owned()); - } - self.extensions_mut().insert(Cookies(cookies)); - } - Ok(Ref::map(self.extensions(), |ext| { - &ext.get::().unwrap().0 - })) - } } impl ClientResponse { /// Create new Request instance pub(crate) fn new(head: ResponseHead, payload: Payload) -> Self { - ClientResponse { head, payload } + ClientResponse { + head, + payload, + timeout: ResponseTimeout::default(), + } } #[inline] @@ -101,15 +124,81 @@ impl ClientResponse { ClientResponse { payload, head: self.head, + timeout: self.timeout, } } + + /// Set a timeout duration for [`ClientResponse`](self::ClientResponse). + /// + /// This duration covers the duration of processing the response body stream + /// and would end it as timeout error when deadline met. + /// + /// Disabled by default. + pub fn timeout(self, dur: Duration) -> Self { + let timeout = match self.timeout { + ResponseTimeout::Disabled(Some(mut timeout)) + | ResponseTimeout::Enabled(mut timeout) => match Instant::now().checked_add(dur) { + Some(deadline) => { + timeout.as_mut().reset(deadline.into()); + ResponseTimeout::Enabled(timeout) + } + None => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }, + _ => ResponseTimeout::Enabled(Box::pin(sleep(dur))), + }; + + Self { + payload: self.payload, + head: self.head, + timeout, + } + } + + /// This method does not enable timeout. It's used to pass the boxed `Sleep` from + /// `SendClientRequest` and reuse it's heap allocation together with it's slot in + /// timer wheel. + pub(crate) fn _timeout(mut self, timeout: Option>>) -> Self { + self.timeout = ResponseTimeout::Disabled(timeout); + self + } + + /// Load request cookies. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + struct Cookies(Vec>); + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(&header::SET_COOKIE) { + let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + cookies.push(Cookie::parse_encoded(s)?.into_owned()); + } + self.extensions_mut().insert(Cookies(cookies)); + } + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } + + /// Return request cookie. + #[cfg(feature = "cookies")] + pub fn cookie(&self, name: &str) -> Option> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies.iter() { + if cookie.name() == name { + return Some(cookie.to_owned()); + } + } + } + None + } } impl ClientResponse where S: Stream>, { - /// Loads http response's body. + /// Loads HTTP response's body. pub fn body(&mut self) -> MessageBody { MessageBody::new(self) } @@ -132,11 +221,11 @@ where { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { - Pin::new(&mut self.get_mut().payload).poll_next(cx) + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { + let this = self.get_mut(); + this.timeout.poll_timeout(cx)?; + + Pin::new(&mut this.payload).poll_next(cx) } } @@ -151,11 +240,13 @@ impl fmt::Debug for ClientResponse { } } -/// Future that resolves to a complete http message body. +const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; + +/// Future that resolves to a complete HTTP message body. pub struct MessageBody { length: Option, - err: Option, - fut: Option>, + timeout: ResponseTimeout, + body: Result, Option>, } impl MessageBody @@ -164,39 +255,38 @@ where { /// Create `MessageBody` for request. pub fn new(res: &mut ClientResponse) -> MessageBody { - let mut len = None; - if let Some(l) = res.headers().get(&CONTENT_LENGTH) { - if let Ok(s) = l.to_str() { - if let Ok(l) = s.parse::() { - len = Some(l) - } else { - return Self::err(PayloadError::UnknownLength); + let length = match res.headers().get(&header::CONTENT_LENGTH) { + Some(value) => { + let len = value.to_str().ok().and_then(|s| s.parse::().ok()); + + match len { + None => return Self::err(PayloadError::UnknownLength), + len => len, } - } else { - return Self::err(PayloadError::UnknownLength); } - } + None => None, + }; MessageBody { - length: len, - err: None, - fut: Some(ReadBody::new(res.take_payload(), 262_144)), + length, + timeout: std::mem::take(&mut res.timeout), + body: Ok(ReadBody::new(res.take_payload(), DEFAULT_BODY_LIMIT)), } } - /// Change max size of payload. By default max size is 256kB + /// Change max size of payload. By default max size is 2048kB pub fn limit(mut self, limit: usize) -> Self { - if let Some(ref mut fut) = self.fut { - fut.limit = limit; + if let Ok(ref mut body) = self.body { + body.limit = limit; } self } fn err(e: PayloadError) -> Self { MessageBody { - fut: None, - err: Some(e), length: None, + timeout: ResponseTimeout::default(), + body: Err(Some(e)), } } } @@ -210,17 +300,20 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - if let Some(err) = this.err.take() { - return Poll::Ready(Err(err)); - } + match this.body { + Err(ref mut err) => Poll::Ready(Err(err.take().unwrap())), + Ok(ref mut body) => { + if let Some(len) = this.length.take() { + if len > body.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + } - if let Some(len) = this.length.take() { - if len > this.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(PayloadError::Overflow)); + this.timeout.poll_timeout(cx)?; + + Pin::new(body).poll(cx) } } - - Pin::new(&mut this.fut.as_mut().unwrap()).poll(cx) } } @@ -233,6 +326,7 @@ where pub struct JsonBody { length: Option, err: Option, + timeout: ResponseTimeout, fut: Option>, _phantom: PhantomData, } @@ -243,9 +337,9 @@ where U: DeserializeOwned, { /// Create `JsonBody` for request. - pub fn new(req: &mut ClientResponse) -> Self { + pub fn new(res: &mut ClientResponse) -> Self { // check content-type - let json = if let Ok(Some(mime)) = req.mime_type() { + let json = if let Ok(Some(mime)) = res.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) } else { false @@ -254,13 +348,15 @@ where return JsonBody { length: None, fut: None, + timeout: ResponseTimeout::default(), err: Some(JsonPayloadError::ContentType), _phantom: PhantomData, }; } let mut len = None; - if let Some(l) = req.headers().get(&CONTENT_LENGTH) { + + if let Some(l) = res.headers().get(&header::CONTENT_LENGTH) { if let Ok(s) = l.to_str() { if let Ok(l) = s.parse::() { len = Some(l) @@ -271,7 +367,8 @@ where JsonBody { length: len, err: None, - fut: Some(ReadBody::new(req.take_payload(), 65536)), + timeout: std::mem::take(&mut res.timeout), + fut: Some(ReadBody::new(res.take_payload(), 65536)), _phantom: PhantomData, } } @@ -306,12 +403,14 @@ where if let Some(len) = self.length.take() { if len > self.fut.as_ref().unwrap().limit { - return Poll::Ready(Err(JsonPayloadError::Payload( - PayloadError::Overflow, - ))); + return Poll::Ready(Err(JsonPayloadError::Payload(PayloadError::Overflow))); } } + self.timeout + .poll_timeout(cx) + .map_err(JsonPayloadError::Payload)?; + let body = ready!(Pin::new(&mut self.get_mut().fut.as_mut().unwrap()).poll(cx))?; Poll::Ready(serde_json::from_slice::(&body).map_err(JsonPayloadError::from)) } @@ -327,7 +426,7 @@ impl ReadBody { fn new(stream: Payload, limit: usize) -> Self { Self { stream, - buf: BytesMut::with_capacity(std::cmp::min(limit, 32768)), + buf: BytesMut::new(), limit, } } @@ -342,20 +441,14 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); - loop { - return match Pin::new(&mut this.stream).poll_next(cx)? { - Poll::Ready(Some(chunk)) => { - if (this.buf.len() + chunk.len()) > this.limit { - Poll::Ready(Err(PayloadError::Overflow)) - } else { - this.buf.extend_from_slice(&chunk); - continue; - } - } - Poll::Ready(None) => Poll::Ready(Ok(this.buf.split().freeze())), - Poll::Pending => Poll::Pending, - }; + while let Some(chunk) = ready!(Pin::new(&mut this.stream).poll_next(cx)?) { + if (this.buf.len() + chunk.len()) > this.limit { + return Poll::Ready(Err(PayloadError::Overflow)); + } + this.buf.extend_from_slice(&chunk); } + + Poll::Ready(Ok(this.buf.split().freeze())) } } @@ -368,14 +461,13 @@ mod tests { #[actix_rt::test] async fn test_body() { - let mut req = TestResponse::with_header(header::CONTENT_LENGTH, "xxxx").finish(); + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "xxxx")).finish(); match req.body().await.err().unwrap() { PayloadError::UnknownLength => {} _ => unreachable!("error"), } - let mut req = - TestResponse::with_header(header::CONTENT_LENGTH, "1000000").finish(); + let mut req = TestResponse::with_header((header::CONTENT_LENGTH, "10000000")).finish(); match req.body().await.err().unwrap() { PayloadError::Overflow => {} _ => unreachable!("error"), @@ -405,9 +497,7 @@ mod tests { JsonPayloadError::Payload(PayloadError::Overflow) => { matches!(other, JsonPayloadError::Payload(PayloadError::Overflow)) } - JsonPayloadError::ContentType => { - matches!(other, JsonPayloadError::ContentType) - } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } @@ -419,23 +509,23 @@ mod tests { assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let mut req = TestResponse::default() - .header( + .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/text"), - ) + )) .finish(); let json = JsonBody::<_, MyObject>::new(&mut req).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let mut req = TestResponse::default() - .header( + .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), - ) - .header( + )) + .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("10000"), - ) + )) .finish(); let json = JsonBody::<_, MyObject>::new(&mut req).limit(100).await; @@ -445,14 +535,14 @@ mod tests { )); let mut req = TestResponse::default() - .header( + .insert_header(( header::CONTENT_TYPE, header::HeaderValue::from_static("application/json"), - ) - .header( + )) + .insert_header(( header::CONTENT_LENGTH, header::HeaderValue::from_static("16"), - ) + )) .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .finish(); diff --git a/awc/src/sender.rs b/awc/src/sender.rs index 9fb821a0e..51fce1913 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -1,43 +1,54 @@ -use std::future::Future; -use std::net; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; -use std::time::Duration; +use std::{ + future::Future, + net, + pin::Pin, + rc::Rc, + task::{Context, Poll}, + time::Duration, +}; +use actix_http::{ + body::BodyStream, + http::{ + header::{self, HeaderMap, HeaderName, IntoHeaderValue}, + Error as HttpError, + }, + RequestHead, RequestHeadType, +}; use actix_rt::time::{sleep, Sleep}; use bytes::Bytes; use derive_more::From; use futures_core::Stream; use serde::Serialize; -use actix_http::body::{Body, BodyStream}; -use actix_http::http::header::{self, IntoHeaderValue}; -use actix_http::http::{Error as HttpError, HeaderMap, HeaderName}; -use actix_http::{Error, RequestHead}; +#[cfg(feature = "__compress")] +use actix_http::{encoding::Decoder, http::header::ContentEncoding, Payload, PayloadStream}; -#[cfg(feature = "compress")] -use actix_http::encoding::Decoder; -#[cfg(feature = "compress")] -use actix_http::http::header::ContentEncoding; -#[cfg(feature = "compress")] -use actix_http::{Payload, PayloadStream}; - -use crate::error::{FreezeRequestError, InvalidUrl, SendRequestError}; -use crate::response::ClientResponse; -use crate::ClientConfig; +use crate::{ + any_body::AnyBody, + error::{FreezeRequestError, InvalidUrl, SendRequestError}, + BoxError, ClientConfig, ClientResponse, ConnectRequest, ConnectResponse, +}; #[derive(Debug, From)] pub(crate) enum PrepForSendingError { Url(InvalidUrl), Http(HttpError), + Json(serde_json::Error), + Form(serde_urlencoded::ser::Error), } impl From for FreezeRequestError { fn from(err: PrepForSendingError) -> FreezeRequestError { match err { - PrepForSendingError::Url(e) => FreezeRequestError::Url(e), - PrepForSendingError::Http(e) => FreezeRequestError::Http(e), + PrepForSendingError::Url(err) => FreezeRequestError::Url(err), + PrepForSendingError::Http(err) => FreezeRequestError::Http(err), + PrepForSendingError::Json(err) => { + FreezeRequestError::Custom(Box::new(err), Box::new("json serialization error")) + } + PrepForSendingError::Form(err) => { + FreezeRequestError::Custom(Box::new(err), Box::new("form serialization error")) + } } } } @@ -47,6 +58,12 @@ impl From for SendRequestError { match err { PrepForSendingError::Url(e) => SendRequestError::Url(e), PrepForSendingError::Http(e) => SendRequestError::Http(e), + PrepForSendingError::Json(err) => { + SendRequestError::Custom(Box::new(err), Box::new("json serialization error")) + } + PrepForSendingError::Form(err) => { + SendRequestError::Custom(Box::new(err), Box::new("form serialization error")) + } } } } @@ -55,7 +72,7 @@ impl From for SendRequestError { #[must_use = "futures do nothing unless polled"] pub enum SendClientRequest { Fut( - Pin>>>, + Pin>>>, // FIXME: use a pinned Sleep instead of box. Option>>, bool, @@ -65,7 +82,7 @@ pub enum SendClientRequest { impl SendClientRequest { pub(crate) fn new( - send: Pin>>>, + send: Pin>>>, response_decompress: bool, timeout: Option, ) -> SendClientRequest { @@ -74,37 +91,34 @@ impl SendClientRequest { } } -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] impl Future for SendClientRequest { - type Output = - Result>>, SendRequestError>; + type Output = Result>>, SendRequestError>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.get_mut(); match this { SendClientRequest::Fut(send, delay, response_decompress) => { - if delay.is_some() { - match Pin::new(delay.as_mut().unwrap()).poll(cx) { - Poll::Pending => {} - _ => return Poll::Ready(Err(SendRequestError::Timeout)), + if let Some(delay) = delay { + if delay.as_mut().poll(cx).is_ready() { + return Poll::Ready(Err(SendRequestError::Timeout)); } } - let res = futures_core::ready!(Pin::new(send).poll(cx)).map(|res| { - res.map_body(|head, payload| { - if *response_decompress { - Payload::Stream(Decoder::from_headers( - payload, - &head.headers, - )) - } else { - Payload::Stream(Decoder::new( - payload, - ContentEncoding::Identity, - )) - } - }) + let res = futures_core::ready!(send.as_mut().poll(cx)).map(|res| { + res.into_client_response()._timeout(delay.take()).map_body( + |head, payload| { + if *response_decompress { + Payload::Stream(Decoder::from_headers(payload, &head.headers)) + } else { + Payload::Stream(Decoder::new( + payload, + ContentEncoding::Identity, + )) + } + }, + ) }); Poll::Ready(res) @@ -117,7 +131,7 @@ impl Future for SendClientRequest { } } -#[cfg(not(feature = "compress"))] +#[cfg(not(feature = "__compress"))] impl Future for SendClientRequest { type Output = Result; @@ -125,13 +139,14 @@ impl Future for SendClientRequest { let this = self.get_mut(); match this { SendClientRequest::Fut(send, delay, _) => { - if delay.is_some() { - match Pin::new(delay.as_mut().unwrap()).poll(cx) { - Poll::Pending => {} - _ => return Poll::Ready(Err(SendRequestError::Timeout)), + if let Some(delay) = delay { + if delay.as_mut().poll(cx).is_ready() { + return Poll::Ready(Err(SendRequestError::Timeout)); } } - Pin::new(send).poll(cx) + send.as_mut() + .poll(cx) + .map_ok(|res| res.into_client_response()._timeout(delay.take())) } SendClientRequest::Err(ref mut e) => match e.take() { Some(e) => Poll::Ready(Err(e)), @@ -147,12 +162,6 @@ impl From for SendClientRequest { } } -impl From for SendClientRequest { - fn from(e: Error) -> Self { - SendClientRequest::Err(Some(e.into())) - } -} - impl From for SendClientRequest { fn from(e: HttpError) -> Self { SendClientRequest::Err(Some(e.into())) @@ -181,19 +190,21 @@ impl RequestSender { body: B, ) -> SendClientRequest where - B: Into, + B: Into, { - let mut connector = config.connector.borrow_mut(); - - let fut = match self { + let req = match self { RequestSender::Owned(head) => { - connector.send_request(head, body.into(), addr) - } - RequestSender::Rc(head, extra_headers) => { - connector.send_request_extra(head, extra_headers, body.into(), addr) + ConnectRequest::Client(RequestHeadType::Owned(head), body.into(), addr) } + RequestSender::Rc(head, extra_headers) => ConnectRequest::Client( + RequestHeadType::Rc(head, extra_headers), + body.into(), + addr, + ), }; + let fut = config.connector.call(req); + SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout)) } @@ -207,11 +218,10 @@ impl RequestSender { ) -> SendClientRequest { let body = match serde_json::to_string(value) { Ok(body) => body, - Err(e) => return Error::from(e).into(), + Err(err) => return PrepForSendingError::Json(err).into(), }; - if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") - { + if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") { return e.into(); } @@ -220,7 +230,9 @@ impl RequestSender { response_decompress, timeout, config, - Body::Bytes(Bytes::from(body)), + AnyBody::Bytes { + body: Bytes::from(body), + }, ) } @@ -234,14 +246,13 @@ impl RequestSender { ) -> SendClientRequest { let body = match serde_urlencoded::to_string(value) { Ok(body) => body, - Err(e) => return Error::from(e).into(), + Err(err) => return PrepForSendingError::Form(err).into(), }; // set content-type - if let Err(e) = self.set_header_if_none( - header::CONTENT_TYPE, - "application/x-www-form-urlencoded", - ) { + if let Err(e) = + self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded") + { return e.into(); } @@ -250,7 +261,9 @@ impl RequestSender { response_decompress, timeout, config, - Body::Bytes(Bytes::from(body)), + AnyBody::Bytes { + body: Bytes::from(body), + }, ) } @@ -264,14 +277,14 @@ impl RequestSender { ) -> SendClientRequest where S: Stream> + Unpin + 'static, - E: Into + 'static, + E: Into + 'static, { self.send_body( addr, response_decompress, timeout, config, - Body::from_message(BodyStream::new(stream)), + AnyBody::new_boxed(BodyStream::new(stream)), ) } @@ -282,14 +295,10 @@ impl RequestSender { timeout: Option, config: &ClientConfig, ) -> SendClientRequest { - self.send_body(addr, response_decompress, timeout, config, Body::Empty) + self.send_body(addr, response_decompress, timeout, config, AnyBody::empty()) } - fn set_header_if_none( - &mut self, - key: HeaderName, - value: V, - ) -> Result<(), HttpError> + fn set_header_if_none(&mut self, key: HeaderName, value: V) -> Result<(), HttpError> where V: IntoHeaderValue, { @@ -297,7 +306,9 @@ impl RequestSender { RequestSender::Owned(head) => { if !head.headers.contains_key(&key) { match value.try_into_value() { - Ok(value) => head.headers.insert(key, value), + Ok(value) => { + head.headers.insert(key, value); + } Err(e) => return Err(e.into()), } } diff --git a/awc/src/test.rs b/awc/src/test.rs index 84646b9f7..1abe78811 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -1,17 +1,17 @@ //! Test helpers for actix http client to use during testing. -use std::convert::TryFrom; - -use actix_http::cookie::{Cookie, CookieJar}; -use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; -use actix_http::http::{Error as HttpError, HeaderName, StatusCode, Version}; +use actix_http::http::header::IntoHeaderPair; +use actix_http::http::{StatusCode, Version}; use actix_http::{h1, Payload, ResponseHead}; use bytes::Bytes; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::ClientResponse; /// Test `ClientResponse` builder pub struct TestResponse { head: ResponseHead, + #[cfg(feature = "cookies")] cookies: CookieJar, payload: Option, } @@ -20,6 +20,7 @@ impl Default for TestResponse { fn default() -> TestResponse { TestResponse { head: ResponseHead::new(StatusCode::OK), + #[cfg(feature = "cookies")] cookies: CookieJar::new(), payload: None, } @@ -28,13 +29,11 @@ impl Default for TestResponse { impl TestResponse { /// Create TestResponse and set header - pub fn with_header(key: K, value: V) -> Self + pub fn with_header(header: H) -> Self where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, + H: IntoHeaderPair, { - Self::default().header(key, value) + Self::default().insert_header(header) } /// Set HTTP version of this response @@ -43,32 +42,32 @@ impl TestResponse { self } - /// Set a header - pub fn set(mut self, hdr: H) -> Self { - if let Ok(value) = hdr.try_into_value() { - self.head.headers.append(H::name(), value); + /// Insert a header + pub fn insert_header(mut self, header: H) -> Self + where + H: IntoHeaderPair, + { + if let Ok((key, value)) = header.try_into_header_pair() { + self.head.headers.insert(key, value); return self; } panic!("Can not set header"); } /// Append a header - pub fn header(mut self, key: K, value: V) -> Self + pub fn append_header(mut self, header: H) -> Self where - HeaderName: TryFrom, - >::Error: Into, - V: IntoHeaderValue, + H: IntoHeaderPair, { - if let Ok(key) = HeaderName::try_from(key) { - if let Ok(value) = value.try_into_value() { - self.head.headers.append(key, value); - return self; - } + if let Ok((key, value)) = header.try_into_header_pair() { + self.head.headers.append(key, value); + return self; } panic!("Can not create header"); } /// Set cookie for this response + #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { self.cookies.add(cookie.into_owned()); self @@ -84,9 +83,14 @@ impl TestResponse { /// Complete response creation and generate `ClientResponse` instance pub fn finish(self) -> ClientResponse { + // allow unused mut when cookies feature is disabled + #[allow(unused_mut)] let mut head = self.head; + #[cfg(feature = "cookies")] for cookie in self.cookies.delta() { + use actix_http::http::header::{self, HeaderValue}; + head.headers.insert( header::SET_COOKIE, HeaderValue::from_str(&cookie.encoded().to_string()).unwrap(), @@ -105,6 +109,8 @@ impl TestResponse { mod tests { use std::time::SystemTime; + use actix_http::http::header::HttpDate; + use super::*; use crate::{cookie, http::header}; @@ -112,7 +118,7 @@ mod tests { fn test_basics() { let res = TestResponse::default() .version(Version::HTTP_2) - .set(header::Date(SystemTime::now().into())) + .insert_header((header::DATE, HttpDate::from(SystemTime::now()))) .cookie(cookie::Cookie::build("name", "value").finish()) .finish(); assert!(res.headers().contains_key(header::SET_COOKIE)); diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 17086cf2a..e2f1f86d0 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -6,7 +6,7 @@ //! //! ```no_run //! use awc::{Client, ws}; -//! use futures_util::{sink::SinkExt, stream::StreamExt}; +//! use futures_util::{sink::SinkExt as _, stream::StreamExt as _}; //! //! #[actix_rt::main] //! async fn main() { @@ -28,28 +28,25 @@ use std::convert::TryFrom; use std::net::SocketAddr; -use std::rc::Rc; use std::{fmt, str}; use actix_codec::Framed; -use actix_http::cookie::{Cookie, CookieJar}; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; +use actix_service::Service; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; -use crate::connect::BoxedSocket; +use crate::connect::{BoxedSocket, ConnectRequest}; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::error::{InvalidUrl, SendRequestError, WsClientError}; -use crate::http::header::{ - self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION, -}; -use crate::http::{ - ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version, -}; +use crate::http::header::{self, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION}; +use crate::http::{ConnectionType, Error as HttpError, Method, StatusCode, Uri, Version}; use crate::response::ClientResponse; use crate::ClientConfig; -/// `WebSocket` connection +/// WebSocket connection. pub struct WebsocketsRequest { pub(crate) head: RequestHead, err: Option, @@ -58,13 +55,15 @@ pub struct WebsocketsRequest { addr: Option, max_size: usize, server_mode: bool, + config: ClientConfig, + + #[cfg(feature = "cookies")] cookies: Option, - config: Rc, } impl WebsocketsRequest { - /// Create new websocket connection - pub(crate) fn new(uri: U, config: Rc) -> Self + /// Create new WebSocket connection + pub(crate) fn new(uri: U, config: ClientConfig) -> Self where Uri: TryFrom, >::Error: Into, @@ -93,6 +92,7 @@ impl WebsocketsRequest { protocols: None, max_size: 65_536, server_mode: false, + #[cfg(feature = "cookies")] cookies: None, } } @@ -106,7 +106,7 @@ impl WebsocketsRequest { self } - /// Set supported websocket protocols + /// Set supported WebSocket protocols pub fn protocols(mut self, protos: U) -> Self where U: IntoIterator, @@ -121,6 +121,7 @@ impl WebsocketsRequest { } /// Set a cookie + #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { if self.cookies.is_none() { let mut jar = CookieJar::new(); @@ -243,7 +244,7 @@ impl WebsocketsRequest { self.header(AUTHORIZATION, format!("Bearer {}", token)) } - /// Complete request construction and connect to a websockets server. + /// Complete request construction and connect to a WebSocket server. pub async fn connect( mut self, ) -> Result<(ClientResponse, Framed), WsClientError> { @@ -274,11 +275,12 @@ impl WebsocketsRequest { } // set cookies + #[cfg(feature = "cookies")] if let Some(ref mut jar) = self.cookies { let cookie: String = jar .delta() // ensure only name=value is written to cookie header - .map(|c| Cookie::new(c.name(), c.value()).encoded().to_string()) + .map(|c| c.stripped().encoded().to_string()) .collect::>() .join("; "); @@ -310,9 +312,8 @@ impl WebsocketsRequest { ); } - // Generate a random key for the `Sec-WebSocket-Key` header. - // a base64-encoded (see Section 4 of [RFC4648]) value that, - // when decoded, is 16 bytes in length (RFC 6455) + // Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded + // (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3). let sec_key: [u8; 16] = rand::random(); let key = base64::encode(&sec_key); @@ -325,28 +326,27 @@ impl WebsocketsRequest { let max_size = self.max_size; let server_mode = self.server_mode; - let fut = self - .config - .connector - .borrow_mut() - .open_tunnel(head, self.addr); + let req = ConnectRequest::Tunnel(head, self.addr); + + let fut = self.config.connector.call(req); // set request timeout - let (head, framed) = if let Some(to) = self.config.timeout { + let res = if let Some(to) = self.config.timeout { timeout(to, fut) .await - .map_err(|_| SendRequestError::Timeout) - .and_then(|res| res)? + .map_err(|_| SendRequestError::Timeout)?? } else { fut.await? }; + let (head, framed) = res.into_tunnel_response(); + // verify response if head.status != StatusCode::SWITCHING_PROTOCOLS { return Err(WsClientError::InvalidResponseStatus(head.status)); } - // Check for "UPGRADE" to websocket header + // check for "UPGRADE" to WebSocket header let has_hdr = if let Some(hdr) = head.headers.get(&header::UPGRADE) { if let Ok(s) = hdr.to_str() { s.to_ascii_lowercase().contains("websocket") @@ -379,12 +379,14 @@ impl WebsocketsRequest { if let Some(hdr_key) = head.headers.get(&header::SEC_WEBSOCKET_ACCEPT) { let encoded = ws::hash_key(key.as_ref()); - if hdr_key.as_bytes() != encoded.as_bytes() { + + if hdr_key.as_bytes() != encoded { log::trace!( - "Invalid challenge response: expected: {} received: {:?}", - encoded, + "Invalid challenge response: expected: {:?} received: {:?}", + &encoded, key ); + return Err(WsClientError::InvalidChallengeResponse( encoded, hdr_key.clone(), @@ -514,7 +516,7 @@ mod tests { "test-origin" ); assert_eq!(req.max_size, 100); - assert_eq!(req.server_mode, true); + assert!(req.server_mode); assert_eq!(req.protocols, Some("v1,v2".to_string())); assert_eq!( req.head.headers.get(header::CONTENT_TYPE).unwrap(), diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 88987e639..5abb63e39 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,30 +1,38 @@ -use std::collections::HashMap; -use std::io::{Read, Write}; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; -use std::time::Duration; +use std::{ + collections::HashMap, + io::{Read, Write}, + net::{IpAddr, Ipv4Addr}, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + time::Duration, +}; -use brotli2::write::BrotliEncoder; +use actix_utils::future::ok; use bytes::Bytes; -use flate2::read::GzDecoder; -use flate2::write::GzEncoder; -use flate2::Compression; -use futures_util::{future::ok, stream}; +use cookie::Cookie; +use futures_util::stream; use rand::Rng; +#[cfg(feature = "compress-brotli")] +use brotli2::write::BrotliEncoder; + +#[cfg(feature = "compress-gzip")] +use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + use actix_http::{ http::{self, StatusCode}, HttpService, }; use actix_http_test::test_server; -use actix_service::{map_config, pipeline_factory}; +use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; use actix_web::{ dev::{AppConfig, BodyEncoding}, - http::{header, Cookie}, - middleware::Compress, - test, web, App, Error, HttpMessage, HttpRequest, HttpResponse, + http::header, + web, App, Error, HttpRequest, HttpResponse, }; -use awc::error::SendRequestError; +use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -50,9 +58,8 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ #[actix_rt::test] async fn test_simple() { - let srv = test::start(|| { - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) + let srv = actix_test::start(|| { + App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); let request = srv.get("/").insert_header(("x-test", "111")).send(); @@ -77,7 +84,7 @@ async fn test_simple() { #[actix_rt::test] async fn test_json() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service( web::resource("/").route(web::to(|_: web::Json| HttpResponse::Ok())), ) @@ -93,7 +100,7 @@ async fn test_json() { #[actix_rt::test] async fn test_form() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to( |_: web::Form>| HttpResponse::Ok(), ))) @@ -112,7 +119,7 @@ async fn test_form() { #[actix_rt::test] async fn test_timeout() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| async { actix_rt::time::sleep(Duration::from_millis(200)).await; Ok::<_, Error>(HttpResponse::Ok().body(STR)) @@ -120,11 +127,8 @@ async fn test_timeout() { }); let connector = awc::Connector::new() - .connector(actix_tls::connect::new_connector( - actix_tls::connect::start_default_resolver().await.unwrap(), - )) - .timeout(Duration::from_secs(15)) - .finish(); + .connector(actix_tls::connect::ConnectorService::default()) + .timeout(Duration::from_secs(15)); let client = awc::Client::builder() .connector(connector) @@ -140,7 +144,7 @@ async fn test_timeout() { #[actix_rt::test] async fn test_timeout_override() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| async { actix_rt::time::sleep(Duration::from_millis(200)).await; Ok::<_, Error>(HttpResponse::Ok().body(STR)) @@ -160,6 +164,79 @@ async fn test_timeout_override() { } } +#[actix_rt::test] +async fn test_response_timeout() { + use futures_util::stream::{once, StreamExt as _}; + + let srv = actix_test::start(|| { + App::new().service(web::resource("/").route(web::to(|| async { + Ok::<_, Error>( + HttpResponse::Ok() + .content_type("application/json") + .streaming(Box::pin(once(async { + actix_rt::time::sleep(Duration::from_millis(200)).await; + Ok::<_, Error>(Bytes::from(STR)) + }))), + ) + }))) + }); + + let client = awc::Client::new(); + + let res = client + .get(srv.url("/")) + .send() + .await + .unwrap() + .timeout(Duration::from_millis(500)) + .body() + .await + .unwrap(); + assert_eq!(std::str::from_utf8(res.as_ref()).unwrap(), STR); + + let res = client + .get(srv.url("/")) + .send() + .await + .unwrap() + .timeout(Duration::from_millis(100)) + .next() + .await + .unwrap(); + match res { + Err(PayloadError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut), + _ => panic!("Response error type is not matched"), + } + + let res = client + .get(srv.url("/")) + .send() + .await + .unwrap() + .timeout(Duration::from_millis(100)) + .body() + .await; + match res { + Err(PayloadError::Io(e)) => assert_eq!(e.kind(), std::io::ErrorKind::TimedOut), + _ => panic!("Response error type is not matched"), + } + + let res = client + .get(srv.url("/")) + .send() + .await + .unwrap() + .timeout(Duration::from_millis(100)) + .json::>() + .await; + match res { + Err(JsonPayloadError::Payload(PayloadError::Io(e))) => { + assert_eq!(e.kind(), std::io::ErrorKind::TimedOut) + } + _ => panic!("Response error type is not matched"), + } +} + #[actix_rt::test] async fn test_connection_reuse() { let num = Arc::new(AtomicUsize::new(0)); @@ -167,7 +244,7 @@ async fn test_connection_reuse() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) @@ -204,7 +281,7 @@ async fn test_connection_force_close() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) @@ -241,7 +318,7 @@ async fn test_connection_server_close() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) @@ -281,7 +358,7 @@ async fn test_connection_wait_queue() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) @@ -298,7 +375,7 @@ async fn test_connection_wait_queue() { .await; let client = awc::Client::builder() - .connector(awc::Connector::new().limit(1).finish()) + .connector(awc::Connector::new().limit(1)) .finish(); // req 1 @@ -329,7 +406,7 @@ async fn test_connection_wait_queue_force_close() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) @@ -347,7 +424,7 @@ async fn test_connection_wait_queue_force_close() { .await; let client = awc::Client::builder() - .connector(awc::Connector::new().limit(1).finish()) + .connector(awc::Connector::new().limit(1)) .finish(); // req 1 @@ -373,7 +450,7 @@ async fn test_connection_wait_queue_force_close() { #[actix_rt::test] async fn test_with_query_parameter() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").to(|req: HttpRequest| { if req.query_string().contains("qp") { HttpResponse::Ok() @@ -391,11 +468,12 @@ async fn test_with_query_parameter() { assert!(res.status().is_success()); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_no_decompress() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new() - .wrap(Compress::default()) + .wrap(actix_web::middleware::Compress::default()) .service(web::resource("/").route(web::to(|| { let mut res = HttpResponse::Ok().body(STR); res.encoding(header::ContentEncoding::Gzip); @@ -435,9 +513,10 @@ async fn test_no_decompress() { assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_client_gzip_encoding() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| { let mut e = GzEncoder::new(Vec::new(), Compression::default()); e.write_all(STR.as_ref()).unwrap(); @@ -458,9 +537,10 @@ async fn test_client_gzip_encoding() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_client_gzip_encoding_large() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|| { let mut e = GzEncoder::new(Vec::new(), Compression::default()); e.write_all(STR.repeat(10).as_ref()).unwrap(); @@ -481,6 +561,7 @@ async fn test_client_gzip_encoding_large() { assert_eq!(bytes, Bytes::from(STR.repeat(10))); } +#[cfg(feature = "compress-gzip")] #[actix_rt::test] async fn test_client_gzip_encoding_large_random() { let data = rand::thread_rng() @@ -489,7 +570,7 @@ async fn test_client_gzip_encoding_large_random() { .map(char::from) .collect::(); - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| { let mut e = GzEncoder::new(Vec::new(), Compression::default()); e.write_all(&data).unwrap(); @@ -509,9 +590,10 @@ async fn test_client_gzip_encoding_large_random() { assert_eq!(bytes, Bytes::from(data)); } +#[cfg(feature = "compress-brotli")] #[actix_rt::test] async fn test_client_brotli_encoding() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| { let mut e = BrotliEncoder::new(Vec::new(), 5); e.write_all(&data).unwrap(); @@ -531,6 +613,7 @@ async fn test_client_brotli_encoding() { assert_eq!(bytes, Bytes::from_static(STR.as_ref())); } +#[cfg(feature = "compress-brotli")] #[actix_rt::test] async fn test_client_brotli_encoding_large_random() { let data = rand::thread_rng() @@ -539,7 +622,7 @@ async fn test_client_brotli_encoding_large_random() { .map(char::from) .collect::(); - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().service(web::resource("/").route(web::to(|data: Bytes| { let mut e = BrotliEncoder::new(Vec::new(), 5); e.write_all(&data).unwrap(); @@ -562,7 +645,7 @@ async fn test_client_brotli_encoding_large_random() { #[actix_rt::test] async fn test_client_deflate_encoding() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().default_service(web::to(|body: Bytes| { HttpResponse::Ok() .encoding(http::ContentEncoding::Br) @@ -587,7 +670,7 @@ async fn test_client_deflate_encoding_large_random() { .take(70_000) .collect::(); - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().default_service(web::to(|body: Bytes| { HttpResponse::Ok() .encoding(http::ContentEncoding::Br) @@ -606,7 +689,7 @@ async fn test_client_deflate_encoding_large_random() { #[actix_rt::test] async fn test_client_streaming_explicit() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().default_service(web::to(|body: web::Payload| { HttpResponse::Ok() .encoding(http::ContentEncoding::Identity) @@ -614,9 +697,8 @@ async fn test_client_streaming_explicit() { })) }); - let body = stream::once(async { - Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) - }); + let body = + stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) }); let req = srv.post("/").send_stream(Box::pin(body)); let mut res = req.await.unwrap(); @@ -628,7 +710,7 @@ async fn test_client_streaming_explicit() { #[actix_rt::test] async fn test_body_streaming_implicit() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().default_service(web::to(|| { let body = stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) @@ -664,7 +746,7 @@ async fn test_client_cookie_handling() { let cookie1b = cookie1.clone(); let cookie2b = cookie2.clone(); - let srv = test::start(move || { + let srv = actix_test::start(move || { let cookie1 = cookie1b.clone(); let cookie2 = cookie2b.clone(); @@ -720,22 +802,19 @@ async fn test_client_cookie_handling() { #[actix_rt::test] async fn client_unread_response() { - let addr = test::unused_addr(); + let addr = actix_test::unused_addr(); + let lst = std::net::TcpListener::bind(addr).unwrap(); std::thread::spawn(move || { - let lst = std::net::TcpListener::bind(addr).unwrap(); - - for stream in lst.incoming() { - let mut stream = stream.unwrap(); - let mut b = [0; 1000]; - let _ = stream.read(&mut b).unwrap(); - let _ = stream.write_all( - b"HTTP/1.1 200 OK\r\n\ + let (mut stream, _) = lst.accept().unwrap(); + let mut b = [0; 1000]; + let _ = stream.read(&mut b).unwrap(); + let _ = stream.write_all( + b"HTTP/1.1 200 OK\r\n\ connection: close\r\n\ \r\n\ welcome!", - ); - } + ); }); // client request @@ -750,7 +829,7 @@ async fn client_unread_response() { #[actix_rt::test] async fn client_basic_auth() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().route( "/", web::to(|req: HttpRequest| { @@ -760,7 +839,7 @@ async fn client_basic_auth() { .unwrap() .to_str() .unwrap() - == "Basic dXNlcm5hbWU6cGFzc3dvcmQ=" + == format!("Basic {}", base64::encode("username:password")) { HttpResponse::Ok() } else { @@ -771,14 +850,14 @@ async fn client_basic_auth() { }); // set authorization header to Basic - let request = srv.get("/").basic_auth("username", Some("password")); + let request = srv.get("/").basic_auth("username", "password"); let response = request.send().await.unwrap(); assert!(response.status().is_success()); } #[actix_rt::test] async fn client_bearer_auth() { - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().route( "/", web::to(|req: HttpRequest| { @@ -803,3 +882,34 @@ async fn client_bearer_auth() { let response = request.send().await.unwrap(); assert!(response.status().is_success()); } + +#[actix_rt::test] +async fn test_local_address() { + let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); + + let srv = actix_test::start(move || { + App::new().service(web::resource("/").route(web::to( + move |req: HttpRequest| async move { + assert_eq!(req.peer_addr().unwrap().ip(), ip); + Ok::<_, Error>(HttpResponse::Ok()) + }, + ))) + }); + let client = awc::Client::builder().local_address(ip).finish(); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status(), 200); + + let client = awc::Client::builder() + .connector( + // connector local address setting should always be override by client builder. + awc::Connector::new().local_address(IpAddr::V4(Ipv4Addr::new(128, 0, 0, 1))), + ) + .local_address(ip) + .finish(); + + let res = client.get(srv.url("/")).send().await.unwrap(); + + assert_eq!(res.status(), 200); +} diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index e500801c4..588c51463 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -1,41 +1,51 @@ #![cfg(feature = "openssl")] + +extern crate tls_openssl as openssl; + use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{map_config, ServiceFactoryExt}; use actix_web::http::Version; use actix_web::{dev::AppConfig, web, App, HttpResponse}; -use open_ssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode}; +use openssl::{ + pkey::PKey, + ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode}, + x509::X509, +}; + +fn tls_config() -> SslAcceptor { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); + let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); -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_certificate(&cert).unwrap(); + builder.set_private_key(&key).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) + Err(openssl::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 || { + let srv = test_server(|| { HttpService::build() .h2(map_config( App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()) }) .await; @@ -48,7 +58,7 @@ async fn test_connection_window_size() { .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); let client = awc::Client::builder() - .connector(awc::Connector::new().ssl(builder.build()).finish()) + .connector(awc::Connector::new().ssl(builder.build())) .initial_window_size(100) .initial_connection_window_size(100) .finish(); diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 2da3d9696..652997de6 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -1,6 +1,6 @@ #![cfg(feature = "rustls")] -extern crate rust_tls as rustls; +extern crate tls_rustls as rustls; use std::{ io::BufReader, @@ -8,44 +8,59 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, }, + time::SystemTime, }; use actix_http::HttpService; use actix_http_test::test_server; -use actix_service::{map_config, pipeline_factory, ServiceFactoryExt}; +use actix_service::{fn_service, map_config, ServiceFactoryExt}; +use actix_tls::connect::rustls::webpki_roots_cert_store; +use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; -use futures_util::future::ok; -use rustls::internal::pemfile::{certs, pkcs8_private_keys}; -use rustls::{ClientConfig, NoClientAuth, ServerConfig}; +use rustls::{ + client::{ServerCertVerified, ServerCertVerifier}, + Certificate, ClientConfig, PrivateKey, ServerConfig, ServerName, +}; +use rustls_pemfile::{certs, pkcs8_private_keys}; fn tls_config() -> ServerConfig { let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap(); let key_file = cert.serialize_private_key_pem(); - let mut config = ServerConfig::new(NoClientAuth::new()); let cert_file = &mut BufReader::new(cert_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes()); - let cert_chain = certs(cert_file).unwrap(); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); - config + ServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap() } mod danger { + use super::*; + pub struct NoCertificateVerification; - impl rustls::ServerCertVerifier for NoCertificateVerification { + impl ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, - _roots: &rustls::RootCertStore, - _presented_certs: &[rustls::Certificate], - _dns_name: webpki::DNSNameRef<'_>, - _ocsp: &[u8], - ) -> Result { - Ok(rustls::ServerCertVerified::assertion()) + _end_entity: &Certificate, + _intermediates: &[Certificate], + _server_name: &ServerName, + _scts: &mut dyn Iterator, + _ocsp_response: &[u8], + _now: SystemTime, + ) -> Result { + Ok(ServerCertVerified::assertion()) } } } @@ -57,15 +72,14 @@ async fn test_connection_reuse_h2() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) .and_then( HttpService::build() .h2(map_config( - App::new() - .service(web::resource("/").route(web::to(HttpResponse::Ok))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) .rustls(tls_config()) @@ -74,16 +88,21 @@ async fn test_connection_reuse_h2() { }) .await; - // disable TLS verification - let mut config = ClientConfig::new(); + let mut config = ClientConfig::builder() + .with_safe_defaults() + .with_root_certificates(webpki_roots_cert_store()) + .with_no_client_auth(); + let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; - config.set_protocols(&protos); + config.alpn_protocols = protos; + + // disable TLS verification config .dangerous() .set_certificate_verifier(Arc::new(danger::NoCertificateVerification)); let client = awc::Client::builder() - .connector(awc::Connector::new().rustls(Arc::new(config)).finish()) + .connector(awc::Connector::new().rustls(Arc::new(config))) .finish(); // req 1 diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index de1514042..811efd4bc 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -1,33 +1,43 @@ #![cfg(feature = "openssl")] + +extern crate tls_openssl as openssl; + use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::Arc; use actix_http::HttpService; use actix_http_test::test_server; -use actix_service::{map_config, pipeline_factory, ServiceFactoryExt}; +use actix_service::{fn_service, map_config, ServiceFactoryExt}; +use actix_utils::future::ok; use actix_web::http::Version; use actix_web::{dev::AppConfig, web, App, HttpResponse}; -use futures_util::future::ok; -use open_ssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode}; +use openssl::{ + pkey::PKey, + ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode}, + x509::X509, +}; + +fn tls_config() -> SslAcceptor { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); + let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); -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_certificate(&cert).unwrap(); + builder.set_private_key(&key).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) + Err(openssl::ssl::AlpnError::NOACK) } }); builder.set_alpn_protos(b"\x02h2").unwrap(); + builder.build() } @@ -38,18 +48,17 @@ async fn test_connection_reuse_h2() { let srv = test_server(move || { let num2 = num2.clone(); - pipeline_factory(move |io| { + fn_service(move |io| { num2.fetch_add(1, Ordering::Relaxed); ok(io) }) .and_then( HttpService::build() .h2(map_config( - App::new() - .service(web::resource("/").route(web::to(HttpResponse::Ok))), + App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) - .openssl(ssl_acceptor()) + .openssl(tls_config()) .map_err(|_| ()), ) }) @@ -63,7 +72,7 @@ async fn test_connection_reuse_h2() { .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); let client = awc::Client::builder() - .connector(awc::Connector::new().ssl(builder.build()).finish()) + .connector(awc::Connector::new().ssl(builder.build())) .finish(); // req 1 diff --git a/awc/tests/test_ws.rs b/awc/tests/test_ws.rs index 8eb912dac..bfc81afbc 100644 --- a/awc/tests/test_ws.rs +++ b/awc/tests/test_ws.rs @@ -3,9 +3,9 @@ use std::io; use actix_codec::Framed; use actix_http::{body::BodySize, h1, ws, Error, HttpService, Request, Response}; use actix_http_test::test_server; +use actix_utils::future::ok; use bytes::Bytes; -use futures_util::future::ok; -use futures_util::{SinkExt, StreamExt}; +use futures_util::{SinkExt as _, StreamExt as _}; async fn ws_service(req: ws::Frame) -> Result { match req { @@ -31,12 +31,12 @@ async fn test_simple() { .send(h1::Message::Item((res.drop_body(), BodySize::None))) .await?; - // start websocket service + // start WebSocket service let framed = framed.replace_codec(ws::Codec::new()); ws::Dispatcher::with(framed, ws_service).await } }) - .finish(|_| ok::<_, Error>(Response::NotFound())) + .finish(|_| ok::<_, Error>(Response::not_found())) .tcp() }) .await; diff --git a/benches/responder.rs b/benches/responder.rs index 61180d575..20aae3351 100644 --- a/benches/responder.rs +++ b/benches/responder.rs @@ -1,12 +1,12 @@ -use std::future::Future; -use std::time::Instant; +use std::{future::Future, time::Instant}; -use actix_http::Response; -use actix_web::http::StatusCode; -use actix_web::test::TestRequest; -use actix_web::{error, Error, HttpRequest, HttpResponse, Responder}; +use actix_http::body::BoxBody; +use actix_utils::future::{ready, Ready}; +use actix_web::{ + error, http::StatusCode, test::TestRequest, Error, HttpRequest, HttpResponse, Responder, +}; use criterion::{criterion_group, criterion_main, Criterion}; -use futures_util::future::{ready, Either, Ready}; +use futures_util::future::{join_all, Either}; // responder simulate the old responder trait. trait FutureResponder { @@ -24,11 +24,11 @@ struct StringResponder(String); impl FutureResponder for StringResponder { type Error = Error; - type Future = Ready>; + type Future = Ready>; fn future_respond_to(self, _: &HttpRequest) -> Self::Future { // this is default builder for string response in both new and old responder trait. - ready(Ok(Response::build(StatusCode::OK) + ready(Ok(HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0))) } @@ -37,7 +37,7 @@ impl FutureResponder for StringResponder { impl FutureResponder for OptionResponder where T: FutureResponder, - T::Future: Future>, + T::Future: Future>, { type Error = Error; type Future = Either>>; @@ -51,24 +51,28 @@ where } impl Responder for StringResponder { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - Response::build(StatusCode::OK) + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + HttpResponse::build(StatusCode::OK) .content_type("text/plain; charset=utf-8") .body(self.0) } } impl Responder for OptionResponder { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = BoxBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self.0 { - Some(t) => t.respond_to(req), - None => Response::from_error(error::ErrorInternalServerError("err")), + Some(t) => t.respond_to(req).map_into_boxed_body(), + None => HttpResponse::from_error(error::ErrorInternalServerError("err")), } } } fn future_responder(c: &mut Criterion) { - let rt = actix_rt::System::new("test"); + let rt = actix_rt::System::new(); let req = TestRequest::default().to_http_request(); c.bench_function("future_responder", move |b| { @@ -79,7 +83,7 @@ fn future_responder(c: &mut Criterion) { .await }); - let futs = futures_util::future::join_all(futs); + let futs = join_all(futs); let start = Instant::now(); @@ -91,7 +95,7 @@ fn future_responder(c: &mut Criterion) { } fn responder(c: &mut Criterion) { - let rt = actix_rt::System::new("test"); + let rt = actix_rt::System::new(); let req = TestRequest::default().to_http_request(); c.bench_function("responder", move |b| { b.iter_custom(|_| { diff --git a/benches/server.rs b/benches/server.rs index 117b6136e..139e24abd 100644 --- a/benches/server.rs +++ b/benches/server.rs @@ -1,4 +1,4 @@ -use actix_web::{test, web, App, HttpResponse}; +use actix_web::{web, App, HttpResponse}; use awc::Client; use criterion::{criterion_group, criterion_main, Criterion}; use futures_util::future::join_all; @@ -29,13 +29,12 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ fn bench_async_burst(c: &mut Criterion) { // We are using System here, since Runtime requires preinitialized tokio // Maybe add to actix_rt docs - let rt = actix_rt::System::new("test"); + let rt = actix_rt::System::new(); let srv = rt.block_on(async { - test::start(|| { - App::new().service( - web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))), - ) + actix_test::start(|| { + App::new() + .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }) }); @@ -43,24 +42,25 @@ fn bench_async_burst(c: &mut Criterion) { c.bench_function("get_body_async_burst", move |b| { b.iter_custom(|iters| { - let client = - rt.block_on(async { Client::new().get(url.clone()).freeze().unwrap() }); + rt.block_on(async { + let client = Client::new().get(url.clone()).freeze().unwrap(); + + let start = std::time::Instant::now(); + // benchmark body - 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(); + let resps = join_all(burst).await; - // 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); - }; + let elapsed = start.elapsed(); - 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 + }) }) }); } diff --git a/benches/service.rs b/benches/service.rs index 8ca6cbe28..87e51f170 100644 --- a/benches/service.rs +++ b/benches/service.rs @@ -9,7 +9,7 @@ use actix_web::test::{init_service, ok_service, TestRequest}; /// Criterion Benchmark for async Service /// Should be used from within criterion group: -/// ```rust,ignore +/// ```ignore /// let mut criterion: ::criterion::Criterion<_> = /// ::criterion::Criterion::default().configure_from_args(); /// bench_async_service(&mut criterion, ok_service(), "async_service_direct"); @@ -25,7 +25,7 @@ pub fn bench_async_service(c: &mut Criterion, srv: S, name: &str) where S: Service + 'static, { - let rt = actix_rt::System::new("test"); + let rt = actix_rt::System::new(); let srv = Rc::new(RefCell::new(srv)); let req = TestRequest::default().to_srv_request(); @@ -51,9 +51,8 @@ where fut.await.unwrap(); } }); - let elapsed = start.elapsed(); // check that at least first request succeeded - elapsed + start.elapsed() }) }); } @@ -67,7 +66,7 @@ async fn index(req: ServiceRequest) -> Result { // Sample results on MacBook Pro '14 // time: [2.0724 us 2.1345 us 2.2074 us] fn async_web_service(c: &mut Criterion) { - let rt = actix_rt::System::new("test"); + let rt = actix_rt::System::new(); let srv = Rc::new(RefCell::new(rt.block_on(init_service( App::new().service(web::service("/").finish(index)), )))); @@ -93,9 +92,8 @@ fn async_web_service(c: &mut Criterion) { fut.await.unwrap(); } }); - let elapsed = start.elapsed(); // check that at least first request succeeded - elapsed + start.elapsed() }) }); } diff --git a/clippy.toml b/clippy.toml new file mode 100644 index 000000000..cef91fde7 --- /dev/null +++ b/clippy.toml @@ -0,0 +1 @@ +msrv = "1.52" diff --git a/codecov.yml b/codecov.yml index e6bc40203..d80835c7f 100644 --- a/codecov.yml +++ b/codecov.yml @@ -4,13 +4,12 @@ coverage: status: project: default: - threshold: 10% # make CI green + threshold: 100% # make CI green patch: default: - threshold: 10% # make CI green + threshold: 100% # make CI green ignore: # ignore code coverage on following paths - "**/tests" - - "test-server" - "**/benches" - "**/examples" diff --git a/docs/graphs/net-only.dot b/docs/graphs/net-only.dot index 9488f3fe7..8a58ec2b8 100644 --- a/docs/graphs/net-only.dot +++ b/docs/graphs/net-only.dot @@ -1,21 +1,36 @@ digraph { + rankdir=TB + subgraph cluster_net { - label="actix/actix-net"; - "actix-codec" - "actix-macros" - "actix-rt" - "actix-server" - "actix-service" - "actix-threadpool" - "actix-tls" - "actix-tracing" - "actix-utils" - "actix-router" + label="actix-net" + "actix-codec" "actix-macros" "actix-rt" "actix-server" "actix-service" + "actix-tls" "actix-tracing" "actix-utils" + } + + subgraph cluster_other { + label="other actix owned crates" + { rank=same; "local-channel" "local-waker" "bytestring" } } - "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } + subgraph cluster_tokio { + label="tokio" + "tokio" "tokio-util" + } + + "actix-codec" -> { "tokio" } + "actix-codec" -> { "tokio-util" }[color=red] + "actix-utils" -> { "local-waker" } "actix-tracing" -> { "actix-service" } "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" "actix-rt" } - "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } - "actix-rt" -> { "actix-macros" "actix-threadpool" } + "actix-tls" -> { "tokio-util" }[color="#009900"] + "actix-server" -> { "actix-service" "actix-rt" "actix-utils" "tokio" } + "actix-rt" -> { "actix-macros" "tokio" } + + "local-channel" -> { "local-waker" } + + // invisible edges to force nicer layout + edge [style=invis] + "actix-macros" -> "tokio" + "actix-service" -> "bytestring" + "actix-macros" -> "bytestring" } diff --git a/docs/graphs/web-focus.dot b/docs/graphs/web-focus.dot index 55a82bb41..63b3eaa82 100644 --- a/docs/graphs/web-focus.dot +++ b/docs/graphs/web-focus.dot @@ -1,30 +1,45 @@ digraph { subgraph cluster_web { - label="actix/actix-web" + label="actix/web" + "awc" - "actix-web" - "actix-files" - "actix-http" - "actix-multipart" - "actix-web-actors" - "actix-web-codegen" - "actix-http-test" + "web" + "files" + "http" + "multipart" + "web-actors" + "web-codegen" + "http-test" + "router" + + { rank=same; "multipart" "web-actors" "http-test" }; + { rank=same; "files" "awc" "web" }; + { rank=same; "web-codegen" "http" }; } - "actix-web" -> { "actix-codec" "actix-service" "actix-utils" "actix-router" "actix-rt" "actix-server" "macros" "threadpool" "actix-tls" "actix-web-codegen" "actix-http" "awc" } - "awc" -> { "actix-codec" "actix-service" "actix-http" "actix-rt" } - "actix-web-actors" -> { "actix" "actix-web" "actix-http" "actix-codec" } - "actix-multipart" -> { "actix-web" "actix-service" "actix-utils" } - "actix-http" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "threadpool" } - "actix-http" -> { "actix" "actix-tls" }[color=blue] // optional - "actix-files" -> { "actix-web" } - "actix-http-test" -> { "actix-service" "actix-codec" "actix-tls" "actix-utils" "actix-rt" "actix-server" "awc" } + "web" -> { "codec" "service" "utils" "router" "rt" "server" "macros" "web-codegen" "http" "awc" } + "web" -> { "tls" }[color=blue] // optional + "awc" -> { "codec" "service" "http" "rt" } + "web-actors" -> { "actix" "web" "http" "codec" } + "multipart" -> { "web" "service" "utils" } + "http" -> { "service" "codec" "utils" "rt" } + "http" -> { "tls" }[color=blue] // optional + "files" -> { "web" } + "http-test" -> { "service" "codec" "utils" "rt" "server" "awc" } + "http-test" -> { "tls" }[color=blue] // optional // net - "actix-utils" -> { "actix-service" "actix-rt" "actix-codec" } - "actix-tracing" -> { "actix-service" } - "actix-tls" -> { "actix-service" "actix-codec" "actix-utils" } - "actix-server" -> { "actix-service" "actix-rt" "actix-codec" "actix-utils" } - "actix-rt" -> { "macros" "threadpool" } + "utils" -> { "service" "rt" "codec" } + "tracing" -> { "service" } + "tls" -> { "service" "codec" "utils" } + "server" -> { "service" "rt" "codec" "utils" } + "rt" -> { "macros" } + + { rank=same; "utils" "codec" }; + { rank=same; "rt" "macros" "service" }; + + // actix + + "actix" -> { "rt" } } diff --git a/docs/graphs/web-only.dot b/docs/graphs/web-only.dot index 9e1bb2805..ee74c292b 100644 --- a/docs/graphs/web-only.dot +++ b/docs/graphs/web-only.dot @@ -9,13 +9,15 @@ digraph { "actix-web-actors" "actix-web-codegen" "actix-http-test" + "actix-test" + "actix-router" } - "actix-web" -> { "actix-web-codegen" "actix-http" "awc" } + "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" } "awc" -> { "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-multipart" -> { "actix-web" } - "actix-http" -> { "actix" }[color=blue] // optional "actix-files" -> { "actix-web" } "actix-http-test" -> { "awc" } + "actix-test" -> { "actix-web" "awc" "actix-http-test" } } diff --git a/examples/basic.rs b/examples/basic.rs index e8ad5fcdb..d29546129 100644 --- a/examples/basic.rs +++ b/examples/basic.rs @@ -29,17 +29,13 @@ async fn main() -> std::io::Result<()> { .service(no_params) .service( web::resource("/resource2/index.html") - .wrap( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_service( - web::route().to(|| HttpResponse::MethodNotAllowed()), - ) + .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) + .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) - .bind("127.0.0.1:8080")? + .bind(("127.0.0.1", 8080))? .workers(1) .run() .await diff --git a/examples/on_connect.rs b/examples/on_connect.rs index ba5a18f3f..9709835e6 100644 --- a/examples/on_connect.rs +++ b/examples/on_connect.rs @@ -2,12 +2,13 @@ //! properties and pass them to a handler through request-local data. //! //! For an example of extracting a client TLS certificate, see: -//! +//! use std::{any::Any, io, net::SocketAddr}; use actix_web::{dev::Extensions, rt::net::TcpStream, web, App, HttpServer}; +#[allow(dead_code)] #[derive(Debug, Clone)] struct ConnectionInfo { bind: SocketAddr, diff --git a/examples/uds.rs b/examples/uds.rs index c0a5d76a6..1db252fef 100644 --- a/examples/uds.rs +++ b/examples/uds.rs @@ -33,12 +33,8 @@ async fn main() -> std::io::Result<()> { .service(no_params) .service( web::resource("/resource2/index.html") - .wrap( - middleware::DefaultHeaders::new().header("X-Version-R2", "0.3"), - ) - .default_service( - web::route().to(|| HttpResponse::MethodNotAllowed()), - ) + .wrap(middleware::DefaultHeaders::new().header("X-Version-R2", "0.3")) + .default_service(web::route().to(HttpResponse::MethodNotAllowed)) .route(web::get().to(index_async)), ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) diff --git a/rustfmt.toml b/rustfmt.toml index 94bd11d51..973e002c0 100644 --- a/rustfmt.toml +++ b/rustfmt.toml @@ -1,2 +1,2 @@ -max_width = 89 +max_width = 96 reorder_imports = true diff --git a/scripts/bump b/scripts/bump new file mode 100755 index 000000000..8b6a3c424 --- /dev/null +++ b/scripts/bump @@ -0,0 +1,111 @@ +#!/bin/sh + +# developed on macOS and probably doesn't work on Linux yet due to minor +# differences in flags on sed + +# requires github cli tool for automatic release draft creation + +set -euo pipefail + +DIR=$1 + +LINUX="" +MACOS="" + +if [ "$(uname)" = "Darwin" ]; then + MACOS="1" +fi + +CARGO_MANIFEST=$DIR/Cargo.toml +CHANGELOG_FILE=$DIR/CHANGES.md +README_FILE=$DIR/README.md + +# get current version +PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)" +CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")" + +CHANGE_CHUNK_FILE="$(mktemp)" +echo saving changelog to $CHANGE_CHUNK_FILE +echo + +# get changelog chunk and save to temp file +cat "$CHANGELOG_FILE" | + # skip up to unreleased heading + sed '1,/Unreleased/ d' | + # take up to previous version heading + sed "/$CURRENT_VERSION/ q" | + # drop last line + sed '$d' \ + >"$CHANGE_CHUNK_FILE" + +# if word count of changelog chunk is 0 then insert filler changelog chunk +if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then + echo "* No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" +fi + +if [ -n "${2-}" ]; then + NEW_VERSION="$2" +else + echo + echo "--- Changes since $CURRENT_VERSION ----" + cat "$CHANGE_CHUNK_FILE" + echo + read -p "Update version to: " NEW_VERSION +fi + +DATE="$(date -u +"%Y-%m-%d")" +echo "updating from $CURRENT_VERSION => $NEW_VERSION ($DATE)" + +# update package.version field +sed -i.bak -E "s/^version ?= ?\"[^\"]+\"$/version = \"$NEW_VERSION\"/" "$CARGO_MANIFEST" + +# update readme +[ -f "$README_FILE" ] && sed -i.bak -E "s#$CURRENT_VERSION([/)])#$NEW_VERSION\1#g" "$README_FILE" + +# update changelog file +( + sed '/Unreleased/ q' "$CHANGELOG_FILE" # up to unreleased heading + echo # blank line + echo # blank line + echo "## $NEW_VERSION - $DATE" # new version heading + cat "$CHANGE_CHUNK_FILE" # previously unreleased changes + sed "/$CURRENT_VERSION/ q" "$CHANGELOG_FILE" | tail -n 1 # the previous version heading + sed "1,/$CURRENT_VERSION/ d" "$CHANGELOG_FILE" # everything after previous version heading +) >"$CHANGELOG_FILE.bak" +mv "$CHANGELOG_FILE.bak" "$CHANGELOG_FILE" + +# done; remove backup files +rm -f $CARGO_MANIFEST.bak +rm -f $CHANGELOG_FILE.bak +rm -f $README_FILE.bak + +echo "manifest, changelog, and readme updated" +echo +echo "check other references:" +rg "$PACKAGE_NAME =" || true +rg "package = \"$PACKAGE_NAME\"" || true + +if [ $MACOS ]; then + printf "prepare $PACKAGE_NAME release $NEW_VERSION" | pbcopy +else + echo + echo "commit message:" + echo "prepare $PACKAGE_NAME release $NEW_VERSION" +fi + +SHORT_PACKAGE_NAME="$(echo $PACKAGE_NAME | sed 's/^actix-web-//' | sed 's/^actix-//')" +GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)" +RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)" + +echo +echo "GitHub release command:" +echo "gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" --prerelease" + +read -p "Submit draft GH release: (y/N) " GH_RELEASE +GH_RELEASE="${GH_RELEASE:-n}" + +if [ "$GH_RELEASE" = 'y' ] || [ "$GH_RELEASE" = 'Y' ]; then + gh release create "$GIT_TAG" --draft --title "$RELEASE_TITLE" --notes-file "$CHANGE_CHUNK_FILE" --prerelease +fi + +echo diff --git a/scripts/ci-test b/scripts/ci-test new file mode 100755 index 000000000..98e13927d --- /dev/null +++ b/scripts/ci-test @@ -0,0 +1,18 @@ +#!/bin/sh + +# run tests matching what CI does for non-linux feature sets + +set -x + +cargo test --lib --tests -p=actix-router --all-features +cargo test --lib --tests -p=actix-http --all-features +cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls +cargo test --lib --tests -p=actix-web-codegen --all-features +cargo test --lib --tests -p=awc --all-features +cargo test --lib --tests -p=actix-http-test --all-features +cargo test --lib --tests -p=actix-test --all-features +cargo test --lib --tests -p=actix-files +cargo test --lib --tests -p=actix-multipart --all-features +cargo test --lib --tests -p=actix-web-actors --all-features + +cargo test --workspace --doc diff --git a/src/app.rs b/src/app.rs index 9c5b806ea..efc108cb9 100644 --- a/src/app.rs +++ b/src/app.rs @@ -1,38 +1,35 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::marker::PhantomData; -use std::rc::Rc; +use std::{cell::RefCell, fmt, future::Future, marker::PhantomData, rc::Rc}; -use actix_http::body::{Body, MessageBody}; -use actix_http::{Extensions, Request}; -use actix_service::boxed::{self, BoxServiceFactory}; +use actix_http::{ + body::{BoxBody, MessageBody}, + Extensions, Request, +}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, + apply, apply_fn_factory, boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt, Transform, }; -use futures_util::future::FutureExt; +use futures_util::future::FutureExt as _; -use crate::app_service::{AppEntry, AppInit, AppRoutingFactory}; -use crate::config::ServiceConfig; -use crate::data::{Data, DataFactory, FnDataFactory}; -use crate::dev::ResourceDef; -use crate::error::Error; -use crate::resource::Resource; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest, - ServiceResponse, +use crate::{ + app_service::{AppEntry, AppInit, AppRoutingFactory}, + config::ServiceConfig, + data::{Data, DataFactory, FnDataFactory}, + dev::ResourceDef, + error::Error, + resource::Resource, + route::Route, + service::{ + AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, + ServiceRequest, ServiceResponse, + }, }; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; - /// Application builder - structure that follows the builder pattern /// for building application instances. pub struct App { endpoint: T, services: Vec>, - default: Option>, + default: Option>, factory_ref: Rc>>, data_factories: Vec, external: Vec, @@ -40,17 +37,18 @@ pub struct App { _phantom: PhantomData, } -impl App { +impl App { /// Create application builder. Application can be configured with a builder-like pattern. #[allow(clippy::new_without_default)] pub fn new() -> Self { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); + App { - endpoint: AppEntry::new(fref.clone()), + endpoint: AppEntry::new(factory_ref.clone()), data_factories: Vec::new(), services: Vec::new(), default: None, - factory_ref: fref, + factory_ref, external: Vec::new(), extensions: Extensions::new(), _phantom: PhantomData, @@ -69,43 +67,79 @@ where InitError = (), >, { - /// Set application data. Application data could be accessed - /// by using `Data` extractor where `T` is data type. + /// Set application (root level) data. /// - /// **Note**: http server accepts an application factory rather than - /// an application instance. Http server constructs an application - /// instance for each thread, thus application data must be constructed - /// multiple times. If you want to share data between different - /// threads, a shared object should be used, e.g. `Arc`. Internally `Data` type - /// uses `Arc` so data could be created outside of app factory and clones could - /// be stored via `App::app_data()` method. + /// Application data stored with `App::app_data()` method is available through the + /// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime. /// - /// ```rust + /// # [`Data`] + /// Any [`Data`] type added here can utilize it's extractor implementation in handlers. + /// Types not wrapped in `Data` cannot use this extractor. See [its docs](Data) for more + /// about its usage and patterns. + /// + /// ``` /// use std::cell::Cell; - /// use actix_web::{web, App, HttpResponse, Responder}; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; /// /// struct MyData { - /// counter: Cell, + /// count: std::cell::Cell, /// } /// - /// async fn index(data: web::Data) -> impl Responder { - /// data.counter.set(data.counter.get() + 1); - /// HttpResponse::Ok() + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) /// } /// - /// let app = App::new() - /// .data(MyData{ counter: Cell::new(0) }) - /// .service( - /// web::resource("/index.html").route( - /// web::get().to(index))); + /// let app = App::new().service( + /// web::resource("/") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route(web::get().to(handler)) + /// ); /// ``` + /// + /// # Shared Mutable State + /// [`HttpServer::new`](crate::HttpServer::new) accepts an application factory rather than an + /// application instance; the factory closure is called on each worker thread independently. + /// Therefore, if you want to share a data object between different workers, a shareable object + /// needs to be created first, outside the `HttpServer::new` closure and cloned into it. + /// [`Data`] is an example of such a sharable object. + /// + /// ```ignore + /// let counter = web::Data::new(AppStateWithCounter { + /// counter: Mutex::new(0), + /// }); + /// + /// HttpServer::new(move || { + /// // move counter object into the closure and clone for each worker + /// + /// App::new() + /// .app_data(counter.clone()) + /// .route("/", web::get().to(handler)) + /// }) + /// ``` + pub fn app_data(mut self, ext: U) -> Self { + self.extensions.insert(ext); + self + } + + /// Add application (root) data after wrapping in `Data`. + /// + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(self, data: U) -> Self { self.app_data(Data::new(data)) } - /// Set application data factory. This function is - /// similar to `.data()` but it accepts data factory. Data object get - /// constructed asynchronously during application initialization. + /// Add application data factory. This function is similar to `.data()` but it accepts a + /// "data factory". Data values are constructed asynchronously during application + /// initialization, before the server starts accepting requests. pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -134,18 +168,6 @@ where self } - /// Set application level arbitrary data item. - /// - /// Application data stored with `App::app_data()` method is available - /// via `HttpRequest::app_data()` method at runtime. - /// - /// This method could be used for storing `Data` as well, in that case - /// data could be accessed by using `Data` extractor. - pub fn app_data(mut self, ext: U) -> Self { - self.extensions.insert(ext); - self - } - /// Run external configuration as part of the application building /// process /// @@ -153,7 +175,7 @@ where /// different module or even library. For example, /// some of the resource's configuration could be moved to different module. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// // this function could be located in different module @@ -186,7 +208,7 @@ where /// This method can be used multiple times with same path, in that case /// multiple resources with one route would be registered for same resource path. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -207,11 +229,11 @@ where ) } - /// Register http service. + /// Register HTTP service. /// /// Http service is any type that implements `HttpServiceFactory` trait. /// - /// Actix web provides several services implementations: + /// Actix Web provides several services implementations: /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. @@ -229,7 +251,7 @@ where /// /// It is possible to use services like `Resource`, `Route`. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index() -> &'static str { @@ -247,7 +269,7 @@ where /// /// It is also possible to use static files as default service. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { @@ -259,7 +281,7 @@ where /// ); /// } /// ``` - pub fn default_service(mut self, f: F) -> Self + pub fn default_service(mut self, svc: F) -> Self where F: IntoServiceFactory, U: ServiceFactory< @@ -270,10 +292,12 @@ where > + 'static, U::InitError: fmt::Debug, { - // create and configure default resource - self.default = Some(Rc::new(boxed::factory(f.into_factory().map_init_err( - |e| log::error!("Can not construct default service: {:?}", e), - )))); + let svc = svc + .into_factory() + .map(|res| res.map_into_boxed_body()) + .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)); + + self.default = Some(Rc::new(boxed::factory(svc))); self } @@ -284,7 +308,7 @@ where /// and are never considered for matching at request time. Calls to /// `HttpRequest::url_for()` will work as expected. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpRequest, HttpResponse, Result}; /// /// async fn index(req: HttpRequest) -> Result { @@ -306,7 +330,7 @@ where U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); - *rdef.name_mut() = name.as_ref().to_string(); + rdef.set_name(name.as_ref()); self.external.push(rdef); self } @@ -326,7 +350,7 @@ where /// the builder chain. Consequently, the *first* middleware registered /// in the builder chain is the *last* to execute during request processing. /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{middleware, web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; @@ -383,7 +407,7 @@ where /// /// Use middleware when you need to read or modify *every* request or response in some way. /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; @@ -422,7 +446,7 @@ where > where B1: MessageBody, - F: FnMut(ServiceRequest, &mut T::Service) -> R + Clone, + F: Fn(ServiceRequest, &T::Service) -> R + Clone, R: Future, Error>>, { App { @@ -466,24 +490,20 @@ where #[cfg(test)] mod tests { use actix_service::Service; + use actix_utils::future::{err, ok}; use bytes::Bytes; - use futures_util::future::{err, ok}; use super::*; use crate::http::{header, HeaderValue, Method, StatusCode}; use crate::middleware::DefaultHeaders; use crate::service::ServiceRequest; - use crate::test::{ - call_service, init_service, read_body, try_init_service, TestRequest, - }; + use crate::test::{call_service, init_service, read_body, try_init_service, TestRequest}; use crate::{web, HttpRequest, HttpResponse}; #[actix_rt::test] async fn test_default_resource() { - let mut srv = init_service( - App::new().service(web::resource("/test").to(HttpResponse::Ok)), - ) - .await; + let srv = + init_service(App::new().service(web::resource("/test").to(HttpResponse::Ok))).await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); @@ -492,7 +512,7 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::NOT_FOUND); - let mut srv = init_service( + let srv = init_service( App::new() .service(web::resource("/test").to(HttpResponse::Ok)) .service( @@ -523,46 +543,53 @@ mod tests { assert_eq!(resp.status(), StatusCode::CREATED); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory() { - let mut srv = - init_service(App::new().data_factory(|| ok::<_, ()>(10usize)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .data_factory(|| ok::<_, ()>(10usize)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = - init_service(App::new().data_factory(|| ok::<_, ()>(10u32)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .data_factory(|| ok::<_, ()>(10u32)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_factory_errors() { - let srv = - try_init_service(App::new().data_factory(|| err::(())).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = try_init_service( + App::new() + .data_factory(|| err::(())) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; assert!(srv.is_err()); } #[actix_rt::test] async fn test_extension() { - let mut srv = init_service(App::new().app_data(10usize).service( - web::resource("/").to(|req: HttpRequest| { + let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( + |req: HttpRequest| { assert_eq!(*req.app_data::().unwrap(), 10); HttpResponse::Ok() - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); @@ -571,7 +598,7 @@ mod tests { #[actix_rt::test] async fn test_wrap() { - let mut srv = init_service( + let srv = init_service( App::new() .wrap( DefaultHeaders::new() @@ -581,7 +608,7 @@ mod tests { ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -591,7 +618,7 @@ mod tests { #[actix_rt::test] async fn test_router_wrap() { - let mut srv = init_service( + let srv = init_service( App::new() .route("/test", web::get().to(HttpResponse::Ok)) .wrap( @@ -601,7 +628,7 @@ mod tests { ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -611,16 +638,14 @@ mod tests { #[actix_rt::test] async fn test_wrap_fn() { - let mut srv = init_service( + let srv = init_service( App::new() .wrap_fn(|req, srv| { let fut = srv.call(req); async move { let mut res = fut.await?; - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }) @@ -628,7 +653,7 @@ mod tests { ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -638,24 +663,22 @@ mod tests { #[actix_rt::test] async fn test_router_wrap_fn() { - let mut srv = init_service( + let srv = init_service( App::new() .route("/test", web::get().to(HttpResponse::Ok)) .wrap_fn(|req, srv| { let fut = srv.call(req); async { let mut res = fut.await?; - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }), ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -665,21 +688,20 @@ mod tests { #[actix_rt::test] async fn test_external_resource() { - let mut srv = init_service( + let srv = init_service( App::new() .external_resource("youtube", "https://youtube.com/watch/{video_id}") .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body( - req.url_for("youtube", &["12345"]).unwrap().to_string(), - ) + HttpResponse::Ok() + .body(req.url_for("youtube", &["12345"]).unwrap().to_string()) }), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); diff --git a/src/app_service.rs b/src/app_service.rs index 8a00f59eb..bca8f2629 100644 --- a/src/app_service.rs +++ b/src/app_service.rs @@ -1,25 +1,25 @@ -use std::cell::RefCell; -use std::rc::Rc; -use std::task::Poll; +use std::{cell::RefCell, mem, rc::Rc}; -use actix_http::{Extensions, Request, Response}; +use actix_http::{Extensions, Request}; use actix_router::{Path, ResourceDef, Router, Url}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; -use actix_service::{fn_service, Service, ServiceFactory}; +use actix_service::{boxed, fn_service, Service, ServiceFactory}; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::config::{AppConfig, AppService}; -use crate::data::FnDataFactory; -use crate::error::Error; -use crate::guard::Guard; -use crate::request::{HttpRequest, HttpRequestPool}; -use crate::rmap::ResourceMap; -use crate::service::{AppServiceFactory, ServiceRequest, ServiceResponse}; +use crate::{ + config::{AppConfig, AppService}, + data::FnDataFactory, + guard::Guard, + request::{HttpRequest, HttpRequestPool}, + rmap::ResourceMap, + service::{ + AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, ServiceRequest, + ServiceResponse, + }, + Error, HttpResponse, +}; type Guards = Vec>; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Service factory to convert `Request` to a `ServiceRequest`. /// It also executes data factories. @@ -37,7 +37,7 @@ where pub(crate) extensions: RefCell>, pub(crate) async_data_factories: Rc<[FnDataFactory]>, pub(crate) services: Rc>>>, - pub(crate) default: Option>, + pub(crate) default: Option>, pub(crate) factory_ref: Rc>>, pub(crate) external: RefCell>, } @@ -65,7 +65,7 @@ where // if no user defined default service exists. let default = self.default.clone().unwrap_or_else(|| { Rc::new(boxed::factory(fn_service(|req: ServiceRequest| async { - Ok(req.into_response(Response::NotFound().finish())) + Ok(req.into_response(HttpResponse::NotFound())) }))) }); @@ -73,11 +73,11 @@ where let mut config = AppService::new(config, default.clone()); // register services - std::mem::take(&mut *self.services.borrow_mut()) + mem::take(&mut *self.services.borrow_mut()) .into_iter() .for_each(|mut srv| srv.register(&mut config)); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); let (config, services) = config.into_services(); @@ -96,13 +96,13 @@ where }); // external resources - for mut rdef in std::mem::take(&mut *self.external.borrow_mut()) { + for mut rdef in mem::take(&mut *self.external.borrow_mut()) { rmap.add(&mut rdef, None); } // complete ResourceMap tree creation let rmap = Rc::new(rmap); - rmap.finish(rmap.clone()); + ResourceMap::finish(&rmap); // construct all async data factory futures let factory_futs = join_all(self.async_data_factories.iter().map(|f| f())); @@ -129,9 +129,9 @@ where let service = endpoint_fut.await?; // populate app data container from (async) data factories. - async_data_factories.iter().for_each(|factory| { + for factory in &async_data_factories { factory.create(&mut app_data); - }); + } Ok(AppInitService { service, @@ -142,7 +142,9 @@ where } } -/// Service that takes a [`Request`] and delegates to a service that take a [`ServiceRequest`]. +/// The [`Service`] that is passed to `actix-http`'s server builder. +/// +/// Wraps a service receiving a [`ServiceRequest`] into one receiving a [`Request`]. pub struct AppInitService where T: Service, Error = Error>, @@ -164,8 +166,7 @@ impl AppInitServiceState { Rc::new(AppInitServiceState { rmap, config, - // TODO: AppConfig can be used to pass user defined HttpRequestPool - // capacity. + // TODO: AppConfig can be used to pass user defined HttpRequestPool capacity. pool: HttpRequestPool::default(), }) } @@ -196,7 +197,7 @@ where actix_service::forward_ready!(service); - fn call(&mut self, req: Request) -> Self::Future { + fn call(&self, req: Request) -> Self::Future { let (head, payload) = req.into_parts(); let req = if let Some(mut req) = self.app_state.pool().pop() { @@ -227,8 +228,14 @@ where } pub struct AppRoutingFactory { - services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, - default: Rc, + services: Rc< + [( + ResourceDef, + BoxedHttpServiceFactory, + RefCell>, + )], + >, + default: Rc, } impl ServiceFactory for AppRoutingFactory { @@ -241,16 +248,15 @@ impl ServiceFactory for AppRoutingFactory { fn new_service(&self, _: ()) -> Self::Future { // construct all services factory future with it's resource def and guards. - let factory_fut = - join_all(self.services.iter().map(|(path, factory, guards)| { - let path = path.clone(); - let guards = guards.borrow_mut().take(); - let factory_fut = factory.new_service(()); - async move { - let service = factory_fut.await?; - Ok((path, guards, service)) - } - })); + let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { + let path = path.clone(); + let guards = guards.borrow_mut().take(); + let factory_fut = factory.new_service(()); + async move { + let service = factory_fut.await?; + Ok((path, guards, service)) + } + })); // construct default service factory future let default_fut = self.default.new_service(()); @@ -275,9 +281,10 @@ impl ServiceFactory for AppRoutingFactory { } } +/// The Actix Web router default entry point. pub struct AppRouting { - router: Router, - default: HttpService, + router: Router, + default: BoxedHttpService, } impl Service for AppRouting { @@ -287,8 +294,8 @@ impl Service for AppRouting { actix_service::always_ready!(); - fn call(&mut self, mut req: ServiceRequest) -> Self::Future { - let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { if !f.check(req.head()) { @@ -349,12 +356,14 @@ mod tests { } } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_drop_data() { let data = Arc::new(AtomicBool::new(false)); { - let mut app = init_service( + let app = init_service( App::new() .data(DropData(data.clone())) .service(web::resource("/test").to(HttpResponse::Ok)), diff --git a/src/config.rs b/src/config.rs index 8e22dc90d..9e77c0f96 100644 --- a/src/config.rs +++ b/src/config.rs @@ -17,8 +17,7 @@ use crate::service::{ }; type Guards = Vec>; -type HttpNewService = - boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; +type HttpNewService = boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Application configuration pub struct AppService { @@ -63,6 +62,8 @@ impl AppService { (self.config, self.services) } + /// Clones inner config and default service, returning new `AppService` with empty service list + /// marked as non-root. pub(crate) fn clone_config(&self) -> Self { AppService { config: self.config.clone(), @@ -72,12 +73,12 @@ impl AppService { } } - /// Service configuration + /// Returns reference to configuration. pub fn config(&self) -> &AppConfig { &self.config } - /// Default resource + /// Returns default handler factory. pub fn default_service(&self) -> Rc { self.default.clone() } @@ -93,23 +94,19 @@ impl AppService { F: IntoServiceFactory, S: ServiceFactory< ServiceRequest, - Config = (), Response = ServiceResponse, Error = Error, + Config = (), InitError = (), > + 'static, { - self.services.push(( - rdef, - boxed::factory(factory.into_factory()), - guards, - nested, - )); + self.services + .push((rdef, boxed::factory(factory.into_factory()), guards, nested)); } } -/// Application connection config -#[derive(Clone)] +/// Application connection config. +#[derive(Debug, Clone)] pub struct AppConfig { secure: bool, host: String, @@ -117,8 +114,14 @@ pub struct AppConfig { } impl AppConfig { - pub(crate) fn new(secure: bool, addr: SocketAddr, host: String) -> Self { - AppConfig { secure, addr, host } + pub(crate) fn new(secure: bool, host: String, addr: SocketAddr) -> Self { + AppConfig { secure, host, addr } + } + + /// Needed in actix-test crate. Semver exempt. + #[doc(hidden)] + pub fn __priv_test_new(secure: bool, host: String, addr: SocketAddr) -> Self { + AppConfig::new(secure, host, addr) } /// Server host name. @@ -141,14 +144,19 @@ impl AppConfig { pub fn local_addr(&self) -> SocketAddr { self.addr } + + #[cfg(test)] + pub(crate) fn set_host(&mut self, host: &str) { + self.host = host.to_owned(); + } } impl Default for AppConfig { fn default() -> Self { AppConfig::new( false, - "127.0.0.1:8080".parse().unwrap(), "localhost:8080".to_owned(), + "127.0.0.1:8080".parse().unwrap(), ) } } @@ -191,6 +199,7 @@ impl ServiceConfig { /// Add shared app data item. /// /// Counterpart to [`App::data()`](crate::App::data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] pub fn data(&mut self, data: U) -> &mut Self { self.app_data(Data::new(data)); self @@ -240,7 +249,7 @@ impl ServiceConfig { U: AsRef, { let mut rdef = ResourceDef::new(url.as_ref()); - *rdef.name_mut() = name.as_ref().to_string(); + rdef.set_name(name.as_ref()); self.external.push(rdef); self } @@ -256,6 +265,8 @@ mod tests { use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{web, App, HttpRequest, HttpResponse}; + // allow deprecated `ServiceConfig::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { let cfg = |cfg: &mut ServiceConfig| { @@ -263,12 +274,12 @@ mod tests { cfg.app_data(15u8); }; - let mut srv = init_service(App::new().configure(cfg).service( - web::resource("/").to(|_: web::Data, req: HttpRequest| { + let srv = init_service(App::new().configure(cfg).service(web::resource("/").to( + |_: web::Data, req: HttpRequest| { assert_eq!(*req.app_data::().unwrap(), 15u8); HttpResponse::Ok() - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); @@ -286,7 +297,7 @@ mod tests { // }); // }; - // let mut srv = + // let srv = // init_service(App::new().configure(cfg).service( // web::resource("/").to(|_: web::Data| HttpResponse::Ok()), // )); @@ -297,7 +308,7 @@ mod tests { // let cfg2 = |cfg: &mut ServiceConfig| { // cfg.data_factory(|| Ok::<_, ()>(10u32)); // }; - // let mut srv = init_service( + // let srv = init_service( // App::new() // .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())) // .configure(cfg2), @@ -309,26 +320,22 @@ mod tests { #[actix_rt::test] async fn test_external_resource() { - let mut srv = init_service( + let srv = init_service( App::new() .configure(|cfg| { - cfg.external_resource( - "youtube", - "https://youtube.com/watch/{video_id}", - ); + cfg.external_resource("youtube", "https://youtube.com/watch/{video_id}"); }) .route( "/test", web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body( - req.url_for("youtube", &["12345"]).unwrap().to_string(), - ) + HttpResponse::Ok() + .body(req.url_for("youtube", &["12345"]).unwrap().to_string()) }), ), ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345")); @@ -336,24 +343,22 @@ mod tests { #[actix_rt::test] async fn test_service() { - let mut srv = init_service(App::new().configure(|cfg| { - cfg.service( - web::resource("/test").route(web::get().to(HttpResponse::Created)), - ) - .route("/index.html", web::get().to(HttpResponse::Ok)); + let srv = init_service(App::new().configure(|cfg| { + cfg.service(web::resource("/test").route(web::get().to(HttpResponse::Created))) + .route("/index.html", web::get().to(HttpResponse::Ok)); })) .await; let req = TestRequest::with_uri("/test") .method(Method::GET) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/index.html") .method(Method::GET) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } } diff --git a/src/data.rs b/src/data.rs index dd0fbed0e..b29e4ecf4 100644 --- a/src/data.rs +++ b/src/data.rs @@ -1,14 +1,14 @@ -use std::any::type_name; -use std::ops::Deref; -use std::sync::Arc; +use std::{any::type_name, ops::Deref, sync::Arc}; -use actix_http::error::{Error, ErrorInternalServerError}; use actix_http::Extensions; -use futures_util::future::{err, ok, LocalBoxFuture, Ready}; +use actix_utils::future::{err, ok, Ready}; +use futures_core::future::LocalBoxFuture; +use serde::Serialize; -use crate::dev::Payload; -use crate::extract::FromRequest; -use crate::request::HttpRequest; +use crate::{ + dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest, + Error, +}; /// Data factory. pub(crate) trait DataFactory { @@ -27,7 +27,7 @@ pub(crate) type FnDataFactory = /// /// Application data can be accessed by using `Data` extractor where `T` is data type. /// -/// **Note**: http server accepts an application factory rather than an application instance. HTTP +/// **Note**: HTTP server accepts an application factory rather than an application instance. HTTP /// server constructs an application instance for each thread, thus application data must be /// constructed multiple times. If you want to share data between different threads, a shareable /// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send` @@ -36,7 +36,12 @@ pub(crate) type FnDataFactory = /// If route data is not set for a handler, using `Data` extractor would cause *Internal /// Server Error* response. /// -/// ```rust +// TODO: document `dyn T` functionality through converting an Arc +// TODO: note equivalence of req.app_data> and Data extractor +// TODO: note that data must be inserted using Data in order to extract it +/// +/// # Examples +/// ``` /// use std::sync::Mutex; /// use actix_web::{web, App, HttpResponse, Responder}; /// @@ -70,7 +75,9 @@ impl Data { pub fn new(state: T) -> Data { Data(Arc::new(state)) } +} +impl Data { /// Get reference to inner app data. pub fn get_ref(&self) -> &T { self.0.as_ref() @@ -102,8 +109,19 @@ impl From> for Data { } } +impl Serialize for Data +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + impl FromRequest for Data { - type Config = (); type Error = Error; type Future = Ready>; @@ -119,7 +137,7 @@ impl FromRequest for Data { type_name::(), ); err(ErrorInternalServerError( - "App data is not configured, to configure use App::data()", + "App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()", )) } } @@ -134,17 +152,19 @@ impl DataFactory for Data { #[cfg(test)] mod tests { - use actix_service::Service; - use std::sync::atomic::{AtomicUsize, Ordering}; - use super::*; - use crate::http::StatusCode; - use crate::test::{self, init_service, TestRequest}; - use crate::{web, App, HttpResponse}; + use crate::{ + dev::Service, + http::StatusCode, + test::{init_service, TestRequest}, + web, App, HttpResponse, + }; + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_data_extractor() { - let mut srv = init_service(App::new().data("TEST".to_string()).service( + let srv = init_service(App::new().data("TEST".to_string()).service( web::resource("/").to(|data: web::Data| { assert_eq!(data.to_lowercase(), "test"); HttpResponse::Ok() @@ -156,16 +176,17 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = - init_service(App::new().data(10u32).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .data(10u32) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); - let mut srv = init_service( + let srv = init_service( App::new() .data(10u32) .data(13u32) @@ -186,29 +207,33 @@ mod tests { #[actix_rt::test] async fn test_app_data_extractor() { - let mut srv = - init_service(App::new().app_data(Data::new(10usize)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .app_data(Data::new(10usize)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - let mut srv = - init_service(App::new().app_data(Data::new(10u32)).service( - web::resource("/").to(|_: web::Data| HttpResponse::Ok()), - )) - .await; + let srv = init_service( + App::new() + .app_data(Data::new(10u32)) + .service(web::resource("/").to(|_: web::Data| HttpResponse::Ok())), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_route_data_extractor() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::resource("/") .data(10usize) @@ -222,7 +247,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::OK); // different type - let mut srv = init_service( + let srv = init_service( App::new().service( web::resource("/") .data(10u32) @@ -235,71 +260,31 @@ mod tests { assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { - let mut srv = init_service(App::new().data(1usize).service( - web::resource("/").data(10usize).route(web::get().to( - |data: web::Data| { - assert_eq!(**data, 10); - HttpResponse::Ok() - }, - )), - )) - .await; + let srv = + init_service(App::new().data(1usize).service( + web::resource("/").data(10usize).route(web::get().to( + |data: web::Data| { + assert_eq!(**data, 10); + HttpResponse::Ok() + }, + )), + )) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); } - #[actix_rt::test] - async fn test_data_drop() { - struct TestData(Arc); - - impl TestData { - fn new(inner: Arc) -> Self { - let _ = inner.fetch_add(1, Ordering::SeqCst); - Self(inner) - } - } - - impl Clone for TestData { - fn clone(&self) -> Self { - let inner = self.0.clone(); - let _ = inner.fetch_add(1, Ordering::SeqCst); - Self(inner) - } - } - - impl Drop for TestData { - fn drop(&mut self) { - let _ = self.0.fetch_sub(1, Ordering::SeqCst); - } - } - - let num = Arc::new(AtomicUsize::new(0)); - let data = TestData::new(num.clone()); - assert_eq!(num.load(Ordering::SeqCst), 1); - - let srv = test::start(move || { - let data = data.clone(); - - App::new() - .data(data) - .service(web::resource("/").to(|_data: Data| async { "ok" })) - }); - - assert!(srv.get("/").send().await.unwrap().status().is_success()); - srv.stop().await; - - assert_eq!(num.load(Ordering::SeqCst), 0); - } - #[actix_rt::test] async fn test_data_from_arc() { let data_new = Data::new(String::from("test-123")); let data_from_arc = Data::from(Arc::new(String::from("test-123"))); - assert_eq!(data_new.0, data_from_arc.0) + assert_eq!(data_new.0, data_from_arc.0); } #[actix_rt::test] @@ -321,4 +306,38 @@ mod tests { let data_arc = Data::from(dyn_arc); assert_eq!(data_arc_box.get_num(), data_arc.get_num()) } + + #[actix_rt::test] + async fn test_dyn_data_into_arc() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + let arc_from_data = data_arc.clone().into_inner(); + assert_eq!(data_arc.get_num(), arc_from_data.get_num()) + } + + #[actix_rt::test] + async fn test_get_ref_from_dyn_data() { + trait TestTrait { + fn get_num(&self) -> i32; + } + struct A {} + impl TestTrait for A { + fn get_num(&self) -> i32 { + 42 + } + } + let dyn_arc: Arc = Arc::new(A {}); + let data_arc = Data::from(dyn_arc); + let ref_data = data_arc.get_ref(); + assert_eq!(data_arc.get_num(), ref_data.get_num()) + } } diff --git a/src/dev.rs b/src/dev.rs new file mode 100644 index 000000000..d4a64985c --- /dev/null +++ b/src/dev.rs @@ -0,0 +1,142 @@ +//! Lower-level types and re-exports. +//! +//! Most users will not have to interact with the types in this module, but it is useful for those +//! writing extractors, middleware, libraries, or interacting with the service API directly. + +pub use crate::config::{AppConfig, AppService}; +#[doc(hidden)] +pub use crate::handler::Handler; +pub use crate::info::{ConnectionInfo, PeerAddr}; +pub use crate::rmap::ResourceMap; +pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; + +pub use crate::types::form::UrlEncoded; +pub use crate::types::json::JsonBody; +pub use crate::types::readlines::Readlines; + +pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead}; +pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; +pub use actix_server::{Server, ServerHandle}; +pub use actix_service::{ + always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, +}; + +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; + +use crate::http::header::ContentEncoding; + +use actix_router::Patterns; + +pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { + match &mut patterns { + Patterns::Single(pat) => { + if !pat.is_empty() && !pat.starts_with('/') { + pat.insert(0, '/'); + }; + } + Patterns::List(pats) => { + for pat in pats { + if !pat.is_empty() && !pat.starts_with('/') { + pat.insert(0, '/'); + }; + } + } + } + + patterns +} +struct Enc(ContentEncoding); + +/// Helper trait that allows to set specific encoding for response. +pub trait BodyEncoding { + /// Get content encoding + fn get_encoding(&self) -> Option; + + /// Set content encoding + /// + /// Must be used with [`crate::middleware::Compress`] to take effect. + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; +} + +impl BodyEncoding for actix_http::ResponseBuilder { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for actix_http::Response { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for crate::HttpResponseBuilder { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +impl BodyEncoding for crate::HttpResponse { + fn get_encoding(&self) -> Option { + self.extensions().get::().map(|enc| enc.0) + } + + fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { + self.extensions_mut().insert(Enc(encoding)); + self + } +} + +// TODO: remove this if it doesn't appear to be needed + +#[allow(dead_code)] +#[derive(Debug)] +pub(crate) enum AnyBody { + None, + Full { body: crate::web::Bytes }, + Boxed { body: actix_http::body::BoxBody }, +} + +impl crate::body::MessageBody for AnyBody { + type Error = crate::BoxError; + + /// Body size hint. + fn size(&self) -> crate::body::BodySize { + match self { + AnyBody::None => crate::body::BodySize::None, + AnyBody::Full { body } => body.size(), + AnyBody::Boxed { body } => body.size(), + } + } + + /// Attempt to pull out the next chunk of body bytes. + fn poll_next( + self: std::pin::Pin<&mut Self>, + cx: &mut std::task::Context<'_>, + ) -> std::task::Poll>> { + match self.get_mut() { + AnyBody::None => std::task::Poll::Ready(None), + AnyBody::Full { body } => { + let bytes = std::mem::take(body); + std::task::Poll::Ready(Some(Ok(bytes))) + } + AnyBody::Boxed { body } => body.as_pin_mut().poll_next(cx), + } + } +} diff --git a/src/error/error.rs b/src/error/error.rs new file mode 100644 index 000000000..be17c1962 --- /dev/null +++ b/src/error/error.rs @@ -0,0 +1,76 @@ +use std::{error::Error as StdError, fmt}; + +use actix_http::{body::BoxBody, Response}; + +use crate::{HttpResponse, ResponseError}; + +/// General purpose actix web error. +/// +/// An actix web error is used to carry errors from `std::error` +/// through actix in a convenient way. It can be created through +/// converting errors with `into()`. +/// +/// Whenever it is created from an external object a response error is created +/// for it that can be used to create an HTTP response from it this means that +/// if you have access to an actix `Error` you can always get a +/// `ResponseError` reference from it. +pub struct Error { + cause: Box, +} + +impl Error { + /// Returns the reference to the underlying `ResponseError`. + pub fn as_response_error(&self) -> &dyn ResponseError { + self.cause.as_ref() + } + + /// Similar to `as_response_error` but downcasts. + pub fn as_error(&self) -> Option<&T> { + ::downcast_ref(self.cause.as_ref()) + } + + /// Shortcut for creating an `HttpResponse`. + pub fn error_response(&self) -> HttpResponse { + self.cause.error_response() + } +} + +impl fmt::Display for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.cause, f) + } +} + +impl fmt::Debug for Error { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + write!(f, "{:?}", &self.cause) + } +} + +impl StdError for Error { + fn source(&self) -> Option<&(dyn std::error::Error + 'static)> { + // TODO: populate if replacement for Box is found + None + } +} + +impl From for Error { + fn from(val: std::convert::Infallible) -> Self { + match val {} + } +} + +/// `Error` for any error that implements `ResponseError` +impl From for Error { + fn from(err: T) -> Error { + Error { + cause: Box::new(err), + } + } +} + +impl From for Response { + fn from(err: Error) -> Response { + err.error_response().into() + } +} diff --git a/src/error/internal.rs b/src/error/internal.rs new file mode 100644 index 000000000..c766ba83e --- /dev/null +++ b/src/error/internal.rs @@ -0,0 +1,318 @@ +use std::{cell::RefCell, fmt, io::Write as _}; + +use actix_http::{ + body::BoxBody, + header::{self, IntoHeaderValue as _}, + StatusCode, +}; +use bytes::{BufMut as _, BytesMut}; + +use crate::{Error, HttpRequest, HttpResponse, Responder, ResponseError}; + +/// Wraps errors to alter the generated response status code. +/// +/// In following example, the `io::Error` is wrapped into `ErrorBadRequest` which will generate a +/// response with the 400 Bad Request status code instead of the usual status code generated by +/// an `io::Error`. +/// +/// # Examples +/// ``` +/// # use std::io; +/// # use actix_web::{error, HttpRequest}; +/// async fn handler_error() -> Result { +/// let err = io::Error::new(io::ErrorKind::Other, "error"); +/// Err(error::ErrorBadRequest(err)) +/// } +/// ``` +pub struct InternalError { + cause: T, + status: InternalErrorType, +} + +enum InternalErrorType { + Status(StatusCode), + Response(RefCell>), +} + +impl InternalError { + /// Constructs an `InternalError` with given status code. + pub fn new(cause: T, status: StatusCode) -> Self { + InternalError { + cause, + status: InternalErrorType::Status(status), + } + } + + /// Constructs an `InternalError` with pre-defined response. + pub fn from_response(cause: T, response: HttpResponse) -> Self { + InternalError { + cause, + status: InternalErrorType::Response(RefCell::new(Some(response))), + } + } +} + +impl fmt::Debug for InternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.cause.fmt(f) + } +} + +impl fmt::Display for InternalError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + self.cause.fmt(f) + } +} + +impl ResponseError for InternalError +where + T: fmt::Debug + fmt::Display, +{ + fn status_code(&self) -> StatusCode { + match self.status { + InternalErrorType::Status(st) => st, + InternalErrorType::Response(ref resp) => { + if let Some(resp) = resp.borrow().as_ref() { + resp.head().status + } else { + StatusCode::INTERNAL_SERVER_ERROR + } + } + } + } + + fn error_response(&self) -> HttpResponse { + match self.status { + InternalErrorType::Status(status) => { + let mut res = HttpResponse::new(status); + let mut buf = BytesMut::new().writer(); + let _ = write!(buf, "{}", self); + + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + + res.set_body(BoxBody::new(buf.into_inner())) + } + + InternalErrorType::Response(ref resp) => { + if let Some(resp) = resp.borrow_mut().take() { + resp + } else { + HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR) + } + } + } + } +} + +impl Responder for InternalError +where + T: fmt::Debug + fmt::Display + 'static, +{ + type Body = BoxBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from_error(self) + } +} + +macro_rules! error_helper { + ($name:ident, $status:ident) => { + paste::paste! { + #[doc = "Helper function that wraps any error and generates a `" $status "` response."] + #[allow(non_snake_case)] + pub fn $name(err: T) -> Error + where + T: fmt::Debug + fmt::Display + 'static, + { + InternalError::new(err, StatusCode::$status).into() + } + } + } +} + +error_helper!(ErrorBadRequest, BAD_REQUEST); +error_helper!(ErrorUnauthorized, UNAUTHORIZED); +error_helper!(ErrorPaymentRequired, PAYMENT_REQUIRED); +error_helper!(ErrorForbidden, FORBIDDEN); +error_helper!(ErrorNotFound, NOT_FOUND); +error_helper!(ErrorMethodNotAllowed, METHOD_NOT_ALLOWED); +error_helper!(ErrorNotAcceptable, NOT_ACCEPTABLE); +error_helper!( + ErrorProxyAuthenticationRequired, + PROXY_AUTHENTICATION_REQUIRED +); +error_helper!(ErrorRequestTimeout, REQUEST_TIMEOUT); +error_helper!(ErrorConflict, CONFLICT); +error_helper!(ErrorGone, GONE); +error_helper!(ErrorLengthRequired, LENGTH_REQUIRED); +error_helper!(ErrorPayloadTooLarge, PAYLOAD_TOO_LARGE); +error_helper!(ErrorUriTooLong, URI_TOO_LONG); +error_helper!(ErrorUnsupportedMediaType, UNSUPPORTED_MEDIA_TYPE); +error_helper!(ErrorRangeNotSatisfiable, RANGE_NOT_SATISFIABLE); +error_helper!(ErrorImATeapot, IM_A_TEAPOT); +error_helper!(ErrorMisdirectedRequest, MISDIRECTED_REQUEST); +error_helper!(ErrorUnprocessableEntity, UNPROCESSABLE_ENTITY); +error_helper!(ErrorLocked, LOCKED); +error_helper!(ErrorFailedDependency, FAILED_DEPENDENCY); +error_helper!(ErrorUpgradeRequired, UPGRADE_REQUIRED); +error_helper!(ErrorPreconditionFailed, PRECONDITION_FAILED); +error_helper!(ErrorPreconditionRequired, PRECONDITION_REQUIRED); +error_helper!(ErrorTooManyRequests, TOO_MANY_REQUESTS); +error_helper!( + ErrorRequestHeaderFieldsTooLarge, + REQUEST_HEADER_FIELDS_TOO_LARGE +); +error_helper!( + ErrorUnavailableForLegalReasons, + UNAVAILABLE_FOR_LEGAL_REASONS +); +error_helper!(ErrorExpectationFailed, EXPECTATION_FAILED); +error_helper!(ErrorInternalServerError, INTERNAL_SERVER_ERROR); +error_helper!(ErrorNotImplemented, NOT_IMPLEMENTED); +error_helper!(ErrorBadGateway, BAD_GATEWAY); +error_helper!(ErrorServiceUnavailable, SERVICE_UNAVAILABLE); +error_helper!(ErrorGatewayTimeout, GATEWAY_TIMEOUT); +error_helper!(ErrorHttpVersionNotSupported, HTTP_VERSION_NOT_SUPPORTED); +error_helper!(ErrorVariantAlsoNegotiates, VARIANT_ALSO_NEGOTIATES); +error_helper!(ErrorInsufficientStorage, INSUFFICIENT_STORAGE); +error_helper!(ErrorLoopDetected, LOOP_DETECTED); +error_helper!(ErrorNotExtended, NOT_EXTENDED); +error_helper!( + ErrorNetworkAuthenticationRequired, + NETWORK_AUTHENTICATION_REQUIRED +); + +#[cfg(test)] +mod tests { + use actix_http::error::ParseError; + + use super::*; + + #[test] + fn test_internal_error() { + let err = InternalError::from_response(ParseError::Method, HttpResponse::Ok().finish()); + let resp: HttpResponse = err.error_response(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_error_helpers() { + let res: HttpResponse = ErrorBadRequest("err").into(); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + let res: HttpResponse = ErrorUnauthorized("err").into(); + assert_eq!(res.status(), StatusCode::UNAUTHORIZED); + + let res: HttpResponse = ErrorPaymentRequired("err").into(); + assert_eq!(res.status(), StatusCode::PAYMENT_REQUIRED); + + let res: HttpResponse = ErrorForbidden("err").into(); + assert_eq!(res.status(), StatusCode::FORBIDDEN); + + let res: HttpResponse = ErrorNotFound("err").into(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); + + let res: HttpResponse = ErrorMethodNotAllowed("err").into(); + assert_eq!(res.status(), StatusCode::METHOD_NOT_ALLOWED); + + let res: HttpResponse = ErrorNotAcceptable("err").into(); + assert_eq!(res.status(), StatusCode::NOT_ACCEPTABLE); + + let res: HttpResponse = ErrorProxyAuthenticationRequired("err").into(); + assert_eq!(res.status(), StatusCode::PROXY_AUTHENTICATION_REQUIRED); + + let res: HttpResponse = ErrorRequestTimeout("err").into(); + assert_eq!(res.status(), StatusCode::REQUEST_TIMEOUT); + + let res: HttpResponse = ErrorConflict("err").into(); + assert_eq!(res.status(), StatusCode::CONFLICT); + + let res: HttpResponse = ErrorGone("err").into(); + assert_eq!(res.status(), StatusCode::GONE); + + let res: HttpResponse = ErrorLengthRequired("err").into(); + assert_eq!(res.status(), StatusCode::LENGTH_REQUIRED); + + let res: HttpResponse = ErrorPreconditionFailed("err").into(); + assert_eq!(res.status(), StatusCode::PRECONDITION_FAILED); + + let res: HttpResponse = ErrorPayloadTooLarge("err").into(); + assert_eq!(res.status(), StatusCode::PAYLOAD_TOO_LARGE); + + let res: HttpResponse = ErrorUriTooLong("err").into(); + assert_eq!(res.status(), StatusCode::URI_TOO_LONG); + + let res: HttpResponse = ErrorUnsupportedMediaType("err").into(); + assert_eq!(res.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); + + let res: HttpResponse = ErrorRangeNotSatisfiable("err").into(); + assert_eq!(res.status(), StatusCode::RANGE_NOT_SATISFIABLE); + + let res: HttpResponse = ErrorExpectationFailed("err").into(); + assert_eq!(res.status(), StatusCode::EXPECTATION_FAILED); + + let res: HttpResponse = ErrorImATeapot("err").into(); + assert_eq!(res.status(), StatusCode::IM_A_TEAPOT); + + let res: HttpResponse = ErrorMisdirectedRequest("err").into(); + assert_eq!(res.status(), StatusCode::MISDIRECTED_REQUEST); + + let res: HttpResponse = ErrorUnprocessableEntity("err").into(); + assert_eq!(res.status(), StatusCode::UNPROCESSABLE_ENTITY); + + let res: HttpResponse = ErrorLocked("err").into(); + assert_eq!(res.status(), StatusCode::LOCKED); + + let res: HttpResponse = ErrorFailedDependency("err").into(); + assert_eq!(res.status(), StatusCode::FAILED_DEPENDENCY); + + let res: HttpResponse = ErrorUpgradeRequired("err").into(); + assert_eq!(res.status(), StatusCode::UPGRADE_REQUIRED); + + let res: HttpResponse = ErrorPreconditionRequired("err").into(); + assert_eq!(res.status(), StatusCode::PRECONDITION_REQUIRED); + + let res: HttpResponse = ErrorTooManyRequests("err").into(); + assert_eq!(res.status(), StatusCode::TOO_MANY_REQUESTS); + + let res: HttpResponse = ErrorRequestHeaderFieldsTooLarge("err").into(); + assert_eq!(res.status(), StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE); + + let res: HttpResponse = ErrorUnavailableForLegalReasons("err").into(); + assert_eq!(res.status(), StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS); + + let res: HttpResponse = ErrorInternalServerError("err").into(); + assert_eq!(res.status(), StatusCode::INTERNAL_SERVER_ERROR); + + let res: HttpResponse = ErrorNotImplemented("err").into(); + assert_eq!(res.status(), StatusCode::NOT_IMPLEMENTED); + + let res: HttpResponse = ErrorBadGateway("err").into(); + assert_eq!(res.status(), StatusCode::BAD_GATEWAY); + + let res: HttpResponse = ErrorServiceUnavailable("err").into(); + assert_eq!(res.status(), StatusCode::SERVICE_UNAVAILABLE); + + let res: HttpResponse = ErrorGatewayTimeout("err").into(); + assert_eq!(res.status(), StatusCode::GATEWAY_TIMEOUT); + + let res: HttpResponse = ErrorHttpVersionNotSupported("err").into(); + assert_eq!(res.status(), StatusCode::HTTP_VERSION_NOT_SUPPORTED); + + let res: HttpResponse = ErrorVariantAlsoNegotiates("err").into(); + assert_eq!(res.status(), StatusCode::VARIANT_ALSO_NEGOTIATES); + + let res: HttpResponse = ErrorInsufficientStorage("err").into(); + assert_eq!(res.status(), StatusCode::INSUFFICIENT_STORAGE); + + let res: HttpResponse = ErrorLoopDetected("err").into(); + assert_eq!(res.status(), StatusCode::LOOP_DETECTED); + + let res: HttpResponse = ErrorNotExtended("err").into(); + assert_eq!(res.status(), StatusCode::NOT_EXTENDED); + + let res: HttpResponse = ErrorNetworkAuthenticationRequired("err").into(); + assert_eq!(res.status(), StatusCode::NETWORK_AUTHENTICATION_REQUIRED); + } +} diff --git a/actix-http/src/macros.rs b/src/error/macros.rs similarity index 74% rename from actix-http/src/macros.rs rename to src/error/macros.rs index 8973aa39b..78b1ed9f6 100644 --- a/actix-http/src/macros.rs +++ b/src/error/macros.rs @@ -1,4 +1,3 @@ -#[macro_export] macro_rules! downcast_get_type_id { () => { /// A helper method to get the type ID of the type @@ -14,8 +13,13 @@ macro_rules! downcast_get_type_id { /// 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. + /// + /// We also take `PrivateHelper` as a parameter, to ensure that + /// safe code cannot obtain a `PrivateHelper` instance by + /// delegating to an existing implementation of `__private_get_type_id__` #[doc(hidden)] - fn __private_get_type_id__(&self) -> (std::any::TypeId, PrivateHelper) + #[allow(dead_code)] + fn __private_get_type_id__(&self, _: PrivateHelper) -> (std::any::TypeId, PrivateHelper) where Self: 'static, { @@ -24,20 +28,23 @@ macro_rules! downcast_get_type_id { }; } -//Generate implementation for dyn $name -#[macro_export] -macro_rules! downcast { +// Generate implementation for dyn $name +macro_rules! downcast_dyn { ($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)] + #[allow(dead_code)] pub struct PrivateHelper(()); impl dyn $name + 'static { /// Downcasts generic body to a specific type. + #[allow(dead_code)] pub fn downcast_ref(&self) -> Option<&T> { - if self.__private_get_type_id__().0 == std::any::TypeId::of::() { + if self.__private_get_type_id__(PrivateHelper(())).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 @@ -50,16 +57,17 @@ macro_rules! downcast { } /// Downcasts a generic body to a mutable specific type. + #[allow(dead_code)] pub fn downcast_mut(&mut self) -> Option<&mut T> { - if self.__private_get_type_id__().0 == std::any::TypeId::of::() { + if self.__private_get_type_id__(PrivateHelper(())).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)) - } + unsafe { Some(&mut *(self as *const dyn $name as *const T as *mut T)) } } else { None } @@ -68,14 +76,17 @@ macro_rules! downcast { }; } +pub(crate) use {downcast_dyn, downcast_get_type_id}; + #[cfg(test)] mod tests { + #![allow(clippy::upper_case_acronyms)] trait MB { downcast_get_type_id!(); } - downcast!(MB); + downcast_dyn!(MB); impl MB for String {} impl MB for () {} @@ -86,7 +97,7 @@ mod tests { 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(); + let body = resp_body.downcast_mut::().unwrap(); body.push('!'); let body = resp_body.downcast_ref::().unwrap(); assert_eq!(body, "hello cast!"); diff --git a/src/error.rs b/src/error/mod.rs similarity index 58% rename from src/error.rs rename to src/error/mod.rs index c0d6f8af9..3ccd5bba6 100644 --- a/src/error.rs +++ b/src/error/mod.rs @@ -3,31 +3,50 @@ pub use actix_http::error::*; use derive_more::{Display, Error, From}; use serde_json::error::Error as JsonError; +use serde_urlencoded::de::Error as FormDeError; +use serde_urlencoded::ser::Error as FormError; use url::ParseError as UrlParseError; -use crate::{http::StatusCode, HttpResponse}; +use crate::http::StatusCode; + +#[allow(clippy::module_inception)] +mod error; +mod internal; +mod macros; +mod response_error; + +pub use self::error::Error; +pub use self::internal::*; +pub use self::response_error::ResponseError; +pub(crate) use macros::{downcast_dyn, downcast_get_type_id}; + +/// A convenience [`Result`](std::result::Result) for Actix Web operations. +/// +/// This type alias is generally used to avoid writing out `actix_http::Error` directly. +pub type Result = std::result::Result; /// Errors which can occur when attempting to generate resource uri. -#[derive(Debug, PartialEq, Display, From)] +#[derive(Debug, PartialEq, Display, Error, From)] +#[non_exhaustive] pub enum UrlGenerationError { /// Resource not found #[display(fmt = "Resource not found")] ResourceNotFound, + /// Not all path pattern covered #[display(fmt = "Not all path pattern covered")] NotEnoughElements, + /// URL parse error #[display(fmt = "{}", _0)] ParseError(UrlParseError), } -impl std::error::Error for UrlGenerationError {} - -/// `InternalServerError` for `UrlGeneratorError` impl ResponseError for UrlGenerationError {} /// A set of errors that can occur during parsing urlencoded payloads #[derive(Debug, Display, Error, From)] +#[non_exhaustive] pub enum UrlencodedError { /// Can not decode chunked transfer encoding. #[display(fmt = "Can not decode chunked transfer encoding.")] @@ -50,66 +69,96 @@ pub enum UrlencodedError { ContentType, /// Parse error. - #[display(fmt = "Parse error.")] - Parse, + #[display(fmt = "Parse error: {}.", _0)] + Parse(FormDeError), + + /// Encoding error. + #[display(fmt = "Encoding error.")] + Encoding, + + /// Serialize error. + #[display(fmt = "Serialize error: {}.", _0)] + Serialize(FormError), /// Payload error. #[display(fmt = "Error that occur during reading payload: {}.", _0)] Payload(PayloadError), } -/// Return `BadRequest` for `UrlencodedError` impl ResponseError for UrlencodedError { fn status_code(&self) -> StatusCode { - match *self { - UrlencodedError::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE, - UrlencodedError::UnknownLength => StatusCode::LENGTH_REQUIRED, + match self { + Self::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE, + Self::UnknownLength => StatusCode::LENGTH_REQUIRED, + Self::Payload(err) => err.status_code(), _ => StatusCode::BAD_REQUEST, } } } /// A set of errors that can occur during parsing json payloads -#[derive(Debug, Display, From)] +#[derive(Debug, Display, Error)] +#[non_exhaustive] pub enum JsonPayloadError { - /// Payload size is bigger than allowed. (default: 32kB) - #[display(fmt = "Json payload size is bigger than allowed")] - Overflow, + /// Payload size is bigger than allowed & content length header set. (default: 2MB) + #[display( + fmt = "JSON payload ({} bytes) is larger than allowed (limit: {} bytes).", + length, + limit + )] + OverflowKnownLength { length: usize, limit: usize }, + + /// Payload size is bigger than allowed but no content length header set. (default: 2MB) + #[display(fmt = "JSON payload has exceeded limit ({} bytes).", limit)] + Overflow { limit: usize }, + /// Content type error #[display(fmt = "Content type error")] ContentType, + /// Deserialize error #[display(fmt = "Json deserialize error: {}", _0)] Deserialize(JsonError), + + /// Serialize error + #[display(fmt = "Json serialize error: {}", _0)] + Serialize(JsonError), + /// Payload error #[display(fmt = "Error that occur during reading payload: {}", _0)] Payload(PayloadError), } -impl std::error::Error for JsonPayloadError {} +impl From for JsonPayloadError { + fn from(err: PayloadError) -> Self { + Self::Payload(err) + } +} -/// Return `BadRequest` for `JsonPayloadError` impl ResponseError for JsonPayloadError { - fn error_response(&self) -> HttpResponse { - match *self { - JsonPayloadError::Overflow => { - HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) - } - _ => HttpResponse::new(StatusCode::BAD_REQUEST), + fn status_code(&self) -> StatusCode { + match self { + Self::OverflowKnownLength { + length: _, + limit: _, + } => StatusCode::PAYLOAD_TOO_LARGE, + Self::Overflow { limit: _ } => StatusCode::PAYLOAD_TOO_LARGE, + Self::Serialize(_) => StatusCode::INTERNAL_SERVER_ERROR, + Self::Payload(err) => err.status_code(), + _ => StatusCode::BAD_REQUEST, } } } /// A set of errors that can occur during parsing request paths -#[derive(Debug, Display, From)] +#[derive(Debug, Display, Error)] +#[non_exhaustive] pub enum PathError { /// Deserialize error #[display(fmt = "Path deserialize error: {}", _0)] Deserialize(serde::de::value::Error), } -impl std::error::Error for PathError {} - /// Return `BadRequest` for `PathError` impl ResponseError for PathError { fn status_code(&self) -> StatusCode { @@ -119,13 +168,13 @@ impl ResponseError for PathError { /// A set of errors that can occur during parsing query strings. #[derive(Debug, Display, Error, From)] +#[non_exhaustive] pub enum QueryPayloadError { /// Query deserialize error. #[display(fmt = "Query deserialize error: {}", _0)] Deserialize(serde::de::value::Error), } -/// Return `BadRequest` for `QueryPayloadError` impl ResponseError for QueryPayloadError { fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST @@ -133,26 +182,26 @@ impl ResponseError for QueryPayloadError { } /// Error type returned when reading body as lines. -#[derive(From, Display, Debug)] +#[derive(Debug, Display, Error, From)] +#[non_exhaustive] pub enum ReadlinesError { - /// Error when decoding a line. #[display(fmt = "Encoding error")] /// Payload size is bigger than allowed. (default: 256kB) EncodingError, + /// Payload error. #[display(fmt = "Error that occur during reading payload: {}", _0)] Payload(PayloadError), + /// Line limit exceeded. #[display(fmt = "Line limit exceeded")] LimitOverflow, + /// ContentType error. #[display(fmt = "Content-type error")] ContentTypeError(ContentTypeError), } -impl std::error::Error for ReadlinesError {} - -/// Return `BadRequest` for `ReadlinesError` impl ResponseError for ReadlinesError { fn status_code(&self) -> StatusCode { match *self { @@ -168,26 +217,31 @@ mod tests { #[test] fn test_urlencoded_error() { - let resp: HttpResponse = - UrlencodedError::Overflow { size: 0, limit: 0 }.error_response(); + let resp = UrlencodedError::Overflow { size: 0, limit: 0 }.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - let resp: HttpResponse = UrlencodedError::UnknownLength.error_response(); + let resp = UrlencodedError::UnknownLength.error_response(); assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); - let resp: HttpResponse = UrlencodedError::ContentType.error_response(); + let resp = UrlencodedError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[test] fn test_json_payload_error() { - let resp: HttpResponse = JsonPayloadError::Overflow.error_response(); + let resp = JsonPayloadError::OverflowKnownLength { + length: 0, + limit: 0, + } + .error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - let resp: HttpResponse = JsonPayloadError::ContentType.error_response(); + let resp = JsonPayloadError::Overflow { limit: 0 }.error_response(); + assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); + let resp = JsonPayloadError::ContentType.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[test] fn test_query_payload_error() { - let resp: HttpResponse = QueryPayloadError::Deserialize( + let resp = QueryPayloadError::Deserialize( serde_urlencoded::from_str::("bad query").unwrap_err(), ) .error_response(); @@ -196,9 +250,9 @@ mod tests { #[test] fn test_readlines_error() { - let resp: HttpResponse = ReadlinesError::LimitOverflow.error_response(); + let resp = ReadlinesError::LimitOverflow.error_response(); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); - let resp: HttpResponse = ReadlinesError::EncodingError.error_response(); + let resp = ReadlinesError::EncodingError.error_response(); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } } diff --git a/src/error/response_error.rs b/src/error/response_error.rs new file mode 100644 index 000000000..7260efa1a --- /dev/null +++ b/src/error/response_error.rs @@ -0,0 +1,152 @@ +//! `ResponseError` trait and foreign impls. + +use std::{ + error::Error as StdError, + fmt, + io::{self, Write as _}, +}; + +use actix_http::{ + body::BoxBody, + header::{self, IntoHeaderValue}, + Response, StatusCode, +}; +use bytes::BytesMut; + +use crate::{ + error::{downcast_dyn, downcast_get_type_id}, + helpers, HttpResponse, +}; + +/// Errors that can generate responses. +// TODO: add std::error::Error bound when replacement for Box is found +pub trait ResponseError: fmt::Debug + fmt::Display { + /// Returns appropriate status code for error. + /// + /// A 500 Internal Server Error is used by default. If [error_response](Self::error_response) is + /// also implemented and does not call `self.status_code()`, then this will not be used. + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } + + /// Creates full response for error. + /// + /// By default, the generated response uses a 500 Internal Server Error status code, a + /// `Content-Type` of `text/plain`, and the body is set to `Self`'s `Display` impl. + fn error_response(&self) -> HttpResponse { + let mut res = HttpResponse::new(self.status_code()); + + let mut buf = BytesMut::new(); + let _ = write!(helpers::MutWriter(&mut buf), "{}", self); + + let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap(); + res.headers_mut().insert(header::CONTENT_TYPE, mime); + + res.set_body(BoxBody::new(buf)) + } + + downcast_get_type_id!(); +} + +downcast_dyn!(ResponseError); + +impl ResponseError for Box {} + +#[cfg(feature = "openssl")] +impl ResponseError for actix_tls::accept::openssl::reexports::Error {} + +impl ResponseError for serde::de::value::Error { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for serde_json::Error {} + +impl ResponseError for serde_urlencoded::ser::Error {} + +impl ResponseError for std::str::Utf8Error { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for std::io::Error { + fn status_code(&self) -> StatusCode { + // TODO: decide if these errors should consider not found or permission errors + match self.kind() { + io::ErrorKind::NotFound => StatusCode::NOT_FOUND, + io::ErrorKind::PermissionDenied => StatusCode::FORBIDDEN, + _ => StatusCode::INTERNAL_SERVER_ERROR, + } + } +} + +impl ResponseError for actix_http::error::HttpError {} + +impl ResponseError for actix_http::Error { + fn status_code(&self) -> StatusCode { + // TODO: map error kinds to status code better + StatusCode::INTERNAL_SERVER_ERROR + } + + fn error_response(&self) -> HttpResponse { + HttpResponse::with_body(self.status_code(), self.to_string()).map_into_boxed_body() + } +} + +impl ResponseError for actix_http::header::InvalidHeaderValue { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for actix_http::error::ParseError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for actix_http::error::BlockingError {} + +impl ResponseError for actix_http::error::PayloadError { + fn status_code(&self) -> StatusCode { + match *self { + actix_http::error::PayloadError::Overflow => StatusCode::PAYLOAD_TOO_LARGE, + _ => StatusCode::BAD_REQUEST, + } + } +} + +impl ResponseError for actix_http::ws::ProtocolError {} + +impl ResponseError for actix_http::error::ContentTypeError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +impl ResponseError for actix_http::ws::HandshakeError { + fn error_response(&self) -> HttpResponse { + Response::from(self).map_into_boxed_body().into() + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_error_casting() { + use actix_http::error::{ContentTypeError, PayloadError}; + + let err = PayloadError::Overflow; + let resp_err: &dyn ResponseError = &err; + + let err = resp_err.downcast_ref::().unwrap(); + assert_eq!(err.to_string(), "Payload reached size limit."); + + let not_err = resp_err.downcast_ref::(); + assert!(not_err.is_none()); + } +} diff --git a/src/extract.rs b/src/extract.rs index 7a677bca4..bb2dabb9f 100644 --- a/src/extract.rs +++ b/src/extract.rs @@ -1,25 +1,65 @@ //! Request extractors use std::{ + convert::Infallible, future::Future, pin::Pin, task::{Context, Poll}, }; -use futures_util::{ - future::{ready, Ready}, - ready, -}; +use actix_http::http::{Method, Uri}; +use actix_utils::future::{ok, Ready}; +use futures_core::ready; +use pin_project_lite::pin_project; use crate::{dev::Payload, Error, HttpRequest}; -/// Trait implemented by types that can be extracted from request. +/// A type that implements [`FromRequest`] is called an **extractor** and can extract data from +/// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`]. /// -/// Types that implement this trait can be used with `Route` handlers. +/// # Configuration +/// An extractor can be customized by injecting the corresponding configuration with one of: +/// +/// - [`App::app_data()`][crate::App::app_data] +/// - [`Scope::app_data()`][crate::Scope::app_data] +/// - [`Resource::app_data()`][crate::Resource::app_data] +/// +/// Here are some built-in extractors and their corresponding configuration. +/// Please refer to the respective documentation for details. +/// +/// | Extractor | Configuration | +/// |-------------|-------------------| +/// | [`Header`] | _None_ | +/// | [`Path`] | [`PathConfig`] | +/// | [`Json`] | [`JsonConfig`] | +/// | [`Form`] | [`FormConfig`] | +/// | [`Query`] | [`QueryConfig`] | +/// | [`Bytes`] | [`PayloadConfig`] | +/// | [`String`] | [`PayloadConfig`] | +/// | [`Payload`] | [`PayloadConfig`] | +/// +/// # Implementing An Extractor +/// To reduce duplicate code in handlers where extracting certain parts of a request has a common +/// structure, you can implement `FromRequest` for your own types. +/// +/// Note that the request payload can only be consumed by one extractor. +/// +/// [`Header`]: crate::web::Header +/// [`Json`]: crate::web::Json +/// [`JsonConfig`]: crate::web::JsonConfig +/// [`Form`]: crate::web::Form +/// [`FormConfig`]: crate::web::FormConfig +/// [`Path`]: crate::web::Path +/// [`PathConfig`]: crate::web::PathConfig +/// [`Query`]: crate::web::Query +/// [`QueryConfig`]: crate::web::QueryConfig +/// [`Payload`]: crate::web::Payload +/// [`PayloadConfig`]: crate::web::PayloadConfig +/// [`String`]: FromRequest#impl-FromRequest-for-String +/// [`Bytes`]: crate::web::Bytes#impl-FromRequest +/// [`Either`]: crate::web::Either +#[doc(alias = "extract", alias = "extractor")] pub trait FromRequest: Sized { - /// Configuration for this extractor. - type Config: Default + 'static; - /// The associated error which can be returned. type Error: Into; @@ -35,27 +75,18 @@ pub trait FromRequest: Sized { fn extract(req: &HttpRequest) -> Self::Future { Self::from_request(req, &mut Payload::None) } - - /// Create and configure config instance. - fn configure(f: F) -> Self::Config - where - F: FnOnce(Self::Config) -> Self::Config, - { - f(Self::Config::default()) - } } /// Optionally extract a field from the request /// /// If the FromRequest for T fails, return None rather than returning an error response /// -/// ## Example -/// -/// ```rust +/// # Examples +/// ``` /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] @@ -66,7 +97,6 @@ pub trait FromRequest: Sized { /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; -/// type Config = (); /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { @@ -101,7 +131,6 @@ where { type Error = Error; type Future = FromRequestOptFuture; - type Config = T::Config; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -111,10 +140,11 @@ where } } -#[pin_project::pin_project] -pub struct FromRequestOptFuture { - #[pin] - fut: Fut, +pin_project! { + pub struct FromRequestOptFuture { + #[pin] + fut: Fut, + } } impl Future for FromRequestOptFuture @@ -141,13 +171,12 @@ where /// /// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response /// -/// ## Example -/// -/// ```rust +/// # Examples +/// ``` /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::error::ErrorBadRequest; /// use futures_util::future::{ok, err, Ready}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// use rand; /// /// #[derive(Debug, Deserialize)] @@ -158,7 +187,6 @@ where /// impl FromRequest for Thing { /// type Error = Error; /// type Future = Ready>; -/// type Config = (); /// /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// if rand::random() { @@ -191,7 +219,6 @@ where { type Error = Error; type Future = FromRequestResFuture; - type Config = T::Config; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { @@ -201,10 +228,11 @@ where } } -#[pin_project::pin_project] -pub struct FromRequestResFuture { - #[pin] - fut: Fut, +pin_project! { + pub struct FromRequestResFuture { + #[pin] + fut: Fut, + } } impl Future for FromRequestResFuture @@ -220,121 +248,163 @@ where } } -#[doc(hidden)] -impl FromRequest for () { - type Error = Error; - type Future = Ready>; - type Config = (); +/// Extract the request's URI. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Uri, web, App, Responder}; +/// +/// async fn handler(uri: Uri) -> impl Responder { +/// format!("Requested path: {}", uri.path()) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Uri { + type Error = Infallible; + type Future = Ready>; - fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { - ready(Ok(())) + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.uri().clone()) } } -macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => { +/// Extract the request's method. +/// +/// # Examples +/// ``` +/// use actix_web::{http::Method, web, App, Responder}; +/// +/// async fn handler(method: Method) -> impl Responder { +/// format!("Request method: {}", method) +/// } +/// +/// let app = App::new().default_service(web::to(handler)); +/// ``` +impl FromRequest for Method { + type Error = Infallible; + type Future = Ready>; - // 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, _: &mut Payload) -> Self::Future { + ok(req.method().clone()) + } +} - // Bring everything into scope, so we don't need - // redundant imports - use super::*; +#[doc(hidden)] +impl FromRequest for () { + type Error = Infallible; + type Future = Ready>; - /// A helper struct to allow us to pin-project through - /// to individual fields - #[pin_project::pin_project] - struct FutWrapper<$($T: FromRequest),+>($(#[pin] $T::Future),+); + fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(()) + } +} - /// 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),+); +#[doc(hidden)] +#[allow(non_snake_case)] +mod tuple_from_req { + use super::*; - fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - $fut_type { - items: <($(Option<$T>,)+)>::default(), - futs: FutWrapper($($T::from_request(req, payload),)+), + macro_rules! tuple_from_req { + ($fut: ident; $($T: ident),*) => { + /// FromRequest implementation for tuple + #[allow(unused_parens)] + impl<$($T: FromRequest + 'static),+> FromRequest for ($($T,)+) + { + type Error = Error; + type Future = $fut<$($T),+>; + + fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { + $fut { + $( + $T: ExtractFuture::Future { + fut: $T::from_request(req, payload) + }, + )+ + } } } - } - #[doc(hidden)] - #[pin_project::pin_project] - pub struct $fut_type<$($T: FromRequest),+> { - items: ($(Option<$T>,)+), - #[pin] - futs: FutWrapper<$($T,)+>, - } + pin_project! { + pub struct $fut<$($T: FromRequest),+> { + $( + #[pin] + $T: ExtractFuture<$T::Future, $T>, + )+ + } + } - impl<$($T: FromRequest),+> Future for $fut_type<$($T),+> - { - type Output = Result<($($T,)+), Error>; + impl<$($T: FromRequest),+> Future for $fut<$($T),+> + { + type Output = Result<($($T,)+), Error>; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let mut 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 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())), + let mut ready = true; + $( + match this.$T.as_mut().project() { + ExtractProj::Future { fut } => match fut.poll(cx) { + Poll::Ready(Ok(output)) => { + let _ = this.$T.as_mut().project_replace(ExtractFuture::Done { output }); + }, + Poll::Ready(Err(e)) => return Poll::Ready(Err(e.into())), + Poll::Pending => ready = false, + }, + ExtractProj::Done { .. } => {}, + ExtractProj::Empty => unreachable!("FromRequest polled after finished"), } - } - )+ + )+ if ready { Poll::Ready(Ok( - ($(this.items.$n.take().unwrap(),)+) + ($( + match this.$T.project_replace(ExtractFuture::Empty) { + ExtractReplaceProj::Done { output } => output, + _ => unreachable!("FromRequest polled after finished"), + }, + )+) )) } else { Poll::Pending } + } } + }; + } + + pin_project! { + #[project = ExtractProj] + #[project_replace = ExtractReplaceProj] + enum ExtractFuture { + Future { + #[pin] + fut: Fut + }, + Done { + output: Res, + }, + Empty } } -}); -#[rustfmt::skip] -mod m { - use super::*; - -tuple_from_req!(TupleFromRequest1, (0, A)); -tuple_from_req!(TupleFromRequest2, (0, A), (1, B)); -tuple_from_req!(TupleFromRequest3, (0, A), (1, B), (2, C)); -tuple_from_req!(TupleFromRequest4, (0, A), (1, B), (2, C), (3, D)); -tuple_from_req!(TupleFromRequest5, (0, A), (1, B), (2, C), (3, D), (4, E)); -tuple_from_req!(TupleFromRequest6, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); -tuple_from_req!(TupleFromRequest7, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); -tuple_from_req!(TupleFromRequest8, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); -tuple_from_req!(TupleFromRequest9, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); -tuple_from_req!(TupleFromRequest10, (0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); + tuple_from_req! { TupleFromRequest1; A } + tuple_from_req! { TupleFromRequest2; A, B } + tuple_from_req! { TupleFromRequest3; A, B, C } + tuple_from_req! { TupleFromRequest4; A, B, C, D } + tuple_from_req! { TupleFromRequest5; A, B, C, D, E } + tuple_from_req! { TupleFromRequest6; A, B, C, D, E, F } + tuple_from_req! { TupleFromRequest7; A, B, C, D, E, F, G } + tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H } + tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I } + tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J } } #[cfg(test)] mod tests { use actix_http::http::header; use bytes::Bytes; - use serde_derive::Deserialize; + use serde::Deserialize; use super::*; use crate::test::TestRequest; @@ -415,4 +485,40 @@ mod tests { .unwrap(); assert!(r.is_err()); } + + #[actix_rt::test] + async fn test_uri() { + let req = TestRequest::default().uri("/foo/bar").to_http_request(); + let uri = Uri::extract(&req).await.unwrap(); + assert_eq!(uri.path(), "/foo/bar"); + } + + #[actix_rt::test] + async fn test_method() { + let req = TestRequest::default().method(Method::GET).to_http_request(); + let method = Method::extract(&req).await.unwrap(); + assert_eq!(method, Method::GET); + } + + #[actix_rt::test] + async fn test_concurrent() { + let (req, mut pl) = TestRequest::default() + .uri("/foo/bar") + .method(Method::GET) + .insert_header((header::CONTENT_TYPE, "application/x-www-form-urlencoded")) + .insert_header((header::CONTENT_LENGTH, "11")) + .set_payload(Bytes::from_static(b"hello=world")) + .to_http_parts(); + let (method, uri, form) = <(Method, Uri, Form)>::from_request(&req, &mut pl) + .await + .unwrap(); + assert_eq!(method, Method::GET); + assert_eq!(uri.path(), "/foo/bar"); + assert_eq!( + form, + Form(Info { + hello: "world".into() + }) + ); + } } diff --git a/src/guard.rs b/src/guard.rs index ba0cbea85..c71d64a29 100644 --- a/src/guard.rs +++ b/src/guard.rs @@ -12,7 +12,7 @@ //! to store extra attributes on a request by using the `Extensions` container. //! Extensions containers are available via the `RequestHead::extensions()` method. //! -//! ```rust +//! ``` //! use actix_web::{web, http, dev, guard, App, HttpResponse}; //! //! fn main() { @@ -26,6 +26,8 @@ //! ``` #![allow(non_snake_case)] use std::convert::TryFrom; +use std::ops::Deref; +use std::rc::Rc; use actix_http::http::{self, header, uri::Uri}; use actix_http::RequestHead; @@ -40,9 +42,15 @@ pub trait Guard { fn check(&self, request: &RequestHead) -> bool; } +impl Guard for Rc { + fn check(&self, request: &RequestHead) -> bool { + self.deref().check(request) + } +} + /// Create guard object for supplied function. /// -/// ```rust +/// ``` /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { @@ -85,7 +93,7 @@ where /// Return guard that matches if any of supplied guards. /// -/// ```rust +/// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { @@ -124,7 +132,7 @@ impl Guard for AnyGuard { /// Return guard that matches if all of the supplied guards. /// -/// ```rust +/// ``` /// use actix_web::{guard, web, App, HttpResponse}; /// /// fn main() { @@ -176,7 +184,7 @@ impl Guard for NotGuard { } } -/// Http method guard +/// HTTP method guard. #[doc(hidden)] pub struct MethodGuard(http::Method); @@ -186,52 +194,52 @@ impl Guard for MethodGuard { } } -/// Guard to match *GET* http method +/// Guard to match *GET* HTTP method. pub fn Get() -> MethodGuard { MethodGuard(http::Method::GET) } -/// Predicate to match *POST* http method +/// Predicate to match *POST* HTTP method. pub fn Post() -> MethodGuard { MethodGuard(http::Method::POST) } -/// Predicate to match *PUT* http method +/// Predicate to match *PUT* HTTP method. pub fn Put() -> MethodGuard { MethodGuard(http::Method::PUT) } -/// Predicate to match *DELETE* http method +/// Predicate to match *DELETE* HTTP method. pub fn Delete() -> MethodGuard { MethodGuard(http::Method::DELETE) } -/// Predicate to match *HEAD* http method +/// Predicate to match *HEAD* HTTP method. pub fn Head() -> MethodGuard { MethodGuard(http::Method::HEAD) } -/// Predicate to match *OPTIONS* http method +/// Predicate to match *OPTIONS* HTTP method. pub fn Options() -> MethodGuard { MethodGuard(http::Method::OPTIONS) } -/// Predicate to match *CONNECT* http method +/// Predicate to match *CONNECT* HTTP method. pub fn Connect() -> MethodGuard { MethodGuard(http::Method::CONNECT) } -/// Predicate to match *PATCH* http method +/// Predicate to match *PATCH* HTTP method. pub fn Patch() -> MethodGuard { MethodGuard(http::Method::PATCH) } -/// Predicate to match *TRACE* http method +/// Predicate to match *TRACE* HTTP method. pub fn Trace() -> MethodGuard { MethodGuard(http::Method::TRACE) } -/// Predicate to match specified http method +/// Predicate to match specified HTTP method. pub fn Method(method: http::Method) -> MethodGuard { MethodGuard(method) } @@ -259,7 +267,7 @@ impl Guard for HeaderGuard { /// Return predicate that matches if request contains specified Host name. /// -/// ```rust +/// ``` /// use actix_web::{web, guard::Host, App, HttpResponse}; /// /// fn main() { diff --git a/src/handler.rs b/src/handler.rs index 1526f050d..e543ecc7f 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -1,25 +1,21 @@ use std::future::Future; -use std::marker::PhantomData; -use std::pin::Pin; -use std::task::{Context, Poll}; -use actix_http::{Error, Response}; -use actix_service::{Service, ServiceFactory}; -use futures_util::future::{ready, Ready}; -use futures_util::ready; -use pin_project::pin_project; +use actix_service::{boxed, fn_service}; -use crate::extract::FromRequest; -use crate::request::HttpRequest; -use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; +use crate::{ + body::MessageBody, + service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, + BoxError, FromRequest, HttpResponse, Responder, +}; -/// A request handler is an async function that accepts zero or more parameters that can be -/// extracted from a request (ie, [`impl FromRequest`](crate::FromRequest)) and returns a type that can be converted into -/// an [`HttpResponse`](crate::HttpResponse) (ie, [`impl Responder`](crate::Responder)). +/// A request handler is an async function that accepts zero or more parameters that can be +/// extracted from a request (i.e., [`impl FromRequest`]) and returns a type that can be converted +/// into an [`HttpResponse`] (that is, it impls the [`Responder`] trait). /// /// If you got the error `the trait Handler<_, _, _> is not implemented`, then your function is not -/// a valid handler. See [Request Handlers](https://actix.rs/docs/handlers/) for more information. +/// a valid handler. See for more information. +/// +/// [`impl FromRequest`]: crate::FromRequest pub trait Handler: Clone + 'static where R: Future, @@ -28,179 +24,67 @@ where fn call(&self, param: T) -> R; } -impl Handler<(), R> for F -where - F: Fn() -> R + Clone + 'static, - R: Future, - R::Output: Responder, -{ - fn call(&self, _: ()) -> R { - (self)() - } -} - -#[doc(hidden)] -/// Extract arguments from request, run factory function and make response. -pub struct HandlerService +pub(crate) fn handler_service(handler: F) -> BoxedHttpServiceFactory where F: Handler, T: FromRequest, R: Future, R::Output: Responder, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { - hnd: F, - _phantom: PhantomData<(T, R)>, -} + boxed::factory(fn_service(move |req: ServiceRequest| { + let handler = handler.clone(); -impl HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - pub fn new(hnd: F) -> Self { - Self { - hnd, - _phantom: PhantomData, + async move { + let (req, mut payload) = req.into_parts(); + + let res = match T::from_request(&req, &mut payload).await { + Err(err) => HttpResponse::from_error(err), + + Ok(data) => handler + .call(data) + .await + .respond_to(&req) + .map_into_boxed_body(), + }; + + Ok(ServiceResponse::new(req, res)) } - } + })) } -impl Clone for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - fn clone(&self) -> Self { - Self { - hnd: self.hnd.clone(), - _phantom: PhantomData, - } - } -} - -impl ServiceFactory for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = Self; - type InitError = (); - type Future = Ready>; - - fn new_service(&self, _: ()) -> Self::Future { - ready(Ok(self.clone())) - } -} - -/// HandlerService is both it's ServiceFactory and Service Type. -impl Service for HandlerService -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = HandlerServiceFuture; - - fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll> { - Poll::Ready(Ok(())) - } - - fn call(&mut self, req: ServiceRequest) -> Self::Future { - let (req, mut payload) = req.into_parts(); - let fut = T::from_request(&req, &mut payload); - HandlerServiceFuture::Extract(fut, Some(req), self.hnd.clone()) - } -} - -#[doc(hidden)] -#[pin_project(project = HandlerProj)] -pub enum HandlerServiceFuture -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - Extract(#[pin] T::Future, Option, F), - Handle(#[pin] R, Option), -} - -impl Future for HandlerServiceFuture -where - F: Handler, - T: FromRequest, - R: Future, - R::Output: Responder, -{ - // Error type in this future is a placeholder type. - // all instances of error must be converted to ServiceResponse and return in Ok. - type Output = Result; - - fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - loop { - match self.as_mut().project() { - HandlerProj::Extract(fut, req, handle) => { - match ready!(fut.poll(cx)) { - Ok(item) => { - let fut = handle.call(item); - let state = HandlerServiceFuture::Handle(fut, req.take()); - self.as_mut().set(state); - } - Err(e) => { - let res: Response = e.into().into(); - let req = req.take().unwrap(); - return Poll::Ready(Ok(ServiceResponse::new(req, res))); - } - }; - } - HandlerProj::Handle(fut, req) => { - let res = ready!(fut.poll(cx)); - let req = req.take().unwrap(); - let res = res.respond_to(&req); - return Poll::Ready(Ok(ServiceResponse::new(req, res))); - } - } - } - } -} - -/// FromRequest trait impl for tuples -macro_rules! factory_tuple ({ $(($n:tt, $T:ident)),+} => { - impl Handler<($($T,)+), Res> for Func - where Func: Fn($($T,)+) -> Res + Clone + 'static, +/// Generates a [`Handler`] trait impl for N-ary functions where N is specified with a sequence of +/// space separated type parameters. +/// +/// # Examples +/// ```ignore +/// factory_tuple! {} // implements Handler for types: fn() -> Res +/// factory_tuple! { A B C } // implements Handler for types: fn(A, B, C) -> Res +/// ``` +macro_rules! factory_tuple ({ $($param:ident)* } => { + impl Handler<($($param,)*), Res> for Func + where Func: Fn($($param),*) -> Res + Clone + 'static, Res: Future, Res::Output: Responder, { - fn call(&self, param: ($($T,)+)) -> Res { - (self)($(param.$n,)+) + #[allow(non_snake_case)] + fn call(&self, ($($param,)*): ($($param,)*)) -> Res { + (self)($($param,)*) } } }); -#[rustfmt::skip] -mod m { - use super::*; - - factory_tuple!((0, A)); - factory_tuple!((0, A), (1, B)); - factory_tuple!((0, A), (1, B), (2, C)); - factory_tuple!((0, A), (1, B), (2, C), (3, D)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I)); - factory_tuple!((0, A), (1, B), (2, C), (3, D), (4, E), (5, F), (6, G), (7, H), (8, I), (9, J)); -} +factory_tuple! {} +factory_tuple! { A } +factory_tuple! { A B } +factory_tuple! { A B C } +factory_tuple! { A B C D } +factory_tuple! { A B C D E } +factory_tuple! { A B C D E F } +factory_tuple! { A B C D E F G } +factory_tuple! { A B C D E F G H } +factory_tuple! { A B C D E F G H I } +factory_tuple! { A B C D E F G H I J } +factory_tuple! { A B C D E F G H I J K } +factory_tuple! { A B C D E F G H I J K L } diff --git a/src/helpers.rs b/src/helpers.rs new file mode 100644 index 000000000..1d2679fce --- /dev/null +++ b/src/helpers.rs @@ -0,0 +1,25 @@ +use std::io; + +use bytes::BufMut; + +/// An `io::Write`r that only requires mutable reference and assumes that there is space available +/// in the buffer for every write operation or that it can be extended implicitly (like +/// `bytes::BytesMut`, for example). +/// +/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not +/// perform a remaining length check before writing. +pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B); + +impl<'a, B> io::Write for MutWriter<'a, B> +where + B: BufMut, +{ + fn write(&mut self, buf: &[u8]) -> io::Result { + self.0.put_slice(buf); + Ok(buf.len()) + } + + fn flush(&mut self) -> io::Result<()> { + Ok(()) + } +} diff --git a/actix-http/src/header/common/accept.rs b/src/http/header/accept.rs similarity index 73% rename from actix-http/src/header/common/accept.rs rename to src/http/header/accept.rs index 775da3394..70e4118cf 100644 --- a/actix-http/src/header/common/accept.rs +++ b/src/http/header/accept.rs @@ -2,11 +2,12 @@ use std::cmp::Ordering; use mime::Mime; -use crate::header::{qitem, QualityItem}; +use super::{qitem, QualityItem}; use crate::http::header; -header! { - /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) +crate::http::header::common_header! { + /// `Accept` header, defined + /// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2) /// /// The `Accept` header field can be used by user agents to specify /// response media types that are acceptable. Accept header fields can @@ -15,8 +16,7 @@ header! { /// in-line image /// /// # ABNF - /// - /// ```text + /// ```plain /// Accept = #( media-range [ accept-params ] ) /// /// media-range = ( "*/*" @@ -27,16 +27,16 @@ header! { /// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ] /// ``` /// - /// # Example values + /// # Example Values /// * `audio/*; q=0.2, audio/basic` /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` /// /// # Examples /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{Accept, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{Accept, qitem}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ /// qitem(mime::TEXT_HTML), @@ -45,10 +45,10 @@ header! { /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{Accept, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{Accept, qitem}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ /// qitem(mime::APPLICATION_JSON), @@ -57,10 +57,10 @@ header! { /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{Accept, QualityItem, q, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{Accept, QualityItem, q, qitem}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Accept(vec![ /// qitem(mime::TEXT_HTML), @@ -77,18 +77,19 @@ header! { /// ]) /// ); /// ``` - (Accept, header::ACCEPT) => (QualityItem)+ + (Accept, header::ACCEPT) => (QualityItem)* - test_accept { + test_parse_and_format { // Tests from the RFC - test_header!( + crate::http::header::common_header_test!( test1, vec![b"audio/*; q=0.2, audio/basic"], Some(Accept(vec![ QualityItem::new("audio/*".parse().unwrap(), q(200)), qitem("audio/basic".parse().unwrap()), ]))); - test_header!( + + crate::http::header::common_header_test!( test2, vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], Some(Accept(vec![ @@ -99,14 +100,15 @@ header! { q(800)), qitem("text/x-c".parse().unwrap()), ]))); + // Custom tests - test_header!( + crate::http::header::common_header_test!( test3, vec![b"text/plain; charset=utf-8"], Some(Accept(vec![ qitem(mime::TEXT_PLAIN_UTF_8), ]))); - test_header!( + crate::http::header::common_header_test!( test4, vec![b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ @@ -116,8 +118,9 @@ header! { #[test] fn test_fuzzing1() { - use crate::test::TestRequest; - let req = TestRequest::default().insert_header((crate::header::ACCEPT, "chunk#;e")).finish(); + let req = test::TestRequest::default() + .insert_header((header::ACCEPT, "chunk#;e")) + .finish(); let header = Accept::parse(&req); assert!(header.is_ok()); } @@ -153,8 +156,12 @@ impl Accept { /// Returns a sorted list of mime types from highest to lowest preference, accounting for /// [q-factor weighting] and specificity. /// - /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2 - pub fn mime_precedence(&self) -> Vec { + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn ranked(&self) -> Vec { + if self.is_empty() { + return vec![]; + } + let mut types = self.0.clone(); // use stable sort so items with equal q-factor and specificity retain listed order @@ -201,27 +208,44 @@ impl Accept { /// If no q-factors are provided, the first mime type is chosen. Note that items without /// q-factors are given the maximum preference value. /// - /// Returns `None` if contained list is empty. + /// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained + /// list is empty. /// - /// [q-factor weighting]: https://tools.ietf.org/html/rfc7231#section-5.3.2 - pub fn mime_preference(&self) -> Option { - let types = self.mime_precedence(); - types.first().cloned() + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn preference(&self) -> Mime { + use actix_http::header::q; + + let mut max_item = None; + let mut max_pref = q(0); + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + max_item.unwrap_or(mime::STAR_STAR) } } #[cfg(test)] mod tests { use super::*; - use crate::header::q; + use crate::http::header::q; #[test] - fn test_mime_precedence() { + fn ranking_precedence() { let test = Accept(vec![]); - assert!(test.mime_precedence().is_empty()); + assert!(test.ranked().is_empty()); let test = Accept(vec![qitem(mime::APPLICATION_JSON)]); - assert_eq!(test.mime_precedence(), vec!(mime::APPLICATION_JSON)); + assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON)); let test = Accept(vec![ qitem(mime::TEXT_HTML), @@ -230,7 +254,7 @@ mod tests { QualityItem::new(mime::STAR_STAR, q(0.8)), ]); assert_eq!( - test.mime_precedence(), + test.ranked(), vec![ mime::TEXT_HTML, "application/xhtml+xml".parse().unwrap(), @@ -245,20 +269,20 @@ mod tests { qitem(mime::IMAGE_PNG), ]); assert_eq!( - test.mime_precedence(), + test.ranked(), vec![mime::IMAGE_PNG, mime::IMAGE_STAR, mime::STAR_STAR] ); } #[test] - fn test_mime_preference() { + fn preference_selection() { let test = Accept(vec![ qitem(mime::TEXT_HTML), "application/xhtml+xml".parse().unwrap(), QualityItem::new("application/xml".parse().unwrap(), q(0.9)), QualityItem::new(mime::STAR_STAR, q(0.8)), ]); - assert_eq!(test.mime_preference(), Some(mime::TEXT_HTML)); + assert_eq!(test.preference(), mime::TEXT_HTML); let test = Accept(vec![ QualityItem::new("video/*".parse().unwrap(), q(0.8)), @@ -267,6 +291,6 @@ mod tests { qitem(mime::IMAGE_SVG), QualityItem::new(mime::IMAGE_STAR, q(0.8)), ]); - assert_eq!(test.mime_preference(), Some(mime::IMAGE_PNG)); + assert_eq!(test.preference(), mime::IMAGE_PNG); } } diff --git a/actix-http/src/header/common/accept_charset.rs b/src/http/header/accept_charset.rs similarity index 60% rename from actix-http/src/header/common/accept_charset.rs rename to src/http/header/accept_charset.rs index db530a8bc..5577ab604 100644 --- a/actix-http/src/header/common/accept_charset.rs +++ b/src/http/header/accept_charset.rs @@ -1,8 +1,8 @@ -use crate::header::{Charset, QualityItem, ACCEPT_CHARSET}; +use super::{Charset, QualityItem, ACCEPT_CHARSET}; -header! { +crate::http::header::common_header! { /// `Accept-Charset` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) + /// [RFC 7231 §5.3.3](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3) /// /// The `Accept-Charset` header field can be sent by a user agent to /// indicate what charsets are acceptable in textual response content. @@ -12,30 +12,29 @@ header! { /// those charsets. /// /// # ABNF - /// - /// ```text + /// ```plain /// Accept-Charset = 1#( ( charset / "*" ) [ weight ] ) /// ``` /// - /// # Example values + /// # Example Values /// * `iso-8859-5, unicode-1-1;q=0.8` /// /// # Examples /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) /// ); /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![ /// QualityItem::new(Charset::Us_Ascii, q(900)), @@ -45,18 +44,18 @@ header! { /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{AcceptCharset, Charset, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptCharset, Charset, qitem}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) /// ); /// ``` (AcceptCharset, ACCEPT_CHARSET) => (QualityItem)+ - test_accept_charset { + test_parse_and_format { // Test case from RFC - test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/actix-http/src/header/common/accept_encoding.rs b/src/http/header/accept_encoding.rs similarity index 50% rename from actix-http/src/header/common/accept_encoding.rs rename to src/http/header/accept_encoding.rs index c90f529bc..85cd0a4f7 100644 --- a/actix-http/src/header/common/accept_encoding.rs +++ b/src/http/header/accept_encoding.rs @@ -1,8 +1,11 @@ -use header::{Encoding, QualityItem}; +use actix_http::header::QualityItem; -header! { - /// `Accept-Encoding` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4) +use super::{common_header, Encoding}; +use crate::http::header; + +common_header! { + /// `Accept-Encoding` header, defined + /// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4) /// /// The `Accept-Encoding` header field can be used by user agents to /// indicate what response content-codings are @@ -11,13 +14,12 @@ header! { /// preferred. /// /// # ABNF - /// - /// ```text + /// ```plain /// Accept-Encoding = #( codings [ weight ] ) /// codings = content-coding / "identity" / "*" /// ``` /// - /// # Example values + /// # Example Values /// * `compress, gzip` /// * `` /// * `*` @@ -26,18 +28,20 @@ header! { /// /// # Examples /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptEncoding, Encoding, qitem}; /// - /// let mut headers = Headers::new(); - /// headers.set( + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( /// AcceptEncoding(vec![qitem(Encoding::Chunked)]) /// ); /// ``` /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptEncoding, Encoding, qitem}; /// - /// let mut headers = Headers::new(); - /// headers.set( + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( /// AcceptEncoding(vec![ /// qitem(Encoding::Chunked), /// qitem(Encoding::Gzip), @@ -46,10 +50,11 @@ header! { /// ); /// ``` /// ``` - /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q, qitem}; /// - /// let mut headers = Headers::new(); - /// headers.set( + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( /// AcceptEncoding(vec![ /// qitem(Encoding::Chunked), /// QualityItem::new(Encoding::Gzip, q(600)), @@ -57,16 +62,18 @@ header! { /// ]) /// ); /// ``` - (AcceptEncoding, "Accept-Encoding") => (QualityItem)* + (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem)* - test_accept_encoding { + test_parse_and_format { // From the RFC - test_header!(test1, vec![b"compress, gzip"]); - test_header!(test2, vec![b""], Some(AcceptEncoding(vec![]))); - test_header!(test3, vec![b"*"]); + common_header_test!(test1, vec![b"compress, gzip"]); + common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![]))); + common_header_test!(test3, vec![b"*"]); + // Note: Removed quality 1 from gzip - test_header!(test4, vec![b"compress;q=0.5, gzip"]); + common_header_test!(test4, vec![b"compress;q=0.5, gzip"]); + // Note: Removed quality 1 from gzip - test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); + common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]); } } diff --git a/src/http/header/accept_language.rs b/src/http/header/accept_language.rs new file mode 100644 index 000000000..229f95ef1 --- /dev/null +++ b/src/http/header/accept_language.rs @@ -0,0 +1,225 @@ +use language_tags::LanguageTag; + +use super::{common_header, Preference, QualityItem}; +use crate::http::header; + +common_header! { + /// `Accept-Language` header, defined + /// in [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) + /// + /// The `Accept-Language` header field can be used by user agents to indicate the set of natural + /// languages that are preferred in the response. + /// + /// The `Accept-Language` header is defined in + /// [RFC 7231 §5.3.5](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.5) using language + /// ranges defined in [RFC 4647 §2.1](https://datatracker.ietf.org/doc/html/rfc4647#section-2.1). + /// + /// # ABNF + /// ```plain + /// Accept-Language = 1#( language-range [ weight ] ) + /// language-range = (1*8ALPHA *("-" 1*8alphanum)) / "*" + /// alphanum = ALPHA / DIGIT + /// weight = OWS ";" OWS "q=" qvalue + /// qvalue = ( "0" [ "." 0*3DIGIT ] ) + /// / ( "1" [ "." 0*3("0") ] ) + /// ``` + /// + /// # Example Values + /// - `da, en-gb;q=0.8, en;q=0.7` + /// - `en-us;q=1.0, en;q=0.5, fr` + /// - `fr-CH, fr;q=0.9, en;q=0.8, de;q=0.7, *;q=0.5` + /// + /// # Examples + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptLanguage, qitem}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( + /// AcceptLanguage(vec![ + /// qitem("en-US".parse().unwrap()) + /// ]) + /// ); + /// ``` + /// + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( + /// AcceptLanguage(vec![ + /// qitem("da".parse().unwrap()), + /// QualityItem::new("en-GB".parse().unwrap(), q(800)), + /// QualityItem::new("en".parse().unwrap(), q(700)), + /// ]) + /// ); + /// ``` + (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* + + test_parse_and_format { + common_header_test!(no_headers, vec![b""; 0], Some(AcceptLanguage(vec![]))); + + common_header_test!(empty_header, vec![b""; 1], Some(AcceptLanguage(vec![]))); + + common_header_test!( + example_from_rfc, + vec![b"da, en-gb;q=0.8, en;q=0.7"] + ); + + + common_header_test!( + not_ordered_by_weight, + vec![b"en-US, en; q=0.5, fr"], + Some(AcceptLanguage(vec![ + qitem("en-US".parse().unwrap()), + QualityItem::new("en".parse().unwrap(), q(500)), + qitem("fr".parse().unwrap()), + ])) + ); + + common_header_test!( + has_wildcard, + vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], + Some(AcceptLanguage(vec![ + qitem("fr-CH".parse().unwrap()), + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("de".parse().unwrap(), q(700)), + QualityItem::new("*".parse().unwrap(), q(500)), + ])) + ); + } +} + +impl AcceptLanguage { + /// Returns a sorted list of languages from highest to lowest precedence, accounting + /// for [q-factor weighting]. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn ranked(&self) -> Vec> { + if self.0.is_empty() { + return vec![]; + } + + let mut types = self.0.clone(); + + // use stable sort so items with equal q-factor retain listed order + types.sort_by(|a, b| { + // sort by q-factor descending + b.quality.cmp(&a.quality) + }); + + types.into_iter().map(|qitem| qitem.item).collect() + } + + /// Extracts the most preferable language, accounting for [q-factor weighting]. + /// + /// If no q-factors are provided, the first language is chosen. Note that items without + /// q-factors are given the maximum preference value. + /// + /// As per the spec, returns [`Preference::Any`] if contained list is empty. + /// + /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 + pub fn preference(&self) -> Preference { + use actix_http::header::q; + + let mut max_item = None; + let mut max_pref = q(0); + + // uses manual max lookup loop since we want the first occurrence in the case of same + // preference but `Iterator::max_by_key` would give us the last occurrence + + for pref in &self.0 { + // only change if strictly greater + // equal items, even while unsorted, still have higher preference if they appear first + if pref.quality > max_pref { + max_pref = pref.quality; + max_item = Some(pref.item.clone()); + } + } + + max_item.unwrap_or(Preference::Any) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::header::*; + + #[test] + fn ranking_precedence() { + let test = AcceptLanguage(vec![]); + assert!(test.ranked().is_empty()); + + let test = AcceptLanguage(vec![qitem("fr-CH".parse().unwrap())]); + assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap())); + + let test = AcceptLanguage(vec![ + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1000)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::new("de".parse().unwrap(), q(700)), + ]); + assert_eq!( + test.ranked(), + vec![ + "fr-CH".parse().unwrap(), + "fr".parse().unwrap(), + "en".parse().unwrap(), + "de".parse().unwrap(), + "*".parse().unwrap(), + ] + ); + + let test = AcceptLanguage(vec![ + qitem("fr".parse().unwrap()), + qitem("fr-CH".parse().unwrap()), + qitem("en".parse().unwrap()), + qitem("*".parse().unwrap()), + qitem("de".parse().unwrap()), + ]); + assert_eq!( + test.ranked(), + vec![ + "fr".parse().unwrap(), + "fr-CH".parse().unwrap(), + "en".parse().unwrap(), + "*".parse().unwrap(), + "de".parse().unwrap(), + ] + ); + } + + #[test] + fn preference_selection() { + let test = AcceptLanguage(vec![ + QualityItem::new("fr".parse().unwrap(), q(900)), + QualityItem::new("fr-CH".parse().unwrap(), q(1000)), + QualityItem::new("en".parse().unwrap(), q(800)), + QualityItem::new("*".parse().unwrap(), q(500)), + QualityItem::new("de".parse().unwrap(), q(700)), + ]); + assert_eq!( + test.preference(), + Preference::Specific("fr-CH".parse().unwrap()) + ); + + let test = AcceptLanguage(vec![ + qitem("fr".parse().unwrap()), + qitem("fr-CH".parse().unwrap()), + qitem("en".parse().unwrap()), + qitem("*".parse().unwrap()), + qitem("de".parse().unwrap()), + ]); + assert_eq!( + test.preference(), + Preference::Specific("fr".parse().unwrap()) + ); + + let test = AcceptLanguage(vec![]); + assert_eq!(test.preference(), Preference::Any); + } +} diff --git a/actix-http/src/header/common/allow.rs b/src/http/header/allow.rs similarity index 65% rename from actix-http/src/header/common/allow.rs rename to src/http/header/allow.rs index 06b1efedc..c8cc153e8 100644 --- a/actix-http/src/header/common/allow.rs +++ b/src/http/header/allow.rs @@ -1,42 +1,42 @@ -use http::header; -use http::Method; +use actix_http::http::Method; -header! { - /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) +use crate::http::header; + +crate::http::header::common_header! { + /// `Allow` header, defined + /// in [RFC 7231 §7.4.1](https://datatracker.ietf.org/doc/html/rfc7231#section-7.4.1) /// /// The `Allow` header field lists the set of methods advertised as - /// supported by the target resource. The purpose of this field is + /// supported by the target resource. The purpose of this field is /// strictly to inform the recipient of valid request methods associated /// with the resource. /// /// # ABNF - /// - /// ```text + /// ```plain /// Allow = #method /// ``` /// - /// # Example values + /// # Example Values /// * `GET, HEAD, PUT` /// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr` /// * `` /// /// # Examples - /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::{header::Allow, Method}; + /// use actix_web::HttpResponse; + /// use actix_web::http::{header::Allow, Method}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Allow(vec![Method::GET]) /// ); /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::{header::Allow, Method}; + /// use actix_web::HttpResponse; + /// use actix_web::http::{header::Allow, Method}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Allow(vec![ /// Method::GET, @@ -47,14 +47,14 @@ header! { /// ``` (Allow, header::ALLOW) => (Method)* - test_allow { + test_parse_and_format { // From the RFC - test_header!( + crate::http::header::common_header_test!( test1, vec![b"GET, HEAD, PUT"], Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); // Own tests - test_header!( + crate::http::header::common_header_test!( test2, vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], Some(HeaderField(vec![ @@ -67,7 +67,7 @@ header! { Method::TRACE, Method::CONNECT, Method::PATCH]))); - test_header!( + crate::http::header::common_header_test!( test3, vec![b""], Some(HeaderField(Vec::::new()))); diff --git a/src/http/header/any_or_some.rs b/src/http/header/any_or_some.rs new file mode 100644 index 000000000..e5a37e495 --- /dev/null +++ b/src/http/header/any_or_some.rs @@ -0,0 +1,70 @@ +use std::{ + fmt::{self, Write as _}, + str, +}; + +/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the +/// underlying type does not support them. +/// +/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) +/// typed header but it does parse `*` successfully. On the other hand, the `mime` crate, used for +/// [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not +/// used in those header types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] +pub enum AnyOrSome { + /// A wildcard value. + Any, + + /// A valid `T`. + Item(T), +} + +impl AnyOrSome { + /// Returns true if item is wildcard (`*`) variant. + pub fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + /// Returns true if item is a valid item (`T`) variant. + pub fn is_item(&self) -> bool { + matches!(self, Self::Item(_)) + } + + /// Returns reference to value in `Item` variant, if it is set. + pub fn item(&self) -> Option<&T> { + match self { + AnyOrSome::Item(ref item) => Some(item), + AnyOrSome::Any => None, + } + } + + /// Consumes the container, returning the value in the `Item` variant, if it is set. + pub fn into_item(self) -> Option { + match self { + AnyOrSome::Item(item) => Some(item), + AnyOrSome::Any => None, + } + } +} + +impl fmt::Display for AnyOrSome { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + AnyOrSome::Any => f.write_char('*'), + AnyOrSome::Item(item) => fmt::Display::fmt(item, f), + } + } +} + +impl str::FromStr for AnyOrSome { + type Err = T::Err; + + #[inline] + fn from_str(s: &str) -> Result { + match s.trim() { + "*" => Ok(Self::Any), + other => other.parse().map(AnyOrSome::Item), + } + } +} diff --git a/src/http/header/cache_control.rs b/src/http/header/cache_control.rs new file mode 100644 index 000000000..490d36558 --- /dev/null +++ b/src/http/header/cache_control.rs @@ -0,0 +1,192 @@ +use std::{fmt, str}; + +use super::common_header; +use crate::http::header; + +common_header! { + /// `Cache-Control` header, defined + /// in [RFC 7234 §5.2](https://datatracker.ietf.org/doc/html/rfc7234#section-5.2). + /// + /// The `Cache-Control` header field is used to specify directives for + /// caches along the request/response chain. Such cache directives are + /// unidirectional in that the presence of a directive in a request does + /// not imply that the same directive is to be given in the response. + /// + /// # ABNF + /// ```text + /// Cache-Control = 1#cache-directive + /// cache-directive = token [ "=" ( token / quoted-string ) ] + /// ``` + /// + /// # Example Values + /// * `no-cache` + /// * `private, community="UCI"` + /// * `max-age=30` + /// + /// # Examples + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{CacheControl, CacheDirective}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); + /// ``` + /// + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{CacheControl, CacheDirective}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header(CacheControl(vec![ + /// CacheDirective::NoCache, + /// CacheDirective::Private, + /// CacheDirective::MaxAge(360u32), + /// CacheDirective::Extension("foo".to_owned(), Some("bar".to_owned())), + /// ])); + /// ``` + (CacheControl, header::CACHE_CONTROL) => (CacheDirective)+ + + test_parse_and_format { + common_header_test!(no_headers, vec![b""; 0], None); + common_header_test!(empty_header, vec![b""; 1], None); + common_header_test!(bad_syntax, vec![b"foo="], None); + + common_header_test!( + multiple_headers, + vec![&b"no-cache"[..], &b"private"[..]], + Some(CacheControl(vec![ + CacheDirective::NoCache, + CacheDirective::Private, + ])) + ); + + common_header_test!( + argument, + vec![b"max-age=100, private"], + Some(CacheControl(vec![ + CacheDirective::MaxAge(100), + CacheDirective::Private, + ])) + ); + + common_header_test!( + extension, + vec![b"foo, bar=baz"], + Some(CacheControl(vec![ + CacheDirective::Extension("foo".to_owned(), None), + CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), + ])) + ); + + #[test] + fn parse_quote_form() { + let req = test::TestRequest::default() + .insert_header((header::CACHE_CONTROL, "max-age=\"200\"")) + .finish(); + + assert_eq!( + Header::parse(&req).ok(), + Some(CacheControl(vec![CacheDirective::MaxAge(200)])) + ) + } + } +} + +/// `CacheControl` contains a list of these directives. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum CacheDirective { + /// "no-cache" + NoCache, + /// "no-store" + NoStore, + /// "no-transform" + NoTransform, + /// "only-if-cached" + OnlyIfCached, + + // request directives + /// "max-age=delta" + MaxAge(u32), + /// "max-stale=delta" + MaxStale(u32), + /// "min-fresh=delta" + MinFresh(u32), + + // response directives + /// "must-revalidate" + MustRevalidate, + /// "public" + Public, + /// "private" + Private, + /// "proxy-revalidate" + ProxyRevalidate, + /// "s-maxage=delta" + SMaxAge(u32), + + /// Extension directives. Optionally include an argument. + Extension(String, Option), +} + +impl fmt::Display for CacheDirective { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + use self::CacheDirective::*; + + let dir_str = match self { + NoCache => "no-cache", + NoStore => "no-store", + NoTransform => "no-transform", + OnlyIfCached => "only-if-cached", + + MaxAge(secs) => return write!(f, "max-age={}", secs), + MaxStale(secs) => return write!(f, "max-stale={}", secs), + MinFresh(secs) => return write!(f, "min-fresh={}", secs), + + MustRevalidate => "must-revalidate", + Public => "public", + Private => "private", + ProxyRevalidate => "proxy-revalidate", + SMaxAge(secs) => return write!(f, "s-maxage={}", secs), + + Extension(name, None) => name.as_str(), + Extension(name, Some(arg)) => return write!(f, "{}={}", name, arg), + }; + + f.write_str(dir_str) + } +} + +impl str::FromStr for CacheDirective { + type Err = Option<::Err>; + + fn from_str(s: &str) -> Result { + use self::CacheDirective::*; + + match s { + "" => Err(None), + + "no-cache" => Ok(NoCache), + "no-store" => Ok(NoStore), + "no-transform" => Ok(NoTransform), + "only-if-cached" => Ok(OnlyIfCached), + "must-revalidate" => Ok(MustRevalidate), + "public" => Ok(Public), + "private" => Ok(Private), + "proxy-revalidate" => Ok(ProxyRevalidate), + + _ => match s.find('=') { + Some(idx) if idx + 1 < s.len() => { + match (&s[..idx], (&s[idx + 1..]).trim_matches('"')) { + ("max-age", secs) => secs.parse().map(MaxAge).map_err(Some), + ("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some), + ("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some), + ("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some), + (left, right) => Ok(Extension(left.to_owned(), Some(right.to_owned()))), + } + } + Some(_) => Err(None), + None => Ok(Extension(s.to_owned(), None)), + }, + } + } +} diff --git a/actix-http/src/header/common/content_disposition.rs b/src/http/header/content_disposition.rs similarity index 86% rename from actix-http/src/header/common/content_disposition.rs rename to src/http/header/content_disposition.rs index ecc59aba3..945a58f7f 100644 --- a/actix-http/src/header/common/content_disposition.rs +++ b/src/http/header/content_disposition.rs @@ -1,16 +1,21 @@ -//! # References +//! The `Content-Disposition` header and associated types. //! -//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt -//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt -//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt -//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/ -//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml +//! # References +//! - "The Content-Disposition Header Field": +//! +//! - "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)": +//! +//! - "Returning Values from Forms: multipart/form-data": +//! +//! - Browser conformance tests at: +//! - IANA assignment: -use lazy_static::lazy_static; +use once_cell::sync::Lazy; use regex::Regex; use std::fmt::{self, Write}; -use crate::header::{self, ExtendedValue, Header, IntoHeaderValue, Writer}; +use super::{ExtendedValue, Header, IntoHeaderValue, Writer}; +use crate::http::header; /// Split at the index of the first `needle` if it exists or at the end. fn split_once(haystack: &str, needle: char) -> (&str, &str) { @@ -33,15 +38,19 @@ fn split_once_and_trim(haystack: &str, needle: char) -> (&str, &str) { /// The implied disposition of the content of the HTTP body. #[derive(Clone, Debug, PartialEq)] pub enum DispositionType { - /// Inline implies default processing + /// Inline implies default processing. Inline, + /// Attachment implies that the recipient should prompt the user to save the response locally, /// rather than process it normally (as per its media type). Attachment, + /// Used in *multipart/form-data* as defined in - /// [RFC7578](https://tools.ietf.org/html/rfc7578) to carry the field name and the file name. + /// [RFC 7578](https://datatracker.ietf.org/doc/html/rfc7578) to carry the field name and + /// optional filename. FormData, - /// Extension type. Should be handled by recipients the same way as Attachment + + /// Extension type. Should be handled by recipients the same way as Attachment. Ext(String), } @@ -63,7 +72,7 @@ impl<'a> From<&'a str> for DispositionType { /// /// # Examples /// ``` -/// use actix_http::http::header::DispositionParam; +/// use actix_web::http::header::DispositionParam; /// /// let param = DispositionParam::Filename(String::from("sample.txt")); /// assert!(param.is_filename()); @@ -75,25 +84,32 @@ pub enum DispositionParam { /// For [`DispositionType::FormData`] (i.e. *multipart/form-data*), the name of an field from /// the form. Name(String), + /// A plain file name. /// - /// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any - /// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where + /// It is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to contain + /// any non-ASCII characters when used in a *Content-Disposition* HTTP response header, where /// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead /// in case there are Unicode characters in file names. Filename(String), + /// An extended file name. It must not exist for `ContentType::Formdata` according to - /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). + /// [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2). FilenameExt(ExtendedValue), + /// An unrecognized regular parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *reg-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *token "=" value*. Recipients should - /// ignore unrecognizable parameters. + /// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as + /// `reg-parameter`, in + /// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as + /// `token "=" value`. Recipients should ignore unrecognizable parameters. Unknown(String, String), + /// An unrecognized extended parameter as defined in - /// [RFC5987](https://tools.ietf.org/html/rfc5987) as *ext-parameter*, in - /// [RFC6266](https://tools.ietf.org/html/rfc6266) as *ext-token "=" ext-value*. The single - /// trailing asterisk is not included. Recipients should ignore unrecognizable parameters. + /// [RFC 5987 §3.2.1](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2.1) as + /// `ext-parameter`, in + /// [RFC 6266 §4.1](https://datatracker.ietf.org/doc/html/rfc6266#section-4.1) as + /// `ext-token "=" ext-value`. The single trailing asterisk is not included. Recipients should + /// ignore unrecognizable parameters. UnknownExt(String, ExtendedValue), } @@ -187,10 +203,10 @@ impl DispositionParam { } /// A *Content-Disposition* header. It is compatible to be used either as -/// [a response header for the main body](https://mdn.io/Content-Disposition#As_a_response_header_for_the_main_body) -/// as (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266), or as +/// [a response header for the main body](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Content-Disposition#as_a_response_header_for_the_main_body) +/// as (re)defined in [RFC 6266](https://datatracker.ietf.org/doc/html/rfc6266), or as /// [a header for a multipart body](https://mdn.io/Content-Disposition#As_a_header_for_a_multipart_body) -/// as (re)defined in [RFC7587](https://tools.ietf.org/html/rfc7578). +/// as (re)defined in [RFC 7587](https://datatracker.ietf.org/doc/html/rfc7578). /// /// In a regular HTTP response, the *Content-Disposition* response header is a header indicating if /// the content is expected to be displayed *inline* in the browser, that is, as a Web page or as @@ -204,8 +220,7 @@ impl DispositionParam { /// itself, *Content-Disposition* has no effect. /// /// # ABNF - -/// ```text +/// ```plain /// content-disposition = "Content-Disposition" ":" /// disposition-type *( ";" disposition-parm ) /// @@ -226,21 +241,19 @@ impl DispositionParam { /// ``` /// /// # Note +/// *filename* is [not supposed](https://datatracker.ietf.org/doc/html/rfc6266#appendix-D) to +/// contain any non-ASCII characters when used in a *Content-Disposition* HTTP response header, +/// where filename* with charset UTF-8 may be used instead in case there are Unicode characters in +/// file names. Filename is [acceptable](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) +/// to be UTF-8 encoded directly in a *Content-Disposition* header for +/// *multipart/form-data*, though. /// -/// filename is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any -/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where -/// filename* with charset UTF-8 may be used instead in case there are Unicode characters in file -/// names. -/// filename is [acceptable](https://tools.ietf.org/html/rfc7578#section-4.2) to be UTF-8 encoded -/// directly in a *Content-Disposition* header for *multipart/form-data*, though. -/// -/// filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within +/// *filename* [must not](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) be used within /// *multipart/form-data*. /// -/// # Example -/// +/// # Examples /// ``` -/// use actix_http::http::header::{ +/// use actix_web::http::header::{ /// Charset, ContentDisposition, DispositionParam, DispositionType, /// ExtendedValue, /// }; @@ -284,14 +297,16 @@ impl DispositionParam { /// ``` /// /// # Security Note -/// /// If "filename" parameter is supplied, do not use the file name blindly, check and possibly /// change to match local file system conventions if applicable, and do not use directory path -/// information that may be present. See [RFC2183](https://tools.ietf.org/html/rfc2183#section-2.3). +/// information that may be present. +/// See [RFC 2183 §2.3](https://datatracker.ietf.org/doc/html/rfc2183#section-2.3). +// TODO: think about using private fields and smallvec #[derive(Clone, Debug, PartialEq)] pub struct ContentDisposition { /// The disposition type pub disposition: DispositionType, + /// Disposition parameters pub parameters: Vec, } @@ -333,7 +348,7 @@ impl ContentDisposition { } else { // regular parameters let value = if left.starts_with('\"') { - // quoted-string: defined in RFC6266 -> RFC2616 Section 3.6 + // quoted-string: defined in RFC 6266 -> RFC 2616 Section 3.6 let mut escaping = false; let mut quoted_string = vec![]; let mut end = None; @@ -384,70 +399,58 @@ impl ContentDisposition { Ok(cd) } - /// Returns `true` if it is [`Inline`](DispositionType::Inline). + /// Returns `true` if type is [`Inline`](DispositionType::Inline). pub fn is_inline(&self) -> bool { matches!(self.disposition, DispositionType::Inline) } - /// Returns `true` if it is [`Attachment`](DispositionType::Attachment). + /// Returns `true` if type is [`Attachment`](DispositionType::Attachment). pub fn is_attachment(&self) -> bool { matches!(self.disposition, DispositionType::Attachment) } - /// Returns `true` if it is [`FormData`](DispositionType::FormData). + /// Returns `true` if type is [`FormData`](DispositionType::FormData). pub fn is_form_data(&self) -> bool { matches!(self.disposition, DispositionType::FormData) } - /// Returns `true` if it is [`Ext`](DispositionType::Ext) and the `disp_type` matches. - pub fn is_ext>(&self, disp_type: T) -> bool { - match self.disposition { - DispositionType::Ext(ref t) - if t.eq_ignore_ascii_case(disp_type.as_ref()) => - { - true - } - _ => false, - } + /// Returns `true` if type is [`Ext`](DispositionType::Ext) and the `disp_type` matches. + pub fn is_ext(&self, disp_type: impl AsRef) -> bool { + matches!( + self.disposition, + DispositionType::Ext(ref t) if t.eq_ignore_ascii_case(disp_type.as_ref()) + ) } /// Return the value of *name* if exists. pub fn get_name(&self) -> Option<&str> { - self.parameters.iter().filter_map(|p| p.as_name()).next() + self.parameters.iter().find_map(DispositionParam::as_name) } /// Return the value of *filename* if exists. pub fn get_filename(&self) -> Option<&str> { self.parameters .iter() - .filter_map(|p| p.as_filename()) - .next() + .find_map(DispositionParam::as_filename) } /// Return the value of *filename\** if exists. pub fn get_filename_ext(&self) -> Option<&ExtendedValue> { self.parameters .iter() - .filter_map(|p| p.as_filename_ext()) - .next() + .find_map(DispositionParam::as_filename_ext) } /// Return the value of the parameter which the `name` matches. - pub fn get_unknown>(&self, name: T) -> Option<&str> { + pub fn get_unknown(&self, name: impl AsRef) -> Option<&str> { let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown(name)) - .next() + self.parameters.iter().find_map(|p| p.as_unknown(name)) } /// Return the value of the extended parameter which the `name` matches. - pub fn get_unknown_ext>(&self, name: T) -> Option<&ExtendedValue> { + pub fn get_unknown_ext(&self, name: impl AsRef) -> Option<&ExtendedValue> { let name = name.as_ref(); - self.parameters - .iter() - .filter_map(|p| p.as_unknown_ext(name)) - .next() + self.parameters.iter().find_map(|p| p.as_unknown_ext(name)) } } @@ -468,7 +471,7 @@ impl Header for ContentDisposition { fn parse(msg: &T) -> Result { if let Some(h) = msg.headers().get(&Self::name()) { - Self::from_raw(&h) + Self::from_raw(h) } else { Err(crate::error::ParseError::Header) } @@ -490,7 +493,9 @@ impl fmt::Display for DispositionParam { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and // backslash should be escaped in quoted-string (i.e. "foobar"). - // Ref: RFC6266 S4.1 -> RFC2616 S3.6 + // + // Ref: RFC 6266 §4.1 -> RFC 2616 §3.6 + // // filename-parm = "filename" "=" value // value = token | quoted-string // quoted-string = ( <"> *(qdtext | quoted-pair ) <"> ) @@ -504,7 +509,7 @@ impl fmt::Display for DispositionParam { // CTL = // - // Ref: RFC7578 S4.2 -> RFC2183 S2 -> RFC2045 S5.1 + // Ref: RFC 7578 S4.2 -> RFC 2183 S2 -> RFC 2045 S5.1 // parameter := attribute "=" value // attribute := token // ; Matching of attributes @@ -520,23 +525,28 @@ impl fmt::Display for DispositionParam { // // // See also comments in test_from_raw_unnecessary_percent_decode. - lazy_static! { - static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap(); - } + + static RE: Lazy = + Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); + match self { DispositionParam::Name(ref value) => write!(f, "name={}", value), + DispositionParam::Filename(ref value) => { write!(f, "filename=\"{}\"", RE.replace_all(value, "\\$0").as_ref()) } + DispositionParam::Unknown(ref name, ref value) => write!( f, "{}=\"{}\"", name, &RE.replace_all(value, "\\$0").as_ref() ), + DispositionParam::FilenameExt(ref ext_value) => { write!(f, "filename*={}", ext_value) } + DispositionParam::UnknownExt(ref name, ref ext_value) => { write!(f, "{}*={}", name, ext_value) } @@ -556,8 +566,7 @@ impl fmt::Display for ContentDisposition { #[cfg(test)] mod tests { use super::{ContentDisposition, DispositionParam, DispositionType}; - use crate::header::shared::Charset; - use crate::header::{ExtendedValue, HeaderValue}; + use crate::http::header::{Charset, ExtendedValue, HeaderValue}; #[test] fn test_from_raw_basic() { @@ -619,8 +628,8 @@ mod tests { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', + 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', + b'a', b't', b'e', b's', ], })], }; @@ -636,8 +645,8 @@ mod tests { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, - b'r', b'a', b't', b'e', b's', + 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', + b'a', b't', b'e', b's', ], })], }; @@ -699,26 +708,22 @@ mod tests { #[test] fn test_from_raw_only_disp() { - let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")) - .unwrap(); + let a = ContentDisposition::from_raw(&HeaderValue::from_static("attachment")).unwrap(); let b = ContentDisposition { disposition: DispositionType::Attachment, parameters: vec![], }; assert_eq!(a, b); - let a = - ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); + let a = ContentDisposition::from_raw(&HeaderValue::from_static("inline ;")).unwrap(); let b = ContentDisposition { disposition: DispositionType::Inline, parameters: vec![], }; assert_eq!(a, b); - let a = ContentDisposition::from_raw(&HeaderValue::from_static( - "unknown-disp-param", - )) - .unwrap(); + let a = ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")) + .unwrap(); let b = ContentDisposition { disposition: DispositionType::Ext(String::from("unknown-disp-param")), parameters: vec![], @@ -749,7 +754,7 @@ mod tests { #[test] fn from_raw_with_unicode() { - /* RFC7578 Section 4.2: + /* RFC 7578 Section 4.2: Some commonly deployed systems use multipart/form-data with file names directly encoded including octets outside the US-ASCII range. The encoding used for the file names is typically UTF-8, although HTML forms will use the charset associated with the form. @@ -757,8 +762,8 @@ mod tests { Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. (And now, only UTF-8 is handled by this implementation.) */ - let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"") - .unwrap(); + let a = + HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, @@ -809,8 +814,7 @@ mod tests { #[test] fn test_from_raw_semicolon() { - let a = - HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); + let a = HeaderValue::from_static("form-data; filename=\"A semicolon here;.pdf\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, @@ -823,9 +827,9 @@ mod tests { #[test] fn test_from_raw_unnecessary_percent_decode() { - // In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with + // In fact, RFC 7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with // non-ASCII characters MAY be percent-encoded. - // On the contrary, RFC6266 or other RFCs related to Content-Disposition response header + // On the contrary, RFC 6266 or other RFCs related to Content-Disposition response header // do not mention such percent-encoding. // So, it appears to be undecidable whether to percent-decode or not without // knowing the usage scenario (multipart/form-data v.s. HTTP response header) and @@ -846,9 +850,8 @@ mod tests { }; assert_eq!(a, b); - let a = HeaderValue::from_static( - "form-data; name=photo; filename=\"%74%65%73%74.png\"", - ); + let a = + HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, @@ -893,8 +896,7 @@ mod tests { #[test] fn test_display_extended() { - let as_string = - "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; + let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates"; let a = HeaderValue::from_static(as_string); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let display_rendered = format!("{}", a); diff --git a/src/http/header/content_language.rs b/src/http/header/content_language.rs new file mode 100644 index 000000000..39ca8da56 --- /dev/null +++ b/src/http/header/content_language.rs @@ -0,0 +1,54 @@ +use language_tags::LanguageTag; + +use super::{common_header, QualityItem, CONTENT_LANGUAGE}; + +common_header! { + /// `Content-Language` header, defined + /// in [RFC 7231 §3.1.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.3.2) + /// + /// The `Content-Language` header field describes the natural language(s) + /// of the intended audience for the representation. Note that this + /// might not be equivalent to all the languages used within the + /// representation. + /// + /// # ABNF + /// ```plain + /// Content-Language = 1#language-tag + /// ``` + /// + /// # Example Values + /// * `da` + /// * `mi, en` + /// + /// # Examples + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( + /// ContentLanguage(vec![ + /// qitem(LanguageTag::parse("en").unwrap()), + /// ]) + /// ); + /// ``` + /// + /// ``` + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{ContentLanguage, LanguageTag, qitem}; + /// + /// let mut builder = HttpResponse::Ok(); + /// builder.insert_header( + /// ContentLanguage(vec![ + /// qitem(LanguageTag::parse("da").unwrap()), + /// qitem(LanguageTag::parse("en-GB").unwrap()), + /// ]) + /// ); + /// ``` + (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ + + test_parse_and_format { + crate::http::header::common_header_test!(test1, vec![b"da"]); + crate::http::header::common_header_test!(test2, vec![b"mi, en"]); + } +} diff --git a/actix-http/src/header/common/content_range.rs b/src/http/header/content_range.rs similarity index 79% rename from actix-http/src/header/common/content_range.rs rename to src/http/header/content_range.rs index 8b7552377..90b3f7fe2 100644 --- a/actix-http/src/header/common/content_range.rs +++ b/src/http/header/content_range.rs @@ -1,81 +1,81 @@ -use std::fmt::{self, Display, Write}; -use std::str::FromStr; - -use crate::error::ParseError; -use crate::header::{ - HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE, +use std::{ + fmt::{self, Display, Write}, + str::FromStr, }; -header! { - /// `Content-Range` header, defined in - /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) +use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE}; +use crate::error::ParseError; + +crate::http::header::common_header! { + /// `Content-Range` header, defined + /// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) (ContentRange, CONTENT_RANGE) => [ContentRangeSpec] - test_content_range { - test_header!(test_bytes, + test_parse_and_format { + crate::http::header::common_header_test!(test_bytes, vec![b"bytes 0-499/500"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: Some(500) }))); - test_header!(test_bytes_unknown_len, + crate::http::header::common_header_test!(test_bytes_unknown_len, vec![b"bytes 0-499/*"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: None }))); - test_header!(test_bytes_unknown_range, + crate::http::header::common_header_test!(test_bytes_unknown_range, vec![b"bytes */500"], Some(ContentRange(ContentRangeSpec::Bytes { range: None, instance_length: Some(500) }))); - test_header!(test_unregistered, + crate::http::header::common_header_test!(test_unregistered, vec![b"seconds 1-2"], Some(ContentRange(ContentRangeSpec::Unregistered { unit: "seconds".to_owned(), resp: "1-2".to_owned() }))); - test_header!(test_no_len, + crate::http::header::common_header_test!(test_no_len, vec![b"bytes 0-499"], None::); - test_header!(test_only_unit, + crate::http::header::common_header_test!(test_only_unit, vec![b"bytes"], None::); - test_header!(test_end_less_than_start, + crate::http::header::common_header_test!(test_end_less_than_start, vec![b"bytes 499-0/500"], None::); - test_header!(test_blank, + crate::http::header::common_header_test!(test_blank, vec![b""], None::); - test_header!(test_bytes_many_spaces, + crate::http::header::common_header_test!(test_bytes_many_spaces, vec![b"bytes 1-2/500 3"], None::); - test_header!(test_bytes_many_slashes, + crate::http::header::common_header_test!(test_bytes_many_slashes, vec![b"bytes 1-2/500/600"], None::); - test_header!(test_bytes_many_dashes, + crate::http::header::common_header_test!(test_bytes_many_dashes, vec![b"bytes 1-2-3/500"], None::); } } -/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2) +/// Content-Range header, defined +/// in [RFC 7233 §4.2](https://datatracker.ietf.org/doc/html/rfc7233#section-4.2) /// /// # ABNF -/// -/// ```text +/// ```plain /// Content-Range = byte-content-range /// / other-content-range /// @@ -91,7 +91,7 @@ header! { /// other-content-range = other-range-unit SP other-range-resp /// other-range-resp = *CHAR /// ``` -#[derive(PartialEq, Clone, Debug)] +#[derive(Debug, Clone, PartialEq, Eq)] pub enum ContentRangeSpec { /// Byte range Bytes { @@ -141,8 +141,7 @@ impl FromStr for ContentRangeSpec { } else { let (first_byte, last_byte) = split_in_two(range, '-').ok_or(ParseError::Header)?; - let first_byte = - first_byte.parse().map_err(|_| ParseError::Header)?; + let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?; let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; if last_byte < first_byte { return Err(ParseError::Header); diff --git a/actix-http/src/header/common/content_type.rs b/src/http/header/content_type.rs similarity index 65% rename from actix-http/src/header/common/content_type.rs rename to src/http/header/content_type.rs index ac5c7e5b8..1fc75d0e2 100644 --- a/actix-http/src/header/common/content_type.rs +++ b/src/http/header/content_type.rs @@ -1,9 +1,9 @@ -use crate::header::CONTENT_TYPE; +use super::CONTENT_TYPE; use mime::Mime; -header! { - /// `Content-Type` header, defined in - /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5) +crate::http::header::common_header! { + /// `Content-Type` header, defined + /// in [RFC 7231 §3.1.1.5](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5) /// /// The `Content-Type` header field indicates the media type of the /// associated representation: either the representation enclosed in the @@ -18,41 +18,38 @@ header! { /// this is an issue, it's possible to implement `Header` on a custom struct. /// /// # ABNF - /// - /// ```text + /// ```plain /// Content-Type = media-type /// ``` /// - /// # Example values - /// + /// # Example Values /// * `text/html; charset=utf-8` /// * `application/json` /// /// # Examples - /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::ContentType; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::ContentType; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ContentType::json() /// ); /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::ContentType; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::ContentType; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ContentType(mime::TEXT_HTML) /// ); /// ``` (ContentType, CONTENT_TYPE) => [Mime] - test_content_type { - test_header!( + test_parse_and_format { + crate::http::header::common_header_test!( test1, vec![b"text/html"], Some(HeaderField(mime::TEXT_HTML))); @@ -60,57 +57,56 @@ header! { } impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` + /// A constructor to easily create a `Content-Type: application/json` /// header. #[inline] pub fn json() -> ContentType { ContentType(mime::APPLICATION_JSON) } - /// A constructor to easily create a `Content-Type: text/plain; + /// A constructor to easily create a `Content-Type: text/plain; /// charset=utf-8` header. #[inline] pub fn plaintext() -> ContentType { ContentType(mime::TEXT_PLAIN_UTF_8) } - /// A constructor to easily create a `Content-Type: text/html` header. + /// A constructor to easily create a `Content-Type: text/html; charset=utf-8` + /// header. #[inline] pub fn html() -> ContentType { - ContentType(mime::TEXT_HTML) + ContentType(mime::TEXT_HTML_UTF_8) } - /// A constructor to easily create a `Content-Type: text/xml` header. + /// A constructor to easily create a `Content-Type: text/xml` header. #[inline] pub fn xml() -> ContentType { ContentType(mime::TEXT_XML) } - /// A constructor to easily create a `Content-Type: + /// A constructor to easily create a `Content-Type: /// application/www-form-url-encoded` header. #[inline] pub fn form_url_encoded() -> ContentType { ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) } - /// A constructor to easily create a `Content-Type: image/jpeg` header. + /// A constructor to easily create a `Content-Type: image/jpeg` header. #[inline] pub fn jpeg() -> ContentType { ContentType(mime::IMAGE_JPEG) } - /// A constructor to easily create a `Content-Type: image/png` header. + /// A constructor to easily create a `Content-Type: image/png` header. #[inline] pub fn png() -> ContentType { ContentType(mime::IMAGE_PNG) } - /// A constructor to easily create a `Content-Type: + /// A constructor to easily create a `Content-Type: /// application/octet-stream` header. #[inline] pub fn octet_stream() -> ContentType { ContentType(mime::APPLICATION_OCTET_STREAM) } } - -impl Eq for ContentType {} diff --git a/actix-http/src/header/common/date.rs b/src/http/header/date.rs similarity index 56% rename from actix-http/src/header/common/date.rs rename to src/http/header/date.rs index e5ace95e6..4063deab1 100644 --- a/actix-http/src/header/common/date.rs +++ b/src/http/header/date.rs @@ -1,38 +1,37 @@ -use crate::header::{HttpDate, DATE}; +use super::{HttpDate, DATE}; use std::time::SystemTime; -header! { - /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) +crate::http::header::common_header! { + /// `Date` header, defined + /// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) /// /// The `Date` header field represents the date and time at which the /// message was originated. /// /// # ABNF - /// - /// ```text + /// ```plain /// Date = HTTP-date /// ``` /// - /// # Example values - /// + /// # Example Values /// * `Tue, 15 Nov 1994 08:12:31 GMT` /// /// # Example /// /// ``` /// use std::time::SystemTime; - /// use actix_http::Response; - /// use actix_http::http::header::Date; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::Date; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// Date(SystemTime::now().into()) /// ); /// ``` (Date, DATE) => [HttpDate] - test_date { - test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); + test_parse_and_format { + crate::http::header::common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); } } diff --git a/actix-http/src/header/shared/encoding.rs b/src/http/header/encoding.rs similarity index 88% rename from actix-http/src/header/shared/encoding.rs rename to src/http/header/encoding.rs index aa49dea45..a61edda67 100644 --- a/actix-http/src/header/shared/encoding.rs +++ b/src/http/header/encoding.rs @@ -1,27 +1,36 @@ use std::{fmt, str}; pub use self::Encoding::{ - Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, + Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd, }; -/// A value to represent an encoding used in `Transfer-Encoding` -/// or `Accept-Encoding` header. -#[derive(Clone, PartialEq, Debug)] +/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header. +#[derive(Debug, Clone, PartialEq, Eq)] pub enum Encoding { /// The `chunked` encoding. Chunked, + /// The `br` encoding. Brotli, + /// The `gzip` encoding. Gzip, + /// The `deflate` encoding. Deflate, + /// The `compress` encoding. Compress, + /// The `identity` encoding. Identity, + /// The `trailers` encoding. Trailers, + + /// The `zstd` encoding. + Zstd, + /// Some other encoding that is less common, can be any String. EncodingExt(String), } @@ -36,6 +45,7 @@ impl fmt::Display for Encoding { Compress => "compress", Identity => "identity", Trailers => "trailers", + Zstd => "zstd", EncodingExt(ref s) => s.as_ref(), }) } @@ -52,6 +62,7 @@ impl str::FromStr for Encoding { "compress" => Ok(Compress), "identity" => Ok(Identity), "trailers" => Ok(Trailers), + "zstd" => Ok(Zstd), _ => Ok(EncodingExt(s.to_owned())), } } diff --git a/actix-http/src/header/shared/entity.rs b/src/http/header/entity.rs similarity index 95% rename from actix-http/src/header/shared/entity.rs rename to src/http/header/entity.rs index eb383cd6f..50b40b7b2 100644 --- a/actix-http/src/header/shared/entity.rs +++ b/src/http/header/entity.rs @@ -1,7 +1,9 @@ -use std::fmt::{self, Display, Write}; -use std::str::FromStr; +use std::{ + fmt::{self, Display, Write}, + str::FromStr, +}; -use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; +use super::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer}; /// check that each char in the slice is either: /// 1. `%x21`, or @@ -15,7 +17,8 @@ fn check_slice_validity(slice: &str) -> bool { slice.bytes().all(entity_validate_char) } -/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3) +/// An entity tag, defined +/// in [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) /// /// An entity tag consists of a string enclosed by two literal double quotes. /// Preceding the first double quote is an optional weakness indicator, @@ -23,8 +26,7 @@ fn check_slice_validity(slice: &str) -> bool { /// `W/"xyzzy"`. /// /// # ABNF -/// -/// ```text +/// ```plain /// entity-tag = [ weak ] opaque-tag /// weak = %x57.2F ; "W/", case-sensitive /// opaque-tag = DQUOTE *etagc DQUOTE @@ -127,9 +129,8 @@ impl Display for EntityTag { impl FromStr for EntityTag { type Err = crate::error::ParseError; - fn from_str(s: &str) -> Result { - let length: usize = s.len(); - let slice = &s[..]; + fn from_str(slice: &str) -> Result { + let length = slice.len(); // Early exits if it doesn't terminate in a DQUOTE. if !slice.ends_with('"') || slice.len() < 2 { return Err(crate::error::ParseError::Header); diff --git a/actix-http/src/header/common/etag.rs b/src/http/header/etag.rs similarity index 63% rename from actix-http/src/header/common/etag.rs rename to src/http/header/etag.rs index 4c1e8d262..4724c917e 100644 --- a/actix-http/src/header/common/etag.rs +++ b/src/http/header/etag.rs @@ -1,7 +1,8 @@ -use crate::header::{EntityTag, ETAG}; +use super::{EntityTag, ETAG}; -header! { - /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) +crate::http::header::common_header! { + /// `ETag` header, defined in + /// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3) /// /// The `ETag` header field in a response provides the current entity-tag /// for the selected representation, as determined at the conclusion of @@ -14,86 +15,83 @@ header! { /// prefixed by a weakness indicator. /// /// # ABNF - /// - /// ```text + /// ```plain /// ETag = entity-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `"xyzzy"` /// * `W/"xyzzy"` /// * `""` /// /// # Examples - /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{ETag, EntityTag}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{ETag, EntityTag}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ETag(EntityTag::new(false, "xyzzy".to_owned())) /// ); /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{ETag, EntityTag}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{ETag, EntityTag}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// ETag(EntityTag::new(true, "xyzzy".to_owned())) /// ); /// ``` (ETag, ETAG) => [EntityTag] - test_etag { + test_parse_and_format { // From the RFC - test_header!(test1, + crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""], Some(ETag(EntityTag::new(false, "xyzzy".to_owned())))); - test_header!(test2, + crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""], Some(ETag(EntityTag::new(true, "xyzzy".to_owned())))); - test_header!(test3, + crate::http::header::common_header_test!(test3, vec![b"\"\""], Some(ETag(EntityTag::new(false, "".to_owned())))); // Own tests - test_header!(test4, + crate::http::header::common_header_test!(test4, vec![b"\"foobar\""], Some(ETag(EntityTag::new(false, "foobar".to_owned())))); - test_header!(test5, + crate::http::header::common_header_test!(test5, vec![b"\"\""], Some(ETag(EntityTag::new(false, "".to_owned())))); - test_header!(test6, + crate::http::header::common_header_test!(test6, vec![b"W/\"weak-etag\""], Some(ETag(EntityTag::new(true, "weak-etag".to_owned())))); - test_header!(test7, + crate::http::header::common_header_test!(test7, vec![b"W/\"\x65\x62\""], Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned())))); - test_header!(test8, + crate::http::header::common_header_test!(test8, vec![b"W/\"\""], Some(ETag(EntityTag::new(true, "".to_owned())))); - test_header!(test9, + crate::http::header::common_header_test!(test9, vec![b"no-dquotes"], None::); - test_header!(test10, + crate::http::header::common_header_test!(test10, vec![b"w/\"the-first-w-is-case-sensitive\""], None::); - test_header!(test11, + crate::http::header::common_header_test!(test11, vec![b""], None::); - test_header!(test12, + crate::http::header::common_header_test!(test12, vec![b"\"unmatched-dquotes1"], None::); - test_header!(test13, + crate::http::header::common_header_test!(test13, vec![b"unmatched-dquotes2\""], None::); - test_header!(test14, + crate::http::header::common_header_test!(test14, vec![b"matched-\"dquotes\""], None::); - test_header!(test15, + crate::http::header::common_header_test!(test15, vec![b"\""], None::); } diff --git a/actix-http/src/header/common/expires.rs b/src/http/header/expires.rs similarity index 60% rename from actix-http/src/header/common/expires.rs rename to src/http/header/expires.rs index 79563955d..5b6c65c53 100644 --- a/actix-http/src/header/common/expires.rs +++ b/src/http/header/expires.rs @@ -1,7 +1,8 @@ -use crate::header::{HttpDate, EXPIRES}; +use super::{HttpDate, EXPIRES}; -header! { - /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) +crate::http::header::common_header! { + /// `Expires` header, defined + /// in [RFC 7234 §5.3](https://datatracker.ietf.org/doc/html/rfc7234#section-5.3) /// /// The `Expires` header field gives the date/time after which the /// response is considered stale. @@ -11,22 +12,21 @@ header! { /// time. /// /// # ABNF - /// - /// ```text + /// ```plain /// Expires = HTTP-date /// ``` /// - /// # Example values + /// # Example Values /// * `Thu, 01 Dec 1994 16:00:00 GMT` /// /// # Example /// /// ``` /// use std::time::{SystemTime, Duration}; - /// use actix_http::Response; - /// use actix_http::http::header::Expires; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::Expires; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// Expires(expiration.into()) @@ -34,8 +34,8 @@ header! { /// ``` (Expires, EXPIRES) => [HttpDate] - test_expires { + test_parse_and_format { // Test case from RFC - test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); } } diff --git a/actix-http/src/header/common/if_match.rs b/src/http/header/if_match.rs similarity index 71% rename from actix-http/src/header/common/if_match.rs rename to src/http/header/if_match.rs index db255e91a..a565b9125 100644 --- a/actix-http/src/header/common/if_match.rs +++ b/src/http/header/if_match.rs @@ -1,8 +1,8 @@ -use crate::header::{EntityTag, IF_MATCH}; +use super::{common_header, EntityTag, IF_MATCH}; -header! { - /// `If-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) +common_header! { + /// `If-Match` header, defined + /// in [RFC 7232 §3.1](https://datatracker.ietf.org/doc/html/rfc7232#section-3.1) /// /// The `If-Match` header field makes the request method conditional on /// the recipient origin server either having at least one current @@ -17,31 +17,28 @@ header! { /// there have been any changes to the representation data. /// /// # ABNF - /// - /// ```text + /// ```plain /// If-Match = "*" / 1#entity-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `"xyzzy"` /// * "xyzzy", "r2d2xxxx", "c3piozzzz" /// /// # Examples - /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::IfMatch; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::IfMatch; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(IfMatch::Any); /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{IfMatch, EntityTag}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{IfMatch, EntityTag}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// IfMatch::Items(vec![ /// EntityTag::new(false, "xyzzy".to_owned()), @@ -52,19 +49,19 @@ header! { /// ``` (IfMatch, IF_MATCH) => {Any / (EntityTag)+} - test_if_match { - test_header!( + test_parse_and_format { + crate::http::header::common_header_test!( test1, vec![b"\"xyzzy\""], Some(HeaderField::Items( vec![EntityTag::new(false, "xyzzy".to_owned())]))); - test_header!( + crate::http::header::common_header_test!( test2, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], Some(HeaderField::Items( vec![EntityTag::new(false, "xyzzy".to_owned()), EntityTag::new(false, "r2d2xxxx".to_owned()), EntityTag::new(false, "c3piozzzz".to_owned())]))); - test_header!(test3, vec![b"*"], Some(IfMatch::Any)); + crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); } } diff --git a/actix-http/src/header/common/if_modified_since.rs b/src/http/header/if_modified_since.rs similarity index 61% rename from actix-http/src/header/common/if_modified_since.rs rename to src/http/header/if_modified_since.rs index 99c7e441d..14d6c3553 100644 --- a/actix-http/src/header/common/if_modified_since.rs +++ b/src/http/header/if_modified_since.rs @@ -1,8 +1,8 @@ -use crate::header::{HttpDate, IF_MODIFIED_SINCE}; +use super::{HttpDate, IF_MODIFIED_SINCE}; -header! { - /// `If-Modified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) +crate::http::header::common_header! { + /// `If-Modified-Since` header, defined + /// in [RFC 7232 §3.3](https://datatracker.ietf.org/doc/html/rfc7232#section-3.3) /// /// The `If-Modified-Since` header field makes a GET or HEAD request /// method conditional on the selected representation's modification date @@ -11,22 +11,21 @@ header! { /// data has not changed. /// /// # ABNF - /// - /// ```text + /// ```plain /// If-Unmodified-Since = HTTP-date /// ``` /// - /// # Example values + /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Example /// /// ``` /// use std::time::{SystemTime, Duration}; - /// use actix_http::Response; - /// use actix_http::http::header::IfModifiedSince; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::IfModifiedSince; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// IfModifiedSince(modified.into()) @@ -34,8 +33,8 @@ header! { /// ``` (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] - test_if_modified_since { + test_parse_and_format { // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/actix-http/src/header/common/if_none_match.rs b/src/http/header/if_none_match.rs similarity index 67% rename from actix-http/src/header/common/if_none_match.rs rename to src/http/header/if_none_match.rs index 464caf1ae..fb1895fc8 100644 --- a/actix-http/src/header/common/if_none_match.rs +++ b/src/http/header/if_none_match.rs @@ -1,8 +1,8 @@ -use crate::header::{EntityTag, IF_NONE_MATCH}; +use super::{EntityTag, IF_NONE_MATCH}; -header! { - /// `If-None-Match` header, defined in - /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) +crate::http::header::common_header! { + /// `If-None-Match` header, defined + /// in [RFC 7232 §3.2](https://datatracker.ietf.org/doc/html/rfc7232#section-3.2) /// /// The `If-None-Match` header field makes the request method conditional /// on a recipient cache or origin server either not having any current @@ -16,13 +16,11 @@ header! { /// the representation data. /// /// # ABNF - /// - /// ```text + /// ```plain /// If-None-Match = "*" / 1#entity-tag /// ``` /// - /// # Example values - /// + /// # Example Values /// * `"xyzzy"` /// * `W/"xyzzy"` /// * `"xyzzy", "r2d2xxxx", "c3piozzzz"` @@ -30,20 +28,19 @@ header! { /// * `*` /// /// # Examples - /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::IfNoneMatch; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::IfNoneMatch; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header(IfNoneMatch::Any); /// ``` /// /// ``` - /// use actix_http::Response; - /// use actix_http::http::header::{IfNoneMatch, EntityTag}; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::{IfNoneMatch, EntityTag}; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// IfNoneMatch::Items(vec![ /// EntityTag::new(false, "xyzzy".to_owned()), @@ -54,20 +51,20 @@ header! { /// ``` (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} - test_if_none_match { - test_header!(test1, vec![b"\"xyzzy\""]); - test_header!(test2, vec![b"W/\"xyzzy\""]); - test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - test_header!(test5, vec![b"*"]); + test_parse_and_format { + crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""]); + crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""]); + crate::http::header::common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); + crate::http::header::common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); + crate::http::header::common_header_test!(test5, vec![b"*"]); } } #[cfg(test)] mod tests { use super::IfNoneMatch; - use crate::header::{EntityTag, Header, IF_NONE_MATCH}; - use crate::test::TestRequest; + use crate::http::header::{EntityTag, Header, IF_NONE_MATCH}; + use actix_http::test::TestRequest; #[test] fn test_if_none_match() { diff --git a/actix-http/src/header/common/if_range.rs b/src/http/header/if_range.rs similarity index 70% rename from actix-http/src/header/common/if_range.rs rename to src/http/header/if_range.rs index 1513b7a44..5af9255f6 100644 --- a/actix-http/src/header/common/if_range.rs +++ b/src/http/header/if_range.rs @@ -1,13 +1,15 @@ use std::fmt::{self, Display, Write}; -use crate::error::ParseError; -use crate::header::{ - self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, - IntoHeaderValue, InvalidHeaderValue, Writer, +use super::{ + from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, IntoHeaderValue, + InvalidHeaderValue, Writer, }; -use crate::httpmessage::HttpMessage; +use crate::error::ParseError; +use crate::http::header; +use crate::HttpMessage; -/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) +/// `If-Range` header, defined +/// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2) /// /// If a client has a partial copy of a representation and wishes to have /// an up-to-date copy of the entire representation, it could use the @@ -23,23 +25,21 @@ use crate::httpmessage::HttpMessage; /// in Range; otherwise, send me the entire representation. /// /// # ABNF -/// -/// ```text +/// ```plain /// If-Range = entity-tag / HTTP-date /// ``` /// -/// # Example values +/// # Example Values /// /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// * `\"xyzzy\"` /// /// # Examples -/// /// ``` -/// use actix_http::Response; -/// use actix_http::http::header::{EntityTag, IfRange}; +/// use actix_web::HttpResponse; +/// use actix_web::http::header::{EntityTag, IfRange}; /// -/// let mut builder = Response::Ok(); +/// let mut builder = HttpResponse::Ok(); /// builder.insert_header( /// IfRange::EntityTag( /// EntityTag::new(false, "abc".to_owned()) @@ -49,9 +49,9 @@ use crate::httpmessage::HttpMessage; /// /// ``` /// use std::time::{Duration, SystemTime}; -/// use actix_http::{http::header::IfRange, Response}; +/// use actix_web::{http::header::IfRange, HttpResponse}; /// -/// let mut builder = Response::Ok(); +/// let mut builder = HttpResponse::Ok(); /// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// IfRange::Date(fetched.into()) @@ -75,13 +75,11 @@ impl Header for IfRange { where T: HttpMessage, { - let etag: Result = - from_one_raw_str(msg.headers().get(&header::IF_RANGE)); + let etag: Result = from_one_raw_str(msg.headers().get(&header::IF_RANGE)); if let Ok(etag) = etag { return Ok(IfRange::EntityTag(etag)); } - let date: Result = - from_one_raw_str(msg.headers().get(&header::IF_RANGE)); + let date: Result = from_one_raw_str(msg.headers().get(&header::IF_RANGE)); if let Ok(date) = date { return Ok(IfRange::Date(date)); } @@ -109,12 +107,13 @@ impl IntoHeaderValue for IfRange { } #[cfg(test)] -mod test_if_range { - use super::IfRange as HeaderField; - use crate::header::*; +mod test_parse_and_format { use std::str; - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - test_header!(test2, vec![b"\"abc\""]); - test_header!(test3, vec![b"this-is-invalid"], None::); + use super::IfRange as HeaderField; + use crate::http::header::*; + + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test2, vec![b"\"abc\""]); + crate::http::header::common_header_test!(test3, vec![b"this-is-invalid"], None::); } diff --git a/actix-http/src/header/common/if_unmodified_since.rs b/src/http/header/if_unmodified_since.rs similarity index 63% rename from actix-http/src/header/common/if_unmodified_since.rs rename to src/http/header/if_unmodified_since.rs index 1c2b4af78..0df6d7ba0 100644 --- a/actix-http/src/header/common/if_unmodified_since.rs +++ b/src/http/header/if_unmodified_since.rs @@ -1,8 +1,8 @@ -use crate::header::{HttpDate, IF_UNMODIFIED_SINCE}; +use super::{HttpDate, IF_UNMODIFIED_SINCE}; -header! { - /// `If-Unmodified-Since` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) +crate::http::header::common_header! { + /// `If-Unmodified-Since` header, defined + /// in [RFC 7232 §3.4](https://datatracker.ietf.org/doc/html/rfc7232#section-3.4) /// /// The `If-Unmodified-Since` header field makes the request method /// conditional on the selected representation's last modification date @@ -11,23 +11,21 @@ header! { /// the user agent does not have an entity-tag for the representation. /// /// # ABNF - /// - /// ```text + /// ```plain /// If-Unmodified-Since = HTTP-date /// ``` /// - /// # Example values - /// + /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Example /// /// ``` /// use std::time::{SystemTime, Duration}; - /// use actix_http::Response; - /// use actix_http::http::header::IfUnmodifiedSince; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::IfUnmodifiedSince; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// IfUnmodifiedSince(modified.into()) @@ -35,8 +33,8 @@ header! { /// ``` (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] - test_if_unmodified_since { + test_parse_and_format { // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/actix-http/src/header/common/last_modified.rs b/src/http/header/last_modified.rs similarity index 57% rename from actix-http/src/header/common/last_modified.rs rename to src/http/header/last_modified.rs index 65608d846..e15443ed1 100644 --- a/actix-http/src/header/common/last_modified.rs +++ b/src/http/header/last_modified.rs @@ -1,8 +1,8 @@ -use crate::header::{HttpDate, LAST_MODIFIED}; +use super::{HttpDate, LAST_MODIFIED}; -header! { - /// `Last-Modified` header, defined in - /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) +crate::http::header::common_header! { + /// `Last-Modified` header, defined + /// in [RFC 7232 §2.2](https://datatracker.ietf.org/doc/html/rfc7232#section-2.2) /// /// The `Last-Modified` header field in a response provides a timestamp /// indicating the date and time at which the origin server believes the @@ -10,23 +10,21 @@ header! { /// conclusion of handling the request. /// /// # ABNF - /// - /// ```text + /// ```plain /// Expires = HTTP-date /// ``` /// - /// # Example values - /// + /// # Example Values /// * `Sat, 29 Oct 1994 19:43:31 GMT` /// /// # Example /// /// ``` /// use std::time::{SystemTime, Duration}; - /// use actix_http::Response; - /// use actix_http::http::header::LastModified; + /// use actix_web::HttpResponse; + /// use actix_web::http::header::LastModified; /// - /// let mut builder = Response::Ok(); + /// let mut builder = HttpResponse::Ok(); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// builder.insert_header( /// LastModified(modified.into()) @@ -34,7 +32,8 @@ header! { /// ``` (LastModified, LAST_MODIFIED) => [HttpDate] - test_last_modified { - // Test case from RFC - test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);} + test_parse_and_format { + // Test case from RFC + crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + } } diff --git a/src/http/header/macros.rs b/src/http/header/macros.rs new file mode 100644 index 000000000..3f530658c --- /dev/null +++ b/src/http/header/macros.rs @@ -0,0 +1,319 @@ +macro_rules! common_header_test_module { + ($id:ident, $tm:ident{$($tf:item)*}) => { + #[cfg(test)] + mod $tm { + #![allow(unused_imports)] + + use ::core::str; + + use ::actix_http::{http::Method, test}; + use ::mime::*; + + use $crate::http::header::{self, *}; + use super::{$id as HeaderField, *}; + + $($tf)* + } + } +} + +#[cfg(test)] +macro_rules! common_header_test { + ($id:ident, $raw:expr) => { + #[test] + fn $id() { + use ::actix_http::test; + + let raw = $raw; + let headers = raw.iter().map(|x| x.to_vec()).collect::>(); + + let mut req = test::TestRequest::default(); + + for item in headers { + req = req.append_header((HeaderField::name(), item)).take(); + } + + let req = req.finish(); + let value = HeaderField::parse(&req); + + let result = format!("{}", value.unwrap()); + let expected = ::std::string::String::from_utf8(raw[0].to_vec()).unwrap(); + + let result_cmp: Vec = result + .to_ascii_lowercase() + .split(' ') + .map(|x| x.to_owned()) + .collect(); + let expected_cmp: Vec = expected + .to_ascii_lowercase() + .split(' ') + .map(|x| x.to_owned()) + .collect(); + + assert_eq!(result_cmp.concat(), expected_cmp.concat()); + } + }; + + ($id:ident, $raw:expr, $exp:expr) => { + #[test] + fn $id() { + use actix_http::test; + + let headers = $raw.iter().map(|x| x.to_vec()).collect::>(); + let mut req = test::TestRequest::default(); + + for item in headers { + req.append_header((HeaderField::name(), item)); + } + + let req = req.finish(); + let val = HeaderField::parse(&req); + + let exp: ::core::option::Option = $exp; + + // test parsing + assert_eq!(val.ok(), exp); + + // test formatting + if let Some(exp) = exp { + let raw = &($raw)[..]; + let mut iter = raw.iter().map(|b| str::from_utf8(&b[..]).unwrap()); + let mut joined = String::new(); + if let Some(s) = iter.next() { + joined.push_str(s); + for s in iter { + joined.push_str(", "); + joined.push_str(s); + } + } + assert_eq!(format!("{}", exp), joined); + } + } + }; +} + +macro_rules! common_header { + // TODO: these docs are wrong, there's no $n or $nn + // $attrs:meta: Attributes associated with the header item (usually docs) + // $id:ident: Identifier of the header + // $n:expr: Lowercase name of the header + // $nn:expr: Nice name of the header + + // List header, zero or more items + ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)*) => { + $(#[$attrs])* + #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] + pub struct $id(pub Vec<$item>); + + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + + #[inline] + fn parse(msg: &M) -> Result { + let headers = msg.headers().get_all(Self::name()); + $crate::http::header::from_comma_delimited(headers).map($id) + } + } + + impl ::core::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + $crate::http::header::fmt_comma_delimited(f, &self.0[..]) + } + } + + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValue; + + #[inline] + fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + use ::core::fmt::Write; + let mut writer = $crate::http::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) + } + } + }; + + // List header, one or more items + ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)+) => { + $(#[$attrs])* + #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] + pub struct $id(pub Vec<$item>); + + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + + #[inline] + fn parse(msg: &M) -> Result{ + let headers = msg.headers().get_all(Self::name()); + + $crate::http::header::from_comma_delimited(headers) + .and_then(|items| { + if items.is_empty() { + Err($crate::error::ParseError::Header) + } else { + Ok($id(items)) + } + }) + } + } + + impl ::core::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + $crate::http::header::fmt_comma_delimited(f, &self.0[..]) + } + } + + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValue; + + #[inline] + fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + use ::core::fmt::Write; + let mut writer = $crate::http::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) + } + } + }; + + // Single value header + ($(#[$attrs:meta])*($id:ident, $name:expr) => [$value:ty]) => { + $(#[$attrs])* + #[derive(Debug, Clone, PartialEq, Eq, ::derive_more::Deref, ::derive_more::DerefMut)] + pub struct $id(pub $value); + + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + + #[inline] + fn parse(msg: &M) -> Result { + let header = msg.headers().get(Self::name()); + $crate::http::header::from_one_raw_str(header).map($id) + } + } + + impl ::core::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + ::core::fmt::Display::fmt(&self.0, f) + } + } + + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValue; + + #[inline] + fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + self.0.try_into_value() + } + } + }; + + // List header, one or more items with "*" option + ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+}) => { + $(#[$attrs])* + #[derive(Clone, Debug, PartialEq)] + pub enum $id { + /// Any value is a match + Any, + /// Only the listed items are a match + Items(Vec<$item>), + } + + impl $crate::http::header::Header for $id { + #[inline] + fn name() -> $crate::http::header::HeaderName { + $name + } + + #[inline] + fn parse(msg: &M) -> Result { + let is_any = msg + .headers() + .get(Self::name()) + .and_then(|hdr| hdr.to_str().ok()) + .map(|hdr| hdr.trim() == "*"); + + if let Some(true) = is_any { + Ok($id::Any) + } else { + let headers = msg.headers().get_all(Self::name()); + Ok($id::Items($crate::http::header::from_comma_delimited(headers)?)) + } + } + } + + impl ::core::fmt::Display for $id { + #[inline] + fn fmt(&self, f: &mut ::core::fmt::Formatter<'_>) -> ::core::fmt::Result { + match *self { + $id::Any => f.write_str("*"), + $id::Items(ref fields) => + $crate::http::header::fmt_comma_delimited(f, &fields[..]) + } + } + } + + impl $crate::http::header::IntoHeaderValue for $id { + type Error = $crate::http::header::InvalidHeaderValue; + + #[inline] + fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> { + use ::core::fmt::Write; + let mut writer = $crate::http::header::Writer::new(); + let _ = write!(&mut writer, "{}", self); + $crate::http::header::HeaderValue::from_maybe_shared(writer.take()) + } + } + }; + + // optional test module + ($(#[$attrs:meta])*($id:ident, $name:expr) => ($item:ty)* $tm:ident{$($tf:item)*}) => { + crate::http::header::common_header! { + $(#[$attrs])* + ($id, $name) => ($item)* + } + + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} + }; + ($(#[$attrs:meta])*($id:ident, $n:expr) => ($item:ty)+ $tm:ident{$($tf:item)*}) => { + crate::http::header::common_header! { + $(#[$attrs])* + ($id, $n) => ($item)+ + } + + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} + }; + ($(#[$attrs:meta])*($id:ident, $name:expr) => [$item:ty] $tm:ident{$($tf:item)*}) => { + crate::http::header::common_header! { + $(#[$attrs])* ($id, $name) => [$item] + } + + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} + }; + ($(#[$attrs:meta])*($id:ident, $name:expr) => {Any / ($item:ty)+} $tm:ident{$($tf:item)*}) => { + crate::http::header::common_header! { + $(#[$attrs])* + ($id, $name) => {Any / ($item)+} + } + + crate::http::header::common_header_test_module! { $id, $tm { $($tf)* }} + }; +} + +pub(crate) use {common_header, common_header_test_module}; + +#[cfg(test)] +pub(crate) use common_header_test; diff --git a/src/http/header/mod.rs b/src/http/header/mod.rs new file mode 100644 index 000000000..98548dadd --- /dev/null +++ b/src/http/header/mod.rs @@ -0,0 +1,100 @@ +//! A Collection of Header implementations for common HTTP Headers. +//! +//! ## Mime Types +//! Several header fields use MIME values for their contents. Keeping with the strongly-typed theme, +//! the [mime] crate is used in such headers as [`ContentType`] and [`Accept`]. + +use std::fmt; + +use bytes::{Bytes, BytesMut}; + +// re-export from actix-http +// - header name / value types +// - relevant traits for converting to header name / value +// - all const header names +// - header map +// - the few typed headers from actix-http +// - header parsing utils +pub use actix_http::http::header::*; + +mod accept; +mod accept_charset; +mod accept_encoding; +mod accept_language; +mod allow; +mod cache_control; +mod content_disposition; +mod content_language; +mod content_range; +mod content_type; +mod date; +mod encoding; +mod entity; +mod etag; +mod expires; +mod if_match; +mod if_modified_since; +mod if_none_match; +mod if_range; +mod if_unmodified_since; +mod last_modified; +mod macros; +mod preference; +// mod range; + +#[cfg(test)] +pub(crate) use macros::common_header_test; +pub(crate) use macros::{common_header, common_header_test_module}; + +pub use self::accept::Accept; +pub use self::accept_charset::AcceptCharset; +pub use self::accept_encoding::AcceptEncoding; +pub use self::accept_language::AcceptLanguage; +pub use self::allow::Allow; +pub use self::cache_control::{CacheControl, CacheDirective}; +pub use self::content_disposition::{ContentDisposition, DispositionParam, DispositionType}; +pub use self::content_language::ContentLanguage; +pub use self::content_range::{ContentRange, ContentRangeSpec}; +pub use self::content_type::ContentType; +pub use self::date::Date; +pub use self::encoding::Encoding; +pub use self::entity::EntityTag; +pub use self::etag::ETag; +pub use self::expires::Expires; +pub use self::if_match::IfMatch; +pub use self::if_modified_since::IfModifiedSince; +pub use self::if_none_match::IfNoneMatch; +pub use self::if_range::IfRange; +pub use self::if_unmodified_since::IfUnmodifiedSince; +pub use self::last_modified::LastModified; +pub use self::preference::Preference; +//pub use self::range::{Range, ByteRangeSpec}; + +/// Format writer ([`fmt::Write`]) for a [`BytesMut`]. +#[derive(Debug, Default)] +struct Writer { + buf: BytesMut, +} + +impl Writer { + pub fn new() -> Writer { + Writer::default() + } + + pub fn take(&mut self) -> Bytes { + self.buf.split().freeze() + } +} + +impl fmt::Write for Writer { + #[inline] + fn write_str(&mut self, s: &str) -> fmt::Result { + self.buf.extend_from_slice(s.as_bytes()); + Ok(()) + } + + #[inline] + fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result { + fmt::write(self, args) + } +} diff --git a/src/http/header/preference.rs b/src/http/header/preference.rs new file mode 100644 index 000000000..979fc7720 --- /dev/null +++ b/src/http/header/preference.rs @@ -0,0 +1,70 @@ +use std::{ + fmt::{self, Write as _}, + str, +}; + +/// A wrapper for types used in header values where wildcard (`*`) items are allowed but the +/// underlying type does not support them. +/// +/// For example, we use the `language-tags` crate for the [`AcceptLanguage`](super::AcceptLanguage) +/// typed header but it does not parse `*` successfully. On the other hand, the `mime` crate, used +/// for [`Accept`](super::Accept), has first-party support for wildcard items so this wrapper is not +/// used in those header types. +#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Hash)] +pub enum Preference { + /// A wildcard value. + Any, + + /// A valid `T`. + Specific(T), +} + +impl Preference { + /// Returns true if preference is the any/wildcard (`*`) value. + pub fn is_any(&self) -> bool { + matches!(self, Self::Any) + } + + /// Returns true if preference is the specific item (`T`) variant. + pub fn is_specific(&self) -> bool { + matches!(self, Self::Specific(_)) + } + + /// Returns reference to value in `Specific` variant, if it is set. + pub fn item(&self) -> Option<&T> { + match self { + Preference::Specific(ref item) => Some(item), + Preference::Any => None, + } + } + + /// Consumes the container, returning the value in the `Specific` variant, if it is set. + pub fn into_item(self) -> Option { + match self { + Preference::Specific(item) => Some(item), + Preference::Any => None, + } + } +} + +impl fmt::Display for Preference { + #[inline] + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Preference::Any => f.write_char('*'), + Preference::Specific(item) => fmt::Display::fmt(item, f), + } + } +} + +impl str::FromStr for Preference { + type Err = T::Err; + + #[inline] + fn from_str(s: &str) -> Result { + match s.trim() { + "*" => Ok(Self::Any), + other => other.parse().map(Preference::Specific), + } + } +} diff --git a/actix-http/src/header/common/range.rs b/src/http/header/range.rs similarity index 92% rename from actix-http/src/header/common/range.rs rename to src/http/header/range.rs index f9e203bb2..11006ffff 100644 --- a/actix-http/src/header/common/range.rs +++ b/src/http/header/range.rs @@ -1,19 +1,21 @@ -use std::fmt::{self, Display}; -use std::str::FromStr; +// TODO: reinstate module -use header::parsing::from_one_raw_str; -use header::{Header, Raw}; +use std::{ + fmt::{self, Display}, + str::FromStr, +}; -/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) +use super::{parsing::from_one_raw_str, Header, Raw}; + +/// `Range` header, defined +/// in [RFC 7233 §3.1](https://datatracker.ietf.org/doc/html/rfc7233#section-3.1) /// -/// The "Range" header field on a GET request modifies the method -/// semantics to request transfer of only one or more sub-ranges of the -/// selected representation data, rather than the entire selected +/// The "Range" header field on a GET request modifies the method semantics to request transfer of +/// only one or more sub-ranges of the selected representation data, rather than the entire selected /// representation data. /// /// # ABNF -/// -/// ```text +/// ```plain /// Range = byte-ranges-specifier / other-ranges-specifier /// other-ranges-specifier = other-range-unit "=" other-range-set /// other-range-set = 1*VCHAR @@ -27,8 +29,7 @@ use header::{Header, Raw}; /// last-byte-pos = 1*DIGIT /// ``` /// -/// # Example values -/// +/// # Example Values /// * `bytes=1000-` /// * `bytes=-2000` /// * `bytes=0-1,30-40` @@ -37,7 +38,6 @@ use header::{Header, Raw}; /// * `custom_unit=xxx-yyy` /// /// # Examples -/// /// ``` /// use hyper::header::{Headers, Range, ByteRangeSpec}; /// @@ -63,6 +63,7 @@ use header::{Header, Raw}; pub enum Range { /// Byte range Bytes(Vec), + /// Custom range, with unit not registered at IANA /// (`other-range-unit`: String , `other-range-set`: String) Unregistered(String, String), @@ -74,8 +75,10 @@ pub enum Range { pub enum ByteRangeSpec { /// Get all bytes between x and y ("x-y") FromTo(u64, u64), + /// Get all bytes starting from x ("x-") AllFrom(u64), + /// Get last x bytes ("-x") Last(u64), } @@ -93,7 +96,7 @@ impl ByteRangeSpec { /// simply ignore the range header and serve the full entity using a `200 /// OK` status code. /// - /// This function closely follows [RFC 7233][1] section 2.1. + /// This function closely follows [RFC 7233 §2.1]. /// As such, it considers ranges to be satisfiable if they meet the /// following conditions: /// @@ -112,7 +115,7 @@ impl ByteRangeSpec { /// value of last-byte-pos with a value that is one less than the current /// length of the selected representation). /// - /// [1]: https://tools.ietf.org/html/rfc7233 + /// [RFC 7233 §2.1]: https://datatracker.ietf.org/doc/html/rfc7233 pub fn to_satisfiable_range(&self, full_length: u64) -> Option<(u64, u64)> { // If the full length is zero, there is no satisfiable end-inclusive range. if full_length == 0 { @@ -238,9 +241,7 @@ impl FromStr for ByteRangeSpec { .or(Err(::Error::Header)) .map(ByteRangeSpec::AllFrom), (Some(start), Some(end)) => match (start.parse(), end.parse()) { - (Ok(start), Ok(end)) if start <= end => { - Ok(ByteRangeSpec::FromTo(start, end)) - } + (Ok(start), Ok(end)) if start <= end => Ok(ByteRangeSpec::FromTo(start, end)), _ => Err(::Error::Header), }, _ => Err(::Error::Header), @@ -248,16 +249,6 @@ impl FromStr for ByteRangeSpec { } } -fn from_comma_delimited(s: &str) -> Vec { - s.split(',') - .filter_map(|x| match x.trim() { - "" => None, - y => Some(y), - }) - .filter_map(|x| x.parse().ok()) - .collect() -} - impl Header for Range { fn header_name() -> &'static str { static NAME: &'static str = "Range"; @@ -286,8 +277,7 @@ mod tests { assert_eq!(r2, r3); let r: Range = Header::parse_header(&"bytes=1-100,200-".into()).unwrap(); - let r2: Range = - Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); + let r2: Range = Header::parse_header(&"bytes= 1-100 , 101-xxx, 200- ".into()).unwrap(); let r3 = Range::Bytes(vec![ ByteRangeSpec::FromTo(1, 100), ByteRangeSpec::AllFrom(200), diff --git a/src/http/mod.rs b/src/http/mod.rs new file mode 100644 index 000000000..fa28a5fa9 --- /dev/null +++ b/src/http/mod.rs @@ -0,0 +1,2 @@ +pub mod header; +pub use actix_http::http::*; diff --git a/src/info.rs b/src/info.rs index c9ddf6ec4..d928a1e63 100644 --- a/src/info.rs +++ b/src/info.rs @@ -1,19 +1,74 @@ -use std::cell::Ref; +use std::{cell::Ref, convert::Infallible, net::SocketAddr}; -use crate::dev::{AppConfig, RequestHead}; -use crate::http::header::{self, HeaderName}; +use actix_utils::future::{err, ok, Ready}; +use derive_more::{Display, Error}; +use once_cell::sync::Lazy; -const X_FORWARDED_FOR: &[u8] = b"x-forwarded-for"; -const X_FORWARDED_HOST: &[u8] = b"x-forwarded-host"; -const X_FORWARDED_PROTO: &[u8] = b"x-forwarded-proto"; +use crate::{ + dev::{AppConfig, Payload, RequestHead}, + http::{ + header::{self, HeaderName}, + uri::{Authority, Scheme}, + }, + FromRequest, HttpRequest, ResponseError, +}; -/// `HttpRequest` connection information +static X_FORWARDED_FOR: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-for")); +static X_FORWARDED_HOST: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-host")); +static X_FORWARDED_PROTO: Lazy = + Lazy::new(|| HeaderName::from_static("x-forwarded-proto")); + +/// Trim whitespace then any quote marks. +fn unquote(val: &str) -> &str { + val.trim().trim_start_matches('"').trim_end_matches('"') +} + +/// Extracts and trims first value for given header name. +fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<&'a str> { + let hdr = req.headers.get(name)?.to_str().ok()?; + let val = hdr.split(',').next()?.trim(); + Some(val) +} + +/// HTTP connection information. +/// +/// `ConnectionInfo` implements `FromRequest` and can be extracted in handlers. +/// +/// # Examples +/// ``` +/// # use actix_web::{HttpResponse, Responder}; +/// use actix_web::dev::ConnectionInfo; +/// +/// async fn handler(conn: ConnectionInfo) -> impl Responder { +/// match conn.host() { +/// "actix.rs" => HttpResponse::Ok().body("Welcome!"), +/// "admin.actix.rs" => HttpResponse::Ok().body("Admin portal."), +/// _ => HttpResponse::NotFound().finish() +/// } +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` +/// +/// # Implementation Notes +/// Parses `Forwarded` header information according to [RFC 7239][rfc7239] but does not try to +/// interpret the values for each property. As such, the getter methods on `ConnectionInfo` return +/// strings instead of IP addresses or other types to acknowledge that they may be +/// [obfuscated][rfc7239-63] or [unknown][rfc7239-62]. +/// +/// If the older, related headers are also present (eg. `X-Forwarded-For`), then `Forwarded` +/// is preferred. +/// +/// [rfc7239]: https://datatracker.ietf.org/doc/html/rfc7239 +/// [rfc7239-62]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.2 +/// [rfc7239-63]: https://datatracker.ietf.org/doc/html/rfc7239#section-6.3 #[derive(Debug, Clone, Default)] pub struct ConnectionInfo { - scheme: String, host: String, - realip_remote_addr: Option, + scheme: String, remote_addr: Option, + realip_remote_addr: Option, } impl ConnectionInfo { @@ -25,105 +80,75 @@ impl ConnectionInfo { Ref::map(req.extensions(), |e| e.get().unwrap()) } - #[allow(clippy::cognitive_complexity, clippy::borrow_interior_mutable_const)] fn new(req: &RequestHead, cfg: &AppConfig) -> ConnectionInfo { let mut host = None; let mut scheme = None; let mut realip_remote_addr = None; - // load forwarded header - for hdr in req.headers.get_all(&header::FORWARDED) { - if let Ok(val) = hdr.to_str() { - for pair in val.split(';') { - for el in pair.split(',') { - let mut items = el.trim().splitn(2, '='); - if let Some(name) = items.next() { - if let Some(val) = items.next() { - match &name.to_lowercase() as &str { - "for" => { - if realip_remote_addr.is_none() { - realip_remote_addr = Some(val.trim()); - } - } - "proto" => { - if scheme.is_none() { - scheme = Some(val.trim()); - } - } - "host" => { - if host.is_none() { - host = Some(val.trim()); - } - } - _ => {} - } - } - } - } + for (name, val) in req + .headers + .get_all(&header::FORWARDED) + .into_iter() + .filter_map(|hdr| hdr.to_str().ok()) + // "for=1.2.3.4, for=5.6.7.8; scheme=https" + .flat_map(|val| val.split(';')) + // ["for=1.2.3.4, for=5.6.7.8", " scheme=https"] + .flat_map(|vals| vals.split(',')) + // ["for=1.2.3.4", " for=5.6.7.8", " scheme=https"] + .flat_map(|pair| { + let mut items = pair.trim().splitn(2, '='); + Some((items.next()?, items.next()?)) + }) + { + // [(name , val ), ... ] + // [("for", "1.2.3.4"), ("for", "5.6.7.8"), ("scheme", "https")] + + // taking the first value for each property is correct because spec states that first + // "for" value is client and rest are proxies; multiple values other properties have + // no defined semantics + // + // > In a chain of proxy servers where this is fully utilized, the first + // > "for" parameter will disclose the client where the request was first + // > made, followed by any subsequent proxy identifiers. + // --- https://datatracker.ietf.org/doc/html/rfc7239#section-5.2 + + match name.trim().to_lowercase().as_str() { + "for" => realip_remote_addr.get_or_insert_with(|| unquote(val)), + "proto" => scheme.get_or_insert_with(|| unquote(val)), + "host" => host.get_or_insert_with(|| unquote(val)), + "by" => { + // TODO: implement https://datatracker.ietf.org/doc/html/rfc7239#section-5.1 + continue; } - } + _ => continue, + }; } - // scheme - if scheme.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_PROTO).unwrap()) - { - if let Ok(h) = h.to_str() { - scheme = h.split(',').next().map(|v| v.trim()); - } - } - if scheme.is_none() { - scheme = req.uri.scheme().map(|a| a.as_str()); - if scheme.is_none() && cfg.secure() { - scheme = Some("https") - } - } - } + let scheme = scheme + .or_else(|| first_header_value(req, &*X_FORWARDED_PROTO)) + .or_else(|| req.uri.scheme().map(Scheme::as_str)) + .or_else(|| Some("https").filter(|_| cfg.secure())) + .unwrap_or("http") + .to_owned(); - // host - if host.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_HOST).unwrap()) - { - if let Ok(h) = h.to_str() { - host = h.split(',').next().map(|v| v.trim()); - } - } - if host.is_none() { - if let Some(h) = req.headers.get(&header::HOST) { - host = h.to_str().ok(); - } - if host.is_none() { - host = req.uri.authority().map(|a| a.as_str()); - if host.is_none() { - host = Some(cfg.host()); - } - } - } - } + let host = host + .or_else(|| first_header_value(req, &*X_FORWARDED_HOST)) + .or_else(|| req.headers.get(&header::HOST)?.to_str().ok()) + .or_else(|| req.uri.authority().map(Authority::as_str)) + .unwrap_or_else(|| cfg.host()) + .to_owned(); - // get remote_addraddr from socketaddr - let remote_addr = req.peer_addr.map(|addr| format!("{}", addr)); + let realip_remote_addr = realip_remote_addr + .or_else(|| first_header_value(req, &*X_FORWARDED_FOR)) + .map(str::to_owned); - if realip_remote_addr.is_none() { - if let Some(h) = req - .headers - .get(&HeaderName::from_lowercase(X_FORWARDED_FOR).unwrap()) - { - if let Ok(h) = h.to_str() { - realip_remote_addr = h.split(',').next().map(|v| v.trim()); - } - } - } + let remote_addr = req.peer_addr.map(|addr| addr.to_string()); ConnectionInfo { + host, + scheme, remote_addr, - scheme: scheme.unwrap_or("http").to_owned(), - host: host.unwrap_or("localhost").to_owned(), - realip_remote_addr: realip_remote_addr.map(|s| s.to_owned()), + realip_remote_addr, } } @@ -152,19 +177,16 @@ impl ConnectionInfo { &self.host } - /// remote_addr address of the request. + /// Remote address of the connection. /// - /// Get remote_addr address from socket address + /// Get remote_addr address from socket address. pub fn remote_addr(&self) -> Option<&str> { - if let Some(ref remote_addr) = self.remote_addr { - Some(remote_addr) - } else { - None - } + self.remote_addr.as_deref() } - /// Real ip remote addr of client initiated HTTP request. + + /// Real IP (remote address) of client that initiated request. /// - /// The addr is resolved through the following headers, in this order: + /// The address is resolved through the following headers, in this order: /// /// - Forwarded /// - X-Forwarded-For @@ -173,16 +195,70 @@ impl ConnectionInfo { /// # Security /// Do not use this function for security purposes, unless you can ensure the Forwarded and /// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket - /// address explicitly, use - /// [`HttpRequest::peer_addr()`](super::web::HttpRequest::peer_addr()) instead. + /// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead. + /// + /// [peer_addr]: crate::web::HttpRequest::peer_addr() #[inline] pub fn realip_remote_addr(&self) -> Option<&str> { - if let Some(ref r) = self.realip_remote_addr { - Some(r) - } else if let Some(ref remote_addr) = self.remote_addr { - Some(remote_addr) - } else { - None + self.realip_remote_addr + .as_deref() + .or_else(|| self.remote_addr.as_deref()) + } +} + +impl FromRequest for ConnectionInfo { + type Error = Infallible; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + ok(req.connection_info().clone()) + } +} + +/// Extractor for peer's socket address. +/// +/// Also see [`HttpRequest::peer_addr`]. +/// +/// # Examples +/// ``` +/// # use actix_web::Responder; +/// use actix_web::dev::PeerAddr; +/// +/// async fn handler(peer_addr: PeerAddr) -> impl Responder { +/// let socket_addr = peer_addr.0; +/// socket_addr.to_string() +/// } +/// # let _svc = actix_web::web::to(handler); +/// ``` +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Display)] +#[display(fmt = "{}", _0)] +pub struct PeerAddr(pub SocketAddr); + +impl PeerAddr { + /// Unwrap into inner `SocketAddr` value. + pub fn into_inner(self) -> SocketAddr { + self.0 + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +#[display(fmt = "Missing peer address")] +pub struct MissingPeerAddr; + +impl ResponseError for MissingPeerAddr {} + +impl FromRequest for PeerAddr { + type Error = MissingPeerAddr; + type Future = Ready>; + + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + match req.peer_addr() { + Some(addr) => ok(PeerAddr(addr)), + None => { + log::error!("Missing peer address."); + err(MissingPeerAddr) + } } } } @@ -192,13 +268,60 @@ mod tests { use super::*; use crate::test::TestRequest; + const X_FORWARDED_FOR: &str = "x-forwarded-for"; + const X_FORWARDED_HOST: &str = "x-forwarded-host"; + const X_FORWARDED_PROTO: &str = "x-forwarded-proto"; + #[test] - fn test_forwarded() { + fn info_default() { let req = TestRequest::default().to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "http"); assert_eq!(info.host(), "localhost:8080"); + } + #[test] + fn host_header() { + let req = TestRequest::default() + .insert_header((header::HOST, "rust-lang.org")) + .to_http_request(); + + let info = req.connection_info(); + assert_eq!(info.scheme(), "http"); + assert_eq!(info.host(), "rust-lang.org"); + assert_eq!(info.realip_remote_addr(), None); + } + + #[test] + fn x_forwarded_for_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + + #[test] + fn x_forwarded_host_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.host(), "192.0.2.60"); + assert_eq!(info.realip_remote_addr(), None); + } + + #[test] + fn x_forwarded_proto_header() { + let req = TestRequest::default() + .insert_header((X_FORWARDED_PROTO, "https")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.scheme(), "https"); + } + + #[test] + fn forwarded_header() { let req = TestRequest::default() .insert_header(( header::FORWARDED, @@ -212,31 +335,118 @@ mod tests { assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); let req = TestRequest::default() - .insert_header((header::HOST, "rust-lang.org")) + .insert_header(( + header::FORWARDED, + "for=192.0.2.60; proto=https; by=203.0.113.43; host=rust-lang.org", + )) .to_http_request(); let info = req.connection_info(); - assert_eq!(info.scheme(), "http"); + assert_eq!(info.scheme(), "https"); assert_eq!(info.host(), "rust-lang.org"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + #[test] + fn forwarded_case_sensitivity() { let req = TestRequest::default() - .insert_header((X_FORWARDED_FOR, "192.0.2.60")) + .insert_header((header::FORWARDED, "For=192.0.2.60")) .to_http_request(); let info = req.connection_info(); assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + #[test] + fn forwarded_weird_whitespace() { let req = TestRequest::default() - .insert_header((X_FORWARDED_HOST, "192.0.2.60")) + .insert_header((header::FORWARDED, "for= 1.2.3.4; proto= https")) .to_http_request(); let info = req.connection_info(); - assert_eq!(info.host(), "192.0.2.60"); - assert_eq!(info.realip_remote_addr(), None); + assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); + assert_eq!(info.scheme(), "https"); let req = TestRequest::default() - .insert_header((X_FORWARDED_PROTO, "https")) + .insert_header((header::FORWARDED, " for = 1.2.3.4 ")) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("1.2.3.4")); + } + + #[test] + fn forwarded_for_quoted() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="192.0.2.60:8080""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60:8080")); + } + + #[test] + fn forwarded_for_ipv6() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]:4711""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("[2001:db8:cafe::17]:4711")); + } + + #[test] + fn forwarded_for_multiple() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, "for=192.0.2.60, for=198.51.100.17")) + .to_http_request(); + let info = req.connection_info(); + // takes the first value + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); + } + + #[test] + fn scheme_from_uri() { + let req = TestRequest::get() + .uri("https://actix.rs/test") .to_http_request(); let info = req.connection_info(); assert_eq!(info.scheme(), "https"); } + + #[test] + fn host_from_uri() { + let req = TestRequest::get() + .uri("https://actix.rs/test") + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.host(), "actix.rs"); + } + + #[test] + fn host_from_server_hostname() { + let mut req = TestRequest::get(); + req.set_server_hostname("actix.rs"); + let req = req.to_http_request(); + + let info = req.connection_info(); + assert_eq!(info.host(), "actix.rs"); + } + + #[actix_rt::test] + async fn conn_info_extract() { + let req = TestRequest::default() + .uri("https://actix.rs/test") + .to_http_request(); + let conn_info = ConnectionInfo::extract(&req).await.unwrap(); + assert_eq!(conn_info.scheme(), "https"); + assert_eq!(conn_info.host(), "actix.rs"); + } + + #[actix_rt::test] + async fn peer_addr_extract() { + let addr = "127.0.0.1:8080".parse().unwrap(); + let req = TestRequest::default().peer_addr(addr).to_http_request(); + let peer_addr = PeerAddr::extract(&req).await.unwrap(); + assert_eq!(peer_addr, PeerAddr(addr)); + + let req = TestRequest::default().to_http_request(); + let res = PeerAddr::extract(&req).await; + assert!(res.is_err()); + } } diff --git a/src/lib.rs b/src/lib.rs index 52471f4b3..f6ec4082a 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,8 +1,7 @@ //! Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust. //! -//! ## Example -//! -//! ```rust,no_run +//! # Examples +//! ```no_run //! use actix_web::{get, web, App, HttpServer, Responder}; //! //! #[get("/{id}/{name}/index.html")] @@ -20,48 +19,47 @@ //! } //! ``` //! -//! ## Documentation & Community Resources -//! +//! # Documentation & Community Resources //! In addition to this API documentation, several other resources are available: //! //! * [Website & User Guide](https://actix.rs/) //! * [Examples Repository](https://github.com/actix/examples) -//! * [Community Chat on Gitter](https://gitter.im/actix/actix-web) +//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x) //! //! To get started navigating the API docs, you may consider looking at the following pages first: //! -//! * [App]: This struct represents an Actix web application and is used to +//! * [`App`]: This struct represents an Actix Web application and is used to //! configure routes and other common application settings. //! -//! * [HttpServer]: This struct represents an HTTP server instance and is +//! * [`HttpServer`]: This struct represents an HTTP server instance and is //! used to instantiate and configure servers. //! -//! * [web]: This module provides essential types for route registration as well as +//! * [`web`]: This module provides essential types for route registration as well as //! common utilities for request handlers. //! -//! * [HttpRequest] and [HttpResponse]: These +//! * [`HttpRequest`] and [`HttpResponse`]: These //! structs represent HTTP requests and responses and expose methods for creating, inspecting, //! and otherwise utilizing them. //! -//! ## Features -//! +//! # Features //! * Supports *HTTP/1.x* and *HTTP/2* //! * Streaming and pipelining //! * Keep-alive and slow requests handling //! * Client/server [WebSockets](https://actix.rs/docs/websockets/) support -//! * Transparent content compression/decompression (br, gzip, deflate) +//! * Transparent content compression/decompression (br, gzip, deflate, zstd) //! * Powerful [request routing](https://actix.rs/docs/url-dispatch/) //! * Multipart streams //! * Static assets //! * SSL support using OpenSSL or Rustls //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) -//! * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html) -//! * Supports [Actix actor framework](https://github.com/actix/actix) -//! * Runs on stable Rust 1.46+ +//! * Includes an async [HTTP client](https://docs.rs/awc/) +//! * Runs on stable Rust 1.52+ //! -//! ## Crate Features -//! -//! * `compress` - content encoding compression support (enabled by default) +//! # Crate Features +//! * `cookies` - cookies support (enabled by default) +//! * `compress-brotli` - brotli content encoding compression support (enabled by default) +//! * `compress-gzip` - gzip and deflate content encoding compression support (enabled by default) +//! * `compress-zstd` - zstd content encoding compression support (enabled by default) //! * `openssl` - HTTPS support via `openssl` crate, supports `HTTP/2` //! * `rustls` - HTTPS support via `rustls` crate, supports `HTTP/2` //! * `secure-cookies` - secure cookies support @@ -75,16 +73,20 @@ mod app; mod app_service; mod config; mod data; +pub mod dev; pub mod error; mod extract; pub mod guard; mod handler; +mod helpers; +pub mod http; mod info; pub mod middleware; mod request; mod request_data; mod resource; mod responder; +mod response; mod rmap; mod route; mod scope; @@ -94,133 +96,24 @@ pub mod test; pub(crate) mod types; pub mod web; -pub use actix_http::Response as HttpResponse; -pub use actix_http::{body, cookie, http, Error, HttpMessage, ResponseError, Result}; +pub use actix_http::{body, HttpMessage}; +#[doc(inline)] pub use actix_rt as rt; pub use actix_web_codegen::*; +#[cfg(feature = "cookies")] +pub use cookie; pub use crate::app::App; +pub use crate::error::{Error, ResponseError, Result}; pub use crate::extract::FromRequest; pub use crate::request::HttpRequest; pub use crate::resource::Resource; pub use crate::responder::Responder; +pub use crate::response::{HttpResponse, HttpResponseBuilder}; pub use crate::route::Route; pub use crate::scope::Scope; pub use crate::server::HttpServer; // TODO: is exposing the error directly really needed pub use crate::types::{Either, EitherExtractError}; -pub mod dev { - //! The `actix-web` prelude for library developers - //! - //! The purpose of this module is to alleviate imports of many common actix - //! traits by adding a glob import to the top of actix heavy modules: - //! - //! ``` - //! # #![allow(unused_imports)] - //! use actix_web::dev::*; - //! ``` - - pub use crate::config::{AppConfig, AppService}; - #[doc(hidden)] - pub use crate::handler::Handler; - pub use crate::info::ConnectionInfo; - pub use crate::rmap::ResourceMap; - pub use crate::service::{ - HttpServiceFactory, ServiceRequest, ServiceResponse, WebService, - }; - - pub use crate::types::form::UrlEncoded; - pub use crate::types::json::JsonBody; - pub use crate::types::readlines::Readlines; - - pub use actix_http::body::{Body, BodySize, MessageBody, ResponseBody, SizedStream}; - #[cfg(feature = "compress")] - pub use actix_http::encoding::Decoder as Decompress; - pub use actix_http::ResponseBuilder as HttpResponseBuilder; - pub use actix_http::{ - Extensions, Payload, PayloadStream, RequestHead, ResponseHead, - }; - pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; - pub use actix_server::Server; - pub use actix_service::{Service, Transform}; - - pub(crate) fn insert_slash(mut patterns: Vec) -> Vec { - for path in &mut patterns { - if !path.is_empty() && !path.starts_with('/') { - path.insert(0, '/'); - }; - } - patterns - } - - use crate::http::header::ContentEncoding; - use actix_http::{Response, ResponseBuilder}; - - struct Enc(ContentEncoding); - - /// Helper trait that allows to set specific encoding for response. - pub trait BodyEncoding { - /// Get content encoding - fn get_encoding(&self) -> Option; - - /// Set content encoding - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self; - } - - impl BodyEncoding for ResponseBuilder { - fn get_encoding(&self) -> Option { - if let Some(ref enc) = self.extensions().get::() { - Some(enc.0) - } else { - None - } - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } - - impl BodyEncoding for Response { - fn get_encoding(&self) -> Option { - if let Some(ref enc) = self.extensions().get::() { - Some(enc.0) - } else { - None - } - } - - fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self { - self.extensions_mut().insert(Enc(encoding)); - self - } - } -} - -pub mod client { - //! Actix web async HTTP client. - //! - //! ```rust - //! use actix_web::client::Client; - //! - //! #[actix_web::main] - //! async fn main() { - //! let mut client = Client::default(); - //! - //! // Create request builder and send request - //! let response = client.get("http://www.rust-lang.org") - //! .insert_header(("User-Agent", "actix-web/3.0")) - //! .send() // <- Send request - //! .await; // <- Wait for response - //! - //! println!("Response: {:?}", response); - //! } - //! ``` - - pub use awc::error::*; - pub use awc::{ - test, Client, ClientBuilder, ClientRequest, ClientResponse, Connector, - }; -} +pub(crate) type BoxError = Box; diff --git a/src/middleware/compat.rs b/src/middleware/compat.rs index d5661fc77..e6ef1806f 100644 --- a/src/middleware/compat.rs +++ b/src/middleware/compat.rs @@ -6,17 +6,18 @@ use std::{ task::{Context, Poll}, }; -use actix_http::body::{Body, MessageBody, ResponseBody}; +use actix_http::body::MessageBody; use actix_service::{Service, Transform}; use futures_core::{future::LocalBoxFuture, ready}; +use pin_project_lite::pin_project; use crate::{error::Error, service::ServiceResponse}; /// Middleware for enabling any middleware to be used in [`Resource::wrap`](crate::Resource::wrap), /// [`Scope::wrap`](crate::Scope::wrap) and [`Condition`](super::Condition). /// -/// # Usage -/// ```rust +/// # Examples +/// ``` /// use actix_web::middleware::{Logger, Compat}; /// use actix_web::{App, web}; /// @@ -49,7 +50,7 @@ where T: Transform, T::Future: 'static, T::Response: MapServiceResponseBody, - Error: From, + T::Error: Into, { type Response = ServiceResponse; type Error = Error; @@ -74,38 +75,41 @@ impl Service for CompatMiddleware where S: Service, S::Response: MapServiceResponseBody, - Error: From, + S::Error: Into, { type Response = ServiceResponse; type Error = Error; type Future = CompatMiddlewareFuture; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx).map_err(From::from) - } + actix_service::forward_ready!(service); - fn call(&mut self, req: Req) -> Self::Future { + fn call(&self, req: Req) -> Self::Future { let fut = self.service.call(req); CompatMiddlewareFuture { fut } } } -#[pin_project::pin_project] -pub struct CompatMiddlewareFuture { - #[pin] - fut: Fut, +pin_project! { + pub struct CompatMiddlewareFuture { + #[pin] + fut: Fut, + } } impl Future for CompatMiddlewareFuture where Fut: Future>, T: MapServiceResponseBody, - Error: From, + E: Into, { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let res = ready!(self.project().fut.poll(cx))?; + let res = match ready!(self.project().fut.poll(cx)) { + Ok(res) => res, + Err(err) => return Poll::Ready(Err(err.into())), + }; + Poll::Ready(Ok(res.map_body())) } } @@ -115,52 +119,62 @@ pub trait MapServiceResponseBody { fn map_body(self) -> ServiceResponse; } -impl MapServiceResponseBody for ServiceResponse { +impl MapServiceResponseBody for ServiceResponse +where + B: MessageBody + Unpin + 'static, +{ fn map_body(self) -> ServiceResponse { - self.map_body(|_, body| ResponseBody::Other(Body::from_message(body))) + self.map_into_boxed_body() } } #[cfg(test)] mod tests { + // easier to code when cookies feature is disabled + #![allow(unused_imports)] + use super::*; use actix_service::IntoService; use crate::dev::ServiceRequest; use crate::http::StatusCode; - use crate::middleware::{Compress, Condition, Logger}; + use crate::middleware::{self, Condition, Logger}; use crate::test::{call_service, init_service, TestRequest}; use crate::{web, App, HttpResponse}; #[actix_rt::test] + #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_scope_middleware() { + use crate::middleware::Compress; + let logger = Logger::default(); let compress = Compress::default(); - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("app") .wrap(Compat::new(logger)) .wrap(Compat::new(compress)) - .service( - web::resource("/test").route(web::get().to(HttpResponse::Ok)), - ), + .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] + #[cfg(all(feature = "cookies", feature = "__compress"))] async fn test_resource_scope_middleware() { + use crate::middleware::Compress; + let logger = Logger::default(); let compress = Compress::default(); - let mut srv = init_service( + let srv = init_service( App::new().service( web::resource("app/test") .wrap(Compat::new(logger)) @@ -171,7 +185,7 @@ mod tests { .await; let req = TestRequest::with_uri("/app/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } @@ -185,11 +199,11 @@ mod tests { let logger = Logger::default(); - let mut mw = Condition::new(true, Compat::new(logger)) + let mw = Condition::new(true, Compat::new(logger)) .new_transform(srv.into_service()) .await .unwrap(); - let resp = call_service(&mut mw, TestRequest::default().to_srv_request()).await; + let resp = call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); } } diff --git a/src/middleware/compress.rs b/src/middleware/compress.rs index 1f05b7ae3..d017e9a5a 100644 --- a/src/middleware/compress.rs +++ b/src/middleware/compress.rs @@ -2,27 +2,29 @@ use std::{ cmp, + convert::TryFrom, future::Future, marker::PhantomData, pin::Pin, - str::FromStr, task::{Context, Poll}, }; use actix_http::{ - body::MessageBody, + body::{EitherBody, MessageBody}, encoding::Encoder, http::header::{ContentEncoding, ACCEPT_ENCODING}, - Error, + StatusCode, }; use actix_service::{Service, Transform}; +use actix_utils::future::{ok, Either, Ready}; use futures_core::ready; -use futures_util::future::{ok, Ready}; -use pin_project::pin_project; +use once_cell::sync::Lazy; +use pin_project_lite::pin_project; use crate::{ dev::BodyEncoding, service::{ServiceRequest, ServiceResponse}, + Error, HttpResponse, }; /// Middleware for compressing response payloads. @@ -30,8 +32,8 @@ use crate::{ /// Use `BodyEncoding` trait for overriding response compression. To disable compression set /// encoding to `ContentEncoding::Identity`. /// -/// # Usage -/// ```rust +/// # Examples +/// ``` /// use actix_web::{web, middleware, App, HttpResponse}; /// /// let app = App::new() @@ -59,7 +61,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Response = ServiceResponse>; + type Response = ServiceResponse>>; type Error = Error; type Transform = CompressMiddleware; type InitError = (); @@ -78,48 +80,93 @@ pub struct CompressMiddleware { encoding: ContentEncoding, } +static SUPPORTED_ALGORITHM_NAMES: Lazy = Lazy::new(|| { + #[allow(unused_mut)] // only unused when no compress features enabled + let mut encoding: Vec<&str> = vec![]; + + #[cfg(feature = "compress-brotli")] + { + encoding.push("br"); + } + + #[cfg(feature = "compress-gzip")] + { + encoding.push("gzip"); + encoding.push("deflate"); + } + + #[cfg(feature = "compress-zstd")] + encoding.push("zstd"); + + assert!( + !encoding.is_empty(), + "encoding can not be empty unless __compress feature has been explicitly enabled by itself" + ); + + encoding.join(", ") +}); + impl Service for CompressMiddleware where - B: MessageBody, S: Service, Error = Error>, + B: MessageBody, { - type Response = ServiceResponse>; + type Response = ServiceResponse>>; type Error = Error; - type Future = CompressResponse; + type Future = Either, Ready>>; actix_service::forward_ready!(service); #[allow(clippy::borrow_interior_mutable_const)] - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { // negotiate content-encoding - let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { - if let Ok(enc) = val.to_str() { - AcceptEncoding::parse(enc, self.encoding) - } else { - ContentEncoding::Identity - } - } else { - ContentEncoding::Identity - }; + let encoding_result = req + .headers() + .get(&ACCEPT_ENCODING) + .and_then(|val| val.to_str().ok()) + .map(|enc| AcceptEncoding::try_parse(enc, self.encoding)); - CompressResponse { - encoding, - fut: self.service.call(req), - _phantom: PhantomData, + match encoding_result { + // Missing header => fallback to identity + None => Either::left(CompressResponse { + encoding: ContentEncoding::Identity, + fut: self.service.call(req), + _phantom: PhantomData, + }), + + // Valid encoding + Some(Ok(encoding)) => Either::left(CompressResponse { + encoding, + fut: self.service.call(req), + _phantom: PhantomData, + }), + + // There is an HTTP header but we cannot match what client as asked for + Some(Err(_)) => { + let res = HttpResponse::with_body( + StatusCode::NOT_ACCEPTABLE, + SUPPORTED_ALGORITHM_NAMES.clone(), + ); + + Either::right(ok(req + .into_response(res) + .map_into_boxed_body() + .map_into_right_body())) + } } } } -#[pin_project] -pub struct CompressResponse -where - S: Service, - B: MessageBody, -{ - #[pin] - fut: S::Future, - encoding: ContentEncoding, - _phantom: PhantomData, +pin_project! { + pub struct CompressResponse + where + S: Service, + { + #[pin] + fut: S::Future, + encoding: ContentEncoding, + _phantom: PhantomData, + } } impl Future for CompressResponse @@ -127,7 +174,7 @@ where B: MessageBody, S: Service, Error = Error>, { - type Output = Result>, Error>; + type Output = Result>>, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); @@ -140,17 +187,19 @@ where *this.encoding }; - Poll::Ready(Ok( - resp.map_body(move |head, body| Encoder::response(enc, head, body)) - )) + Poll::Ready(Ok(resp.map_body(move |head, body| { + EitherBody::left(Encoder::response(enc, head, body)) + }))) } - Err(e) => Poll::Ready(Err(e)), + + Err(err) => Poll::Ready(Err(err)), } } } struct AcceptEncoding { encoding: ContentEncoding, + // TODO: use Quality or QualityItem quality: f64, } @@ -177,42 +226,149 @@ impl PartialOrd for AcceptEncoding { impl PartialEq for AcceptEncoding { fn eq(&self, other: &AcceptEncoding) -> bool { - self.quality == other.quality + self.encoding == other.encoding && self.quality == other.quality } } +/// Parse q-factor from quality strings. +/// +/// If parse fail, then fallback to default value which is 1. +/// More details available here: +fn parse_quality(parts: &[&str]) -> f64 { + for part in parts { + if part.trim().starts_with("q=") { + return part[2..].parse().unwrap_or(1.0); + } + } + + 1.0 +} + +#[derive(Debug, PartialEq, Eq)] +enum AcceptEncodingError { + /// This error occurs when client only support compressed response and server do not have any + /// algorithm that match client accepted algorithms. + CompressionAlgorithmMismatch, +} + impl AcceptEncoding { fn new(tag: &str) -> Option { let parts: Vec<&str> = tag.split(';').collect(); let encoding = match parts.len() { 0 => return None, - _ => ContentEncoding::from(parts[0]), - }; - let quality = match parts.len() { - 1 => encoding.quality(), - _ => f64::from_str(parts[1]).unwrap_or(0.0), + _ => match ContentEncoding::try_from(parts[0]) { + Err(_) => return None, + Ok(x) => x, + }, }; + + let quality = parse_quality(&parts[1..]); + if quality <= 0.0 || quality > 1.0 { + return None; + } + Some(AcceptEncoding { encoding, quality }) } - /// Parse a raw Accept-Encoding header value into an ordered list. - pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { - let mut encodings: Vec<_> = raw + /// Parse a raw Accept-Encoding header value into an ordered list then return the best match + /// based on middleware configuration. + pub fn try_parse( + raw: &str, + encoding: ContentEncoding, + ) -> Result { + let mut encodings = raw .replace(' ', "") .split(',') - .map(|l| AcceptEncoding::new(l)) - .collect(); + .filter_map(AcceptEncoding::new) + .collect::>(); + encodings.sort(); for enc in encodings { - if let Some(enc) = enc { - if encoding == ContentEncoding::Auto { - return enc.encoding; - } else if encoding == enc.encoding { - return encoding; - } + if encoding == ContentEncoding::Auto || encoding == enc.encoding { + return Ok(enc.encoding); } } - ContentEncoding::Identity + + // Special case if user cannot accept uncompressed data. + // See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding + // TODO: account for whitespace + if raw.contains("*;q=0") || raw.contains("identity;q=0") { + return Err(AcceptEncodingError::CompressionAlgorithmMismatch); + } + + Ok(ContentEncoding::Identity) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + macro_rules! assert_parse_eq { + ($raw:expr, $result:expr) => { + assert_eq!( + AcceptEncoding::try_parse($raw, ContentEncoding::Auto), + Ok($result) + ); + }; + } + + macro_rules! assert_parse_fail { + ($raw:expr) => { + assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err()); + }; + } + + #[test] + fn test_parse_encoding() { + // Test simple case + assert_parse_eq!("br", ContentEncoding::Br); + assert_parse_eq!("gzip", ContentEncoding::Gzip); + assert_parse_eq!("deflate", ContentEncoding::Deflate); + assert_parse_eq!("zstd", ContentEncoding::Zstd); + + // Test space, trim, missing values + assert_parse_eq!("br,,,,", ContentEncoding::Br); + assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip); + + // Test float number parsing + assert_parse_eq!("br;q=1 ,", ContentEncoding::Br); + assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br); + + // Test wildcard + assert_parse_eq!("*", ContentEncoding::Identity); + assert_parse_eq!("*;q=1.0", ContentEncoding::Identity); + } + + #[test] + fn test_parse_encoding_qfactor_ordering() { + assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip); + assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd); + + assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br); + assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip); + } + + #[test] + fn test_parse_encoding_qfactor_invalid() { + // Out of range + assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity); + assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity); + + // Disabled + assert_parse_eq!("gzip;q=0", ContentEncoding::Identity); + } + + #[test] + fn test_parse_compression_required() { + // Check we fallback to identity if there is an unsupported compression algorithm + assert_parse_eq!("compress", ContentEncoding::Identity); + + // User do not want any compression + assert_parse_fail!("compress, identity;q=0"); + assert_parse_fail!("compress, identity;q=0.0"); + assert_parse_fail!("compress, *;q=0"); + assert_parse_fail!("compress, *;q=0.0"); } } diff --git a/src/middleware/condition.rs b/src/middleware/condition.rs index d61e7d576..d1ba7ee4d 100644 --- a/src/middleware/condition.rs +++ b/src/middleware/condition.rs @@ -3,7 +3,9 @@ use std::task::{Context, Poll}; use actix_service::{Service, Transform}; -use futures_util::future::{Either, FutureExt, LocalBoxFuture}; +use actix_utils::future::Either; +use futures_core::future::LocalBoxFuture; +use futures_util::future::FutureExt as _; /// Middleware for conditionally enabling other middleware. /// @@ -11,8 +13,8 @@ use futures_util::future::{Either, FutureExt, LocalBoxFuture}; /// control such middlewares like `Logger` or `Compress` directly. See the [`Compat`](super::Compat) /// middleware for a workaround. /// -/// # Usage -/// ```rust +/// # Examples +/// ``` /// use actix_web::middleware::{Condition, NormalizePath}; /// use actix_web::App; /// @@ -76,17 +78,17 @@ where type Error = E::Error; type Future = Either; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + fn poll_ready(&self, cx: &mut Context<'_>) -> Poll> { match self { ConditionMiddleware::Enable(service) => service.poll_ready(cx), ConditionMiddleware::Disable(service) => service.poll_ready(cx), } } - fn call(&mut self, req: Req) -> Self::Future { + fn call(&self, req: Req) -> Self::Future { match self { - ConditionMiddleware::Enable(service) => Either::Left(service.call(req)), - ConditionMiddleware::Disable(service) => Either::Right(service.call(req)), + ConditionMiddleware::Enable(service) => Either::left(service.call(req)), + ConditionMiddleware::Disable(service) => Either::right(service.call(req)), } } } @@ -94,7 +96,7 @@ where #[cfg(test)] mod tests { use actix_service::IntoService; - use futures_util::future::ok; + use actix_utils::future::ok; use super::*; use crate::{ @@ -120,15 +122,13 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - let mut mw = Condition::new(true, mw) + let mw = Condition::new(true, mw) .new_transform(srv.into_service()) .await .unwrap(); - let resp = - test::call_service(&mut mw, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } @@ -138,16 +138,14 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mw = - ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); + let mw = ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500); - let mut mw = Condition::new(false, mw) + let mw = Condition::new(false, mw) .new_transform(srv.into_service()) .await .unwrap(); - let resp = - test::call_service(&mut mw, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE), None); } } diff --git a/src/middleware/default_headers.rs b/src/middleware/default_headers.rs index 5d55fd830..426810247 100644 --- a/src/middleware/default_headers.rs +++ b/src/middleware/default_headers.rs @@ -9,10 +9,9 @@ use std::{ task::{Context, Poll}, }; -use futures_util::{ - future::{ready, Ready}, - ready, -}; +use actix_utils::future::{ready, Ready}; +use futures_core::ready; +use pin_project_lite::pin_project; use crate::{ dev::{Service, Transform}, @@ -28,8 +27,8 @@ use crate::{ /// /// Headers with the same key that are already set in a response will *not* be overwritten. /// -/// # Usage -/// ```rust +/// # Examples +/// ``` /// use actix_web::{web, http, middleware, App, HttpResponse}; /// /// fn main() { @@ -143,7 +142,7 @@ where actix_service::forward_ready!(service); - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { let inner = self.inner.clone(); let fut = self.service.call(req); @@ -155,12 +154,13 @@ where } } -#[pin_project::pin_project] -pub struct DefaultHeaderFuture, B> { - #[pin] - fut: S::Future, - inner: Rc, - _body: PhantomData, +pin_project! { + pub struct DefaultHeaderFuture, B> { + #[pin] + fut: S::Future, + inner: Rc, + _body: PhantomData, + } } impl Future for DefaultHeaderFuture @@ -188,7 +188,7 @@ where #[cfg(test)] mod tests { use actix_service::IntoService; - use futures_util::future::ok; + use actix_utils::future::ok; use super::*; use crate::{ @@ -200,7 +200,7 @@ mod tests { #[actix_rt::test] async fn test_default_headers() { - let mut mw = DefaultHeaders::new() + let mw = DefaultHeaders::new() .header(CONTENT_TYPE, "0001") .new_transform(ok_service()) .await @@ -218,7 +218,7 @@ mod tests { .finish(), )) }; - let mut mw = DefaultHeaders::new() + let mw = DefaultHeaders::new() .header(CONTENT_TYPE, "0001") .new_transform(srv.into_service()) .await @@ -229,9 +229,8 @@ mod tests { #[actix_rt::test] async fn test_content_type() { - let srv = - |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())); - let mut mw = DefaultHeaders::new() + let srv = |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())); + let mw = DefaultHeaders::new() .add_content_type() .new_transform(srv.into_service()) .await diff --git a/src/middleware/err_handlers.rs b/src/middleware/err_handlers.rs index 44962aa98..1a834c1e8 100644 --- a/src/middleware/err_handlers.rs +++ b/src/middleware/err_handlers.rs @@ -10,11 +10,12 @@ use std::{ use actix_service::{Service, Transform}; use ahash::AHashMap; use futures_core::{future::LocalBoxFuture, ready}; +use pin_project_lite::pin_project; use crate::{ dev::{ServiceRequest, ServiceResponse}, - error::{Error, Result}, http::StatusCode, + Error, Result, }; /// Return type for [`ErrorHandlers`] custom handlers. @@ -33,8 +34,8 @@ type ErrorHandler = dyn Fn(ServiceResponse) -> Result Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { let handlers = self.handlers.clone(); let fut = self.service.call(req); ErrorHandlersFuture::ServiceFuture { fut, handlers } } } -#[pin_project::pin_project(project = ErrorHandlersProj)] -pub enum ErrorHandlersFuture -where - Fut: Future, -{ - ServiceFuture { - #[pin] - fut: Fut, - handlers: Handlers, - }, - HandlerFuture { - fut: LocalBoxFuture<'static, Fut::Output>, - }, +pin_project! { + #[project = ErrorHandlersProj] + pub enum ErrorHandlersFuture + where + Fut: Future, + { + ServiceFuture { + #[pin] + fut: Fut, + handlers: Handlers, + }, + HandlerFuture { + fut: LocalBoxFuture<'static, Fut::Output>, + }, + } } impl Future for ErrorHandlersFuture @@ -175,7 +178,8 @@ where #[cfg(test)] mod tests { use actix_service::IntoService; - use futures_util::future::{ok, FutureExt}; + use actix_utils::future::ok; + use futures_util::future::FutureExt as _; use super::*; use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; @@ -196,14 +200,13 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mut mw = ErrorHandlers::new() + let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500) .new_transform(srv.into_service()) .await .unwrap(); - let resp = - test::call_service(&mut mw, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } @@ -223,14 +226,13 @@ mod tests { ok(req.into_response(HttpResponse::InternalServerError().finish())) }; - let mut mw = ErrorHandlers::new() + let mw = ErrorHandlers::new() .handler(StatusCode::INTERNAL_SERVER_ERROR, render_500_async) .new_transform(srv.into_service()) .await .unwrap(); - let resp = - test::call_service(&mut mw, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } } diff --git a/src/middleware/logger.rs b/src/middleware/logger.rs index 66bd7e464..f89b13a1c 100644 --- a/src/middleware/logger.rs +++ b/src/middleware/logger.rs @@ -13,18 +13,19 @@ use std::{ }; use actix_service::{Service, Transform}; +use actix_utils::future::{ready, Ready}; use bytes::Bytes; -use futures_util::future::{ok, Ready}; +use futures_core::ready; use log::{debug, warn}; +use pin_project_lite::pin_project; use regex::{Regex, RegexSet}; -use time::OffsetDateTime; +use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ - dev::{BodySize, MessageBody, ResponseBody}, - error::{Error, Result}, - http::{HeaderName, StatusCode}, + body::{BodySize, MessageBody}, + http::HeaderName, service::{ServiceRequest, ServiceResponse}, - HttpResponse, + Error, HttpResponse, Result, }; /// Middleware for logging request and response summaries to the terminal. @@ -42,8 +43,8 @@ use crate::{ /// 127.0.0.1:54278 "GET /test HTTP/1.1" 404 20 "-" "HTTPie/2.2.0" 0.001074 /// ``` /// -/// # Usage -/// ```rust +/// # Examples +/// ``` /// use actix_web::{middleware::Logger, App}; /// /// // access logs are printed with the INFO level so ensure it is enabled by default @@ -124,7 +125,7 @@ impl Logger { /// It is convention to print "-" to indicate no output instead of an empty string. /// /// # Example - /// ```rust + /// ``` /// # use actix_web::{http::HeaderValue, middleware::Logger}; /// # fn parse_jwt_id (_req: Option<&HeaderValue>) -> String { "jwt_uid".to_owned() } /// Logger::new("example %{JWT_ID}xi") @@ -137,9 +138,9 @@ impl Logger { ) -> Self { let inner = Rc::get_mut(&mut self.0).unwrap(); - let ft = inner.format.0.iter_mut().find(|ft| { - matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label) - }); + let ft = inner.format.0.iter_mut().find( + |ft| matches!(ft, FormatText::CustomRequest(unit_label, _) if label == unit_label), + ); if let Some(FormatText::CustomRequest(_, request_fn)) = ft { // replace into None or previously registered fn using same label @@ -180,8 +181,8 @@ where { type Response = ServiceResponse>; type Error = Error; - type InitError = (); type Transform = LoggerMiddleware; + type InitError = (); type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { @@ -195,10 +196,10 @@ where } } - ok(LoggerMiddleware { + ready(Ok(LoggerMiddleware { service, inner: self.0.clone(), - }) + })) } } @@ -219,7 +220,7 @@ where actix_service::forward_ready!(service); - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { if self.inner.exclude.contains(req.path()) || self.inner.exclude_regex.is_match(req.path()) { @@ -246,17 +247,18 @@ where } } -#[pin_project::pin_project] -pub struct LoggerResponse -where - B: MessageBody, - S: Service, -{ - #[pin] - fut: S::Future, - time: OffsetDateTime, - format: Option, - _phantom: PhantomData, +pin_project! { + pub struct LoggerResponse + where + B: MessageBody, + S: Service, + { + #[pin] + fut: S::Future, + time: OffsetDateTime, + format: Option, + _phantom: PhantomData, + } } impl Future for LoggerResponse @@ -269,15 +271,13 @@ where fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); - let res = match futures_util::ready!(this.fut.poll(cx)) { + let res = match ready!(this.fut.poll(cx)) { Ok(res) => res, Err(e) => return Poll::Ready(Err(e)), }; if let Some(error) = res.response().error() { - if res.response().head().status != StatusCode::INTERNAL_SERVER_ERROR { - debug!("Error in response: {:?}", error); - } + debug!("Error in response: {:?}", error); } if let Some(ref mut format) = this.format { @@ -289,44 +289,45 @@ where let time = *this.time; let format = this.format.take(); - Poll::Ready(Ok(res.map_body(move |_, body| { - ResponseBody::Body(StreamLog { - body, - time, - format, - size: 0, - }) + Poll::Ready(Ok(res.map_body(move |_, body| StreamLog { + body, + time, + format, + size: 0, }))) } } -use pin_project::{pin_project, pinned_drop}; - -#[pin_project(PinnedDrop)] -pub struct StreamLog { - #[pin] - body: ResponseBody, - format: Option, - size: usize, - time: OffsetDateTime, -} - -#[pinned_drop] -impl PinnedDrop for StreamLog { - fn drop(self: Pin<&mut Self>) { - if let Some(ref format) = self.format { - let render = |fmt: &mut fmt::Formatter<'_>| { - for unit in &format.0 { - unit.render(fmt, self.size, self.time)?; - } - Ok(()) - }; - log::info!("{}", FormatDisplay(&render)); +pin_project! { + pub struct StreamLog { + #[pin] + body: B, + format: Option, + size: usize, + time: OffsetDateTime, + } + impl PinnedDrop for StreamLog { + fn drop(this: Pin<&mut Self>) { + if let Some(ref format) = this.format { + let render = |fmt: &mut fmt::Formatter<'_>| { + for unit in &format.0 { + unit.render(fmt, this.size, this.time)?; + } + Ok(()) + }; + log::info!("{}", FormatDisplay(&render)); + } } } } -impl MessageBody for StreamLog { +impl MessageBody for StreamLog +where + B: MessageBody, + B::Error: Into, +{ + type Error = Error; + fn size(&self) -> BodySize { self.body.size() } @@ -334,14 +335,16 @@ impl MessageBody for StreamLog { fn poll_next( self: Pin<&mut Self>, cx: &mut Context<'_>, - ) -> Poll>> { + ) -> Poll>> { let this = self.project(); - match this.body.poll_next(cx) { - Poll::Ready(Some(Ok(chunk))) => { + + match ready!(this.body.poll_next(cx)) { + Some(Ok(chunk)) => { *this.size += chunk.len(); Poll::Ready(Some(Ok(chunk))) } - val => val, + Some(Err(err)) => Poll::Ready(Some(Err(err.into()))), + None => Poll::Ready(None), } } } @@ -363,8 +366,7 @@ impl Format { /// Returns `None` if the format string syntax is incorrect. pub fn new(s: &str) -> Format { log::trace!("Access log format: {}", s); - let fmt = - Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[atPrUsbTD]?)").unwrap(); + let fmt = Regex::new(r"%(\{([A-Za-z0-9\-_]+)\}([aioe]|xi)|[%atPrUsbTD]?)").unwrap(); let mut idx = 0; let mut results = Vec::new(); @@ -380,17 +382,17 @@ impl Format { results.push(match cap.get(3).unwrap().as_str() { "a" => { if key.as_str() == "r" { - FormatText::RealIPRemoteAddr + FormatText::RealIpRemoteAddr } else { unreachable!() } } - "i" => FormatText::RequestHeader( - HeaderName::try_from(key.as_str()).unwrap(), - ), - "o" => FormatText::ResponseHeader( - HeaderName::try_from(key.as_str()).unwrap(), - ), + "i" => { + FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap()) + } + "o" => { + FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap()) + } "e" => FormatText::EnvironHeader(key.as_str().to_owned()), "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None), _ => unreachable!(), @@ -434,7 +436,7 @@ enum FormatText { Time, TimeMillis, RemoteAddr, - RealIPRemoteAddr, + RealIpRemoteAddr, UrlPath, RequestHeader(HeaderName), ResponseHeader(HeaderName), @@ -533,9 +535,7 @@ impl FormatText { }; } FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), - FormatText::RequestTime => { - *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")) - } + FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()), FormatText::RequestHeader(ref name) => { let s = if let Some(val) = req.headers().get(name) { if let Ok(s) = val.to_str() { @@ -549,16 +549,15 @@ impl FormatText { *self = FormatText::Str(s.to_string()); } FormatText::RemoteAddr => { - let s = if let Some(ref peer) = req.connection_info().remote_addr() { + let s = if let Some(peer) = req.connection_info().remote_addr() { FormatText::Str((*peer).to_string()) } else { FormatText::Str("-".to_string()) }; *self = s; } - FormatText::RealIPRemoteAddr => { - let s = if let Some(remote) = req.connection_info().realip_remote_addr() - { + FormatText::RealIpRemoteAddr => { + let s = if let Some(remote) = req.connection_info().realip_remote_addr() { FormatText::Str(remote.to_string()) } else { FormatText::Str("-".to_string()) @@ -592,7 +591,7 @@ impl<'a> fmt::Display for FormatDisplay<'a> { #[cfg(test)] mod tests { use actix_service::{IntoService, Service, Transform}; - use futures_util::future::ok; + use actix_utils::future::ok; use super::*; use crate::http::{header, StatusCode}; @@ -609,7 +608,7 @@ mod tests { }; let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test"); - let mut srv = logger.new_transform(srv.into_service()).await.unwrap(); + let srv = logger.new_transform(srv.into_service()).await.unwrap(); let req = TestRequest::default() .insert_header(( @@ -629,10 +628,10 @@ mod tests { .finish(), )) }; - let logger = Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test") - .exclude_regex("\\w"); + let logger = + Logger::new("%% %{User-Agent}i %{X-Test}o %{HOME}e %D test").exclude_regex("\\w"); - let mut srv = logger.new_transform(srv.into_service()).await.unwrap(); + let srv = logger.new_transform(srv.into_service()).await.unwrap(); let req = TestRequest::default() .insert_header(( @@ -643,6 +642,38 @@ mod tests { let _res = srv.call(req).await.unwrap(); } + #[actix_rt::test] + async fn test_escape_percent() { + let mut format = Format::new("%%{r}a"); + + let req = TestRequest::default() + .insert_header(( + header::FORWARDED, + header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), + )) + .to_srv_request(); + + let now = OffsetDateTime::now_utc(); + for unit in &mut format.0 { + unit.render_request(now, &req); + } + + let resp = HttpResponse::build(StatusCode::OK).force_close().finish(); + for unit in &mut format.0 { + unit.render_response(&resp); + } + + let entry_time = OffsetDateTime::now_utc(); + let render = |fmt: &mut fmt::Formatter<'_>| { + for unit in &format.0 { + unit.render(fmt, 1024, entry_time)?; + } + Ok(()) + }; + let s = format!("{}", FormatDisplay(&render)); + assert_eq!(s, "%{r}a"); + } + #[actix_rt::test] async fn test_url_path() { let mut format = Format::new("%T %U"); @@ -733,7 +764,7 @@ mod tests { Ok(()) }; let s = format!("{}", FormatDisplay(&render)); - assert!(s.contains(&now.format("%Y-%m-%dT%H:%M:%S"))); + assert!(s.contains(&now.format(&Rfc3339).unwrap())); } #[actix_rt::test] @@ -743,9 +774,7 @@ mod tests { let req = TestRequest::default() .insert_header(( header::FORWARDED, - header::HeaderValue::from_static( - "for=192.0.2.60;proto=http;by=203.0.113.43", - ), + header::HeaderValue::from_static("for=192.0.2.60;proto=http;by=203.0.113.43"), )) .to_srv_request(); @@ -806,7 +835,7 @@ mod tests { captured.to_owned() }); - let mut srv = logger.new_transform(test::ok_service()).await.unwrap(); + let srv = logger.new_transform(test::ok_service()).await.unwrap(); let req = TestRequest::default().to_srv_request(); srv.call(req).await.unwrap(); diff --git a/src/middleware/mod.rs b/src/middleware/mod.rs index e24782f07..d19cb64e9 100644 --- a/src/middleware/mod.rs +++ b/src/middleware/mod.rs @@ -14,7 +14,48 @@ pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; pub use self::logger::Logger; pub use self::normalize::{NormalizePath, TrailingSlash}; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] mod compress; -#[cfg(feature = "compress")] + +#[cfg(feature = "__compress")] pub use self::compress::Compress; + +#[cfg(test)] +mod tests { + use crate::{http::StatusCode, App}; + + use super::*; + + #[test] + fn common_combinations() { + // ensure there's no reason that the built-in middleware cannot compose + + let _ = App::new() + .wrap(Compat::new(Logger::default())) + .wrap(Condition::new(true, DefaultHeaders::new())) + .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { + Ok(ErrorHandlerResponse::Response(res)) + })) + .wrap(Logger::default()) + .wrap(NormalizePath::new(TrailingSlash::Trim)); + + let _ = App::new() + .wrap(NormalizePath::new(TrailingSlash::Trim)) + .wrap(Logger::default()) + .wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| { + Ok(ErrorHandlerResponse::Response(res)) + })) + .wrap(DefaultHeaders::new().header("X-Test2", "X-Value2")) + .wrap(Condition::new(true, DefaultHeaders::new())) + .wrap(Compat::new(Logger::default())); + + #[cfg(feature = "__compress")] + { + let _ = App::new().wrap(Compress::default()).wrap(Logger::default()); + let _ = App::new().wrap(Logger::default()).wrap(Compress::default()); + let _ = App::new().wrap(Compat::new(Compress::default())); + let _ = App::new().wrap(Condition::new(true, Compat::new(Compress::default()))); + } + } +} diff --git a/src/middleware/normalize.rs b/src/middleware/normalize.rs index 89cab9073..8ad0bb3f0 100644 --- a/src/middleware/normalize.rs +++ b/src/middleware/normalize.rs @@ -2,8 +2,8 @@ use actix_http::http::{PathAndQuery, Uri}; use actix_service::{Service, Transform}; +use actix_utils::future::{ready, Ready}; use bytes::Bytes; -use futures_util::future::{ready, Ready}; use regex::Regex; use crate::{ @@ -53,45 +53,63 @@ impl Default for TrailingSlash { /// trailing slashes or else they will be inaccessible (or vice versa when using the /// `TrailingSlash::Always` behavior), as shown in the example tests below. /// -/// # Usage -/// ```rust +/// # Examples +/// ``` /// use actix_web::{web, middleware, App}; /// -/// # actix_web::rt::System::new("doctest").block_on(async { +/// # actix_web::rt::System::new().block_on(async { /// let app = App::new() -/// .wrap(middleware::NormalizePath::default()) +/// .wrap(middleware::NormalizePath::trim()) /// .route("/test", web::get().to(|| async { "test" })) /// .route("/unmatchable/", web::get().to(|| async { "unmatchable" })); /// /// use actix_web::http::StatusCode; /// use actix_web::test::{call_service, init_service, TestRequest}; /// -/// let mut app = init_service(app).await; +/// let app = init_service(app).await; /// /// let req = TestRequest::with_uri("/test").to_request(); -/// let res = call_service(&mut app, req).await; +/// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::OK); /// /// let req = TestRequest::with_uri("/test/").to_request(); -/// let res = call_service(&mut app, req).await; +/// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::OK); /// /// let req = TestRequest::with_uri("/unmatchable").to_request(); -/// let res = call_service(&mut app, req).await; +/// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// /// let req = TestRequest::with_uri("/unmatchable/").to_request(); -/// let res = call_service(&mut app, req).await; +/// let res = call_service(&app, req).await; /// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// # }) /// ``` -#[derive(Debug, Clone, Copy, Default)] +#[derive(Debug, Clone, Copy)] pub struct NormalizePath(TrailingSlash); +impl Default for NormalizePath { + fn default() -> Self { + log::warn!( + "`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \ + in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`." + ); + + Self(TrailingSlash::Trim) + } +} + impl NormalizePath { /// Create new `NormalizePath` middleware with the specified trailing slash style. pub fn new(trailing_slash_style: TrailingSlash) -> Self { - NormalizePath(trailing_slash_style) + Self(trailing_slash_style) + } + + /// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics. + /// + /// Use this instead of `NormalizePath::default()` to avoid deprecation warning. + pub fn trim() -> Self { + Self::new(TrailingSlash::Trim) } } @@ -132,154 +150,237 @@ where actix_service::forward_ready!(service); - fn call(&mut self, mut req: ServiceRequest) -> Self::Future { + fn call(&self, mut req: ServiceRequest) -> Self::Future { let head = req.head_mut(); let original_path = head.uri.path(); - // Either adds a string to the end (duplicates will be removed anyways) or trims all slashes from the end - let path = match self.trailing_slash_behavior { - TrailingSlash::Always => original_path.to_string() + "/", - TrailingSlash::MergeOnly => original_path.to_string(), - TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), - }; - - // normalize multiple /'s to one / - let path = self.merge_slash.replace_all(&path, "/"); - - // Ensure root paths are still resolvable. If resulting path is blank after previous step - // it means the path was one or more slashes. Reduce to single slash. - let path = if path.is_empty() { "/" } else { path.as_ref() }; - - // Check whether the path has been changed - // - // This check was previously implemented as string length comparison - // - // That approach fails when a trailing slash is added, - // and a duplicate slash is removed, - // since the length of the strings remains the same - // - // For example, the path "/v1//s" will be normalized to "/v1/s/" - // Both of the paths have the same length, - // so the change can not be deduced from the length comparison - if path != original_path { - let mut parts = head.uri.clone().into_parts(); - let query = parts.path_and_query.as_ref().and_then(|pq| pq.query()); - - let path = if let Some(q) = query { - Bytes::from(format!("{}?{}", path, q)) - } else { - Bytes::copy_from_slice(path.as_bytes()) + // An empty path here means that the URI has no valid path. We skip normalization in this + // case, because adding a path can make the URI invalid + if !original_path.is_empty() { + // Either adds a string to the end (duplicates will be removed anyways) or trims all + // slashes from the end + let path = match self.trailing_slash_behavior { + TrailingSlash::Always => format!("{}/", original_path), + TrailingSlash::MergeOnly => original_path.to_string(), + TrailingSlash::Trim => original_path.trim_end_matches('/').to_string(), }; - parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap()); - let uri = Uri::from_parts(parts).unwrap(); - req.match_info_mut().get_mut().update(&uri); - req.head_mut().uri = uri; + // normalize multiple /'s to one / + let path = self.merge_slash.replace_all(&path, "/"); + + // Ensure root paths are still resolvable. If resulting path is blank after previous + // step it means the path was one or more slashes. Reduce to single slash. + let path = if path.is_empty() { "/" } else { path.as_ref() }; + + // Check whether the path has been changed + // + // This check was previously implemented as string length comparison + // + // That approach fails when a trailing slash is added, + // and a duplicate slash is removed, + // since the length of the strings remains the same + // + // For example, the path "/v1//s" will be normalized to "/v1/s/" + // Both of the paths have the same length, + // so the change can not be deduced from the length comparison + if path != original_path { + let mut parts = head.uri.clone().into_parts(); + let query = parts.path_and_query.as_ref().and_then(|pq| pq.query()); + + let path = match query { + Some(q) => Bytes::from(format!("{}?{}", path, q)), + None => Bytes::copy_from_slice(path.as_bytes()), + }; + parts.path_and_query = Some(PathAndQuery::from_maybe_shared(path).unwrap()); + + let uri = Uri::from_parts(parts).unwrap(); + req.match_info_mut().get_mut().update(&uri); + req.head_mut().uri = uri; + } } - self.service.call(req) } } #[cfg(test)] mod tests { + use actix_http::StatusCode; use actix_service::IntoService; use super::*; use crate::{ dev::ServiceRequest, + guard::fn_guard, test::{call_service, init_service, TestRequest}, web, App, HttpResponse, }; #[actix_rt::test] async fn test_wrap() { - let mut app = init_service( + let app = init_service( App::new() .wrap(NormalizePath::default()) .service(web::resource("/").to(HttpResponse::Ok)) - .service(web::resource("/v1/something").to(HttpResponse::Ok)), + .service(web::resource("/v1/something").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), ) .await; - let req = TestRequest::with_uri("/").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); + let test_uris = vec![ + "/", + "/?query=test", + "///", + "/v1//something", + "/v1//something////", + "//v1/something", + "//v1//////something", + "/v2//something?query=test", + "/v2//something////?query=test", + "//v2/something?query=test", + "//v2//////something?query=test", + ]; - let req = TestRequest::with_uri("/?query=test").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); - - let req = TestRequest::with_uri("///").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); - - let req = TestRequest::with_uri("/v1//something////").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); - - let req2 = TestRequest::with_uri("//v1/something").to_request(); - let res2 = call_service(&mut app, req2).await; - assert!(res2.status().is_success()); - - let req3 = TestRequest::with_uri("//v1//////something").to_request(); - let res3 = call_service(&mut app, req3).await; - assert!(res3.status().is_success()); - - let req4 = TestRequest::with_uri("/v1//something").to_request(); - let res4 = call_service(&mut app, req4).await; - assert!(res4.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } } #[actix_rt::test] async fn trim_trailing_slashes() { - let mut app = init_service( + let app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::Trim)) .service(web::resource("/").to(HttpResponse::Ok)) - .service(web::resource("/v1/something").to(HttpResponse::Ok)), + .service(web::resource("/v1/something").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), ) .await; - // root paths should still work - let req = TestRequest::with_uri("/").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); + let test_uris = vec![ + "/", + "///", + "/v1/something", + "/v1/something/", + "/v1/something////", + "//v1//something", + "//v1//something//", + "/v2/something?query=test", + "/v2/something/?query=test", + "/v2/something////?query=test", + "//v2//something?query=test", + "//v2//something//?query=test", + ]; - let req = TestRequest::with_uri("/?query=test").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } + } - let req = TestRequest::with_uri("///").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); + #[actix_rt::test] + async fn trim_root_trailing_slashes_with_query() { + let app = init_service( + App::new().wrap(NormalizePath(TrailingSlash::Trim)).service( + web::resource("/") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), + ) + .await; - let req = TestRequest::with_uri("/v1/something////").to_request(); - let res = call_service(&mut app, req).await; - assert!(res.status().is_success()); + let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"]; - let req2 = TestRequest::with_uri("/v1/something/").to_request(); - let res2 = call_service(&mut app, req2).await; - assert!(res2.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } + } - let req3 = TestRequest::with_uri("//v1//something//").to_request(); - let res3 = call_service(&mut app, req3).await; - assert!(res3.status().is_success()); + #[actix_rt::test] + async fn ensure_trailing_slash() { + let app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::Always)) + .service(web::resource("/").to(HttpResponse::Ok)) + .service(web::resource("/v1/something/").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something/") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), + ) + .await; - let req4 = TestRequest::with_uri("//v1//something").to_request(); - let res4 = call_service(&mut app, req4).await; - assert!(res4.status().is_success()); + let test_uris = vec![ + "/", + "///", + "/v1/something", + "/v1/something/", + "/v1/something////", + "//v1//something", + "//v1//something//", + "/v2/something?query=test", + "/v2/something/?query=test", + "/v2/something////?query=test", + "//v2//something?query=test", + "//v2//something//?query=test", + ]; + + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } + } + + #[actix_rt::test] + async fn ensure_root_trailing_slash_with_query() { + let app = init_service( + App::new() + .wrap(NormalizePath(TrailingSlash::Always)) + .service( + web::resource("/") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), + ) + .await; + + let test_uris = vec!["/?query=test", "//?query=test", "///?query=test"]; + + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert!(res.status().is_success(), "Failed uri: {}", uri); + } } #[actix_rt::test] async fn keep_trailing_slash_unchanged() { - let mut app = init_service( + let app = init_service( App::new() .wrap(NormalizePath(TrailingSlash::MergeOnly)) .service(web::resource("/").to(HttpResponse::Ok)) .service(web::resource("/v1/something").to(HttpResponse::Ok)) - .service(web::resource("/v1/").to(HttpResponse::Ok)), + .service(web::resource("/v1/").to(HttpResponse::Ok)) + .service( + web::resource("/v2/something") + .guard(fn_guard(|req| req.uri.query() == Some("query=test"))) + .to(HttpResponse::Ok), + ), ) .await; @@ -295,15 +396,35 @@ mod tests { ("/v1////", true), ("//v1//", true), ("///v1", false), + ("/v2/something?query=test", true), + ("/v2/something/?query=test", false), + ("/v2/something//?query=test", false), + ("//v2//something?query=test", true), ]; - for (path, success) in tests { - let req = TestRequest::with_uri(path).to_request(); - let res = call_service(&mut app, req).await; - assert_eq!(res.status().is_success(), success); + for (uri, success) in tests { + let req = TestRequest::with_uri(uri).to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status().is_success(), success, "Failed uri: {}", uri); } } + #[actix_rt::test] + async fn no_path() { + let app = init_service( + App::new() + .wrap(NormalizePath::default()) + .service(web::resource("/").to(HttpResponse::Ok)), + ) + .await; + + // This URI will be interpreted as an authority form, i.e. there is no path nor scheme + // (https://datatracker.ietf.org/doc/html/rfc7230#section-5.3.3) + let req = TestRequest::with_uri("eh").to_request(); + let res = call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::NOT_FOUND); + } + #[actix_rt::test] async fn test_in_place_normalization() { let srv = |req: ServiceRequest| { @@ -311,26 +432,23 @@ mod tests { ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; - let mut normalize = NormalizePath::default() + let normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); - let req = TestRequest::with_uri("/v1//something////").to_srv_request(); - let res = normalize.call(req).await.unwrap(); - assert!(res.status().is_success()); + let test_uris = vec![ + "/v1//something////", + "///v1/something", + "//v1///something", + "/v1//something", + ]; - let req2 = TestRequest::with_uri("///v1/something").to_srv_request(); - let res2 = normalize.call(req2).await.unwrap(); - assert!(res2.status().is_success()); - - let req3 = TestRequest::with_uri("//v1///something").to_srv_request(); - let res3 = normalize.call(req3).await.unwrap(); - assert!(res3.status().is_success()); - - let req4 = TestRequest::with_uri("/v1//something").to_srv_request(); - let res4 = normalize.call(req4).await.unwrap(); - assert!(res4.status().is_success()); + for uri in test_uris { + let req = TestRequest::with_uri(uri).to_srv_request(); + let res = normalize.call(req).await.unwrap(); + assert!(res.status().is_success(), "Failed uri: {}", uri); + } } #[actix_rt::test] @@ -342,7 +460,7 @@ mod tests { ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; - let mut normalize = NormalizePath::default() + let normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); @@ -359,7 +477,7 @@ mod tests { ready(Ok(req.into_response(HttpResponse::Ok().finish()))) }; - let mut normalize = NormalizePath::default() + let normalize = NormalizePath::default() .new_transform(srv.into_service()) .await .unwrap(); diff --git a/src/request.rs b/src/request.rs index fe60cb169..0027f9b4b 100644 --- a/src/request.rs +++ b/src/request.rs @@ -1,24 +1,32 @@ -use std::cell::{Ref, RefCell, RefMut}; -use std::rc::Rc; -use std::{fmt, net}; +use std::{ + cell::{Ref, RefCell, RefMut}, + fmt, net, + rc::Rc, + str, +}; -use actix_http::http::{HeaderMap, Method, Uri, Version}; -use actix_http::{Error, Extensions, HttpMessage, Message, Payload, RequestHead}; +use actix_http::{ + http::{HeaderMap, Method, Uri, Version}, + Extensions, HttpMessage, Message, Payload, RequestHead, +}; use actix_router::{Path, Url}; -use futures_util::future::{ok, Ready}; +use actix_utils::future::{ok, Ready}; +#[cfg(feature = "cookies")] +use cookie::{Cookie, ParseError as CookieParseError}; use smallvec::SmallVec; -use crate::app_service::AppInitServiceState; -use crate::config::AppConfig; -use crate::error::UrlGenerationError; -use crate::extract::FromRequest; -use crate::info::ConnectionInfo; -use crate::rmap::ResourceMap; +use crate::{ + app_service::AppInitServiceState, config::AppConfig, error::UrlGenerationError, + info::ConnectionInfo, rmap::ResourceMap, Error, FromRequest, +}; +#[cfg(feature = "cookies")] +struct Cookies(Vec>); + +/// An incoming request. #[derive(Clone)] -/// An HTTP Request pub struct HttpRequest { - /// # Panics + /// # Invariant /// `Rc` is used exclusively and NO `Weak` /// is allowed anywhere in the code. Weak pointer is purposely ignored when /// doing `Rc`'s ref counter check. Expect panics if this invariant is violated. @@ -62,7 +70,7 @@ impl HttpRequest { } /// This method returns mutable reference to the request head. - /// panics if multiple references of http request exists. + /// panics if multiple references of HTTP request exists. #[inline] pub(crate) fn head_mut(&mut self) -> &mut RequestHead { &mut Rc::get_mut(&mut self.inner).unwrap().head @@ -103,11 +111,7 @@ impl HttpRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } + self.uri().query().unwrap_or_default() } /// Get a reference to the Path parameters. @@ -159,7 +163,7 @@ impl HttpRequest { /// Generate url for named resource /// - /// ```rust + /// ``` /// # use actix_web::{web, App, HttpRequest, HttpResponse}; /// # /// fn index(req: HttpRequest) -> HttpResponse { @@ -175,16 +179,12 @@ impl HttpRequest { /// ); /// } /// ``` - pub fn url_for( - &self, - name: &str, - elements: U, - ) -> Result + pub fn url_for(&self, name: &str, elements: U) -> Result where U: IntoIterator, I: AsRef, { - self.resource_map().url_for(&self, name, elements) + self.resource_map().url_for(self, name, elements) } /// Generate url for named resource @@ -199,15 +199,17 @@ impl HttpRequest { #[inline] /// Get a reference to a `ResourceMap` of current application. pub fn resource_map(&self) -> &ResourceMap { - &self.app_state().rmap() + self.app_state().rmap() } - /// Peer socket address + /// Peer socket address. /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. + /// Peer address is the directly connected peer's socket address. If a proxy is used in front of + /// the Actix Web server, then it would be address of this proxy. /// /// To get client connection information `.connection_info()` should be used. + /// + /// Will only return None when called in unit tests. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -233,7 +235,7 @@ impl HttpRequest { /// /// If `App::data` was used to store object, use `Data`: /// - /// ```rust,ignore + /// ```ignore /// let opt_t = req.app_data::>(); /// ``` pub fn app_data(&self) -> Option<&T> { @@ -250,6 +252,42 @@ impl HttpRequest { fn app_state(&self) -> &AppInitServiceState { &*self.inner.app_state } + + /// Load request cookies. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + use actix_http::http::header::COOKIE; + + if self.extensions().get::().is_none() { + let mut cookies = Vec::new(); + for hdr in self.headers().get_all(COOKIE) { + let s = str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?; + for cookie_str in s.split(';').map(|s| s.trim()) { + if !cookie_str.is_empty() { + cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned()); + } + } + } + self.extensions_mut().insert(Cookies(cookies)); + } + + Ok(Ref::map(self.extensions(), |ext| { + &ext.get::().unwrap().0 + })) + } + + /// Return request cookie. + #[cfg(feature = "cookies")] + pub fn cookie(&self, name: &str) -> Option> { + if let Ok(cookies) = self.cookies() { + for cookie in cookies.iter() { + if cookie.name() == name { + return Some(cookie.to_owned()); + } + } + } + None + } } impl HttpMessage for HttpRequest { @@ -302,11 +340,10 @@ impl Drop for HttpRequest { /// It is possible to get `HttpRequest` as an extractor handler parameter /// -/// ## Example -/// -/// ```rust +/// # Examples +/// ``` /// use actix_web::{web, App, HttpRequest}; -/// use serde_derive::Deserialize; +/// use serde::Deserialize; /// /// /// extract `Thing` from request /// async fn index(req: HttpRequest) -> String { @@ -321,7 +358,6 @@ impl Drop for HttpRequest { /// } /// ``` impl FromRequest for HttpRequest { - type Config = (); type Error = Error; type Future = Ready>; @@ -431,12 +467,14 @@ mod tests { } #[test] + #[cfg(feature = "cookies")] fn test_no_request_cookies() { let req = TestRequest::default().to_http_request(); assert!(req.cookies().unwrap().is_empty()); } #[test] + #[cfg(feature = "cookies")] fn test_request_cookies() { let req = TestRequest::default() .append_header((header::COOKIE, "cookie1=value1")) @@ -445,10 +483,10 @@ mod tests { { let cookies = req.cookies().unwrap(); assert_eq!(cookies.len(), 2); - assert_eq!(cookies[0].name(), "cookie2"); - assert_eq!(cookies[0].value(), "value2"); - assert_eq!(cookies[1].name(), "cookie1"); - assert_eq!(cookies[1].value(), "value1"); + assert_eq!(cookies[0].name(), "cookie1"); + assert_eq!(cookies[0].value(), "value1"); + assert_eq!(cookies[1].name(), "cookie2"); + assert_eq!(cookies[1].value(), "value2"); } let cookie = req.cookie("cookie1"); @@ -470,9 +508,9 @@ mod tests { #[test] fn test_url_for() { let mut res = ResourceDef::new("/user/{name}.{ext}"); - *res.name_mut() = "index".to_string(); + res.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut res, None); assert!(rmap.has_resource("/user/test.html")); assert!(!rmap.has_resource("/test/unknown")); @@ -500,9 +538,9 @@ mod tests { #[test] fn test_url_for_static() { let mut rdef = ResourceDef::new("/index.html"); - *rdef.name_mut() = "index".to_string(); + rdef.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); @@ -521,9 +559,9 @@ mod tests { #[test] fn test_match_name() { let mut rdef = ResourceDef::new("/index.html"); - *rdef.name_mut() = "index".to_string(); + rdef.set_name("index"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); assert!(rmap.has_resource("/index.html")); @@ -540,11 +578,10 @@ mod tests { fn test_url_for_external() { let mut rdef = ResourceDef::new("https://youtube.com/watch/{video_id}"); - *rdef.name_mut() = "youtube".to_string(); + rdef.set_name("youtube"); - let mut rmap = ResourceMap::new(ResourceDef::new("")); + let mut rmap = ResourceMap::new(ResourceDef::prefix("")); rmap.add(&mut rdef, None); - assert!(rmap.has_resource("https://youtube.com/watch/unknown")); let req = TestRequest::default().rmap(rmap).to_http_request(); let url = req.url_for("youtube", &["oHg5SJYRHA0"]); @@ -556,7 +593,7 @@ mod tests { #[actix_rt::test] async fn test_drop_http_request_pool() { - let mut srv = init_service(App::new().service(web::resource("/").to( + let srv = init_service(App::new().service(web::resource("/").to( |req: HttpRequest| { HttpResponse::Ok() .insert_header(("pool_cap", req.app_state().pool().cap)) @@ -566,7 +603,7 @@ mod tests { .await; let req = TestRequest::default().to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; drop(srv); @@ -575,34 +612,34 @@ mod tests { #[actix_rt::test] async fn test_data() { - let mut srv = init_service(App::new().app_data(10usize).service( - web::resource("/").to(|req: HttpRequest| { + let srv = init_service(App::new().app_data(10usize).service(web::resource("/").to( + |req: HttpRequest| { if req.app_data::().is_some() { HttpResponse::Ok() } else { HttpResponse::BadRequest() } - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); - let mut srv = init_service(App::new().app_data(10u32).service( - web::resource("/").to(|req: HttpRequest| { + let srv = init_service(App::new().app_data(10u32).service(web::resource("/").to( + |req: HttpRequest| { if req.app_data::().is_some() { HttpResponse::Ok() } else { HttpResponse::BadRequest() } - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } @@ -614,7 +651,7 @@ mod tests { HttpResponse::Ok().body(num.to_string()) } - let mut srv = init_service( + let srv = init_service( App::new() .app_data(88usize) .service(web::resource("/").route(web::get().to(echo_usize))) @@ -645,7 +682,7 @@ mod tests { HttpResponse::Ok().body(num.to_string()) } - let mut srv = init_service( + let srv = init_service( App::new() .app_data(88usize) .service(web::resource("/").route(web::get().to(echo_usize))) @@ -668,6 +705,8 @@ mod tests { assert_eq!(body, Bytes::from_static(b"1")); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_extensions_dropped() { struct Tracker { @@ -685,18 +724,18 @@ mod tests { let tracker = Rc::new(RefCell::new(Tracker { dropped: false })); { let tracker2 = Rc::clone(&tracker); - let mut srv = init_service(App::new().data(10u32).service( - web::resource("/").to(move |req: HttpRequest| { + let srv = init_service(App::new().data(10u32).service(web::resource("/").to( + move |req: HttpRequest| { req.extensions_mut().insert(Foo { tracker: Rc::clone(&tracker2), }); HttpResponse::Ok() - }), - )) + }, + ))) .await; let req = TestRequest::default().to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } @@ -705,7 +744,7 @@ mod tests { #[actix_rt::test] async fn extract_path_pattern() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("/user/{id}") .service(web::resource("/profile").route(web::get().to( @@ -727,17 +766,17 @@ mod tests { .await; let req = TestRequest::get().uri("/user/22/profile").to_request(); - let res = call_service(&mut srv, req).await; + let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let req = TestRequest::get().uri("/user/22/not-exist").to_request(); - let res = call_service(&mut srv, req).await; + let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] async fn extract_path_pattern_complex() { - let mut srv = init_service( + let srv = init_service( App::new() .service(web::scope("/user").service(web::scope("/{id}").service( web::resource("").to(move |req: HttpRequest| { @@ -759,15 +798,15 @@ mod tests { .await; let req = TestRequest::get().uri("/user/test").to_request(); - let res = call_service(&mut srv, req).await; + let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let req = TestRequest::get().uri("/").to_request(); - let res = call_service(&mut srv, req).await; + let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); let req = TestRequest::get().uri("/not-exist").to_request(); - let res = call_service(&mut srv, req).await; + let res = call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::OK); } } diff --git a/src/request_data.rs b/src/request_data.rs index 285154884..575dc1eb3 100644 --- a/src/request_data.rs +++ b/src/request_data.rs @@ -1,9 +1,8 @@ use std::{any::type_name, ops::Deref}; -use actix_http::error::{Error, ErrorInternalServerError}; -use futures_util::future; +use actix_utils::future::{err, ok, Ready}; -use crate::{dev::Payload, FromRequest, HttpRequest}; +use crate::{dev::Payload, error::ErrorInternalServerError, Error, FromRequest, HttpRequest}; /// Request-local data extractor. /// @@ -23,7 +22,7 @@ use crate::{dev::Payload, FromRequest, HttpRequest}; /// provided to make this potential foot-gun more obvious. /// /// # Example -/// ```rust,no_run +/// ```no_run /// # use actix_web::{web, HttpResponse, HttpRequest, Responder}; /// /// #[derive(Debug, Clone, PartialEq)] @@ -65,13 +64,12 @@ impl Deref for ReqData { } impl FromRequest for ReqData { - type Config = (); type Error = Error; - type Future = future::Ready>; + type Future = Ready>; fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { if let Some(st) = req.extensions().get::() { - future::ok(ReqData(st.clone())) + ok(ReqData(st.clone())) } else { log::debug!( "Failed to construct App-level ReqData extractor. \ @@ -79,7 +77,7 @@ impl FromRequest for ReqData { req.path(), type_name::(), ); - future::err(ErrorInternalServerError( + err(ErrorInternalServerError( "Missing expected request extension data", )) } @@ -102,7 +100,7 @@ mod tests { #[actix_rt::test] async fn req_data_extractor() { - let mut srv = init_service( + let srv = init_service( App::new() .wrap_fn(|req, srv| { if req.method() == Method::POST { @@ -142,7 +140,7 @@ mod tests { #[actix_rt::test] async fn req_data_internal_mutability() { - let mut srv = init_service( + let srv = init_service( App::new() .wrap_fn(|req, srv| { let data_before = Rc::new(RefCell::new(42u32)); diff --git a/src/resource.rs b/src/resource.rs index 3844b429d..fc417bac2 100644 --- a/src/resource.rs +++ b/src/resource.rs @@ -1,30 +1,28 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::rc::Rc; -use std::task::Poll; +use std::{cell::RefCell, fmt, future::Future, rc::Rc}; -use actix_http::{Error, Extensions, Response}; -use actix_router::IntoPattern; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; +use actix_http::Extensions; +use actix_router::{IntoPatterns, Patterns}; use actix_service::{ - apply, apply_fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory, + apply, apply_fn_factory, boxed, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::data::Data; -use crate::dev::{insert_slash, AppService, HttpServiceFactory, ResourceDef}; -use crate::extract::FromRequest; -use crate::guard::Guard; -use crate::handler::Handler; -use crate::responder::Responder; -use crate::route::{Route, RouteService}; -use crate::service::{ServiceRequest, ServiceResponse}; - -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; +use crate::{ + body::MessageBody, + data::Data, + dev::{ensure_leading_slash, AppService, ResourceDef}, + guard::Guard, + handler::Handler, + responder::Responder, + route::{Route, RouteService}, + service::{ + BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, + ServiceResponse, + }, + BoxError, Error, FromRequest, HttpResponse, +}; /// *Resource* is an entry in resources table which corresponds to requested URL. /// @@ -36,7 +34,7 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// and check guards for specific route, if request matches all /// guards, route considered matched and route handler get called. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { @@ -50,17 +48,17 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// Default behavior could be overridden with `default_resource()` method. pub struct Resource { endpoint: T, - rdef: Vec, + rdef: Patterns, name: Option, routes: Vec, app_data: Option, guards: Vec>, - default: HttpNewService, + default: BoxedHttpServiceFactory, factory_ref: Rc>>, } impl Resource { - pub fn new(path: T) -> Resource { + pub fn new(path: T) -> Resource { let fref = Rc::new(RefCell::new(None)); Resource { @@ -72,7 +70,7 @@ impl Resource { guards: Vec::new(), app_data: None, default: boxed::factory(fn_service(|req: ServiceRequest| async { - Ok(req.into_response(Response::MethodNotAllowed().finish())) + Ok(req.into_response(HttpResponse::MethodNotAllowed())) })), } } @@ -98,7 +96,7 @@ where /// Add match guard to a resource. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -131,7 +129,7 @@ where /// Register a new route. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App, HttpResponse}; /// /// fn main() { @@ -148,7 +146,7 @@ where /// Multiple routes could be added to a resource. Resource object uses /// match guards for route selection. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App}; /// /// fn main() { @@ -168,40 +166,38 @@ where self } - /// Provide resource specific data. This method allows to add extractor - /// configuration or specific state available via `Data` extractor. - /// Provided data is available for all routes registered for the current resource. - /// Resource data overrides data registered by `App::data()` method. - /// - /// ```rust - /// use actix_web::{web, App, FromRequest}; - /// - /// /// extract text data from request - /// async fn index(body: String) -> String { - /// format!("Body {}!", body) - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::resource("/index.html") - /// // limit size of the payload - /// .data(String::configure(|cfg| { - /// cfg.limit(4096) - /// })) - /// .route( - /// web::get() - /// // register handler - /// .to(index) - /// )); - /// } - /// ``` - pub fn data(self, data: U) -> Self { - self.app_data(Data::new(data)) - } - /// Add resource data. /// - /// Data of different types from parent contexts will still be accessible. + /// Data of different types from parent contexts will still be accessible. Any `Data` types + /// set here can be extracted in handlers using the `Data` extractor. + /// + /// # Examples + /// ``` + /// use std::cell::Cell; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; + /// + /// struct MyData { + /// count: std::cell::Cell, + /// } + /// + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) + /// } + /// + /// let app = App::new().service( + /// web::resource("/") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route(web::get().to(handler)) + /// ); + /// ``` pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) @@ -210,9 +206,17 @@ where self } + /// Add resource data after wrapping in `Data`. + /// + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] + pub fn data(self, data: U) -> Self { + self.app_data(Data::new(data)) + } + /// Register a new route and add handler. This route matches all requests. /// - /// ```rust + /// ``` /// use actix_web::*; /// /// fn index(req: HttpRequest) -> HttpResponse { @@ -224,8 +228,7 @@ where /// /// This is shortcut for: /// - /// ```rust - /// # extern crate actix_web; + /// ``` /// # use actix_web::*; /// # fn index(req: HttpRequest) -> HttpResponse { unimplemented!() } /// App::new().service(web::resource("/").route(web::route().to(index))); @@ -236,6 +239,8 @@ where I: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { self.routes.push(Route::new().to(handler)); self @@ -290,7 +295,7 @@ where /// Resource level middlewares are not allowed to change response /// type (i.e modify response's body). /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; @@ -328,7 +333,7 @@ where >, > where - F: FnMut(ServiceRequest, &mut T::Service) -> R + Clone, + F: Fn(ServiceRequest, &T::Service) -> R + Clone, R: Future>, { Resource { @@ -358,9 +363,10 @@ where U::InitError: fmt::Debug, { // create and configure default resource - self.default = boxed::factory(f.into_factory().map_init_err(|e| { - log::error!("Can not construct default service: {:?}", e) - })); + self.default = boxed::factory( + f.into_factory() + .map_init_err(|e| log::error!("Can not construct default service: {:?}", e)), + ); self } @@ -384,44 +390,38 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_slash(self.rdef.clone())) + ResourceDef::new(ensure_leading_slash(self.rdef.clone())) } else { ResourceDef::new(self.rdef.clone()) }; if let Some(ref name) = self.name { - *rdef.name_mut() = name.clone(); + rdef.set_name(name); } - config.register_service(rdef, guards, self, None) - } -} - -impl IntoServiceFactory for Resource -where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - >, -{ - fn into_factory(self) -> T { *self.factory_ref.borrow_mut() = Some(ResourceFactory { routes: self.routes, - app_data: self.app_data.map(Rc::new), default: self.default, }); - self.endpoint + let resource_data = self.app_data.map(Rc::new); + + // wraps endpoint service (including middleware) call and injects app data for this scope + let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { + if let Some(ref data) = resource_data { + req.add_data_container(Rc::clone(data)); + } + + srv.call(req) + }); + + config.register_service(rdef, guards, endpoint, None) } } pub struct ResourceFactory { routes: Vec, - app_data: Option>, - default: HttpNewService, + default: BoxedHttpServiceFactory, } impl ServiceFactory for ResourceFactory { @@ -437,10 +437,7 @@ impl ServiceFactory for ResourceFactory { let default_fut = self.default.new_service(()); // construct route service factory futures - let factory_fut = - join_all(self.routes.iter().map(|route| route.new_service(()))); - - let app_data = self.app_data.clone(); + let factory_fut = join_all(self.routes.iter().map(|route| route.new_service(()))); Box::pin(async move { let default = default_fut.await?; @@ -449,19 +446,14 @@ impl ServiceFactory for ResourceFactory { .into_iter() .collect::, _>>()?; - Ok(ResourceService { - app_data, - default, - routes, - }) + Ok(ResourceService { routes, default }) }) } } pub struct ResourceService { routes: Vec, - app_data: Option>, - default: HttpService, + default: BoxedHttpService, } impl Service for ResourceService { @@ -471,21 +463,13 @@ impl Service for ResourceService { actix_service::always_ready!(); - fn call(&mut self, mut req: ServiceRequest) -> Self::Future { - for route in self.routes.iter_mut() { + fn call(&self, mut req: ServiceRequest) -> Self::Future { + for route in &self.routes { if route.check(&mut req) { - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - return route.call(req); } } - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - self.default.call(req) } } @@ -520,31 +504,33 @@ mod tests { use actix_rt::time::sleep; use actix_service::Service; - use futures_util::future::ok; + use actix_utils::future::ok; - use crate::http::{header, HeaderValue, Method, StatusCode}; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, TestRequest}; - use crate::{guard, web, App, Error, HttpResponse}; + use crate::{ + guard, + http::{header, HeaderValue, Method, StatusCode}, + middleware::DefaultHeaders, + service::{ServiceRequest, ServiceResponse}, + test::{call_service, init_service, TestRequest}, + web, App, Error, HttpMessage, HttpResponse, + }; #[actix_rt::test] async fn test_middleware() { - let mut srv = - init_service( - App::new().service( - web::resource("/test") - .name("test") - .wrap(DefaultHeaders::new().header( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - )) - .route(web::get().to(HttpResponse::Ok)), - ), - ) - .await; + let srv = init_service( + App::new().service( + web::resource("/test") + .name("test") + .wrap( + DefaultHeaders::new() + .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), + ) + .route(web::get().to(HttpResponse::Ok)), + ), + ) + .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -554,7 +540,7 @@ mod tests { #[actix_rt::test] async fn test_middleware_fn() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::resource("/test") .wrap_fn(|req, srv| { @@ -574,7 +560,7 @@ mod tests { ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -584,20 +570,19 @@ mod tests { #[actix_rt::test] async fn test_to() { - let mut srv = - init_service(App::new().service(web::resource("/test").to(|| async { - sleep(Duration::from_millis(100)).await; - Ok::<_, Error>(HttpResponse::Ok()) - }))) - .await; + let srv = init_service(App::new().service(web::resource("/test").to(|| async { + sleep(Duration::from_millis(100)).await; + Ok::<_, Error>(HttpResponse::Ok()) + }))) + .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_pattern() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::resource(["/test", "/test2"]) .to(|| async { Ok::<_, Error>(HttpResponse::Ok()) }), @@ -605,16 +590,16 @@ mod tests { ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test2").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_default_resource() { - let mut srv = init_service( + let srv = init_service( App::new() .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))) .default_service(|r: ServiceRequest| { @@ -623,16 +608,16 @@ mod tests { ) .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); - let mut srv = init_service( + let srv = init_service( App::new().service( web::resource("/test") .route(web::get().to(HttpResponse::Ok)) @@ -644,19 +629,19 @@ mod tests { .await; let req = TestRequest::with_uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] async fn test_resource_guards() { - let mut srv = init_service( + let srv = init_service( App::new() .service( web::resource("/test/{p}") @@ -679,25 +664,27 @@ mod tests { let req = TestRequest::with_uri("/test/it") .method(Method::GET) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test/it") .method(Method::PUT) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/test/it") .method(Method::DELETE) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::NO_CONTENT); } + // allow deprecated `{App, Resource}::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data() { - let mut srv = init_service( + let srv = init_service( App::new() .data(1.0f64) .data(1usize) @@ -723,13 +710,15 @@ mod tests { .await; let req = TestRequest::get().uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated `{App, Resource}::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { - let mut srv = init_service( + let srv = init_service( App::new().data(1usize).service( web::resource("/test") .data(10usize) @@ -742,7 +731,42 @@ mod tests { .await; let req = TestRequest::get().uri("/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + + #[actix_rt::test] + async fn test_middleware_app_data() { + let srv = init_service( + App::new().service( + web::resource("test") + .app_data(1usize) + .wrap_fn(|req, srv| { + assert_eq!(req.app_data::(), Some(&1usize)); + req.extensions_mut().insert(1usize); + srv.call(req) + }) + .route(web::get().to(HttpResponse::Ok)) + .default_service(|req: ServiceRequest| async move { + let (req, _) = req.into_parts(); + + assert_eq!(req.extensions().get::(), Some(&1)); + + Ok(ServiceResponse::new( + req, + HttpResponse::BadRequest().finish(), + )) + }), + ), + ) + .await; + + let req = TestRequest::get().uri("/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::post().uri("/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } } diff --git a/src/responder.rs b/src/responder.rs index d5771d520..9d8a0e8ed 100644 --- a/src/responder.rs +++ b/src/responder.rs @@ -1,24 +1,25 @@ -use std::fmt; +use std::borrow::Cow; use actix_http::{ - error::InternalError, + body::{BoxBody, EitherBody, MessageBody}, http::{header::IntoHeaderPair, Error as HttpError, HeaderMap, StatusCode}, - ResponseBuilder, }; use bytes::{Bytes, BytesMut}; -use crate::{Error, HttpRequest, HttpResponse}; +use crate::{BoxError, Error, HttpRequest, HttpResponse, HttpResponseBuilder}; /// Trait implemented by types that can be converted to an HTTP response. /// /// Any types that implement this trait can be used in the return type of a handler. pub trait Responder { + type Body: MessageBody + 'static; + /// Convert self to `HttpResponse`. - fn respond_to(self, req: &HttpRequest) -> HttpResponse; + fn respond_to(self, req: &HttpRequest) -> HttpResponse; /// Override a status code for a Responder. /// - /// ```rust + /// ``` /// use actix_web::{http::StatusCode, HttpRequest, Responder}; /// /// fn index(req: HttpRequest) -> impl Responder { @@ -36,7 +37,7 @@ pub trait Responder { /// /// Overrides other headers with the same name. /// - /// ```rust + /// ``` /// use actix_web::{web, HttpRequest, Responder}; /// use serde::Serialize; /// @@ -60,17 +61,52 @@ pub trait Responder { } impl Responder for HttpResponse { + type Body = BoxBody; + #[inline] - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + fn respond_to(self, _: &HttpRequest) -> HttpResponse { self } } -impl Responder for Option { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { +impl Responder for actix_http::Response { + type Body = BoxBody; + + #[inline] + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + HttpResponse::from(self) + } +} + +impl Responder for HttpResponseBuilder { + type Body = BoxBody; + + #[inline] + fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { + self.finish() + } +} + +impl Responder for actix_http::ResponseBuilder { + type Body = BoxBody; + + #[inline] + fn respond_to(mut self, req: &HttpRequest) -> HttpResponse { + self.finish().map_into_boxed_body().respond_to(req) + } +} + +impl Responder for Option +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Some(t) => t.respond_to(req), - None => HttpResponse::build(StatusCode::NOT_FOUND).finish(), + Some(val) => val.respond_to(req).map_into_left_body(), + None => HttpResponse::new(StatusCode::NOT_FOUND).map_into_right_body(), } } } @@ -78,85 +114,75 @@ impl Responder for Option { impl Responder for Result where T: Responder, + ::Error: Into, E: Into, { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Ok(val) => val.respond_to(req), - Err(e) => HttpResponse::from_error(e.into()), + Ok(val) => val.respond_to(req).map_into_left_body(), + Err(err) => HttpResponse::from_error(err.into()).map_into_right_body(), } } } -impl Responder for ResponseBuilder { - #[inline] - fn respond_to(mut self, _: &HttpRequest) -> HttpResponse { - self.finish() - } -} - impl Responder for (T, StatusCode) { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = T::Body; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { let mut res = self.0.respond_to(req); *res.status_mut() = self.1; res } } -impl Responder for &'static str { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(self) - } +macro_rules! impl_responder_by_forward_into_base_response { + ($res:ty, $body:ty) => { + impl Responder for $res { + type Body = $body; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let res: actix_http::Response<_> = self.into(); + res.into() + } + } + }; + + ($res:ty) => { + impl_responder_by_forward_into_base_response!($res, $res); + }; } -impl Responder for &'static [u8] { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(self) - } +impl_responder_by_forward_into_base_response!(&'static [u8]); +impl_responder_by_forward_into_base_response!(Bytes); +impl_responder_by_forward_into_base_response!(BytesMut); + +impl_responder_by_forward_into_base_response!(&'static str); +impl_responder_by_forward_into_base_response!(String); + +macro_rules! impl_into_string_responder { + ($res:ty) => { + impl Responder for $res { + type Body = String; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { + let string: String = self.into(); + let res: actix_http::Response<_> = string.into(); + res.into() + } + } + }; } -impl Responder for String { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(self) - } -} - -impl<'a> Responder for &'a String { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(mime::TEXT_PLAIN_UTF_8) - .body(self) - } -} - -impl Responder for Bytes { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(self) - } -} - -impl Responder for BytesMut { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::Ok() - .content_type(mime::APPLICATION_OCTET_STREAM) - .body(self) - } -} +impl_into_string_responder!(&'_ String); +impl_into_string_responder!(Cow<'_, str>); /// Allows overriding status code and headers for a responder. pub struct CustomResponder { responder: T, status: Option, - headers: Option, - error: Option, + headers: Result, } impl CustomResponder { @@ -164,14 +190,13 @@ impl CustomResponder { CustomResponder { responder, status: None, - headers: None, - error: None, + headers: Ok(HeaderMap::new()), } } /// Override a status code for the Responder's response. /// - /// ```rust + /// ``` /// use actix_web::{HttpRequest, Responder, http::StatusCode}; /// /// fn index(req: HttpRequest) -> impl Responder { @@ -187,7 +212,7 @@ impl CustomResponder { /// /// Overrides other headers with the same name. /// - /// ```rust + /// ``` /// use actix_web::{web, HttpRequest, Responder}; /// use serde::Serialize; /// @@ -206,44 +231,42 @@ impl CustomResponder { where H: IntoHeaderPair, { - if self.headers.is_none() { - self.headers = Some(HeaderMap::new()); + if let Ok(ref mut headers) = self.headers { + match header.try_into_header_pair() { + Ok((key, value)) => headers.append(key, value), + Err(e) => self.headers = Err(e.into()), + }; } - match header.try_into_header_pair() { - Ok((key, value)) => self.headers.as_mut().unwrap().append(key, value), - Err(e) => self.error = Some(e.into()), - }; - self } } -impl Responder for CustomResponder { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { +impl Responder for CustomResponder +where + T: Responder, + ::Error: Into, +{ + type Body = EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { + let headers = match self.headers { + Ok(headers) => headers, + Err(err) => return HttpResponse::from_error(err).map_into_right_body(), + }; + let mut res = self.responder.respond_to(req); if let Some(status) = self.status { *res.status_mut() = status; } - if let Some(ref headers) = self.headers { - for (k, v) in headers { - // TODO: before v4, decide if this should be append instead - res.headers_mut().insert(k.clone(), v.clone()); - } + for (k, v) in headers { + // TODO: before v4, decide if this should be append instead + res.headers_mut().insert(k, v); } - res - } -} - -impl Responder for InternalError -where - T: fmt::Debug + fmt::Display + 'static, -{ - fn respond_to(self, _: &HttpRequest) -> HttpResponse { - HttpResponse::from_error(self.into()) + res.map_into_left_body() } } @@ -252,19 +275,21 @@ pub(crate) mod tests { use actix_service::Service; use bytes::{Bytes, BytesMut}; + use actix_http::body::to_bytes; + use super::*; - use crate::dev::{Body, ResponseBody}; - use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode}; - use crate::test::{init_service, TestRequest}; - use crate::{error, web, App}; + use crate::{ + error, + http::{header::CONTENT_TYPE, HeaderValue, StatusCode}, + test::{assert_body_eq, init_service, TestRequest}, + web, App, + }; #[actix_rt::test] async fn test_option_responder() { - let mut srv = init_service( + let srv = init_service( App::new() - .service( - web::resource("/none").to(|| async { Option::<&'static str>::None }), - ) + .service(web::resource("/none").to(|| async { Option::<&'static str>::None })) .service(web::resource("/some").to(|| async { Some("some") })), ) .await; @@ -276,97 +301,116 @@ pub(crate) mod tests { let req = TestRequest::with_uri("/some").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), StatusCode::OK); - match resp.response().body() { - ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"some")); - } - _ => panic!(), - } - } - - pub(crate) trait BodyTest { - fn bin_ref(&self) -> &[u8]; - fn body(&self) -> &Body; - } - - impl BodyTest for ResponseBody { - fn bin_ref(&self) -> &[u8] { - match self { - ResponseBody::Body(ref b) => match b { - Body::Bytes(ref bin) => &bin, - _ => panic!(), - }, - ResponseBody::Other(ref b) => match b { - Body::Bytes(ref bin) => &bin, - _ => panic!(), - }, - } - } - fn body(&self) -> &Body { - match self { - ResponseBody::Body(ref b) => b, - ResponseBody::Other(ref b) => b, - } - } + assert_body_eq!(resp, b"some"); } #[actix_rt::test] async fn test_responder() { let req = TestRequest::default().to_http_request(); - let resp = "test".respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = "test".respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = b"test".respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), ); - let resp = "test".to_string().respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let res = b"test".respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = "test".to_string().respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = (&"test".to_string()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = (&"test".to_string()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); - - let resp = Bytes::from_static(b"test").respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), - HeaderValue::from_static("application/octet-stream") + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), ); - let resp = BytesMut::from(b"test".as_ref()).respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); + let s = String::from("test"); + let res = Cow::Borrowed(s.as_str()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Cow::<'_, str>::Owned(s).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Cow::Borrowed("test").respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("text/plain; charset=utf-8") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = Bytes::from_static(b"test").respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/octet-stream") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); + + let res = BytesMut::from(b"test".as_ref()).respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!( + res.headers().get(CONTENT_TYPE).unwrap(), + HeaderValue::from_static("application/octet-stream") + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); // InternalError - let resp = - error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + let res = error::InternalError::new("err", StatusCode::BAD_REQUEST).respond_to(&req); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); } #[actix_rt::test] @@ -376,15 +420,17 @@ pub(crate) mod tests { // Result let resp = Ok::<_, Error>("test".to_string()).respond_to(&req); assert_eq!(resp.status(), StatusCode::OK); - assert_eq!(resp.body().bin_ref(), b"test"); assert_eq!( resp.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("text/plain; charset=utf-8") ); + assert_eq!( + to_bytes(resp.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); - let res = - Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) - .respond_to(&req); + let res = Err::(error::InternalError::new("err", StatusCode::BAD_REQUEST)) + .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); } @@ -398,7 +444,10 @@ pub(crate) mod tests { .respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let res = "test" .to_string() @@ -406,11 +455,14 @@ pub(crate) mod tests { .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.body().bin_ref(), b"test"); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("json") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } #[actix_rt::test] @@ -418,17 +470,23 @@ pub(crate) mod tests { let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::BAD_REQUEST).respond_to(&req); assert_eq!(res.status(), StatusCode::BAD_REQUEST); - assert_eq!(res.body().bin_ref(), b"test"); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); let req = TestRequest::default().to_http_request(); let res = ("test".to_string(), StatusCode::OK) .with_header((CONTENT_TYPE, mime::APPLICATION_JSON)) .respond_to(&req); assert_eq!(res.status(), StatusCode::OK); - assert_eq!(res.body().bin_ref(), b"test"); assert_eq!( res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/json") ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } } diff --git a/src/response/builder.rs b/src/response/builder.rs new file mode 100644 index 000000000..b5bef2e99 --- /dev/null +++ b/src/response/builder.rs @@ -0,0 +1,570 @@ +use std::{ + cell::{Ref, RefMut}, + convert::TryInto, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{ + body::{BodyStream, BoxBody, MessageBody}, + http::{ + header::{self, HeaderName, IntoHeaderPair, IntoHeaderValue}, + ConnectionType, Error as HttpError, StatusCode, + }, + Extensions, Response, ResponseHead, +}; +use bytes::Bytes; +use futures_core::Stream; +use serde::Serialize; + +#[cfg(feature = "cookies")] +use actix_http::http::header::HeaderValue; +#[cfg(feature = "cookies")] +use cookie::{Cookie, CookieJar}; + +use crate::{ + error::{Error, JsonPayloadError}, + BoxError, HttpResponse, +}; + +/// An HTTP response builder. +/// +/// This type can be used to construct an instance of `Response` through a builder-like pattern. +pub struct HttpResponseBuilder { + res: Option>, + err: Option, + #[cfg(feature = "cookies")] + cookies: Option, +} + +impl HttpResponseBuilder { + #[inline] + /// Create response builder + pub fn new(status: StatusCode) -> Self { + Self { + res: Some(Response::with_body(status, BoxBody::new(()))), + err: None, + #[cfg(feature = "cookies")] + cookies: None, + } + } + + /// Set HTTP status code of this response. + #[inline] + pub fn status(&mut self, status: StatusCode) -> &mut Self { + if let Some(parts) = self.inner() { + parts.status = status; + } + self + } + + /// Insert a header, replacing any that were set with an equivalent field name. + /// + /// ``` + /// use actix_web::{HttpResponse, http::header}; + /// + /// HttpResponse::Ok() + /// .insert_header(header::ContentType(mime::APPLICATION_JSON)) + /// .insert_header(("X-TEST", "value")) + /// .finish(); + /// ``` + pub fn insert_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + { + if let Some(parts) = self.inner() { + match header.try_into_header_pair() { + Ok((key, value)) => { + parts.headers.insert(key, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + + self + } + + /// Append a header, keeping any that were set with an equivalent field name. + /// + /// ``` + /// use actix_web::{HttpResponse, http::header}; + /// + /// HttpResponse::Ok() + /// .append_header(header::ContentType(mime::APPLICATION_JSON)) + /// .append_header(("X-TEST", "value1")) + /// .append_header(("X-TEST", "value2")) + /// .finish(); + /// ``` + pub fn append_header(&mut self, header: H) -> &mut Self + where + H: IntoHeaderPair, + { + if let Some(parts) = self.inner() { + match header.try_into_header_pair() { + Ok((key, value)) => parts.headers.append(key, value), + Err(e) => self.err = Some(e.into()), + }; + } + + self + } + + /// Replaced with [`Self::insert_header()`]. + #[deprecated( + since = "4.0.0", + note = "Replaced with `insert_header((key, value))`. Will be removed in v5." + )] + pub fn set_header(&mut self, key: K, value: V) -> &mut Self + where + K: TryInto, + K::Error: Into, + V: IntoHeaderValue, + { + if self.err.is_some() { + return self; + } + + match (key.try_into(), value.try_into_value()) { + (Ok(name), Ok(value)) => return self.insert_header((name, value)), + (Err(err), _) => self.err = Some(err.into()), + (_, Err(err)) => self.err = Some(err.into()), + } + + self + } + + /// Replaced with [`Self::append_header()`]. + #[deprecated( + since = "4.0.0", + note = "Replaced with `append_header((key, value))`. Will be removed in v5." + )] + pub fn header(&mut self, key: K, value: V) -> &mut Self + where + K: TryInto, + K::Error: Into, + V: IntoHeaderValue, + { + if self.err.is_some() { + return self; + } + + match (key.try_into(), value.try_into_value()) { + (Ok(name), Ok(value)) => return self.append_header((name, value)), + (Err(err), _) => self.err = Some(err.into()), + (_, Err(err)) => self.err = Some(err.into()), + } + + self + } + + /// Set the custom reason for the response. + #[inline] + pub fn reason(&mut self, reason: &'static str) -> &mut Self { + if let Some(parts) = self.inner() { + parts.reason = Some(reason); + } + self + } + + /// Set connection type to KeepAlive + #[inline] + pub fn keep_alive(&mut self) -> &mut Self { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::KeepAlive); + } + self + } + + /// Set connection type to Upgrade + #[inline] + pub fn upgrade(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::Upgrade); + } + + if let Ok(value) = value.try_into_value() { + self.insert_header((header::UPGRADE, value)); + } + + self + } + + /// Force close connection, even if it is marked as keep-alive + #[inline] + pub fn force_close(&mut self) -> &mut Self { + if let Some(parts) = self.inner() { + parts.set_connection_type(ConnectionType::Close); + } + self + } + + /// Disable chunked transfer encoding for HTTP/1.1 streaming responses. + #[inline] + pub fn no_chunking(&mut self, len: u64) -> &mut Self { + let mut buf = itoa::Buffer::new(); + self.insert_header((header::CONTENT_LENGTH, buf.format(len))); + + if let Some(parts) = self.inner() { + parts.no_chunking(true); + } + self + } + + /// Set response content type. + #[inline] + pub fn content_type(&mut self, value: V) -> &mut Self + where + V: IntoHeaderValue, + { + if let Some(parts) = self.inner() { + match value.try_into_value() { + Ok(value) => { + parts.headers.insert(header::CONTENT_TYPE, value); + } + Err(e) => self.err = Some(e.into()), + }; + } + self + } + + /// Set a cookie. + /// + /// ``` + /// use actix_web::{HttpResponse, cookie::Cookie}; + /// + /// HttpResponse::Ok() + /// .cookie( + /// Cookie::build("name", "value") + /// .domain("www.rust-lang.org") + /// .path("/") + /// .secure(true) + /// .http_only(true) + /// .finish(), + /// ) + /// .finish(); + /// ``` + #[cfg(feature = "cookies")] + pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self { + if self.cookies.is_none() { + let mut jar = CookieJar::new(); + jar.add(cookie.into_owned()); + self.cookies = Some(jar) + } else { + self.cookies.as_mut().unwrap().add(cookie.into_owned()); + } + self + } + + /// Remove cookie. + /// + /// A `Set-Cookie` header is added that will delete a cookie with the same name from the client. + /// + /// ``` + /// use actix_web::{HttpRequest, HttpResponse, Responder}; + /// + /// async fn handler(req: HttpRequest) -> impl Responder { + /// let mut builder = HttpResponse::Ok(); + /// + /// if let Some(ref cookie) = req.cookie("name") { + /// builder.del_cookie(cookie); + /// } + /// + /// builder.finish() + /// } + /// ``` + #[cfg(feature = "cookies")] + pub fn del_cookie(&mut self, cookie: &Cookie<'_>) -> &mut Self { + if self.cookies.is_none() { + self.cookies = Some(CookieJar::new()) + } + let jar = self.cookies.as_mut().unwrap(); + let cookie = cookie.clone().into_owned(); + jar.add_original(cookie.clone()); + jar.remove(cookie); + self + } + + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref<'_, Extensions> { + self.res + .as_ref() + .expect("cannot reuse response builder") + .extensions() + } + + /// Mutable reference to a the response's extensions + pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { + self.res + .as_mut() + .expect("cannot reuse response builder") + .extensions_mut() + } + + /// Set a body and build the `HttpResponse`. + /// + /// `HttpResponseBuilder` can not be used after this call. + pub fn body(&mut self, body: B) -> HttpResponse + where + B: MessageBody + 'static, + { + match self.message_body(body) { + Ok(res) => res.map_into_boxed_body(), + Err(err) => HttpResponse::from_error(err), + } + } + + /// Set a body and build the `HttpResponse`. + /// + /// `HttpResponseBuilder` can not be used after this call. + pub fn message_body(&mut self, body: B) -> Result, Error> { + if let Some(err) = self.err.take() { + return Err(err.into()); + } + + let res = self + .res + .take() + .expect("cannot reuse response builder") + .set_body(body); + + #[allow(unused_mut)] // mut is only unused when cookies are disabled + let mut res = HttpResponse::from(res); + + #[cfg(feature = "cookies")] + if let Some(ref jar) = self.cookies { + for cookie in jar.delta() { + match HeaderValue::from_str(&cookie.to_string()) { + Ok(val) => res.headers_mut().append(header::SET_COOKIE, val), + Err(err) => return Err(err.into()), + }; + } + } + + Ok(res) + } + + /// Set a streaming body and build the `HttpResponse`. + /// + /// `HttpResponseBuilder` can not be used after this call. + #[inline] + pub fn streaming(&mut self, stream: S) -> HttpResponse + where + S: Stream> + 'static, + E: Into + 'static, + { + self.body(BodyStream::new(stream)) + } + + /// Set a JSON body and build the `HttpResponse`. + /// + /// `HttpResponseBuilder` can not be used after this call. + pub fn json(&mut self, value: impl Serialize) -> HttpResponse { + match serde_json::to_string(&value) { + Ok(body) => { + let contains = if let Some(parts) = self.inner() { + parts.headers.contains_key(header::CONTENT_TYPE) + } else { + true + }; + + if !contains { + self.insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); + } + + self.body(body) + } + Err(err) => HttpResponse::from_error(JsonPayloadError::Serialize(err)), + } + } + + /// Set an empty body and build the `HttpResponse`. + /// + /// `HttpResponseBuilder` can not be used after this call. + #[inline] + pub fn finish(&mut self) -> HttpResponse { + self.body(()) + } + + /// This method construct new `HttpResponseBuilder` + pub fn take(&mut self) -> Self { + Self { + res: self.res.take(), + err: self.err.take(), + #[cfg(feature = "cookies")] + cookies: self.cookies.take(), + } + } + + #[inline] + fn inner(&mut self) -> Option<&mut ResponseHead> { + if self.err.is_some() { + return None; + } + + self.res.as_mut().map(Response::head_mut) + } +} + +impl From for HttpResponse { + fn from(mut builder: HttpResponseBuilder) -> Self { + builder.finish() + } +} + +impl From for Response { + fn from(mut builder: HttpResponseBuilder) -> Self { + builder.finish().into() + } +} + +impl Future for HttpResponseBuilder { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { + Poll::Ready(Ok(self.finish())) + } +} + +#[cfg(test)] +mod tests { + use actix_http::body; + + use super::*; + use crate::http::{ + header::{self, HeaderValue, CONTENT_TYPE}, + StatusCode, + }; + + #[test] + fn test_basic_builder() { + let resp = HttpResponse::Ok() + .insert_header(("X-TEST", "value")) + .finish(); + assert_eq!(resp.status(), StatusCode::OK); + } + + #[test] + fn test_upgrade() { + let resp = HttpResponseBuilder::new(StatusCode::OK) + .upgrade("websocket") + .finish(); + assert!(resp.upgrade()); + assert_eq!( + resp.headers().get(header::UPGRADE).unwrap(), + HeaderValue::from_static("websocket") + ); + } + + #[test] + fn test_force_close() { + let resp = HttpResponseBuilder::new(StatusCode::OK) + .force_close() + .finish(); + assert!(!resp.keep_alive()) + } + + #[test] + fn test_content_type() { + let resp = HttpResponseBuilder::new(StatusCode::OK) + .content_type("text/plain") + .body(Bytes::new()); + assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") + } + + #[actix_rt::test] + async fn test_json() { + let resp = HttpResponse::Ok().json(vec!["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("application/json")); + assert_eq!( + body::to_bytes(resp.into_body()).await.unwrap().as_ref(), + br#"["v1","v2","v3"]"# + ); + + let resp = HttpResponse::Ok().json(&["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("application/json")); + assert_eq!( + body::to_bytes(resp.into_body()).await.unwrap().as_ref(), + br#"["v1","v2","v3"]"# + ); + + // content type override + let resp = HttpResponse::Ok() + .insert_header((CONTENT_TYPE, "text/json")) + .json(&vec!["v1", "v2", "v3"]); + let ct = resp.headers().get(CONTENT_TYPE).unwrap(); + assert_eq!(ct, HeaderValue::from_static("text/json")); + assert_eq!( + body::to_bytes(resp.into_body()).await.unwrap().as_ref(), + br#"["v1","v2","v3"]"# + ); + } + + #[actix_rt::test] + async fn test_serde_json_in_body() { + let resp = HttpResponse::Ok().body( + serde_json::to_vec(&serde_json::json!({ "test-key": "test-value" })).unwrap(), + ); + + assert_eq!( + body::to_bytes(resp.into_body()).await.unwrap().as_ref(), + br#"{"test-key":"test-value"}"# + ); + } + + #[test] + fn response_builder_header_insert_kv() { + let mut res = HttpResponse::Ok(); + res.insert_header(("Content-Type", "application/octet-stream")); + let res = res.finish(); + + assert_eq!( + res.headers().get("Content-Type"), + Some(&HeaderValue::from_static("application/octet-stream")) + ); + } + + #[test] + fn response_builder_header_insert_typed() { + let mut res = HttpResponse::Ok(); + res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); + let res = res.finish(); + + assert_eq!( + res.headers().get("Content-Type"), + Some(&HeaderValue::from_static("application/octet-stream")) + ); + } + + #[test] + fn response_builder_header_append_kv() { + let mut res = HttpResponse::Ok(); + res.append_header(("Content-Type", "application/octet-stream")); + res.append_header(("Content-Type", "application/json")); + let res = res.finish(); + + let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); + assert_eq!(headers.len(), 2); + assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); + assert!(headers.contains(&HeaderValue::from_static("application/json"))); + } + + #[test] + fn response_builder_header_append_typed() { + let mut res = HttpResponse::Ok(); + res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM)); + res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)); + let res = res.finish(); + + let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect(); + assert_eq!(headers.len(), 2); + assert!(headers.contains(&HeaderValue::from_static("application/octet-stream"))); + assert!(headers.contains(&HeaderValue::from_static("application/json"))); + } +} diff --git a/actix-http/src/httpcodes.rs b/src/response/http_codes.rs similarity index 86% rename from actix-http/src/httpcodes.rs rename to src/response/http_codes.rs index f421d8e23..44ddb78f9 100644 --- a/actix-http/src/httpcodes.rs +++ b/src/response/http_codes.rs @@ -1,21 +1,19 @@ //! Status code based HTTP response builders. -#![allow(non_upper_case_globals)] +use actix_http::http::StatusCode; -use http::StatusCode; - -use crate::response::{Response, ResponseBuilder}; +use crate::{HttpResponse, HttpResponseBuilder}; macro_rules! static_resp { ($name:ident, $status:expr) => { #[allow(non_snake_case, missing_docs)] - pub fn $name() -> ResponseBuilder { - ResponseBuilder::new($status) + pub fn $name() -> HttpResponseBuilder { + HttpResponseBuilder::new($status) } }; } -impl Response { +impl HttpResponse { static_resp!(Continue, StatusCode::CONTINUE); static_resp!(SwitchingProtocols, StatusCode::SWITCHING_PROTOCOLS); static_resp!(Processing, StatusCode::PROCESSING); @@ -67,6 +65,14 @@ impl Response { static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS); + static_resp!( + RequestHeaderFieldsTooLarge, + StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE + ); + static_resp!( + UnavailableForLegalReasons, + StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS + ); static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR); static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED); @@ -81,13 +87,12 @@ impl Response { #[cfg(test)] mod tests { - use crate::body::Body; - use crate::response::Response; - use http::StatusCode; + use crate::http::StatusCode; + use crate::HttpResponse; #[test] fn test_build() { - let resp = Response::Ok().body(Body::Empty); + let resp = HttpResponse::Ok().finish(); assert_eq!(resp.status(), StatusCode::OK); } } diff --git a/src/response/mod.rs b/src/response/mod.rs new file mode 100644 index 000000000..8401db9d2 --- /dev/null +++ b/src/response/mod.rs @@ -0,0 +1,10 @@ +mod builder; +mod http_codes; +#[allow(clippy::module_inception)] +mod response; + +pub use self::builder::HttpResponseBuilder; +pub use self::response::HttpResponse; + +#[cfg(feature = "cookies")] +pub use self::response::CookieIter; diff --git a/src/response/response.rs b/src/response/response.rs new file mode 100644 index 000000000..97de21e42 --- /dev/null +++ b/src/response/response.rs @@ -0,0 +1,349 @@ +use std::{ + cell::{Ref, RefMut}, + fmt, + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; + +use actix_http::{ + body::{BoxBody, EitherBody, MessageBody}, + http::{header::HeaderMap, StatusCode}, + Extensions, Response, ResponseHead, +}; + +#[cfg(feature = "cookies")] +use { + actix_http::http::{ + header::{self, HeaderValue}, + Error as HttpError, + }, + cookie::Cookie, +}; + +use crate::{error::Error, HttpResponseBuilder}; + +/// An outgoing response. +pub struct HttpResponse { + res: Response, + pub(crate) error: Option, +} + +impl HttpResponse { + /// Constructs a response. + #[inline] + pub fn new(status: StatusCode) -> Self { + Self { + res: Response::new(status), + error: None, + } + } + + /// Constructs a response builder with specific HTTP status. + #[inline] + pub fn build(status: StatusCode) -> HttpResponseBuilder { + HttpResponseBuilder::new(status) + } + + /// Create an error response. + #[inline] + pub fn from_error(error: impl Into) -> Self { + let error = error.into(); + let mut response = error.as_response_error().error_response(); + response.error = Some(error); + response + } +} + +impl HttpResponse { + /// Constructs a response with body + #[inline] + pub fn with_body(status: StatusCode, body: B) -> Self { + Self { + res: Response::with_body(status, body), + error: None, + } + } + + /// Returns a reference to response head. + #[inline] + pub fn head(&self) -> &ResponseHead { + self.res.head() + } + + /// Returns a mutable reference to response head. + #[inline] + pub fn head_mut(&mut self) -> &mut ResponseHead { + self.res.head_mut() + } + + /// The source `error` for this response + #[inline] + pub fn error(&self) -> Option<&Error> { + self.error.as_ref() + } + + /// Get the response status code + #[inline] + pub fn status(&self) -> StatusCode { + self.res.status() + } + + /// Set the `StatusCode` for this response + #[inline] + pub fn status_mut(&mut self) -> &mut StatusCode { + self.res.status_mut() + } + + /// Get the headers from the response + #[inline] + pub fn headers(&self) -> &HeaderMap { + self.res.headers() + } + + /// Get a mutable reference to the headers + #[inline] + pub fn headers_mut(&mut self) -> &mut HeaderMap { + self.res.headers_mut() + } + + /// Get an iterator for the cookies set by this response. + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> CookieIter<'_> { + CookieIter { + iter: self.headers().get_all(header::SET_COOKIE), + } + } + + /// Add a cookie to this response + #[cfg(feature = "cookies")] + pub fn add_cookie(&mut self, cookie: &Cookie<'_>) -> Result<(), HttpError> { + HeaderValue::from_str(&cookie.to_string()) + .map(|c| { + self.headers_mut().append(header::SET_COOKIE, c); + }) + .map_err(|e| e.into()) + } + + /// Remove all cookies with the given name from this response. Returns + /// the number of cookies removed. + #[cfg(feature = "cookies")] + pub fn del_cookie(&mut self, name: &str) -> usize { + let headers = self.headers_mut(); + + let vals: Vec = headers + .get_all(header::SET_COOKIE) + .map(|v| v.to_owned()) + .collect(); + + headers.remove(header::SET_COOKIE); + + let mut count: usize = 0; + for v in vals { + if let Ok(s) = v.to_str() { + if let Ok(c) = Cookie::parse_encoded(s) { + if c.name() == name { + count += 1; + continue; + } + } + } + + // put set-cookie header head back if it does not validate + headers.append(header::SET_COOKIE, v); + } + + count + } + + /// Connection upgrade status + #[inline] + pub fn upgrade(&self) -> bool { + self.res.upgrade() + } + + /// Keep-alive status for this connection + pub fn keep_alive(&self) -> bool { + self.res.keep_alive() + } + + /// Responses extensions + #[inline] + pub fn extensions(&self) -> Ref<'_, Extensions> { + self.res.extensions() + } + + /// Mutable reference to a the response's extensions + #[inline] + pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> { + self.res.extensions_mut() + } + + /// Get body of this response + #[inline] + pub fn body(&self) -> &B { + self.res.body() + } + + /// Set a body + pub fn set_body(self, body: B2) -> HttpResponse { + HttpResponse { + res: self.res.set_body(body), + error: None, + // error: self.error, ?? + } + } + + /// Split response and body + pub fn into_parts(self) -> (HttpResponse<()>, B) { + let (head, body) = self.res.into_parts(); + + ( + HttpResponse { + res: head, + error: None, + }, + body, + ) + } + + /// Drop request's body + pub fn drop_body(self) -> HttpResponse<()> { + HttpResponse { + res: self.res.drop_body(), + error: None, + } + } + + /// Set a body and return previous body value + pub fn map_body(self, f: F) -> HttpResponse + where + F: FnOnce(&mut ResponseHead, B) -> B2, + { + HttpResponse { + res: self.res.map_body(f), + error: self.error, + } + } + + // TODO: docs for the body map methods below + + #[inline] + pub fn map_into_left_body(self) -> HttpResponse> { + self.map_body(|_, body| EitherBody::left(body)) + } + + #[inline] + pub fn map_into_right_body(self) -> HttpResponse> { + self.map_body(|_, body| EitherBody::right(body)) + } + + #[inline] + pub fn map_into_boxed_body(self) -> HttpResponse + where + B: MessageBody + 'static, + { + // TODO: avoid double boxing with down-casting, if it improves perf + self.map_body(|_, body| BoxBody::new(body)) + } + + /// Extract response body + pub fn into_body(self) -> B { + self.res.into_body() + } +} + +impl fmt::Debug for HttpResponse +where + B: MessageBody, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("HttpResponse") + .field("error", &self.error) + .field("res", &self.res) + .finish() + } +} + +impl From> for HttpResponse { + fn from(res: Response) -> Self { + HttpResponse { res, error: None } + } +} + +impl From for HttpResponse { + fn from(err: Error) -> Self { + HttpResponse::from_error(err) + } +} + +impl From> for Response { + fn from(res: HttpResponse) -> Self { + // this impl will always be called as part of dispatcher + + // TODO: expose cause somewhere? + // if let Some(err) = res.error { + // return Response::from_error(err); + // } + + res.res + } +} + +// Future is only implemented for BoxBody payload type because it's the most useful for making +// simple handlers without async blocks. Making it generic over all MessageBody types requires a +// future impl on Response which would cause it's body field to be, undesirably, Option. +// +// This impl is not particularly efficient due to the Response construction and should probably +// not be invoked if performance is important. Prefer an async fn/block in such cases. +impl Future for HttpResponse { + type Output = Result, Error>; + + fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll { + if let Some(err) = self.error.take() { + return Poll::Ready(Err(err)); + } + + Poll::Ready(Ok(mem::replace( + &mut self.res, + Response::new(StatusCode::default()), + ))) + } +} + +#[cfg(feature = "cookies")] +pub struct CookieIter<'a> { + iter: header::map::GetAll<'a>, +} + +#[cfg(feature = "cookies")] +impl<'a> Iterator for CookieIter<'a> { + type Item = Cookie<'a>; + + #[inline] + fn next(&mut self) -> Option> { + for v in self.iter.by_ref() { + if let Ok(c) = Cookie::parse_encoded(v.to_str().ok()?) { + return Some(c); + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::header::{HeaderValue, COOKIE}; + + #[test] + fn test_debug() { + let resp = HttpResponse::Ok() + .append_header((COOKIE, HeaderValue::from_static("cookie1=value1; "))) + .append_header((COOKIE, HeaderValue::from_static("cookie2=value2; "))) + .finish(); + let dbg = format!("{:?}", resp); + assert!(dbg.contains("HttpResponse")); + } +} diff --git a/src/rmap.rs b/src/rmap.rs index 3c8805d57..8466eda28 100644 --- a/src/rmap.rs +++ b/src/rmap.rs @@ -10,44 +10,75 @@ use crate::request::HttpRequest; #[derive(Clone, Debug)] pub struct ResourceMap { - root: ResourceDef, + pattern: ResourceDef, + + /// Named resources within the tree or, for external resources, + /// it points to isolated nodes outside the tree. + named: AHashMap>, + parent: RefCell>, - named: AHashMap, - patterns: Vec<(ResourceDef, Option>)>, + + /// Must be `None` for "edge" nodes. + nodes: Option>>, } impl ResourceMap { + /// Creates a _container_ node in the `ResourceMap` tree. pub fn new(root: ResourceDef) -> Self { ResourceMap { - root, - parent: RefCell::new(Weak::new()), + pattern: root, named: AHashMap::default(), - patterns: Vec::new(), + parent: RefCell::new(Weak::new()), + nodes: Some(Vec::new()), } } + /// Adds a (possibly nested) resource. + /// + /// To add a non-prefix pattern, `nested` must be `None`. + /// To add external resource, supply a pattern without a leading `/`. + /// The root pattern of `nested`, if present, should match `pattern`. pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option>) { - pattern.set_id(self.patterns.len() as u16); - self.patterns.push((pattern.clone(), nested)); - if !pattern.name().is_empty() { - self.named - .insert(pattern.name().to_string(), pattern.clone()); + pattern.set_id(self.nodes.as_ref().unwrap().len() as u16); + + if let Some(new_node) = nested { + assert_eq!(&new_node.pattern, pattern, "`patern` and `nested` mismatch"); + self.named.extend(new_node.named.clone().into_iter()); + self.nodes.as_mut().unwrap().push(new_node); + } else { + let new_node = Rc::new(ResourceMap { + pattern: pattern.clone(), + named: AHashMap::default(), + parent: RefCell::new(Weak::new()), + nodes: None, + }); + + if let Some(name) = pattern.name() { + self.named.insert(name.to_owned(), Rc::clone(&new_node)); + } + + let is_external = match pattern.pattern() { + Some(p) => !p.is_empty() && !p.starts_with('/'), + None => false, + }; + + // Don't add external resources to the tree + if !is_external { + self.nodes.as_mut().unwrap().push(new_node); + } } } - pub(crate) fn finish(&self, current: Rc) { - for (_, nested) in &self.patterns { - if let Some(ref nested) = nested { - *nested.parent.borrow_mut() = Rc::downgrade(¤t); - nested.finish(nested.clone()); - } + pub(crate) fn finish(self: &Rc) { + for node in self.nodes.iter().flatten() { + node.parent.replace(Rc::downgrade(self)); + ResourceMap::finish(node); } } /// Generate url for named resource /// - /// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. - /// url_for) for detailed information. + /// Check [`HttpRequest::url_for`] for detailed information. pub fn url_for( &self, req: &HttpRequest, @@ -58,192 +89,97 @@ impl ResourceMap { U: IntoIterator, I: AsRef, { - let mut path = String::new(); let mut elements = elements.into_iter(); - if self.patterns_for(name, &mut path, &mut elements)?.is_some() { - if path.starts_with('/') { - let conn = req.connection_info(); - Ok(Url::parse(&format!( - "{}://{}{}", - conn.scheme(), - conn.host(), - path - ))?) - } else { - Ok(Url::parse(&path)?) - } + let path = self + .named + .get(name) + .ok_or(UrlGenerationError::ResourceNotFound)? + .root_rmap_fn(String::with_capacity(24), |mut acc, node| { + node.pattern + .resource_path_from_iter(&mut acc, &mut elements) + .then(|| acc) + }) + .ok_or(UrlGenerationError::NotEnoughElements)?; + + if path.starts_with('/') { + let conn = req.connection_info(); + Ok(Url::parse(&format!( + "{}://{}{}", + conn.scheme(), + conn.host(), + path + ))?) } else { - Err(UrlGenerationError::ResourceNotFound) + Ok(Url::parse(&path)?) } } pub fn has_resource(&self, path: &str) -> bool { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.has_resource(&path[plen..]); - } - } else if pattern.is_match(path) || pattern.pattern() == "" && path == "/" { - return true; - } - } - false + self.find_matching_node(path).is_some() } /// Returns the name of the route that matches the given path or None if no full match - /// is possible. + /// is possible or the matching resource is not named. pub fn match_name(&self, path: &str) -> Option<&str> { - let path = if path.is_empty() { "/" } else { path }; - - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(plen) = pattern.is_prefix_match(path) { - return rmap.match_name(&path[plen..]); - } - } else if pattern.is_match(path) { - return match pattern.name() { - "" => None, - s => Some(s), - }; - } - } - - None + self.find_matching_node(path)?.pattern.name() } /// Returns the full resource pattern matched against a path or None if no full match /// is possible. pub fn match_pattern(&self, path: &str) -> Option { - let path = if path.is_empty() { "/" } else { path }; - - // ensure a full match exists - if !self.has_resource(path) { - return None; - } - - Some(self.traverse_resource_pattern(path)) + self.find_matching_node(path)?.root_rmap_fn( + String::with_capacity(24), + |mut acc, node| { + acc.push_str(node.pattern.pattern()?); + Some(acc) + }, + ) } - /// Takes remaining path and tries to match it up against a resource definition within the - /// current resource map recursively, returning a concatenation of all resource prefixes and - /// patterns matched in the tree. - /// - /// Should only be used after checking the resource exists in the map so that partial match - /// patterns are not returned. - fn traverse_resource_pattern(&self, remaining: &str) -> String { - for (pattern, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if let Some(prefix_len) = pattern.is_prefix_match(remaining) { - let prefix = pattern.pattern().to_owned(); - - return [ - prefix, - rmap.traverse_resource_pattern(&remaining[prefix_len..]), - ] - .concat(); - } - } else if pattern.is_match(remaining) { - return pattern.pattern().to_owned(); - } - } - - String::new() + fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> { + self._find_matching_node(path).flatten() } - fn patterns_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> + /// Returns `None` if root pattern doesn't match; + /// `Some(None)` if root pattern matches but there is no matching child pattern. + /// Don't search sideways when `Some(none)` is returned. + fn _find_matching_node(&self, path: &str) -> Option> { + let matched_len = self.pattern.find_match(path)?; + let path = &path[matched_len..]; + + Some(match &self.nodes { + // find first sub-node to match remaining path + Some(nodes) => nodes + .iter() + .filter_map(|node| node._find_matching_node(path)) + .next() + .flatten(), + + // only terminate at edge nodes + None => Some(self), + }) + } + + /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context. + fn root_rmap_fn(&self, init: B, mut f: F) -> Option where - U: Iterator, - I: AsRef, + F: FnMut(B, &ResourceMap) -> Option, { - if self.pattern_for(name, path, elements)?.is_some() { - Ok(Some(())) - } else { - self.parent_pattern_for(name, path, elements) - } + self._root_rmap_fn(init, &mut f) } - fn pattern_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> + /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map. + fn _root_rmap_fn(&self, init: B, f: &mut F) -> Option where - U: Iterator, - I: AsRef, + F: FnMut(B, &ResourceMap) -> Option, { - if let Some(pattern) = self.named.get(name) { - if pattern.pattern().starts_with('/') { - self.fill_root(path, elements)?; - } - if pattern.resource_path(path, elements) { - Ok(Some(())) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } else { - for (_, rmap) in &self.patterns { - if let Some(ref rmap) = rmap { - if rmap.pattern_for(name, path, elements)?.is_some() { - return Ok(Some(())); - } - } - } - Ok(None) - } - } + let data = match self.parent.borrow().upgrade() { + Some(ref parent) => parent._root_rmap_fn(init, f)?, + None => init, + }; - fn fill_root( - &self, - path: &mut String, - elements: &mut U, - ) -> Result<(), UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = self.parent.borrow().upgrade() { - parent.fill_root(path, elements)?; - } - if self.root.resource_path(path, elements) { - Ok(()) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } - - fn parent_pattern_for( - &self, - name: &str, - path: &mut String, - elements: &mut U, - ) -> Result, UrlGenerationError> - where - U: Iterator, - I: AsRef, - { - if let Some(ref parent) = self.parent.borrow().upgrade() { - if let Some(pattern) = parent.named.get(name) { - self.fill_root(path, elements)?; - if pattern.resource_path(path, elements) { - Ok(Some(())) - } else { - Err(UrlGenerationError::NotEnoughElements) - } - } else { - parent.parent_pattern_for(name, path, elements) - } - } else { - Ok(None) - } + f(data, self) } } @@ -255,7 +191,7 @@ mod tests { fn extract_matched_pattern() { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); @@ -271,9 +207,10 @@ mod tests { &mut ResourceDef::root_prefix("/user/{id}"), Some(Rc::new(user_map)), ); + root.add(&mut ResourceDef::new("/info"), None); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // sanity check resource map setup @@ -284,7 +221,7 @@ mod tests { assert!(root.has_resource("/v2")); assert!(!root.has_resource("/v33")); - assert!(root.has_resource("/user/22")); + assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/profile")); @@ -329,15 +266,15 @@ mod tests { let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut rdef = ResourceDef::new("/info"); - *rdef.name_mut() = "root_info".to_owned(); + rdef.set_name("root_info"); root.add(&mut rdef, None); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); let mut rdef = ResourceDef::new("/"); user_map.add(&mut rdef, None); let mut rdef = ResourceDef::new("/post/{post_id}"); - *rdef.name_mut() = "user_post".to_owned(); + rdef.set_name("user_post"); user_map.add(&mut rdef, None); root.add( @@ -346,14 +283,14 @@ mod tests { ); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // sanity check resource map setup assert!(root.has_resource("/info")); assert!(!root.has_resource("/bar")); - assert!(root.has_resource("/user/22")); + assert!(!root.has_resource("/user/22")); assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/post/55")); @@ -373,7 +310,7 @@ mod tests { // ref: https://github.com/actix/actix-web/issues/1582 let mut root = ResourceMap::new(ResourceDef::root_prefix("")); - let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); + let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}")); user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None); @@ -389,20 +326,119 @@ mod tests { ); let root = Rc::new(root); - root.finish(Rc::clone(&root)); + ResourceMap::finish(&root); // check root has no parent assert!(root.parent.borrow().upgrade().is_none()); // check child has parent reference - assert!(root.patterns[0].1.is_some()); + assert!(root.nodes.as_ref().unwrap()[0] + .parent + .borrow() + .upgrade() + .is_some()); // check child's parent root id matches root's root id - assert_eq!( - root.patterns[0].1.as_ref().unwrap().root.id(), - root.root.id() - ); + assert!(Rc::ptr_eq( + &root.nodes.as_ref().unwrap()[0] + .parent + .borrow() + .upgrade() + .unwrap(), + &root + )); let output = format!("{:?}", root); assert!(output.starts_with("ResourceMap {")); assert!(output.ends_with(" }")); } + + #[test] + fn short_circuit() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut user_root = ResourceDef::prefix("/user"); + let mut user_map = ResourceMap::new(user_root.clone()); + user_map.add(&mut ResourceDef::new("/u1"), None); + user_map.add(&mut ResourceDef::new("/u2"), None); + + root.add(&mut ResourceDef::new("/user/u3"), None); + root.add(&mut user_root, Some(Rc::new(user_map))); + root.add(&mut ResourceDef::new("/user/u4"), None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(rmap.has_resource("/user/u1")); + assert!(rmap.has_resource("/user/u2")); + assert!(rmap.has_resource("/user/u3")); + assert!(!rmap.has_resource("/user/u4")); + } + + #[test] + fn url_for() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut user_scope_rdef = ResourceDef::prefix("/user"); + let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone()); + + let mut user_rdef = ResourceDef::new("/{user_id}"); + let mut user_map = ResourceMap::new(user_rdef.clone()); + + let mut post_rdef = ResourceDef::new("/post/{sub_id}"); + post_rdef.set_name("post"); + + user_map.add(&mut post_rdef, None); + user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map))); + root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map))); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + let url = rmap + .url_for(&req, "post", &["u123", "foobar"]) + .unwrap() + .to_string(); + assert_eq!(url, "http://localhost:8888/user/u123/post/foobar"); + + assert!(rmap.url_for(&req, "missing", &["u123"]).is_err()); + } + + #[test] + fn external_resource_with_no_name() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef = ResourceDef::new("https://duck.com/{query}"); + root.add(&mut rdef, None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(!rmap.has_resource("https://duck.com/abc")); + } + + #[test] + fn external_resource_with_name() { + let mut root = ResourceMap::new(ResourceDef::prefix("")); + + let mut rdef = ResourceDef::new("https://duck.com/{query}"); + rdef.set_name("duck"); + root.add(&mut rdef, None); + + let rmap = Rc::new(root); + ResourceMap::finish(&rmap); + + assert!(!rmap.has_resource("https://duck.com/abc")); + + let mut req = crate::test::TestRequest::default(); + req.set_server_hostname("localhost:8888"); + let req = req.to_http_request(); + + assert_eq!( + rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(), + "https://duck.com/abcd" + ); + } } diff --git a/src/route.rs b/src/route.rs index 8a3d1da9f..1eb323068 100644 --- a/src/route.rs +++ b/src/route.rs @@ -1,48 +1,26 @@ -#![allow(clippy::rc_buffer)] // inner value is mutated before being shared (`Rc::get_mut`) +use std::{future::Future, mem, rc::Rc}; -use std::future::Future; -use std::pin::Pin; -use std::rc::Rc; -use std::task::{Context, Poll}; +use actix_http::http::Method; +use actix_service::{ + boxed::{self, BoxService}, + fn_service, Service, ServiceFactory, ServiceFactoryExt, +}; +use futures_core::future::LocalBoxFuture; -use actix_http::{http::Method, Error}; -use actix_service::{Service, ServiceFactory}; -use futures_util::future::{ready, FutureExt, LocalBoxFuture}; - -use crate::extract::FromRequest; -use crate::guard::{self, Guard}; -use crate::handler::{Handler, HandlerService}; -use crate::responder::Responder; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::HttpResponse; - -type BoxedRouteService = Box< - dyn Service< - ServiceRequest, - Response = ServiceResponse, - Error = Error, - Future = LocalBoxFuture<'static, Result>, - >, ->; - -type BoxedRouteNewService = Box< - dyn ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - InitError = (), - Service = BoxedRouteService, - Future = LocalBoxFuture<'static, Result>, - >, ->; +use crate::{ + body::MessageBody, + guard::{self, Guard}, + handler::{handler_service, Handler}, + service::{BoxedHttpServiceFactory, ServiceRequest, ServiceResponse}, + BoxError, Error, FromRequest, HttpResponse, Responder, +}; /// Resource route definition /// /// Route uses builder-like pattern for configuration. /// If handler is not explicitly set, default *404 Not Found* handler is used. pub struct Route { - service: BoxedRouteNewService, + service: BoxedHttpServiceFactory, guards: Rc>>, } @@ -51,57 +29,39 @@ impl Route { #[allow(clippy::new_without_default)] pub fn new() -> Route { Route { - service: Box::new(RouteNewService::new(HandlerService::new(|| { - ready(HttpResponse::NotFound()) - }))), + service: boxed::factory(fn_service(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::NotFound())) + })), guards: Rc::new(Vec::new()), } } pub(crate) fn take_guards(&mut self) -> Vec> { - std::mem::take(Rc::get_mut(&mut self.guards).unwrap()) + mem::take(Rc::get_mut(&mut self.guards).unwrap()) } } impl ServiceFactory for Route { - type Config = (); type Response = ServiceResponse; type Error = Error; - type InitError = (); + type Config = (); type Service = RouteService; - type Future = CreateRouteService; + type InitError = (); + type Future = LocalBoxFuture<'static, Result>; fn new_service(&self, _: ()) -> Self::Future { - CreateRouteService { - fut: self.service.new_service(()), - guards: self.guards.clone(), - } - } -} + let fut = self.service.new_service(()); + let guards = self.guards.clone(); -pub struct CreateRouteService { - fut: LocalBoxFuture<'static, Result>, - guards: Rc>>, -} - -impl Future for CreateRouteService { - type Output = Result; - - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.get_mut(); - - match this.fut.as_mut().poll(cx)? { - Poll::Ready(service) => Poll::Ready(Ok(RouteService { - service, - guards: this.guards.clone(), - })), - Poll::Pending => Poll::Pending, - } + Box::pin(async move { + let service = fut.await?; + Ok(RouteService { service, guards }) + }) } } pub struct RouteService { - service: BoxedRouteService, + service: BoxService, guards: Rc>>, } @@ -121,11 +81,9 @@ impl Service for RouteService { type Error = Error; type Future = LocalBoxFuture<'static, Result>; - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } + actix_service::forward_ready!(service); - fn call(&mut self, req: ServiceRequest) -> Self::Future { + fn call(&self, req: ServiceRequest) -> Self::Future { self.service.call(req) } } @@ -133,7 +91,7 @@ impl Service for RouteService { impl Route { /// Add method guard to the route. /// - /// ```rust + /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( @@ -153,7 +111,7 @@ impl Route { /// Add guard to the route. /// - /// ```rust + /// ``` /// # use actix_web::*; /// # fn main() { /// App::new().service(web::resource("/path").route( @@ -171,9 +129,10 @@ impl Route { /// Set handler function, use request extractors for parameters. /// - /// ```rust + /// # Examples + /// ``` /// use actix_web::{web, http, App}; - /// use serde_derive::Deserialize; + /// use serde::Deserialize; /// /// #[derive(Deserialize)] /// struct Info { @@ -195,9 +154,9 @@ impl Route { /// /// It is possible to use multiple extractors for one handler function. /// - /// ```rust + /// ``` /// # use std::collections::HashMap; - /// # use serde_derive::Deserialize; + /// # use serde::Deserialize; /// use actix_web::{web, App}; /// /// #[derive(Deserialize)] @@ -223,100 +182,73 @@ impl Route { T: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody, + <::Body as MessageBody>::Error: Into, { - self.service = Box::new(RouteNewService::new(HandlerService::new(handler))); + self.service = handler_service(handler); self } -} -struct RouteNewService -where - T: ServiceFactory, -{ - service: T, -} - -impl RouteNewService -where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - >, - T::Future: 'static, - T::Service: 'static, - >::Future: 'static, -{ - pub fn new(service: T) -> Self { - RouteNewService { service } - } -} - -impl ServiceFactory for RouteNewService -where - T: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - >, - T::Future: 'static, - T::Service: 'static, - >::Future: 'static, -{ - type Response = ServiceResponse; - type Error = Error; - type Config = (); - type Service = BoxedRouteService; - type InitError = (); - type Future = LocalBoxFuture<'static, Result>; - - fn new_service(&self, _: ()) -> Self::Future { - self.service - .new_service(()) - .map(|result| match result { - Ok(service) => { - let service = Box::new(RouteServiceWrapper { service }) as _; - Ok(service) - } - Err(_) => Err(()), - }) - .boxed_local() - } -} - -struct RouteServiceWrapper> { - service: T, -} - -impl Service for RouteServiceWrapper -where - T::Future: 'static, - T: Service, -{ - type Response = ServiceResponse; - type Error = Error; - type Future = LocalBoxFuture<'static, Result>; - - fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { - self.service.poll_ready(cx) - } - - fn call(&mut self, req: ServiceRequest) -> Self::Future { - Box::pin(self.service.call(req)) + /// Set raw service to be constructed and called as the request handler. + /// + /// # Examples + /// ``` + /// # use std::convert::Infallible; + /// # use futures_util::future::LocalBoxFuture; + /// # use actix_web::{*, dev::*, http::header}; + /// struct HelloWorld; + /// + /// impl Service for HelloWorld { + /// type Response = ServiceResponse; + /// type Error = Infallible; + /// type Future = LocalBoxFuture<'static, Result>; + /// + /// always_ready!(); + /// + /// fn call(&self, req: ServiceRequest) -> Self::Future { + /// let (req, _) = req.into_parts(); + /// + /// let res = HttpResponse::Ok() + /// .insert_header(header::ContentType::plaintext()) + /// .body("Hello world!"); + /// + /// Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) + /// } + /// } + /// + /// App::new().route( + /// "/", + /// web::get().service(fn_factory(|| async { Ok(HelloWorld) })), + /// ); + /// ``` + pub fn service(mut self, service_factory: S) -> Self + where + S: ServiceFactory< + ServiceRequest, + Response = ServiceResponse, + Error = E, + InitError = (), + Config = (), + > + 'static, + E: Into + 'static, + { + self.service = boxed::factory(service_factory.map_err(Into::into)); + self } } #[cfg(test)] mod tests { - use std::time::Duration; + use std::{convert::Infallible, time::Duration}; use actix_rt::time::sleep; use bytes::Bytes; - use serde_derive::Serialize; + use futures_core::future::LocalBoxFuture; + use serde::Serialize; - use crate::http::{Method, StatusCode}; + use crate::dev::{always_ready, fn_factory, fn_service, Service}; + use crate::http::{header, Method, StatusCode}; + use crate::service::{ServiceRequest, ServiceResponse}; use crate::test::{call_service, init_service, read_body, TestRequest}; use crate::{error, web, App, HttpResponse}; @@ -327,7 +259,7 @@ mod tests { #[actix_rt::test] async fn test_route() { - let mut srv = init_service( + let srv = init_service( App::new() .service( web::resource("/test") @@ -337,7 +269,7 @@ mod tests { })) .route(web::post().to(|| async { sleep(Duration::from_millis(100)).await; - Ok::<_, ()>(HttpResponse::Created()) + Ok::<_, Infallible>(HttpResponse::Created()) })) .route(web::delete().to(|| async { sleep(Duration::from_millis(100)).await; @@ -356,38 +288,99 @@ mod tests { let req = TestRequest::with_uri("/test") .method(Method::GET) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/test") .method(Method::POST) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::CREATED); let req = TestRequest::with_uri("/test") .method(Method::PUT) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/test") .method(Method::DELETE) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/test") .method(Method::HEAD) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); let req = TestRequest::with_uri("/json").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!(body, Bytes::from_static(b"{\"name\":\"test\"}")); } + + #[actix_rt::test] + async fn test_service_handler() { + struct HelloWorld; + + impl Service for HelloWorld { + type Response = ServiceResponse; + type Error = crate::Error; + type Future = LocalBoxFuture<'static, Result>; + + always_ready!(); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let (req, _) = req.into_parts(); + + let res = HttpResponse::Ok() + .insert_header(header::ContentType::plaintext()) + .body("Hello world!"); + + Box::pin(async move { Ok(ServiceResponse::new(req, res)) }) + } + } + + let srv = init_service( + App::new() + .route( + "/hello", + web::get().service(fn_factory(|| async { Ok(HelloWorld) })), + ) + .route( + "/bye", + web::get().service(fn_factory(|| async { + Ok::<_, ()>(fn_service(|req: ServiceRequest| async { + let (req, _) = req.into_parts(); + + let res = HttpResponse::Ok() + .insert_header(header::ContentType::plaintext()) + .body("Goodbye, and thanks for all the fish!"); + + Ok::<_, Infallible>(ServiceResponse::new(req, res)) + })) + })), + ), + ) + .await; + + let req = TestRequest::get().uri("/hello").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp).await; + assert_eq!(body, Bytes::from_static(b"Hello world!")); + + let req = TestRequest::get().uri("/bye").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + let body = read_body(resp).await; + assert_eq!( + body, + Bytes::from_static(b"Goodbye, and thanks for all the fish!") + ); + } } diff --git a/src/scope.rs b/src/scope.rs index 290a35eb1..ff013671b 100644 --- a/src/scope.rs +++ b/src/scope.rs @@ -1,34 +1,28 @@ -use std::cell::RefCell; -use std::fmt; -use std::future::Future; -use std::rc::Rc; -use std::task::Poll; +use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; use actix_http::Extensions; use actix_router::{ResourceDef, Router}; -use actix_service::boxed::{self, BoxService, BoxServiceFactory}; use actix_service::{ - apply, apply_fn_factory, IntoServiceFactory, Service, ServiceFactory, + apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; -use crate::config::ServiceConfig; -use crate::data::Data; -use crate::dev::{AppService, HttpServiceFactory}; -use crate::error::Error; -use crate::guard::Guard; -use crate::resource::Resource; -use crate::rmap::ResourceMap; -use crate::route::Route; -use crate::service::{ - AppServiceFactory, ServiceFactoryWrapper, ServiceRequest, ServiceResponse, +use crate::{ + config::ServiceConfig, + data::Data, + dev::AppService, + guard::Guard, + rmap::ResourceMap, + service::{ + AppServiceFactory, BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, + ServiceFactoryWrapper, ServiceRequest, ServiceResponse, + }, + Error, Resource, Route, }; type Guards = Vec>; -type HttpService = BoxService; -type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; /// Resources scope. /// @@ -41,15 +35,15 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err /// You can get variable path segments from `HttpRequest::match_info()`. /// `Path` extractor also is able to extract scope level variable segments. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// fn main() { /// let app = App::new().service( /// web::scope("/{project_id}/") -/// .service(web::resource("/path1").to(|| async { HttpResponse::Ok() })) +/// .service(web::resource("/path1").to(|| async { "OK" })) /// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) -/// .service(web::resource("/path3").route(web::head().to(|| HttpResponse::MethodNotAllowed()))) +/// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed))) /// ); /// } /// ``` @@ -64,7 +58,7 @@ pub struct Scope { app_data: Option, services: Vec>, guards: Vec>, - default: Option>, + default: Option>, external: Vec, factory_ref: Rc>>, } @@ -72,16 +66,17 @@ pub struct Scope { impl Scope { /// Create a new scope pub fn new(path: &str) -> Scope { - let fref = Rc::new(RefCell::new(None)); + let factory_ref = Rc::new(RefCell::new(None)); + Scope { - endpoint: ScopeEndpoint::new(fref.clone()), + endpoint: ScopeEndpoint::new(Rc::clone(&factory_ref)), rdef: path.to_string(), app_data: None, guards: Vec::new(), services: Vec::new(), default: None, external: Vec::new(), - factory_ref: fref, + factory_ref, } } } @@ -98,7 +93,7 @@ where { /// Add match guard to a scope. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, App, HttpRequest, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -121,39 +116,38 @@ where self } - /// Set or override application data. Application data could be accessed - /// by using `Data` extractor where `T` is data type. - /// - /// ```rust - /// use std::cell::Cell; - /// use actix_web::{web, App, HttpResponse, Responder}; - /// - /// struct MyData { - /// counter: Cell, - /// } - /// - /// async fn index(data: web::Data) -> impl Responder { - /// data.counter.set(data.counter.get() + 1); - /// HttpResponse::Ok() - /// } - /// - /// fn main() { - /// let app = App::new().service( - /// web::scope("/app") - /// .data(MyData{ counter: Cell::new(0) }) - /// .service( - /// web::resource("/index.html").route( - /// web::get().to(index))) - /// ); - /// } - /// ``` - pub fn data(self, data: U) -> Self { - self.app_data(Data::new(data)) - } - /// Add scope data. /// - /// Data of different types from parent contexts will still be accessible. + /// Data of different types from parent contexts will still be accessible. Any `Data` types + /// set here can be extracted in handlers using the `Data` extractor. + /// + /// # Examples + /// ``` + /// use std::cell::Cell; + /// use actix_web::{web, App, HttpRequest, HttpResponse, Responder}; + /// + /// struct MyData { + /// count: std::cell::Cell, + /// } + /// + /// async fn handler(req: HttpRequest, counter: web::Data) -> impl Responder { + /// // note this cannot use the Data extractor because it was not added with it + /// let incr = *req.app_data::().unwrap(); + /// assert_eq!(incr, 3); + /// + /// // update counter using other value from app data + /// counter.count.set(counter.count.get() + incr); + /// + /// HttpResponse::Ok().body(counter.count.get().to_string()) + /// } + /// + /// let app = App::new().service( + /// web::scope("/app") + /// .app_data(3usize) + /// .app_data(web::Data::new(MyData { count: Default::default() })) + /// .route("/", web::get().to(handler)) + /// ); + /// ``` pub fn app_data(mut self, data: U) -> Self { self.app_data .get_or_insert_with(Extensions::new) @@ -162,15 +156,20 @@ where self } - /// Run external configuration as part of the scope building - /// process + /// Add scope data after wrapping in `Data`. /// - /// This function is useful for moving parts of configuration to a - /// different module or even library. For example, - /// some of the resource's configuration could be moved to different module. + /// Deprecated in favor of [`app_data`](Self::app_data). + #[deprecated(since = "4.0.0", note = "Use `.app_data(Data::new(val))` instead.")] + pub fn data(self, data: U) -> Self { + self.app_data(Data::new(data)) + } + + /// Run external configuration as part of the scope building process. /// - /// ```rust - /// # extern crate actix_web; + /// This function is useful for moving parts of configuration to a different module or library. + /// For example, some of the resource's configuration could be moved to different module. + /// + /// ``` /// use actix_web::{web, middleware, App, HttpResponse}; /// /// // this function could be located in different module @@ -191,32 +190,35 @@ where /// .route("/index.html", web::get().to(|| HttpResponse::Ok())); /// } /// ``` - pub fn configure(mut self, f: F) -> Self + pub fn configure(mut self, cfg_fn: F) -> Self where F: FnOnce(&mut ServiceConfig), { let mut cfg = ServiceConfig::new(); - f(&mut cfg); + cfg_fn(&mut cfg); + self.services.extend(cfg.services); self.external.extend(cfg.external); + // TODO: add Extensions::is_empty check and conditionally insert data self.app_data .get_or_insert_with(Extensions::new) .extend(cfg.app_data); + self } - /// Register http service. + /// Register HTTP service. /// /// This is similar to `App's` service registration. /// - /// Actix web provides several services implementations: + /// Actix Web provides several services implementations: /// /// * *Resource* is an entry in resource table which corresponds to requested URL. /// * *Scope* is a set of resources with common root path. /// * "StaticFiles" is a service for static files support /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpRequest}; /// /// struct AppState; @@ -248,7 +250,7 @@ where /// This method can be called multiple times, in that case /// multiple resources with one route would be registered for same resource path. /// - /// ```rust + /// ``` /// use actix_web::{web, App, HttpResponse}; /// /// async fn index(data: web::Path<(String, String)>) -> &'static str { @@ -342,7 +344,7 @@ where /// to Route or Application level middleware, in that Scope-level middleware /// can not modify ServiceResponse. /// - /// ```rust + /// ``` /// use actix_service::Service; /// use actix_web::{web, App}; /// use actix_web::http::{header::CONTENT_TYPE, HeaderValue}; @@ -380,7 +382,7 @@ where >, > where - F: FnMut(ServiceRequest, &mut T::Service) -> R + Clone, + F: Fn(ServiceRequest, &T::Service) -> R + Clone, R: Future>, { Scope { @@ -419,13 +421,12 @@ where let mut rmap = ResourceMap::new(ResourceDef::root_prefix(&self.rdef)); // external resources - for mut rdef in std::mem::take(&mut self.external) { + for mut rdef in mem::take(&mut self.external) { rmap.add(&mut rdef, None); } // complete scope pipeline creation *self.factory_ref.borrow_mut() = Some(ScopeFactory { - app_data: self.app_data.take().map(Rc::new), default, services: cfg .into_services() @@ -447,20 +448,36 @@ where Some(self.guards) }; + let scope_data = self.app_data.map(Rc::new); + + // wraps endpoint service (including middleware) call and injects app data for this scope + let endpoint = apply_fn_factory(self.endpoint, move |mut req: ServiceRequest, srv| { + if let Some(ref data) = scope_data { + req.add_data_container(Rc::clone(data)); + } + + srv.call(req) + }); + // register final service config.register_service( ResourceDef::root_prefix(&self.rdef), guards, - self.endpoint, + endpoint, Some(Rc::new(rmap)), ) } } pub struct ScopeFactory { - app_data: Option>, - services: Rc<[(ResourceDef, HttpNewService, RefCell>)]>, - default: Rc, + services: Rc< + [( + ResourceDef, + BoxedHttpServiceFactory, + RefCell>, + )], + >, + default: Rc, } impl ServiceFactory for ScopeFactory { @@ -476,18 +493,15 @@ impl ServiceFactory for ScopeFactory { let default_fut = self.default.new_service(()); // construct all services factory future with it's resource def and guards. - let factory_fut = - join_all(self.services.iter().map(|(path, factory, guards)| { - let path = path.clone(); - let guards = guards.borrow_mut().take(); - let factory_fut = factory.new_service(()); - async move { - let service = factory_fut.await?; - Ok((path, guards, service)) - } - })); - - let app_data = self.app_data.clone(); + let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| { + let path = path.clone(); + let guards = guards.borrow_mut().take(); + let factory_fut = factory.new_service(()); + async move { + let service = factory_fut.await?; + Ok((path, guards, service)) + } + })); Box::pin(async move { let default = default_fut.await?; @@ -504,19 +518,14 @@ impl ServiceFactory for ScopeFactory { }) .finish(); - Ok(ScopeService { - app_data, - router, - default, - }) + Ok(ScopeService { router, default }) }) } } pub struct ScopeService { - app_data: Option>, - router: Router>>, - default: HttpService, + router: Router>>, + default: BoxedHttpService, } impl Service for ScopeService { @@ -526,8 +535,8 @@ impl Service for ScopeService { actix_service::always_ready!(); - fn call(&mut self, mut req: ServiceRequest) -> Self::Future { - let res = self.router.recognize_mut_checked(&mut req, |req, guards| { + fn call(&self, mut req: ServiceRequest) -> Self::Future { + let res = self.router.recognize_fn(&mut req, |req, guards| { if let Some(ref guards) = guards { for f in guards { if !f.check(req.head()) { @@ -538,10 +547,6 @@ impl Service for ScopeService { true }); - if let Some(ref app_data) = self.app_data { - req.add_data_container(app_data.clone()); - } - if let Some((srv, _info)) = res { srv.call(req) } else { @@ -577,22 +582,25 @@ impl ServiceFactory for ScopeEndpoint { #[cfg(test)] mod tests { use actix_service::Service; + use actix_utils::future::ok; use bytes::Bytes; - use futures_util::future::ok; - use crate::dev::{Body, ResponseBody}; - use crate::http::{header, HeaderValue, Method, StatusCode}; - use crate::middleware::DefaultHeaders; - use crate::service::ServiceRequest; - use crate::test::{call_service, init_service, read_body, TestRequest}; - use crate::{guard, web, App, HttpRequest, HttpResponse}; + use crate::{ + guard, + http::{header, HeaderValue, Method, StatusCode}, + middleware::DefaultHeaders, + service::{ServiceRequest, ServiceResponse}, + test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, + web, App, HttpMessage, HttpRequest, HttpResponse, + }; #[actix_rt::test] async fn test_scope() { - let mut srv = init_service(App::new().service( - web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), - )) - .await; + let srv = + init_service(App::new().service( + web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), + )) + .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); @@ -601,7 +609,7 @@ mod tests { #[actix_rt::test] async fn test_scope_root() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("/app") .service(web::resource("").to(HttpResponse::Ok)) @@ -621,9 +629,10 @@ mod tests { #[actix_rt::test] async fn test_scope_root2() { - let mut srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("").to(HttpResponse::Ok)), - )) + let srv = init_service( + App::new() + .service(web::scope("/app/").service(web::resource("").to(HttpResponse::Ok))), + ) .await; let req = TestRequest::with_uri("/app").to_request(); @@ -637,9 +646,10 @@ mod tests { #[actix_rt::test] async fn test_scope_root3() { - let mut srv = init_service(App::new().service( - web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok)), - )) + let srv = init_service( + App::new() + .service(web::scope("/app/").service(web::resource("/").to(HttpResponse::Ok))), + ) .await; let req = TestRequest::with_uri("/app").to_request(); @@ -653,7 +663,7 @@ mod tests { #[actix_rt::test] async fn test_scope_route() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("app") .route("/path1", web::get().to(HttpResponse::Ok)) @@ -681,7 +691,7 @@ mod tests { #[actix_rt::test] async fn test_scope_route_without_leading_slash() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("app").service( web::resource("path1") @@ -711,7 +721,7 @@ mod tests { #[actix_rt::test] async fn test_scope_guard() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("/app") .guard(guard::Get()) @@ -735,35 +745,26 @@ mod tests { #[actix_rt::test] async fn test_scope_variable_segment() { - let mut srv = - init_service(App::new().service(web::scope("/ab-{project}").service( - web::resource("/path1").to(|r: HttpRequest| { - HttpResponse::Ok() - .body(format!("project: {}", &r.match_info()["project"])) - }), - ))) - .await; + let srv = init_service(App::new().service(web::scope("/ab-{project}").service( + web::resource("/path1").to(|r: HttpRequest| { + HttpResponse::Ok().body(format!("project: {}", &r.match_info()["project"])) + }), + ))) + .await; let req = TestRequest::with_uri("/ab-project1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::OK); - - match resp.response().body() { - ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: project1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_body_eq!(res, b"project: project1"); let req = TestRequest::with_uri("/aa-project1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_nested_scope() { - let mut srv = init_service(App::new().service(web::scope("/app").service( + let srv = init_service(App::new().service(web::scope("/app").service( web::scope("/t1").service(web::resource("/path1").to(HttpResponse::Created)), ))) .await; @@ -775,7 +776,7 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_no_slash() { - let mut srv = init_service(App::new().service(web::scope("/app").service( + let srv = init_service(App::new().service(web::scope("/app").service( web::scope("t1").service(web::resource("/path1").to(HttpResponse::Created)), ))) .await; @@ -787,7 +788,7 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_root() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("/app").service( web::scope("/t1") @@ -809,7 +810,7 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_filter() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("/app").service( web::scope("/t1") @@ -835,7 +836,7 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_with_variable_segment() { - let mut srv = init_service(App::new().service(web::scope("/app").service( + let srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project_id}").service(web::resource("/path1").to( |r: HttpRequest| { HttpResponse::Created() @@ -846,21 +847,14 @@ mod tests { .await; let req = TestRequest::with_uri("/app/project_1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - - match resp.response().body() { - ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: project_1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::CREATED); + assert_body_eq!(res, b"project: project_1"); } #[actix_rt::test] async fn test_nested2_scope_with_variable_segment() { - let mut srv = init_service(App::new().service(web::scope("/app").service( + let srv = init_service(App::new().service(web::scope("/app").service( web::scope("/{project}").service(web::scope("/{id}").service( web::resource("/path1").to(|r: HttpRequest| { HttpResponse::Created().body(format!( @@ -874,25 +868,18 @@ mod tests { .await; let req = TestRequest::with_uri("/app/test/1/path1").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::CREATED); - - match resp.response().body() { - ResponseBody::Body(Body::Bytes(ref b)) => { - let bytes = b.clone(); - assert_eq!(bytes, Bytes::from_static(b"project: test - 1")); - } - _ => panic!(), - } + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::CREATED); + assert_body_eq!(res, b"project: test - 1"); let req = TestRequest::with_uri("/app/test/1/path2").to_request(); - let resp = srv.call(req).await.unwrap(); - assert_eq!(resp.status(), StatusCode::NOT_FOUND); + let res = srv.call(req).await.unwrap(); + assert_eq!(res.status(), StatusCode::NOT_FOUND); } #[actix_rt::test] async fn test_default_resource() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("/app") .service(web::resource("/path1").to(HttpResponse::Ok)) @@ -914,12 +901,9 @@ mod tests { #[actix_rt::test] async fn test_default_resource_propagation() { - let mut srv = init_service( + let srv = init_service( App::new() - .service( - web::scope("/app1") - .default_service(web::resource("").to(HttpResponse::BadRequest)), - ) + .service(web::scope("/app1").default_service(web::to(HttpResponse::BadRequest))) .service(web::scope("/app2")) .default_service(|r: ServiceRequest| { ok(r.into_response(HttpResponse::MethodNotAllowed())) @@ -942,24 +926,20 @@ mod tests { #[actix_rt::test] async fn test_middleware() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("app") .wrap( - DefaultHeaders::new().header( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ), + DefaultHeaders::new() + .header(header::CONTENT_TYPE, HeaderValue::from_static("0001")), ) - .service( - web::resource("/test").route(web::get().to(HttpResponse::Ok)), - ), + .service(web::resource("/test").route(web::get().to(HttpResponse::Ok))), ), ) .await; let req = TestRequest::with_uri("/app/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -969,17 +949,15 @@ mod tests { #[actix_rt::test] async fn test_middleware_fn() { - let mut srv = init_service( + let srv = init_service( App::new().service( web::scope("app") .wrap_fn(|req, srv| { let fut = srv.call(req); async move { let mut res = fut.await?; - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); Ok(res) } }) @@ -989,7 +967,7 @@ mod tests { .await; let req = TestRequest::with_uri("/app/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); assert_eq!( resp.headers().get(header::CONTENT_TYPE).unwrap(), @@ -997,9 +975,46 @@ mod tests { ); } + #[actix_rt::test] + async fn test_middleware_app_data() { + let srv = init_service( + App::new().service( + web::scope("app") + .app_data(1usize) + .wrap_fn(|req, srv| { + assert_eq!(req.app_data::(), Some(&1usize)); + req.extensions_mut().insert(1usize); + srv.call(req) + }) + .route("/test", web::get().to(HttpResponse::Ok)) + .default_service(|req: ServiceRequest| async move { + let (req, _) = req.into_parts(); + + assert_eq!(req.extensions().get::(), Some(&1)); + + Ok(ServiceResponse::new( + req, + HttpResponse::BadRequest().finish(), + )) + }), + ), + ) + .await; + + let req = TestRequest::with_uri("/app/test").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/app/default").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + } + + // allow deprecated {App, Scope}::data + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { - let mut srv = init_service(App::new().data(1usize).service( + let srv = init_service(App::new().data(1usize).service( web::scope("app").data(10usize).route( "/t", web::get().to(|data: web::Data| { @@ -1011,13 +1026,15 @@ mod tests { .await; let req = TestRequest::with_uri("/app/t").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } + // allow deprecated `{App, Scope}::data` + #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { - let mut srv = init_service(App::new().data(1usize).service( + let srv = init_service(App::new().data(1usize).service( web::scope("app").data(10usize).default_service(web::to( |data: web::Data| { assert_eq!(**data, 10); @@ -1028,13 +1045,13 @@ mod tests { .await; let req = TestRequest::with_uri("/app/t").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_override_app_data() { - let mut srv = init_service(App::new().app_data(web::Data::new(1usize)).service( + let srv = init_service(App::new().app_data(web::Data::new(1usize)).service( web::scope("app").app_data(web::Data::new(10usize)).route( "/t", web::get().to(|data: web::Data| { @@ -1046,17 +1063,16 @@ mod tests { .await; let req = TestRequest::with_uri("/app/t").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } #[actix_rt::test] async fn test_scope_config() { - let mut srv = - init_service(App::new().service(web::scope("/app").configure(|s| { - s.route("/path1", web::get().to(HttpResponse::Ok)); - }))) - .await; + let srv = init_service(App::new().service(web::scope("/app").configure(|s| { + s.route("/path1", web::get().to(HttpResponse::Ok)); + }))) + .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); @@ -1065,13 +1081,12 @@ mod tests { #[actix_rt::test] async fn test_scope_config_2() { - let mut srv = - init_service(App::new().service(web::scope("/app").configure(|s| { - s.service(web::scope("/v1").configure(|s| { - s.route("/", web::get().to(HttpResponse::Ok)); - })); - }))) - .await; + let srv = init_service(App::new().service(web::scope("/app").configure(|s| { + s.service(web::scope("/v1").configure(|s| { + s.route("/", web::get().to(HttpResponse::Ok)); + })); + }))) + .await; let req = TestRequest::with_uri("/app/v1/").to_request(); let resp = srv.call(req).await.unwrap(); @@ -1080,24 +1095,19 @@ mod tests { #[actix_rt::test] async fn test_url_for_external() { - let mut srv = - init_service(App::new().service(web::scope("/app").configure(|s| { - s.service(web::scope("/v1").configure(|s| { - s.external_resource( - "youtube", - "https://youtube.com/watch/{video_id}", - ); - s.route( - "/", - web::get().to(|req: HttpRequest| { - HttpResponse::Ok().body( - req.url_for("youtube", &["xxxxxx"]).unwrap().to_string(), - ) - }), - ); - })); - }))) - .await; + let srv = init_service(App::new().service(web::scope("/app").configure(|s| { + s.service(web::scope("/v1").configure(|s| { + s.external_resource("youtube", "https://youtube.com/watch/{video_id}"); + s.route( + "/", + web::get().to(|req: HttpRequest| { + HttpResponse::Ok() + .body(req.url_for("youtube", &["xxxxxx"]).unwrap().to_string()) + }), + ); + })); + }))) + .await; let req = TestRequest::with_uri("/app/v1/").to_request(); let resp = srv.call(req).await.unwrap(); @@ -1108,7 +1118,7 @@ mod tests { #[actix_rt::test] async fn test_url_for_nested() { - let mut srv = init_service(App::new().service(web::scope("/a").service( + let srv = init_service(App::new().service(web::scope("/a").service( web::scope("/b").service(web::resource("/c/{stuff}").name("c").route( web::get().to(|req: HttpRequest| { HttpResponse::Ok() @@ -1119,7 +1129,7 @@ mod tests { .await; let req = TestRequest::with_uri("/a/b/c/test").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let body = read_body(resp).await; assert_eq!( @@ -1127,4 +1137,70 @@ mod tests { Bytes::from_static(b"http://localhost:8080/a/b/c/12345") ); } + + #[actix_rt::test] + async fn dynamic_scopes() { + let srv = init_service( + App::new().service( + web::scope("/{a}/").service( + web::scope("/{b}/") + .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) + .route( + "/", + web::get().to(|_: HttpRequest| HttpResponse::Accepted()), + ) + .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), + ), + ), + ) + .await; + + // note the unintuitive behavior with trailing slashes on scopes with dynamic segments + let req = TestRequest::with_uri("/a//b//c").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/a//b/").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/a//b//").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + let req = TestRequest::with_uri("/a//b//c/d").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + + let srv = init_service( + App::new().service( + web::scope("/{a}").service( + web::scope("/{b}") + .route("", web::get().to(|_: HttpRequest| HttpResponse::Created())) + .route( + "/", + web::get().to(|_: HttpRequest| HttpResponse::Accepted()), + ) + .route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())), + ), + ), + ) + .await; + + let req = TestRequest::with_uri("/a/b/c").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::OK); + + let req = TestRequest::with_uri("/a/b").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::CREATED); + + let req = TestRequest::with_uri("/a/b/").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::ACCEPTED); + + let req = TestRequest::with_uri("/a/b/c/d").to_request(); + let resp = call_service(&srv, req).await; + assert_eq!(resp.status(), StatusCode::NOT_FOUND); + } } diff --git a/src/server.rs b/src/server.rs index 8bfb27b77..3db849410 100644 --- a/src/server.rs +++ b/src/server.rs @@ -6,25 +6,18 @@ use std::{ sync::{Arc, Mutex}, }; -use actix_http::{ - body::MessageBody, Error, Extensions, HttpService, KeepAlive, Request, Response, -}; +use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response}; use actix_server::{Server, ServerBuilder}; -use actix_service::{map_config, IntoServiceFactory, Service, ServiceFactory}; - -#[cfg(unix)] -use actix_http::Protocol; -#[cfg(unix)] -use actix_service::pipeline_factory; -#[cfg(unix)] -use futures_util::future::ok; +use actix_service::{ + map_config, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, +}; #[cfg(feature = "openssl")] -use actix_tls::accept::openssl::{AlpnError, SslAcceptor, SslAcceptorBuilder}; +use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; #[cfg(feature = "rustls")] -use actix_tls::accept::rustls::ServerConfig as RustlsServerConfig; +use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; -use crate::config::AppConfig; +use crate::{config::AppConfig, Error}; struct Socket { scheme: &'static str, @@ -40,9 +33,9 @@ struct Config { /// An HTTP Server. /// -/// Create new http server with application factory. +/// Create new HTTP server with application factory. /// -/// ```rust,no_run +/// ```no_run /// use actix_web::{web, App, HttpResponse, HttpServer}; /// /// #[actix_rt::main] @@ -78,15 +71,17 @@ impl HttpServer where F: Fn() -> I + Send + Clone + 'static, I: IntoServiceFactory, + S: ServiceFactory + 'static, S::Error: Into + 'static, S::InitError: fmt::Debug, S::Response: Into> + 'static, >::Future: 'static, S::Service: 'static, + B: MessageBody + 'static, { - /// Create new http server with application factory + /// Create new HTTP server with application factory pub fn new(factory: F) -> Self { HttpServer { factory, @@ -109,11 +104,11 @@ where /// [Extensions] container so that request-local data can be passed to middleware and handlers. /// /// For example: - /// - `actix_tls::openssl::SslStream` when using openssl. - /// - `actix_tls::rustls::TlsStream` when using rustls. + /// - `actix_tls::accept::openssl::TlsStream` when using openssl. + /// - `actix_tls::accept::rustls::TlsStream` when using rustls. /// - `actix_web::rt::net::TcpStream` when no encryption is used. /// - /// See `on_connect` example for additional details. + /// See the `on_connect` example for additional details. pub fn on_connect(self, f: CB) -> HttpServer where CB: Fn(&dyn Any, &mut Extensions) + Send + Sync + 'static, @@ -131,8 +126,7 @@ where /// Set number of workers to start. /// - /// By default http server uses number of available logical cpu as threads - /// count. + /// By default, server uses number of available logical CPU as thread count. pub fn workers(mut self, num: usize) -> Self { self.builder = self.builder.workers(num); self @@ -161,7 +155,7 @@ where /// /// By default max connections is set to a 25k. pub fn max_connections(mut self, num: usize) -> Self { - self.builder = self.builder.maxconn(num); + self.builder = self.builder.max_concurrent_connections(num); self } @@ -178,6 +172,16 @@ where self } + /// Set max number of threads for each worker's blocking task thread pool. + /// + /// One thread pool is set up **per worker**; not shared across workers. + /// + /// By default set to 512 / workers. + pub fn worker_max_blocking_threads(mut self, num: usize) -> Self { + self.builder = self.builder.worker_max_blocking_threads(num); + self + } + /// Set server keep-alive setting. /// /// By default keep alive is set to a 5 seconds. @@ -225,7 +229,7 @@ where self } - /// Stop actix system. + /// Stop Actix `System` after server shutdown. pub fn system_exit(mut self) -> Self { self.builder = self.builder.system_exit(); self @@ -257,7 +261,7 @@ where /// Get addresses of bound sockets and the scheme for it. /// /// This is useful when the server is bound from different sources - /// with some sockets listening on http and some listening on https + /// with some sockets listening on HTTP and some listening on HTTPS /// and the user should be presented with an enumeration of which /// socket requires which protocol. pub fn addrs_with_scheme(&self) -> Vec<(net::SocketAddr, &str)> { @@ -278,32 +282,33 @@ where }); let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen( - format!("actix-web-service-{}", addr), - lst, - move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let c = cfg.lock().unwrap(); + let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .local_addr(addr); + let mut svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) + .local_addr(addr); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + if let Some(handler) = on_connect_fn.clone() { + svc = svc.on_connect_ext(move |io: &_, ext: _| { + (handler)(io as &dyn Any, ext) + }) + }; - svc.finish(map_config(factory(), move |_| { - AppConfig::new(false, addr, host.clone()) - })) - .tcp() - }, - )?; + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { + AppConfig::new(false, host.clone(), addr) + })) + .tcp() + })?; Ok(self) } @@ -335,39 +340,43 @@ where let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen( - format!("actix-web-service-{}", addr), - lst, - move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let c = cfg.lock().unwrap(); + let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) + .local_addr(addr); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (&*handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| { + (&*handler)(io as &dyn Any, ext) + }) + } else { + svc + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { + AppConfig::new(true, host.clone(), addr) + })) + .openssl(acceptor.clone()) + })?; - svc.finish(map_config(factory(), move |_| { - AppConfig::new(true, addr, host.clone()) - })) - .openssl(acceptor.clone()) - }, - )?; Ok(self) } #[cfg(feature = "rustls")] /// Use listener for accepting incoming tls connection requests /// - /// This method sets alpn protocols to "h2" and "http/1.1" + /// This method prepends alpn protocols "h2" and "http/1.1" to configured ones pub fn listen_rustls( self, lst: net::TcpListener, @@ -392,32 +401,33 @@ where let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen( - format!("actix-web-service-{}", addr), - lst, - move || { - let c = cfg.lock().unwrap(); - let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let c = cfg.lock().unwrap(); + let host = c.host.clone().unwrap_or_else(|| format!("{}", addr)); - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .client_disconnect(c.client_shutdown); + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + } else { + svc + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { + AppConfig::new(true, host.clone(), addr) + })) + .rustls(config.clone()) + })?; - svc.finish(map_config(factory(), move |_| { - AppConfig::new(true, addr, host.clone()) - })) - .rustls(config.clone()) - }, - )?; Ok(self) } @@ -434,10 +444,7 @@ where Ok(self) } - fn bind2( - &self, - addr: A, - ) -> io::Result> { + fn bind2(&self, addr: A) -> io::Result> { let mut err = None; let mut success = false; let mut sockets = Vec::new(); @@ -452,17 +459,15 @@ where } } - if !success { - if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } - } else { + if success { Ok(sockets) + } else if let Some(e) = err.take() { + Err(e) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can not bind to address.", + )) } } @@ -470,11 +475,7 @@ where /// Start listening for incoming tls connections. /// /// This method sets alpn protocols to "h2" and "http/1.1" - pub fn bind_openssl( - mut self, - addr: A, - builder: SslAcceptorBuilder, - ) -> io::Result + pub fn bind_openssl(mut self, addr: A, builder: SslAcceptorBuilder) -> io::Result where A: net::ToSocketAddrs, { @@ -491,7 +492,7 @@ where #[cfg(feature = "rustls")] /// Start listening for incoming tls connections. /// - /// This method sets alpn protocols to "h2" and "http/1.1" + /// This method prepends alpn protocols "h2" and "http/1.1" to configured ones pub fn bind_rustls( mut self, addr: A, @@ -506,69 +507,68 @@ where #[cfg(unix)] /// Start listening for unix domain (UDS) connections on existing listener. - pub fn listen_uds( - mut self, - lst: std::os::unix::net::UnixListener, - ) -> io::Result { + pub fn listen_uds(mut self, lst: std::os::unix::net::UnixListener) -> io::Result { + use actix_http::Protocol; use actix_rt::net::UnixStream; + use actix_service::{fn_service, ServiceFactoryExt as _}; let cfg = self.config.clone(); let factory = self.factory.clone(); - let socket_addr = net::SocketAddr::new( - net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), - 8080, - ); + let socket_addr = + net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); self.sockets.push(Socket { scheme: "http", addr: socket_addr, }); - let addr = format!("actix-web-service-{:?}", lst.local_addr()?); + let addr = lst.local_addr()?; + let name = format!("actix-web-service-{:?}", addr); let on_connect_fn = self.on_connect_fn.clone(); - self.builder = self.builder.listen_uds(addr, lst, move || { + self.builder = self.builder.listen_uds(name, lst, move || { let c = cfg.lock().unwrap(); let config = AppConfig::new( false, - socket_addr, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), + socket_addr, ); - pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))).and_then( - { - let svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout); + fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then({ + let mut svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown); - let svc = if let Some(handler) = on_connect_fn.clone() { - svc.on_connect_ext(move |io: &_, ext: _| { - (&*handler)(io as &dyn Any, ext) - }) - } else { - svc - }; + if let Some(handler) = on_connect_fn.clone() { + svc = svc + .on_connect_ext(move |io: &_, ext: _| (&*handler)(io as &dyn Any, ext)); + } - svc.finish(map_config(factory(), move |_| config.clone())) - }, - ) + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| config.clone())) + }) })?; Ok(self) } - #[cfg(unix)] /// Start listening for incoming unix domain connections. + #[cfg(unix)] pub fn bind_uds(mut self, addr: A) -> io::Result where A: AsRef, { + use actix_http::Protocol; use actix_rt::net::UnixStream; + use actix_service::{fn_service, ServiceFactoryExt as _}; let cfg = self.config.clone(); let factory = self.factory.clone(); - let socket_addr = net::SocketAddr::new( - net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), - 8080, - ); + let socket_addr = + net::SocketAddr::new(net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)), 8080); + self.sockets.push(Socket { scheme: "http", addr: socket_addr, @@ -581,18 +581,24 @@ where let c = cfg.lock().unwrap(); let config = AppConfig::new( false, - socket_addr, c.host.clone().unwrap_or_else(|| format!("{}", socket_addr)), + socket_addr, ); - pipeline_factory(|io: UnixStream| ok((io, Protocol::Http1, None))) - .and_then( - HttpService::build() - .keep_alive(c.keep_alive) - .client_timeout(c.client_timeout) - .finish(map_config(factory(), move |_| config.clone())), - ) + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + fn_service(|io: UnixStream| async { Ok((io, Protocol::Http1, None)) }).and_then( + HttpService::build() + .keep_alive(c.keep_alive) + .client_timeout(c.client_timeout) + .client_disconnect(c.client_shutdown) + .finish(map_config(fac, move |_| config.clone())), + ) }, )?; + Ok(self) } } @@ -610,14 +616,14 @@ where { /// Start listening for incoming connections. /// - /// This method starts number of http workers in separate threads. + /// This method starts number of HTTP workers in separate threads. /// For each address this method starts separate thread which does /// `accept()` in a loop. /// /// This methods panics if no socket address can be bound or an `Actix` system is not yet /// configured. /// - /// ```rust,no_run + /// ```no_run /// use std::io; /// use actix_web::{web, App, HttpResponse, HttpServer}; /// @@ -630,30 +636,24 @@ where /// } /// ``` pub fn run(self) -> Server { - self.builder.start() + self.builder.run() } } -fn create_tcp_listener( - addr: net::SocketAddr, - backlog: u32, -) -> io::Result { +fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result { use socket2::{Domain, Protocol, Socket, Type}; - let domain = match addr { - net::SocketAddr::V4(_) => Domain::ipv4(), - net::SocketAddr::V6(_) => Domain::ipv6(), - }; - let socket = Socket::new(domain, Type::stream(), Some(Protocol::tcp()))?; + let domain = Domain::for_address(addr); + let socket = Socket::new(domain, Type::STREAM, Some(Protocol::TCP))?; socket.set_reuse_address(true)?; socket.bind(&addr.into())?; // clamp backlog to max u32 that fits in i32 range let backlog = cmp::min(backlog, i32::MAX as u32) as i32; socket.listen(backlog)?; - Ok(socket.into_tcp_listener()) + Ok(net::TcpListener::from(socket)) } -#[cfg(feature = "openssl")] /// Configure `SslAcceptorBuilder` with custom server flags. +#[cfg(feature = "openssl")] fn openssl_acceptor(mut builder: SslAcceptorBuilder) -> io::Result { builder.set_alpn_select_callback(|_, protocols| { const H2: &[u8] = b"\x02h2"; diff --git a/src/service.rs b/src/service.rs index 596eedd7f..df9e809e4 100644 --- a/src/service.rs +++ b/src/service.rs @@ -1,27 +1,46 @@ -use std::cell::{Ref, RefMut}; -use std::rc::Rc; -use std::{fmt, net}; - -use actix_http::body::{Body, MessageBody, ResponseBody}; -use actix_http::http::{HeaderMap, Method, StatusCode, Uri, Version}; -use actix_http::{ - Error, Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, - ResponseHead, +use std::{ + cell::{Ref, RefMut}, + fmt, net, + rc::Rc, }; -use actix_router::{IntoPattern, Path, Resource, ResourceDef, Url}; -use actix_service::{IntoServiceFactory, ServiceFactory}; -use crate::config::{AppConfig, AppService}; -use crate::dev::insert_slash; -use crate::guard::Guard; -use crate::info::ConnectionInfo; -use crate::request::HttpRequest; -use crate::rmap::ResourceMap; +use actix_http::{ + body::{BoxBody, EitherBody, MessageBody}, + http::{HeaderMap, Method, StatusCode, Uri, Version}, + Extensions, HttpMessage, Payload, PayloadStream, RequestHead, Response, ResponseHead, +}; +use actix_router::{IntoPatterns, Path, Patterns, Resource, ResourceDef, Url}; +use actix_service::{ + boxed::{BoxService, BoxServiceFactory}, + IntoServiceFactory, ServiceFactory, +}; +#[cfg(feature = "cookies")] +use cookie::{Cookie, ParseError as CookieParseError}; + +use crate::{ + config::{AppConfig, AppService}, + dev::ensure_leading_slash, + guard::Guard, + info::ConnectionInfo, + rmap::ResourceMap, + Error, HttpRequest, HttpResponse, +}; + +pub(crate) type BoxedHttpService = BoxService, Error>; +pub(crate) type BoxedHttpServiceFactory = + BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; pub trait HttpServiceFactory { fn register(self, config: &mut AppService); } +impl HttpServiceFactory for Vec { + fn register(self, config: &mut AppService) { + self.into_iter() + .for_each(|factory| factory.register(config)); + } +} + pub(crate) trait AppServiceFactory { fn register(&mut self, config: &mut AppService); } @@ -49,9 +68,9 @@ where } } -/// An service http request +/// A service level request wrapper. /// -/// ServiceRequest allows mutable access to request's internal structures +/// Allows mutable access to request's internal structures. pub struct ServiceRequest { req: HttpRequest, payload: Payload, @@ -69,6 +88,12 @@ impl ServiceRequest { (self.req, self.payload) } + /// Get mutable access to inner `HttpRequest` and `Payload` + #[inline] + pub fn parts_mut(&mut self) -> (&mut HttpRequest, &mut Payload) { + (&mut self.req, &mut self.payload) + } + /// Construct request from parts. pub fn from_parts(req: HttpRequest, payload: Payload) -> Self { Self { req, payload } @@ -87,20 +112,21 @@ impl ServiceRequest { /// Create service response #[inline] pub fn into_response>>(self, res: R) -> ServiceResponse { - ServiceResponse::new(self.req, res.into()) + let res = HttpResponse::from(res.into()); + ServiceResponse::new(self.req, res) } /// Create service response for error #[inline] - pub fn error_response>(self, err: E) -> ServiceResponse { - let res: Response = err.into().into(); - ServiceResponse::new(self.req, res.into_body()) + pub fn error_response>(self, err: E) -> ServiceResponse { + let res = HttpResponse::from_error(err.into()); + ServiceResponse::new(self.req, res) } /// This method returns reference to the request head #[inline] pub fn head(&self) -> &RequestHead { - &self.req.head() + self.req.head() } /// This method returns reference to the request head @@ -150,19 +176,17 @@ impl ServiceRequest { /// E.g., id=10 #[inline] pub fn query_string(&self) -> &str { - if let Some(query) = self.uri().query().as_ref() { - query - } else { - "" - } + self.uri().query().unwrap_or_default() } - /// Peer socket address + /// Peer socket address. /// - /// Peer address is actual socket address, if proxy is used in front of - /// actix http server, then peer address would be address of this proxy. + /// Peer address is the directly connected peer's socket address. If a proxy is used in front of + /// the Actix Web server, then it would be address of this proxy. /// /// To get client connection information `ConnectionInfo` should be used. + /// + /// Will only return None when called in unit tests. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -197,14 +221,14 @@ impl ServiceRequest { self.req.match_pattern() } - #[inline] /// Get a mutable reference to the Path parameters. + #[inline] pub fn match_info_mut(&mut self) -> &mut Path { self.req.match_info_mut() } - #[inline] /// Get a reference to a `ResourceMap` of current application. + #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } @@ -226,6 +250,17 @@ impl ServiceRequest { None } + #[cfg(feature = "cookies")] + pub fn cookies(&self) -> Result>>, CookieParseError> { + self.req.cookies() + } + + /// Return request cookie. + #[cfg(feature = "cookies")] + pub fn cookie(&self, name: &str) -> Option> { + self.req.cookie(name) + } + /// Set request payload. pub fn set_payload(&mut self, payload: Payload) { self.payload = payload; @@ -299,36 +334,35 @@ impl fmt::Debug for ServiceRequest { } } -pub struct ServiceResponse { +/// A service level response wrapper. +pub struct ServiceResponse { request: HttpRequest, - response: Response, + response: HttpResponse, +} + +impl ServiceResponse { + /// Create service response from the error + pub fn from_err>(err: E, request: HttpRequest) -> Self { + let response = HttpResponse::from_error(err); + ServiceResponse { request, response } + } } impl ServiceResponse { /// Create service response instance - pub fn new(request: HttpRequest, response: Response) -> Self { + pub fn new(request: HttpRequest, response: HttpResponse) -> Self { ServiceResponse { request, response } } - /// Create service response from the error - pub fn from_err>(err: E, request: HttpRequest) -> Self { - let e: Error = err.into(); - let res: Response = e.into(); - ServiceResponse { - request, - response: res.into_body(), - } - } - /// Create service response for error #[inline] - pub fn error_response>(self, err: E) -> Self { - Self::from_err(err, self.request) + pub fn error_response>(self, err: E) -> ServiceResponse { + ServiceResponse::from_err(err, self.request) } /// Create service response #[inline] - pub fn into_response(self, response: Response) -> ServiceResponse { + pub fn into_response(self, response: HttpResponse) -> ServiceResponse { ServiceResponse::new(self.request, response) } @@ -340,13 +374,13 @@ impl ServiceResponse { /// Get reference to response #[inline] - pub fn response(&self) -> &Response { + pub fn response(&self) -> &HttpResponse { &self.response } /// Get mutable reference to response #[inline] - pub fn response_mut(&mut self) -> &mut Response { + pub fn response_mut(&mut self) -> &mut HttpResponse { &mut self.response } @@ -362,38 +396,24 @@ impl ServiceResponse { self.response.headers() } - #[inline] /// Returns mutable response's headers. + #[inline] pub fn headers_mut(&mut self) -> &mut HeaderMap { self.response.headers_mut() } - /// Execute closure and in case of error convert it to response. - pub fn checked_expr(mut self, f: F) -> Self - where - F: FnOnce(&mut Self) -> Result<(), E>, - E: Into, - { - match f(&mut self) { - Ok(_) => self, - Err(err) => { - let res: Response = err.into().into(); - ServiceResponse::new(self.request, res.into_body()) - } - } - } - /// Extract response body - pub fn take_body(&mut self) -> ResponseBody { - self.response.take_body() + pub fn into_body(self) -> B { + self.response.into_body() } } impl ServiceResponse { /// Set a new body + #[inline] pub fn map_body(self, f: F) -> ServiceResponse where - F: FnOnce(&mut ResponseHead, ResponseBody) -> ResponseBody, + F: FnOnce(&mut ResponseHead, B) -> B2, { let response = self.response.map_body(f); @@ -402,15 +422,43 @@ impl ServiceResponse { request: self.request, } } + + #[inline] + pub fn map_into_left_body(self) -> ServiceResponse> { + self.map_body(|_, body| EitherBody::left(body)) + } + + #[inline] + pub fn map_into_right_body(self) -> ServiceResponse> { + self.map_body(|_, body| EitherBody::right(body)) + } + + #[inline] + pub fn map_into_boxed_body(self) -> ServiceResponse + where + B: MessageBody + 'static, + { + self.map_body(|_, body| BoxBody::new(body)) + } } -impl From> for Response { - fn from(res: ServiceResponse) -> Response { +impl From> for HttpResponse { + fn from(res: ServiceResponse) -> HttpResponse { res.response } } -impl fmt::Debug for ServiceResponse { +impl From> for Response { + fn from(res: ServiceResponse) -> Response { + res.response.into() + } +} + +impl fmt::Debug for ServiceResponse +where + B: MessageBody, + B::Error: Into, +{ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { let res = writeln!( f, @@ -429,14 +477,14 @@ impl fmt::Debug for ServiceResponse { } pub struct WebService { - rdef: Vec, + rdef: Patterns, name: Option, guards: Vec>, } impl WebService { /// Create new `WebService` instance. - pub fn new(path: T) -> Self { + pub fn new(path: T) -> Self { WebService { rdef: path.patterns(), name: None, @@ -446,7 +494,7 @@ impl WebService { /// Set service name. /// - /// Name is used for url generation. + /// Name is used for URL generation. pub fn name(mut self, name: &str) -> Self { self.name = Some(name.to_string()); self @@ -454,7 +502,7 @@ impl WebService { /// Add match guard to a web service. /// - /// ```rust + /// ``` /// use actix_web::{web, guard, dev, App, Error, HttpResponse}; /// /// async fn index(req: dev::ServiceRequest) -> Result { @@ -498,7 +546,7 @@ impl WebService { struct WebServiceImpl { srv: T, - rdef: Vec, + rdef: Patterns, name: Option, guards: Vec>, } @@ -521,28 +569,85 @@ where }; let mut rdef = if config.is_root() || !self.rdef.is_empty() { - ResourceDef::new(insert_slash(self.rdef)) + ResourceDef::new(ensure_leading_slash(self.rdef)) } else { ResourceDef::new(self.rdef) }; + if let Some(ref name) = self.name { - *rdef.name_mut() = name.clone(); + rdef.set_name(name); } + config.register_service(rdef, guards, self.srv, None) } } +/// Macro helping register different types of services at the sametime. +/// +/// The service type must be implementing [`HttpServiceFactory`](self::HttpServiceFactory) trait. +/// +/// The max number of services can be grouped together is 12. +/// +/// # Examples +/// ``` +/// use actix_web::{services, web, App}; +/// +/// let services = services![ +/// web::resource("/test2").to(|| async { "test2" }), +/// web::scope("/test3").route("/", web::get().to(|| async { "test3" })) +/// ]; +/// +/// let app = App::new().service(services); +/// +/// // services macro just convert multiple services to a tuple. +/// // below would also work without importing the macro. +/// let app = App::new().service(( +/// web::resource("/test2").to(|| async { "test2" }), +/// web::scope("/test3").route("/", web::get().to(|| async { "test3" })) +/// )); +/// ``` +#[macro_export] +macro_rules! services { + ($($x:expr),+ $(,)?) => { + ($($x,)+) + } +} + +/// HttpServiceFactory trait impl for tuples +macro_rules! service_tuple ({ $($T:ident)+ } => { + impl<$($T: HttpServiceFactory),+> HttpServiceFactory for ($($T,)+) { + #[allow(non_snake_case)] + fn register(self, config: &mut AppService) { + let ($($T,)*) = self; + $($T.register(config);)+ + } + } +}); + +service_tuple! { A } +service_tuple! { A B } +service_tuple! { A B C } +service_tuple! { A B C D } +service_tuple! { A B C D E } +service_tuple! { A B C D E F } +service_tuple! { A B C D E F G } +service_tuple! { A B C D E F G H } +service_tuple! { A B C D E F G H I } +service_tuple! { A B C D E F G H I J } +service_tuple! { A B C D E F G H I J K } +service_tuple! { A B C D E F G H I J K L } + #[cfg(test)] mod tests { use super::*; use crate::test::{init_service, TestRequest}; use crate::{guard, http, web, App, HttpResponse}; use actix_service::Service; - use futures_util::future::ok; + use actix_utils::future::ok; #[actix_rt::test] async fn test_service() { - let mut srv = init_service( + let srv = init_service( App::new().service(web::service("/test").name("test").finish( |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), )), @@ -552,7 +657,7 @@ mod tests { let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); - let mut srv = init_service( + let srv = init_service( App::new().service(web::service("/test").guard(guard::Get()).finish( |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), )), @@ -565,22 +670,22 @@ mod tests { assert_eq!(resp.status(), http::StatusCode::NOT_FOUND); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_service_data() { - let mut srv = init_service( - App::new() - .data(42u32) - .service(web::service("/test").name("test").finish( - |req: ServiceRequest| { - assert_eq!( - req.app_data::>().unwrap().as_ref(), - &42 - ); - ok(req.into_response(HttpResponse::Ok().finish())) - }, - )), - ) - .await; + let srv = + init_service( + App::new() + .data(42u32) + .service(web::service("/test").name("test").finish( + |req: ServiceRequest| { + assert_eq!(req.app_data::>().unwrap().as_ref(), &42); + ok(req.into_response(HttpResponse::Ok().finish())) + }, + )), + ) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); @@ -606,4 +711,80 @@ mod tests { assert!(s.contains("ServiceResponse")); assert!(s.contains("x-test")); } + + #[actix_rt::test] + async fn test_services_macro() { + let scoped = services![ + web::service("/scoped_test1").name("scoped_test1").finish( + |req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().finish())) + } + ), + web::resource("/scoped_test2").to(|| async { "test2" }), + ]; + + let services = services![ + web::service("/test1") + .name("test") + .finish(|req: ServiceRequest| async { + Ok(req.into_response(HttpResponse::Ok().finish())) + }), + web::resource("/test2").to(|| async { "test2" }), + web::scope("/test3").service(scoped) + ]; + + let srv = init_service(App::new().service(services)).await; + + let req = TestRequest::with_uri("/test1").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + + let req = TestRequest::with_uri("/test2").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + + let req = TestRequest::with_uri("/test3/scoped_test1").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + + let req = TestRequest::with_uri("/test3/scoped_test2").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + } + + #[actix_rt::test] + async fn test_services_vec() { + let services = vec![ + web::resource("/test1").to(|| async { "test1" }), + web::resource("/test2").to(|| async { "test2" }), + ]; + + let scoped = vec![ + web::resource("/scoped_test1").to(|| async { "test1" }), + web::resource("/scoped_test2").to(|| async { "test2" }), + ]; + + let srv = init_service( + App::new() + .service(services) + .service(web::scope("/test3").service(scoped)), + ) + .await; + + let req = TestRequest::with_uri("/test1").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + + let req = TestRequest::with_uri("/test2").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + + let req = TestRequest::with_uri("/test3/scoped_test1").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + + let req = TestRequest::with_uri("/test3/scoped_test2").to_request(); + let resp = srv.call(req).await.unwrap(); + assert_eq!(resp.status(), http::StatusCode::OK); + } } diff --git a/src/test.rs b/src/test.rs index 769cece55..2cd01039d 100644 --- a/src/test.rs +++ b/src/test.rs @@ -1,68 +1,62 @@ //! Various helpers for Actix applications to use during testing. -use std::net::SocketAddr; -use std::rc::Rc; -use std::sync::mpsc; -use std::{fmt, net, thread, time}; - -use actix_codec::{AsyncRead, AsyncWrite, Framed}; -use actix_http::http::header::{ContentType, IntoHeaderPair}; -use actix_http::http::{Method, StatusCode, Uri, Version}; -use actix_http::test::TestRequest as HttpTestRequest; -use actix_http::{cookie::Cookie, ws, Extensions, HttpService, Request}; -use actix_router::{Path, ResourceDef, Url}; -use actix_rt::{time::sleep, System}; -use actix_service::{ - map_config, IntoService, IntoServiceFactory, Service, ServiceFactory, -}; -use awc::error::PayloadError; -use awc::{Client, ClientRequest, ClientResponse, Connector}; -use bytes::{Bytes, BytesMut}; -use futures_core::Stream; -use futures_util::future::ok; -use futures_util::StreamExt; -use serde::de::DeserializeOwned; -use serde::Serialize; -use socket2::{Domain, Protocol, Socket, Type}; +use std::{borrow::Cow, net::SocketAddr, rc::Rc}; pub use actix_http::test::TestBuffer; +use actix_http::{ + http::{header::IntoHeaderPair, Method, StatusCode, Uri, Version}, + test::TestRequest as HttpTestRequest, + Extensions, Request, +}; +use actix_router::{Path, ResourceDef, Url}; +use actix_service::{IntoService, IntoServiceFactory, Service, ServiceFactory}; +use actix_utils::future::{ok, poll_fn}; +use futures_core::Stream; +use futures_util::StreamExt as _; +use serde::{de::DeserializeOwned, Serialize}; -use crate::app_service::AppInitServiceState; -use crate::config::AppConfig; -use crate::data::Data; -use crate::dev::{Body, MessageBody, Payload, Server}; -use crate::rmap::ResourceMap; -use crate::service::{ServiceRequest, ServiceResponse}; -use crate::{Error, HttpRequest, HttpResponse}; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; +use crate::{ + app_service::AppInitServiceState, + body::{self, BoxBody, MessageBody}, + config::AppConfig, + data::Data, + dev::Payload, + http::header::ContentType, + rmap::ResourceMap, + service::{ServiceRequest, ServiceResponse}, + web::{Bytes, BytesMut}, + Error, HttpRequest, HttpResponse, HttpResponseBuilder, +}; -/// Create service that always responds with `HttpResponse::Ok()` +/// Create service that always responds with `HttpResponse::Ok()` and no body. pub fn ok_service( -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { default_service(StatusCode::OK) } -/// Create service that responds with response with specified status code +/// Create service that always responds with given status code and no body. pub fn default_service( status_code: StatusCode, -) -> impl Service, Error = Error> { +) -> impl Service, Error = Error> { (move |req: ServiceRequest| { - ok(req.into_response(HttpResponse::build(status_code).finish())) + ok(req.into_response(HttpResponseBuilder::new(status_code).finish())) }) .into_service() } -/// This method accepts application builder instance, and constructs -/// service. +/// Initialize service from application builder instance. /// -/// ```rust +/// ``` /// use actix_service::Service; /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_init_service() { -/// let mut app = test::init_service( +/// let app = test::init_service( /// App::new() -/// .service(web::resource("/test").to(|| async { HttpResponse::Ok() })) +/// .service(web::resource("/test").to(|| async { "OK" })) /// ).await; /// /// // Create request object @@ -78,31 +72,21 @@ pub async fn init_service( ) -> impl Service, Error = E> where R: IntoServiceFactory, - S: ServiceFactory< - Request, - Config = AppConfig, - Response = ServiceResponse, - Error = E, - >, + S: ServiceFactory, Error = E>, S::InitError: std::fmt::Debug, { try_init_service(app) .await - .expect("service initilization failed") + .expect("service initialization failed") } -/// Fallible version of init_service that allows testing data factory errors. +/// Fallible version of [`init_service`] that allows testing initialization errors. pub(crate) async fn try_init_service( app: R, ) -> Result, Error = E>, S::InitError> where R: IntoServiceFactory, - S: ServiceFactory< - Request, - Config = AppConfig, - Response = ServiceResponse, - Error = E, - >, + S: ServiceFactory, Error = E>, S::InitError: std::fmt::Debug, { let srv = app.into_factory(); @@ -111,12 +95,12 @@ where /// Calls service and waits for response future completion. /// -/// ```rust +/// ``` /// use actix_web::{test, web, App, HttpResponse, http::StatusCode}; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_response() { -/// let mut app = test::init_service( +/// let app = test::init_service( /// App::new() /// .service(web::resource("/test").to(|| async { /// HttpResponse::Ok() @@ -127,11 +111,11 @@ where /// let req = test::TestRequest::with_uri("/test").to_request(); /// /// // Call application -/// let resp = test::call_service(&mut app, req).await; +/// let resp = test::call_service(&app, req).await; /// assert_eq!(resp.status(), StatusCode::OK); /// } /// ``` -pub async fn call_service(app: &mut S, req: R) -> S::Response +pub async fn call_service(app: &S, req: R) -> S::Response where S: Service, Error = E>, E: std::fmt::Debug, @@ -141,13 +125,13 @@ where /// Helper function that returns a response body of a TestRequest /// -/// ```rust +/// ``` /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_index() { -/// let mut app = test::init_service( +/// let app = test::init_service( /// App::new().service( /// web::resource("/index.html") /// .route(web::post().to(|| async { @@ -160,37 +144,41 @@ where /// .header(header::CONTENT_TYPE, "application/json") /// .to_request(); /// -/// let result = test::read_response(&mut app, req).await; +/// let result = test::read_response(&app, req).await; /// assert_eq!(result, Bytes::from_static(b"welcome!")); /// } /// ``` -pub async fn read_response(app: &mut S, req: Request) -> Bytes +pub async fn read_response(app: &S, req: Request) -> Bytes where S: Service, Error = Error>, B: MessageBody + Unpin, + B::Error: Into, { - let mut resp = app + let resp = app .call(req) .await - .unwrap_or_else(|_| panic!("read_response failed at application call")); + .unwrap_or_else(|e| panic!("read_response failed at application call: {}", e)); - let mut body = resp.take_body(); + let body = resp.into_body(); let mut bytes = BytesMut::new(); - while let Some(item) = body.next().await { - bytes.extend_from_slice(&item.unwrap()); + + actix_rt::pin!(body); + while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { + bytes.extend_from_slice(&item.map_err(Into::into).unwrap()); } + bytes.freeze() } /// Helper function that returns a response body of a ServiceResponse. /// -/// ```rust +/// ``` /// use actix_web::{test, web, App, HttpResponse, http::header}; /// use bytes::Bytes; /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_index() { -/// let mut app = test::init_service( +/// let app = test::init_service( /// App::new().service( /// web::resource("/index.html") /// .route(web::post().to(|| async { @@ -203,26 +191,30 @@ where /// .header(header::CONTENT_TYPE, "application/json") /// .to_request(); /// -/// let resp = test::call_service(&mut app, req).await; +/// let resp = test::call_service(&app, req).await; /// let result = test::read_body(resp).await; /// assert_eq!(result, Bytes::from_static(b"welcome!")); /// } /// ``` -pub async fn read_body(mut res: ServiceResponse) -> Bytes +pub async fn read_body(res: ServiceResponse) -> Bytes where B: MessageBody + Unpin, + B::Error: Into, { - let mut body = res.take_body(); + let body = res.into_body(); let mut bytes = BytesMut::new(); - while let Some(item) = body.next().await { - bytes.extend_from_slice(&item.unwrap()); + + actix_rt::pin!(body); + while let Some(item) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { + bytes.extend_from_slice(&item.map_err(Into::into).unwrap()); } + bytes.freeze() } /// Helper function that returns a deserialized response body of a ServiceResponse. /// -/// ```rust +/// ``` /// use actix_web::{App, test, web, HttpResponse, http::header}; /// use serde::{Serialize, Deserialize}; /// @@ -232,9 +224,9 @@ where /// name: String, /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_post_person() { -/// let mut app = test::init_service( +/// let app = test::init_service( /// App::new().service( /// web::resource("/people") /// .route(web::post().to(|person: web::Json| async { @@ -260,12 +252,16 @@ where pub async fn read_body_json(res: ServiceResponse) -> T where B: MessageBody + Unpin, + B::Error: Into, T: DeserializeOwned, { let body = read_body(res).await; serde_json::from_slice(&body).unwrap_or_else(|e| { - panic!("read_response_json failed during deserialization: {}", e) + panic!( + "read_response_json failed during deserialization of body: {:?}, {}", + body, e + ) }) } @@ -280,9 +276,17 @@ where Ok(data.freeze()) } +pub async fn load_body(body: B) -> Result +where + B: MessageBody + Unpin, + B::Error: Into, +{ + body::to_bytes(body).await.map_err(Into::into) +} + /// Helper function that returns a deserialized response body of a TestRequest /// -/// ```rust +/// ``` /// use actix_web::{App, test, web, HttpResponse, http::header}; /// use serde::{Serialize, Deserialize}; /// @@ -292,9 +296,9 @@ where /// name: String /// } /// -/// #[actix_rt::test] +/// #[actix_web::test] /// async fn test_add_person() { -/// let mut app = test::init_service( +/// let app = test::init_service( /// App::new().service( /// web::resource("/people") /// .route(web::post().to(|person: web::Json| async { @@ -314,16 +318,21 @@ where /// let result: Person = test::read_response_json(&mut app, req).await; /// } /// ``` -pub async fn read_response_json(app: &mut S, req: Request) -> T +pub async fn read_response_json(app: &S, req: Request) -> T where S: Service, Error = Error>, B: MessageBody + Unpin, + B::Error: Into, T: DeserializeOwned, { let body = read_response(app, req).await; - serde_json::from_slice(&body) - .unwrap_or_else(|_| panic!("read_response_json failed during deserialization")) + serde_json::from_slice(&body).unwrap_or_else(|_| { + panic!( + "read_response_json failed during deserialization of body: {:?}", + body + ) + }) } /// Test `Request` builder. @@ -335,7 +344,7 @@ where /// * `TestRequest::to_srv_response` creates `ServiceResponse` instance. /// * `TestRequest::to_http_request` creates `HttpRequest` instance, which is used for testing handlers. /// -/// ```rust +/// ``` /// use actix_web::{test, HttpRequest, HttpResponse, HttpMessage}; /// use actix_web::http::{header, StatusCode}; /// @@ -347,8 +356,8 @@ where /// } /// } /// -/// #[test] -/// fn test_index() { +/// #[actix_web::test] +/// async fn test_index() { /// let req = test::TestRequest::default().insert_header("content-type", "text/plain") /// .to_http_request(); /// @@ -367,6 +376,8 @@ pub struct TestRequest { path: Path, peer_addr: Option, app_data: Extensions, + #[cfg(feature = "cookies")] + cookies: CookieJar, } impl Default for TestRequest { @@ -378,6 +389,8 @@ impl Default for TestRequest { path: Path::new(Url::new(Uri::default())), peer_addr: None, app_data: Extensions::new(), + #[cfg(feature = "cookies")] + cookies: CookieJar::new(), } } } @@ -451,24 +464,37 @@ impl TestRequest { } /// Set cookie for this request. + #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { - self.req.cookie(cookie); + self.cookies.add(cookie.into_owned()); self } - /// Set request path pattern parameter - pub fn param(mut self, name: &'static str, value: &'static str) -> Self { + /// Set request path pattern parameter. + /// + /// # Examples + /// ``` + /// use actix_web::test::TestRequest; + /// + /// let req = TestRequest::default().param("foo", "bar"); + /// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned()); + /// ``` + pub fn param( + mut self, + name: impl Into>, + value: impl Into>, + ) -> Self { self.path.add_static(name, value); self } - /// Set peer addr + /// Set peer addr. pub fn peer_addr(mut self, addr: SocketAddr) -> Self { self.peer_addr = Some(addr); self } - /// Set request payload + /// Set request payload. pub fn set_payload>(mut self, data: B) -> Self { self.req.set_payload(data); self @@ -487,8 +513,7 @@ impl TestRequest { /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is /// set to `application/json`. pub fn set_json(mut self, data: &T) -> Self { - let bytes = - serde_json::to_string(data).expect("Failed to serialize test data to json"); + let bytes = serde_json::to_string(data).expect("Failed to serialize test data to json"); self.req.set_payload(bytes); self.req.insert_header(ContentType::json()); self @@ -515,21 +540,46 @@ impl TestRequest { self } + fn finish(&mut self) -> Request { + // mut used when cookie feature is enabled + #[allow(unused_mut)] + let mut req = self.req.finish(); + + #[cfg(feature = "cookies")] + { + use actix_http::http::header::{HeaderValue, COOKIE}; + + let cookie: String = self + .cookies + .delta() + // ensure only name=value is written to cookie header + .map(|c| c.stripped().encoded().to_string()) + .collect::>() + .join("; "); + + if !cookie.is_empty() { + req.headers_mut() + .insert(COOKIE, HeaderValue::from_str(&cookie).unwrap()); + } + } + + req + } + /// Complete request creation and generate `Request` instance pub fn to_request(mut self) -> Request { - let mut req = self.req.finish(); + let mut req = self.finish(); req.head_mut().peer_addr = self.peer_addr; req } /// Complete request creation and generate `ServiceRequest` instance pub fn to_srv_request(mut self) -> ServiceRequest { - let (mut head, payload) = self.req.finish().into_parts(); + let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); - let app_state = - AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); ServiceRequest::new( HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)), @@ -544,24 +594,22 @@ impl TestRequest { /// Complete request creation and generate `HttpRequest` instance pub fn to_http_request(mut self) -> HttpRequest { - let (mut head, _) = self.req.finish().into_parts(); + let (mut head, _) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); - let app_state = - AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)) } /// Complete request creation and generate `HttpRequest` and `Payload` instances pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { - let (mut head, payload) = self.req.finish().into_parts(); + let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; self.path.get_mut().update(&head.uri); - let app_state = - AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); + let app_state = AppInitServiceState::new(Rc::new(self.rmap), self.config.clone()); let req = HttpRequest::new(self.path, head, app_state, Rc::new(self.app_data)); @@ -569,7 +617,7 @@ impl TestRequest { } /// Complete request creation, calls service and waits for response future completion. - pub async fn send_request(self, app: &mut S) -> S::Response + pub async fn send_request(self, app: &S) -> S::Response where S: Service, Error = E>, E: std::fmt::Debug, @@ -577,439 +625,36 @@ impl TestRequest { let req = self.to_request(); call_service(app, req).await } + + #[cfg(test)] + pub fn set_server_hostname(&mut self, host: &str) { + self.config.set_host(host) + } } -/// Start test server with default configuration -/// -/// Test server is very simple server that simplify process of writing -/// integration tests cases for actix web applications. -/// -/// # Examples -/// -/// ```rust -/// use actix_web::{web, test, App, HttpResponse, Error}; -/// -/// async fn my_handler() -> Result { -/// Ok(HttpResponse::Ok().into()) -/// } -/// -/// #[actix_rt::test] -/// async fn test_example() { -/// let mut srv = test::start( -/// || App::new().service( -/// web::resource("/").to(my_handler)) -/// ); -/// -/// let req = srv.get("/"); -/// let response = req.send().await.unwrap(); -/// assert!(response.status().is_success()); -/// } -/// ``` -pub fn start(factory: F) -> TestServer -where - F: Fn() -> I + Send + Clone + 'static, - I: IntoServiceFactory, - S: ServiceFactory + 'static, - S::Error: Into + 'static, - S::InitError: fmt::Debug, - S::Response: Into> + 'static, - >::Future: 'static, - B: MessageBody + 'static, -{ - start_with(TestServerConfig::default(), factory) -} - -/// Start test server with custom configuration -/// -/// Test server could be configured in different ways, for details check -/// `TestServerConfig` docs. -/// -/// # Examples -/// -/// ```rust -/// use actix_web::{web, test, App, HttpResponse, Error}; -/// -/// async fn my_handler() -> Result { -/// Ok(HttpResponse::Ok().into()) -/// } -/// -/// #[actix_rt::test] -/// async fn test_example() { -/// let mut srv = test::start_with(test::config().h1(), || -/// App::new().service(web::resource("/").to(my_handler)) -/// ); -/// -/// let req = srv.get("/"); -/// let response = req.send().await.unwrap(); -/// assert!(response.status().is_success()); -/// } -/// ``` -pub fn start_with(cfg: TestServerConfig, factory: F) -> TestServer -where - F: Fn() -> I + Send + Clone + 'static, - I: IntoServiceFactory, - S: ServiceFactory + 'static, - S::Error: Into + 'static, - S::InitError: fmt::Debug, - S::Response: Into> + 'static, - >::Future: 'static, - B: MessageBody + 'static, -{ - let (tx, rx) = mpsc::channel(); - - let ssl = match cfg.stream { - StreamType::Tcp => false, - #[cfg(feature = "openssl")] - StreamType::Openssl(_) => true, - #[cfg(feature = "rustls")] - StreamType::Rustls(_) => true, +/// Reduces boilerplate code when testing expected response payloads. +#[cfg(test)] +macro_rules! assert_body_eq { + ($res:ident, $expected:expr) => { + assert_eq!( + ::actix_http::body::to_bytes($res.into_body()) + .await + .expect("body read should have succeeded"), + Bytes::from_static($expected), + ) }; - - // run server in separate thread - thread::spawn(move || { - let sys = System::new("actix-test-server"); - let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); - let local_addr = tcp.local_addr().unwrap(); - let factory = factory.clone(); - let cfg = cfg.clone(); - let ctimeout = cfg.client_timeout; - let builder = Server::build().workers(1).disable_signals(); - - let srv = match cfg.stream { - StreamType::Tcp => match cfg.tp { - HttpVer::Http1 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(false, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .h1(map_config(factory(), move |_| cfg.clone())) - .tcp() - }), - HttpVer::Http2 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(false, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .h2(map_config(factory(), move |_| cfg.clone())) - .tcp() - }), - HttpVer::Both => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(false, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .finish(map_config(factory(), move |_| cfg.clone())) - .tcp() - }), - }, - #[cfg(feature = "openssl")] - StreamType::Openssl(acceptor) => match cfg.tp { - HttpVer::Http1 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .h1(map_config(factory(), move |_| cfg.clone())) - .openssl(acceptor.clone()) - }), - HttpVer::Http2 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .h2(map_config(factory(), move |_| cfg.clone())) - .openssl(acceptor.clone()) - }), - HttpVer::Both => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .finish(map_config(factory(), move |_| cfg.clone())) - .openssl(acceptor.clone()) - }), - }, - #[cfg(feature = "rustls")] - StreamType::Rustls(config) => match cfg.tp { - HttpVer::Http1 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .h1(map_config(factory(), move |_| cfg.clone())) - .rustls(config.clone()) - }), - HttpVer::Http2 => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .h2(map_config(factory(), move |_| cfg.clone())) - .rustls(config.clone()) - }), - HttpVer::Both => builder.listen("test", tcp, move || { - let cfg = - AppConfig::new(true, local_addr, format!("{}", local_addr)); - HttpService::build() - .client_timeout(ctimeout) - .finish(map_config(factory(), move |_| cfg.clone())) - .rustls(config.clone()) - }), - }, - } - .unwrap(); - - sys.block_on(async { - let srv = srv.start(); - tx.send((System::current(), srv, local_addr)).unwrap(); - }); - - sys.run() - }); - - let (system, server, addr) = rx.recv().unwrap(); - - let client = { - let connector = { - #[cfg(feature = "openssl")] - { - use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - - 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)); - Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) - .ssl(builder.build()) - .finish() - } - #[cfg(not(feature = "openssl"))] - { - Connector::new() - .conn_lifetime(time::Duration::from_secs(0)) - .timeout(time::Duration::from_millis(30000)) - .finish() - } - }; - - Client::builder().connector(connector).finish() - }; - - TestServer { - ssl, - addr, - client, - system, - server, - } -} - -#[derive(Clone)] -pub struct TestServerConfig { - tp: HttpVer, - stream: StreamType, - client_timeout: u64, -} - -#[derive(Clone)] -enum HttpVer { - Http1, - Http2, - Both, -} - -#[derive(Clone)] -enum StreamType { - Tcp, - #[cfg(feature = "openssl")] - Openssl(open_ssl::ssl::SslAcceptor), - #[cfg(feature = "rustls")] - Rustls(rust_tls::ServerConfig), -} - -impl Default for TestServerConfig { - fn default() -> Self { - TestServerConfig::new() - } -} - -/// Create default test server config -pub fn config() -> TestServerConfig { - TestServerConfig::new() -} - -impl TestServerConfig { - /// Create default server configuration - pub(crate) fn new() -> TestServerConfig { - TestServerConfig { - tp: HttpVer::Both, - stream: StreamType::Tcp, - client_timeout: 5000, - } - } - - /// Start http/1.1 server only - pub fn h1(mut self) -> Self { - self.tp = HttpVer::Http1; - self - } - - /// Start http/2 server only - pub fn h2(mut self) -> Self { - self.tp = HttpVer::Http2; - self - } - - /// Start openssl server - #[cfg(feature = "openssl")] - pub fn openssl(mut self, acceptor: open_ssl::ssl::SslAcceptor) -> Self { - self.stream = StreamType::Openssl(acceptor); - self - } - - /// Start rustls server - #[cfg(feature = "rustls")] - pub fn rustls(mut self, config: rust_tls::ServerConfig) -> Self { - self.stream = StreamType::Rustls(config); - self - } - - /// Set server client timeout in milliseconds for first request. - pub fn client_timeout(mut self, val: u64) -> Self { - self.client_timeout = val; - self - } -} - -/// Get first available unused address -pub fn unused_addr() -> net::SocketAddr { - let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap(); - let socket = - Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap(); - socket.bind(&addr.into()).unwrap(); - socket.set_reuse_address(true).unwrap(); - let tcp = socket.into_tcp_listener(); - tcp.local_addr().unwrap() -} - -/// Test server controller -pub struct TestServer { - addr: net::SocketAddr, - client: awc::Client, - system: actix_rt::System, - ssl: bool, - server: Server, -} - -impl TestServer { - /// Construct test server url - pub fn addr(&self) -> net::SocketAddr { - self.addr - } - - /// Construct test server url - pub fn url(&self, uri: &str) -> String { - let scheme = if self.ssl { "https" } else { "http" }; - - if uri.starts_with('/') { - format!("{}://localhost:{}{}", scheme, self.addr.port(), uri) - } else { - format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri) - } - } - - /// Create `GET` request - pub fn get>(&self, path: S) -> ClientRequest { - self.client.get(self.url(path.as_ref()).as_str()) - } - - /// Create `POST` request - pub fn post>(&self, path: S) -> ClientRequest { - self.client.post(self.url(path.as_ref()).as_str()) - } - - /// Create `HEAD` request - pub fn head>(&self, path: S) -> ClientRequest { - self.client.head(self.url(path.as_ref()).as_str()) - } - - /// Create `PUT` request - pub fn put>(&self, path: S) -> ClientRequest { - self.client.put(self.url(path.as_ref()).as_str()) - } - - /// Create `PATCH` request - pub fn patch>(&self, path: S) -> ClientRequest { - self.client.patch(self.url(path.as_ref()).as_str()) - } - - /// Create `DELETE` request - pub fn delete>(&self, path: S) -> ClientRequest { - self.client.delete(self.url(path.as_ref()).as_str()) - } - - /// Create `OPTIONS` request - pub fn options>(&self, path: S) -> ClientRequest { - self.client.options(self.url(path.as_ref()).as_str()) - } - - /// Connect to test http server - pub fn request>(&self, method: Method, path: S) -> ClientRequest { - self.client.request(method, path.as_ref()) - } - - pub async fn load_body( - &mut self, - mut response: ClientResponse, - ) -> Result - where - S: Stream> + Unpin + 'static, - { - response.body().limit(10_485_760).await - } - - /// Connect to websocket server at a given path - pub async fn ws_at( - &mut self, - path: &str, - ) -> Result, awc::error::WsClientError> - { - let url = self.url(path); - let connect = self.client.ws(url).connect(); - connect.await.map(|(_, framed)| framed) - } - - /// Connect to a websocket server - pub async fn ws( - &mut self, - ) -> Result, awc::error::WsClientError> - { - self.ws_at("/").await - } - - /// Gracefully stop http server - pub async fn stop(self) { - self.server.stop(true).await; - self.system.stop(); - sleep(time::Duration::from_millis(100)).await; - } -} - -impl Drop for TestServer { - fn drop(&mut self) { - self.system.stop() - } } +#[cfg(test)] +pub(crate) use assert_body_eq; + #[cfg(test)] mod tests { - use actix_http::httpmessage::HttpMessage; - use serde::{Deserialize, Serialize}; use std::time::SystemTime; + use actix_http::HttpMessage; + use serde::{Deserialize, Serialize}; + use super::*; use crate::{http::header, web, App, HttpResponse, Responder}; @@ -1043,7 +688,7 @@ mod tests { #[actix_rt::test] async fn test_request_methods() { - let mut app = init_service( + let app = init_service( App::new().service( web::resource("/index.html") .route(web::put().to(|| HttpResponse::Ok().body("put!"))) @@ -1058,7 +703,7 @@ mod tests { .insert_header((header::CONTENT_TYPE, "application/json")) .to_request(); - let result = read_response(&mut app, put_req).await; + let result = read_response(&app, put_req).await; assert_eq!(result, Bytes::from_static(b"put!")); let patch_req = TestRequest::patch() @@ -1066,17 +711,17 @@ mod tests { .insert_header((header::CONTENT_TYPE, "application/json")) .to_request(); - let result = read_response(&mut app, patch_req).await; + let result = read_response(&app, patch_req).await; assert_eq!(result, Bytes::from_static(b"patch!")); let delete_req = TestRequest::delete().uri("/index.html").to_request(); - let result = read_response(&mut app, delete_req).await; + let result = read_response(&app, delete_req).await; assert_eq!(result, Bytes::from_static(b"delete!")); } #[actix_rt::test] async fn test_response() { - let mut app = init_service( + let app = init_service( App::new().service( web::resource("/index.html") .route(web::post().to(|| HttpResponse::Ok().body("welcome!"))), @@ -1089,13 +734,13 @@ mod tests { .insert_header((header::CONTENT_TYPE, "application/json")) .to_request(); - let result = read_response(&mut app, req).await; + let result = read_response(&app, req).await; assert_eq!(result, Bytes::from_static(b"welcome!")); } #[actix_rt::test] async fn test_send_request() { - let mut app = init_service( + let app = init_service( App::new().service( web::resource("/index.html") .route(web::get().to(|| HttpResponse::Ok().body("welcome!"))), @@ -1105,7 +750,7 @@ mod tests { let resp = TestRequest::get() .uri("/index.html") - .send_request(&mut app) + .send_request(&app) .await; let result = read_body(resp).await; @@ -1120,7 +765,7 @@ mod tests { #[actix_rt::test] async fn test_response_json() { - let mut app = init_service(App::new().service(web::resource("/people").route( + let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; @@ -1133,13 +778,13 @@ mod tests { .set_payload(payload) .to_request(); - let result: Person = read_response_json(&mut app, req).await; + let result: Person = read_response_json(&app, req).await; assert_eq!(&result.id, "12345"); } #[actix_rt::test] async fn test_body_json() { - let mut app = init_service(App::new().service(web::resource("/people").route( + let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; @@ -1150,7 +795,7 @@ mod tests { .uri("/people") .insert_header((header::CONTENT_TYPE, "application/json")) .set_payload(payload) - .send_request(&mut app) + .send_request(&app) .await; let result: Person = read_body_json(resp).await; @@ -1159,7 +804,7 @@ mod tests { #[actix_rt::test] async fn test_request_response_form() { - let mut app = init_service(App::new().service(web::resource("/people").route( + let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Form| HttpResponse::Ok().json(person)), ))) .await; @@ -1176,14 +821,14 @@ mod tests { assert_eq!(req.content_type(), "application/x-www-form-urlencoded"); - let result: Person = read_response_json(&mut app, req).await; + let result: Person = read_response_json(&app, req).await; assert_eq!(&result.id, "12345"); assert_eq!(&result.name, "User name"); } #[actix_rt::test] async fn test_request_response_json() { - let mut app = init_service(App::new().service(web::resource("/people").route( + let app = init_service(App::new().service(web::resource("/people").route( web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), ))) .await; @@ -1200,7 +845,7 @@ mod tests { assert_eq!(req.content_type(), "application/json"); - let result: Person = read_response_json(&mut app, req).await; + let result: Person = read_response_json(&app, req).await; assert_eq!(&result.id, "12345"); assert_eq!(&result.name, "User name"); } @@ -1213,21 +858,22 @@ mod tests { match res { Ok(value) => Ok(HttpResponse::Ok() .content_type("text/plain") - .body(format!("Async with block value: {}", value))), + .body(format!("Async with block value: {:?}", value))), Err(_) => panic!("Unexpected"), } } - let mut app = init_service( - App::new().service(web::resource("/index.html").to(async_with_block)), - ) - .await; + let app = + init_service(App::new().service(web::resource("/index.html").to(async_with_block))) + .await; let req = TestRequest::post().uri("/index.html").to_request(); let res = app.call(req).await.unwrap(); assert!(res.status().is_success()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_server_data() { async fn handler(data: web::Data) -> impl Responder { @@ -1235,7 +881,7 @@ mod tests { HttpResponse::Ok() } - let mut app = init_service( + let app = init_service( App::new() .data(10usize) .service(web::resource("/index.html").to(handler)), @@ -1246,55 +892,4 @@ mod tests { let res = app.call(req).await.unwrap(); assert!(res.status().is_success()); } - - #[actix_rt::test] - async fn test_actor() { - use crate::Error; - use actix::prelude::*; - - struct MyActor; - - impl Actor for MyActor { - type Context = Context; - } - - struct Num(usize); - - impl Message for Num { - type Result = usize; - } - - impl Handler for MyActor { - type Result = usize; - - fn handle(&mut self, msg: Num, _: &mut Self::Context) -> Self::Result { - msg.0 - } - } - - let addr = MyActor.start(); - - async fn actor_handler( - addr: Data>, - ) -> Result { - // `?` operator tests "actors" feature flag on actix-http - let res = addr.send(Num(1)).await?; - - if res == 1 { - Ok(HttpResponse::Ok()) - } else { - Ok(HttpResponse::BadRequest()) - } - } - - let srv = App::new() - .data(addr.clone()) - .service(web::resource("/").to(actor_handler)); - - let mut app = init_service(srv).await; - - let req = TestRequest::post().uri("/").to_request(); - let res = app.call(req).await.unwrap(); - assert!(res.status().is_success()); - } } diff --git a/src/types/either.rs b/src/types/either.rs index d72a14fd0..3c759736e 100644 --- a/src/types/either.rs +++ b/src/types/either.rs @@ -1,10 +1,18 @@ //! For either helper, see [`Either`]. +use std::{ + future::Future, + mem, + pin::Pin, + task::{Context, Poll}, +}; + use bytes::Bytes; -use futures_util::{future::LocalBoxFuture, FutureExt, TryFutureExt}; +use futures_core::ready; +use pin_project_lite::pin_project; use crate::{ - dev, + body, dev, web::{Form, Json}, Error, FromRequest, HttpRequest, HttpResponse, Responder, }; @@ -138,10 +146,12 @@ where L: Responder, R: Responder, { - fn respond_to(self, req: &HttpRequest) -> HttpResponse { + type Body = body::EitherBody; + + fn respond_to(self, req: &HttpRequest) -> HttpResponse { match self { - Either::Left(a) => a.respond_to(req), - Either::Right(b) => b.respond_to(req), + Either::Left(a) => a.respond_to(req).map_into_left_body(), + Either::Right(b) => b.respond_to(req).map_into_right_body(), } } } @@ -179,41 +189,113 @@ where R: FromRequest + 'static, { type Error = EitherExtractError; - type Future = LocalBoxFuture<'static, Result>; - type Config = (); + type Future = EitherExtractFut; fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { - let req2 = req.clone(); - - Bytes::from_request(req, payload) - .map_err(EitherExtractError::Bytes) - .and_then(|bytes| bytes_to_l_or_r(req2, bytes)) - .boxed_local() + EitherExtractFut { + req: req.clone(), + state: EitherExtractState::Bytes { + bytes: Bytes::from_request(req, payload), + }, + } } } -async fn bytes_to_l_or_r( - req: HttpRequest, - bytes: Bytes, -) -> Result, EitherExtractError> +pin_project! { + pub struct EitherExtractFut + where + R: FromRequest, + L: FromRequest, + { + req: HttpRequest, + #[pin] + state: EitherExtractState, + } +} + +pin_project! { + #[project = EitherExtractProj] + pub enum EitherExtractState + where + L: FromRequest, + R: FromRequest, + { + Bytes { + #[pin] + bytes: ::Future, + }, + Left { + #[pin] + left: L::Future, + fallback: Bytes, + }, + Right { + #[pin] + right: R::Future, + left_err: Option, + }, + } +} + +impl Future for EitherExtractFut where - L: FromRequest + 'static, - R: FromRequest + 'static, + L: FromRequest, + R: FromRequest, + LF: Future> + 'static, + RF: Future> + 'static, + LE: Into, + RE: Into, { - let fallback = bytes.clone(); - let a_err; + type Output = Result, EitherExtractError>; - let mut pl = payload_from_bytes(bytes); - match L::from_request(&req, &mut pl).await { - Ok(a_data) => return Ok(Either::Left(a_data)), - // store A's error for returning if B also fails - Err(err) => a_err = err, - }; - - let mut pl = payload_from_bytes(fallback); - match R::from_request(&req, &mut pl).await { - Ok(b_data) => return Ok(Either::Right(b_data)), - Err(b_err) => Err(EitherExtractError::Extract(a_err, b_err)), + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let mut this = self.project(); + let ready = loop { + let next = match this.state.as_mut().project() { + EitherExtractProj::Bytes { bytes } => { + let res = ready!(bytes.poll(cx)); + match res { + Ok(bytes) => { + let fallback = bytes.clone(); + let left = + L::from_request(this.req, &mut payload_from_bytes(bytes)); + EitherExtractState::Left { left, fallback } + } + Err(err) => break Err(EitherExtractError::Bytes(err)), + } + } + EitherExtractProj::Left { left, fallback } => { + let res = ready!(left.poll(cx)); + match res { + Ok(extracted) => break Ok(Either::Left(extracted)), + Err(left_err) => { + let right = R::from_request( + this.req, + &mut payload_from_bytes(mem::take(fallback)), + ); + EitherExtractState::Right { + left_err: Some(left_err), + right, + } + } + } + } + EitherExtractProj::Right { right, left_err } => { + let res = ready!(right.poll(cx)); + match res { + Ok(data) => break Ok(Either::Right(data)), + Err(err) => { + break Err(EitherExtractError::Extract( + left_err.take().unwrap(), + err, + )); + } + } + } + }; + this.state.set(next); + }; + Poll::Ready(ready) } } @@ -276,13 +358,12 @@ mod tests { .set_payload(Bytes::from_static(b"!@$%^&*()")) .to_http_parts(); - let payload = - Either::, Json>, Bytes>::from_request( - &req, &mut pl, - ) - .await - .unwrap() - .unwrap_right(); + let payload = Either::, Json>, Bytes>::from_request( + &req, &mut pl, + ) + .await + .unwrap() + .unwrap_right(); assert_eq!(&payload.as_ref(), &b"!@$%^&*()"); } @@ -294,15 +375,14 @@ mod tests { }) .to_http_parts(); - let form = - Either::, Json>, Bytes>::from_request( - &req, &mut pl, - ) - .await - .unwrap() - .unwrap_left() - .unwrap_right() - .into_inner(); + let form = Either::, Json>, Bytes>::from_request( + &req, &mut pl, + ) + .await + .unwrap() + .unwrap_left() + .unwrap_right() + .into_inner(); assert_eq!(&form.hello, "world"); } } diff --git a/src/types/form.rs b/src/types/form.rs index 96e09ee1c..9c09c6b73 100644 --- a/src/types/form.rs +++ b/src/types/form.rs @@ -1,6 +1,7 @@ //! For URL encoded form helper documentation, see [`Form`]. use std::{ + borrow::Cow, fmt, future::Future, ops, @@ -12,17 +13,16 @@ use std::{ use actix_http::Payload; use bytes::BytesMut; use encoding_rs::{Encoding, UTF_8}; -use futures_util::{ - future::{FutureExt, LocalBoxFuture}, - StreamExt, -}; +use futures_core::{future::LocalBoxFuture, ready}; +use futures_util::{FutureExt as _, StreamExt as _}; use serde::{de::DeserializeOwned, Serialize}; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ - error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, web, - Error, HttpMessage, HttpRequest, HttpResponse, Responder, + body::EitherBody, error::UrlencodedError, extract::FromRequest, + http::header::CONTENT_LENGTH, web, Error, HttpMessage, HttpRequest, HttpResponse, + Responder, }; /// URL encoded payload extractor and responder. @@ -31,9 +31,9 @@ use crate::{ /// /// # Extractor /// To extract typed data from a request body, the inner type `T` must implement the -/// [`serde::Deserialize`] trait. +/// [`DeserializeOwned`] trait. /// -/// Use [`FormConfig`] to configure extraction process. +/// Use [`FormConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web}; @@ -82,7 +82,11 @@ use crate::{ /// }) /// } /// ``` -#[derive(PartialEq, Eq, PartialOrd, Ord)] +/// +/// # Panics +/// URL encoded forms consist of unordered `key=value` pairs, therefore they cannot be decoded into +/// any type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Form(pub T); impl Form { @@ -106,43 +110,66 @@ impl ops::DerefMut for Form { } } +impl Serialize for Form +where + T: Serialize, +{ + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) + } +} + /// See [here](#extractor) for example of usage as an extractor. impl FromRequest for Form where T: DeserializeOwned + 'static, { - type Config = FormConfig; type Error = Error; - type Future = LocalBoxFuture<'static, Result>; + type Future = FormExtractFut; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { - let req2 = req.clone(); - let (limit, err_handler) = req - .app_data::() - .or_else(|| { - req.app_data::>() - .map(|d| d.as_ref()) - }) - .map(|c| (c.limit, c.err_handler.clone())) - .unwrap_or((16384, None)); + let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone(); - UrlEncoded::new(req, payload) - .limit(limit) - .map(move |res| match res { - Err(err) => match err_handler { - Some(err_handler) => Err((err_handler)(err, &req2)), - None => Err(err.into()), - }, - Ok(item) => Ok(Form(item)), - }) - .boxed_local() + FormExtractFut { + fut: UrlEncoded::new(req, payload).limit(limit), + req: req.clone(), + err_handler, + } } } -impl fmt::Debug for Form { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) +type FormErrHandler = Option Error>>; + +pub struct FormExtractFut { + fut: UrlEncoded, + err_handler: FormErrHandler, + req: HttpRequest, +} + +impl Future for FormExtractFut +where + T: DeserializeOwned + 'static, +{ + type Output = Result, Error>; + + fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.get_mut(); + + let res = ready!(Pin::new(&mut this.fut).poll(cx)); + + let res = match res { + Err(err) => match &this.err_handler { + Some(err_handler) => Err((err_handler)(err, &this.req)), + None => Err(err.into()), + }, + Ok(item) => Ok(Form(item)), + }; + + Poll::Ready(res) } } @@ -154,12 +181,21 @@ impl fmt::Display for Form { /// See [here](#responder) for example of usage as a handler return type. impl Responder for Form { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_urlencoded::to_string(&self.0) { - Ok(body) => HttpResponse::Ok() + Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_WWW_FORM_URLENCODED) - .body(body), - Err(err) => HttpResponse::from_error(err.into()), + .message_body(body) + { + Ok(res) => res.map_into_left_body(), + Err(err) => HttpResponse::from_error(err).map_into_right_body(), + }, + + Err(err) => { + HttpResponse::from_error(UrlencodedError::Serialize(err)).map_into_right_body() + } } } } @@ -189,7 +225,7 @@ impl Responder for Form { #[derive(Clone)] pub struct FormConfig { limit: usize, - err_handler: Option Error>>, + err_handler: FormErrHandler, } impl FormConfig { @@ -207,14 +243,26 @@ impl FormConfig { self.err_handler = Some(Rc::new(f)); self } + + /// Extract payload config from app data. + /// + /// Checks both `T` and `Data`, in that order, and falls back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } } +/// Allow shared refs used as default. +const DEFAULT_CONFIG: FormConfig = FormConfig { + limit: 16_384, // 2^14 bytes (~16kB) + err_handler: None, +}; + impl Default for FormConfig { fn default() -> Self { - FormConfig { - limit: 16_384, // 2^14 bytes (~16kB) - err_handler: None, - } + DEFAULT_CONFIG } } @@ -226,9 +274,9 @@ impl Default for FormConfig { /// - content type is not `application/x-www-form-urlencoded` /// - content length is greater than [limit](UrlEncoded::limit()) pub struct UrlEncoded { - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] stream: Option>, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] stream: Option, limit: usize, @@ -264,10 +312,15 @@ impl UrlEncoded { } }; - #[cfg(feature = "compress")] - let payload = Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let payload = payload.take(); + let payload = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; UrlEncoded { encoding, @@ -342,16 +395,14 @@ where } if encoding == UTF_8 { - serde_urlencoded::from_bytes::(&body) - .map_err(|_| UrlencodedError::Parse) + serde_urlencoded::from_bytes::(&body).map_err(UrlencodedError::Parse) } else { let body = encoding .decode_without_bom_handling_and_without_replacement(&body) - .map(|s| s.into_owned()) - .ok_or(UrlencodedError::Parse)?; + .map(Cow::into_owned) + .ok_or(UrlencodedError::Encoding)?; - serde_urlencoded::from_str::(&body) - .map_err(|_| UrlencodedError::Parse) + serde_urlencoded::from_str::(&body).map_err(UrlencodedError::Parse) } } .boxed_local(), @@ -367,11 +418,14 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::http::{ - header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, - StatusCode, - }; use crate::test::TestRequest; + use crate::{ + http::{ + header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, + StatusCode, + }, + test::assert_body_eq, + }; #[derive(Deserialize, Serialize, Debug, PartialEq)] struct Info { @@ -402,12 +456,8 @@ mod tests { UrlencodedError::Overflow { .. } => { matches!(other, UrlencodedError::Overflow { .. }) } - UrlencodedError::UnknownLength => { - matches!(other, UrlencodedError::UnknownLength) - } - UrlencodedError::ContentType => { - matches!(other, UrlencodedError::ContentType) - } + UrlencodedError::UnknownLength => matches!(other, UrlencodedError::UnknownLength), + UrlencodedError::ContentType => matches!(other, UrlencodedError::ContentType), _ => false, } } @@ -483,15 +533,13 @@ mod tests { hello: "world".to_string(), counter: 123, }); - let resp = form.respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); + let res = form.respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(CONTENT_TYPE).unwrap(), + res.headers().get(CONTENT_TYPE).unwrap(), HeaderValue::from_static("application/x-www-form-urlencoded") ); - - use crate::responder::tests::BodyTest; - assert_eq!(resp.body().bin_ref(), b"hello=world&counter=123"); + assert_body_eq!(res, b"hello=world&counter=123"); } #[actix_rt::test] diff --git a/src/types/header.rs b/src/types/header.rs new file mode 100644 index 000000000..6ea77faf6 --- /dev/null +++ b/src/types/header.rs @@ -0,0 +1,102 @@ +//! For header extractor helper documentation, see [`Header`](crate::types::Header). + +use std::{fmt, ops}; + +use actix_utils::future::{err, ok, Ready}; + +use crate::{ + dev::Payload, error::ParseError, extract::FromRequest, http::header::Header as ParseHeader, + HttpRequest, +}; + +/// Extract typed headers from the request. +/// +/// To extract a header, the inner type `T` must implement the +/// [`Header`](crate::http::header::Header) trait. +/// +/// # Examples +/// ``` +/// use actix_web::{get, web, http::header}; +/// +/// #[get("/")] +/// async fn index(date: web::Header) -> String { +/// format!("Request was sent at {}", date.to_string()) +/// } +/// ``` +#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] +pub struct Header(pub T); + +impl Header { + /// Unwrap into the inner `T` value. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl ops::Deref for Header { + type Target = T; + + fn deref(&self) -> &T { + &self.0 + } +} + +impl ops::DerefMut for Header { + fn deref_mut(&mut self) -> &mut T { + &mut self.0 + } +} + +impl fmt::Display for Header +where + T: fmt::Display, +{ + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + fmt::Display::fmt(&self.0, f) + } +} + +impl FromRequest for Header +where + T: ParseHeader, +{ + type Error = ParseError; + type Future = Ready>; + + #[inline] + fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { + match ParseHeader::parse(req) { + Ok(header) => ok(Header(header)), + Err(e) => err(e), + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::http::{header, Method}; + use crate::test::TestRequest; + + #[actix_rt::test] + async fn test_header_extract() { + let (req, mut pl) = TestRequest::default() + .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) + .insert_header((header::ALLOW, header::Allow(vec![Method::GET]))) + .to_http_parts(); + + let s = Header::::from_request(&req, &mut pl) + .await + .unwrap(); + assert_eq!(s.into_inner().0, mime::APPLICATION_JSON); + + let s = Header::::from_request(&req, &mut pl) + .await + .unwrap(); + assert_eq!(s.into_inner().0, vec![Method::GET]); + + assert!(Header::::from_request(&req, &mut pl) + .await + .is_err()); + } +} diff --git a/src/types/json.rs b/src/types/json.rs index 41618b409..2b4d220e2 100644 --- a/src/types/json.rs +++ b/src/types/json.rs @@ -11,14 +11,15 @@ use std::{ }; use bytes::BytesMut; -use futures_util::{ready, stream::Stream}; +use futures_core::{ready, stream::Stream as _}; use serde::{de::DeserializeOwned, Serialize}; use actix_http::Payload; -#[cfg(feature = "compress")] +#[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ + body::EitherBody, error::{Error, JsonPayloadError}, extract::FromRequest, http::header::CONTENT_LENGTH, @@ -34,7 +35,7 @@ use crate::{ /// To extract typed data from a request body, the inner type `T` must implement the /// [`serde::Deserialize`] trait. /// -/// Use [`JsonConfig`] to configure extraction process. +/// Use [`JsonConfig`] to configure extraction options. /// /// ``` /// use actix_web::{post, web, App}; @@ -73,6 +74,7 @@ use crate::{ /// }) /// } /// ``` +#[derive(Debug)] pub struct Json(pub T); impl Json { @@ -96,21 +98,18 @@ impl ops::DerefMut for Json { } } -impl fmt::Debug for Json -where - T: fmt::Debug, -{ +impl fmt::Display for Json { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - write!(f, "Json: {:?}", self.0) + fmt::Display::fmt(&self.0, f) } } -impl fmt::Display for Json -where - T: fmt::Display, -{ - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - fmt::Display::fmt(&self.0, f) +impl Serialize for Json { + fn serialize(&self, serializer: S) -> Result + where + S: serde::Serializer, + { + self.0.serialize(serializer) } } @@ -118,36 +117,42 @@ where /// /// If serialization failed impl Responder for Json { - fn respond_to(self, _: &HttpRequest) -> HttpResponse { + type Body = EitherBody; + + fn respond_to(self, _: &HttpRequest) -> HttpResponse { match serde_json::to_string(&self.0) { - Ok(body) => HttpResponse::Ok() + Ok(body) => match HttpResponse::Ok() .content_type(mime::APPLICATION_JSON) - .body(body), - Err(err) => HttpResponse::from_error(err.into()), + .message_body(body) + { + Ok(res) => res.map_into_left_body(), + Err(err) => HttpResponse::from_error(err).map_into_right_body(), + }, + + Err(err) => { + HttpResponse::from_error(JsonPayloadError::Serialize(err)).map_into_right_body() + } } } } /// See [here](#extractor) for example of usage as an extractor. -impl FromRequest for Json -where - T: DeserializeOwned + 'static, -{ +impl FromRequest for Json { type Error = Error; type Future = JsonExtractFut; - type Config = JsonConfig; #[inline] fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { let config = JsonConfig::from_req(req); let limit = config.limit; - let ctype = config.content_type.as_deref(); + let ctype_required = config.content_type_required; + let ctype_fn = config.content_type.as_deref(); let err_handler = config.err_handler.clone(); JsonExtractFut { req: Some(req.clone()), - fut: JsonBody::new(req, payload, ctype).limit(limit), + fut: JsonBody::new(req, payload, ctype_fn, ctype_required).limit(limit), err_handler, } } @@ -162,10 +167,7 @@ pub struct JsonExtractFut { err_handler: JsonErrorHandler, } -impl Future for JsonExtractFut -where - T: DeserializeOwned + 'static, -{ +impl Future for JsonExtractFut { type Output = Result, Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -197,7 +199,7 @@ where /// `Json` extractor configuration. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{error, post, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; @@ -221,7 +223,7 @@ where /// .content_type(|mime| mime == mime::TEXT_PLAIN) /// // use custom error handler /// .error_handler(|err, req| { -/// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() +/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() /// }); /// /// App::new() @@ -233,10 +235,11 @@ pub struct JsonConfig { limit: usize, err_handler: JsonErrorHandler, content_type: Option bool + Send + Sync>>, + content_type_required: bool, } impl JsonConfig { - /// Set maximum accepted payload size. By default this limit is 32kB. + /// Set maximum accepted payload size. By default this limit is 2MB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self @@ -260,6 +263,12 @@ impl JsonConfig { self } + /// Sets whether or not the request must have a `Content-Type` header to be parsed. + pub fn content_type_required(mut self, content_type_required: bool) -> Self { + self.content_type_required = content_type_required; + self + } + /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall /// back to the default payload config. fn from_req(req: &HttpRequest) -> &Self { @@ -269,11 +278,14 @@ impl JsonConfig { } } +const DEFAULT_LIMIT: usize = 2_097_152; // 2 mb + /// Allow shared refs used as default. const DEFAULT_CONFIG: JsonConfig = JsonConfig { - limit: 32_768, // 2^15 bytes, (~32kB) + limit: DEFAULT_LIMIT, err_handler: None, content_type: None, + content_type_required: true, }; impl Default for JsonConfig { @@ -284,19 +296,22 @@ impl Default for JsonConfig { /// Future that resolves to some `T` when parsed from a JSON payload. /// -/// Form can be deserialized from any type `T` that implements [`serde::Deserialize`]. +/// Can deserialize any type `T` that implements [`Deserialize`][serde::Deserialize]. /// /// Returns error if: -/// - content type is not `application/json` -/// - content length is greater than [limit](JsonBody::limit()) +/// - `Content-Type` is not `application/json` when `ctype_required` (passed to [`new`][Self::new]) +/// is `true`. +/// - `Content-Length` is greater than [limit](JsonBody::limit()). +/// - The payload, when consumed, is not valid JSON. pub enum JsonBody { Error(Option), Body { limit: usize, + /// Length as reported by `Content-Length` header, if present. length: Option, - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] payload: Decompress, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] payload: Payload, buf: BytesMut, _res: PhantomData, @@ -305,27 +320,27 @@ pub enum JsonBody { impl Unpin for JsonBody {} -impl JsonBody -where - T: DeserializeOwned + 'static, -{ +impl JsonBody { /// Create a new future to decode a JSON request payload. #[allow(clippy::borrow_interior_mutable_const)] pub fn new( req: &HttpRequest, payload: &mut Payload, - ctype: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, + ctype_fn: Option<&(dyn Fn(mime::Mime) -> bool + Send + Sync)>, + ctype_required: bool, ) -> Self { // check content-type - let json = if let Ok(Some(mime)) = req.mime_type() { + let can_parse_json = if let Ok(Some(mime)) = req.mime_type() { mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) - || ctype.map_or(false, |predicate| predicate(mime)) + || ctype_fn.map_or(false, |predicate| predicate(mime)) } else { - false + // if `ctype_required` is false, assume payload is + // json even when content-type header is missing + !ctype_required }; - if !json { + if !can_parse_json { return JsonBody::Error(Some(JsonPayloadError::ContentType)); } @@ -335,17 +350,22 @@ where .and_then(|l| l.to_str().ok()) .and_then(|s| s.parse::().ok()); - // Notice the content_length is not checked against limit of json config here. + // Notice the content-length is not checked against limit of json config here. // As the internal usage always call JsonBody::limit after JsonBody::new. // And limit check to return an error variant of JsonBody happens there. - #[cfg(feature = "compress")] - let payload = Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let payload = payload.take(); + let payload = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; JsonBody::Body { - limit: 262_144, + limit: DEFAULT_LIMIT, length, payload, buf: BytesMut::with_capacity(8192), @@ -353,7 +373,7 @@ where } } - /// Set maximum accepted payload size. The default limit is 256kB. + /// Set maximum accepted payload size. The default limit is 2MB. pub fn limit(self, limit: usize) -> Self { match self { JsonBody::Body { @@ -364,7 +384,10 @@ where } => { if let Some(len) = length { if len > limit { - return JsonBody::Error(Some(JsonPayloadError::Overflow)); + return JsonBody::Error(Some(JsonPayloadError::OverflowKnownLength { + length: len, + limit, + })); } } @@ -381,10 +404,7 @@ where } } -impl Future for JsonBody -where - T: DeserializeOwned + 'static, -{ +impl Future for JsonBody { type Output = Result; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { @@ -401,14 +421,18 @@ where match res { Some(chunk) => { let chunk = chunk?; - if (buf.len() + chunk.len()) > *limit { - return Poll::Ready(Err(JsonPayloadError::Overflow)); + let buf_len = buf.len() + chunk.len(); + if buf_len > *limit { + return Poll::Ready(Err(JsonPayloadError::Overflow { + limit: *limit, + })); } else { buf.extend_from_slice(&chunk); } } None => { - let json = serde_json::from_slice::(&buf)?; + let json = serde_json::from_slice::(buf) + .map_err(JsonPayloadError::Deserialize)?; return Poll::Ready(Ok(json)); } } @@ -430,7 +454,7 @@ mod tests { header::{self, CONTENT_LENGTH, CONTENT_TYPE}, StatusCode, }, - test::{load_stream, TestRequest}, + test::{assert_body_eq, load_body, TestRequest}, }; #[derive(Serialize, Deserialize, PartialEq, Debug)] @@ -440,10 +464,13 @@ mod tests { fn json_eq(err: JsonPayloadError, other: JsonPayloadError) -> bool { match err { - JsonPayloadError::Overflow => matches!(other, JsonPayloadError::Overflow), - JsonPayloadError::ContentType => { - matches!(other, JsonPayloadError::ContentType) + JsonPayloadError::Overflow { .. } => { + matches!(other, JsonPayloadError::Overflow { .. }) } + JsonPayloadError::OverflowKnownLength { .. } => { + matches!(other, JsonPayloadError::OverflowKnownLength { .. }) + } + JsonPayloadError::ContentType => matches!(other, JsonPayloadError::ContentType), _ => false, } } @@ -455,15 +482,13 @@ mod tests { let j = Json(MyObject { name: "test".to_string(), }); - let resp = j.respond_to(&req); - assert_eq!(resp.status(), StatusCode::OK); + let res = j.respond_to(&req); + assert_eq!(res.status(), StatusCode::OK); assert_eq!( - resp.headers().get(header::CONTENT_TYPE).unwrap(), + res.headers().get(header::CONTENT_TYPE).unwrap(), header::HeaderValue::from_static("application/json") ); - - use crate::responder::tests::BodyTest; - assert_eq!(resp.body().bin_ref(), b"{\"name\":\"test\"}"); + assert_body_eq!(res, b"{\"name\":\"test\"}"); } #[actix_rt::test] @@ -482,17 +507,17 @@ mod tests { let msg = MyObject { name: "invalid request".to_string(), }; - let resp = HttpResponse::BadRequest() - .body(serde_json::to_string(&msg).unwrap()); + let resp = + HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); InternalError::from_response(err, resp).into() })) .to_http_parts(); let s = Json::::from_request(&req, &mut pl).await; - let mut resp = HttpResponse::from_error(s.err().unwrap()); + let resp = HttpResponse::from_error(s.err().unwrap()); assert_eq!(resp.status(), StatusCode::BAD_REQUEST); - let body = load_stream(resp.take_body()).await.unwrap(); + let body = load_body(resp.into_body()).await.unwrap(); let msg: MyObject = serde_json::from_slice(&body).unwrap(); assert_eq!(msg.name, "invalid request"); } @@ -535,7 +560,7 @@ mod tests { let s = Json::::from_request(&req, &mut pl).await; assert!(format!("{}", s.err().unwrap()) - .contains("Json payload size is bigger than allowed")); + .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).")); let (req, mut pl) = TestRequest::default() .insert_header(( @@ -560,7 +585,7 @@ mod tests { #[actix_rt::test] async fn test_json_body() { let (req, mut pl) = TestRequest::default().to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() @@ -569,7 +594,7 @@ mod tests { header::HeaderValue::from_static("application/text"), )) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert!(json_eq(json.err().unwrap(), JsonPayloadError::ContentType)); let (req, mut pl) = TestRequest::default() @@ -583,10 +608,33 @@ mod tests { )) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None) + let json = JsonBody::::new(&req, &mut pl, None, true) .limit(100) .await; - assert!(json_eq(json.err().unwrap(), JsonPayloadError::Overflow)); + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::OverflowKnownLength { + length: 10000, + limit: 100 + } + )); + + let (req, mut pl) = TestRequest::default() + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("application/json"), + )) + .set_payload(Bytes::from_static(&[0u8; 1000])) + .to_http_parts(); + + let json = JsonBody::::new(&req, &mut pl, None, true) + .limit(100) + .await; + + assert!(json_eq( + json.err().unwrap(), + JsonPayloadError::Overflow { limit: 100 } + )); let (req, mut pl) = TestRequest::default() .insert_header(( @@ -600,7 +648,7 @@ mod tests { .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) .to_http_parts(); - let json = JsonBody::::new(&req, &mut pl, None).await; + let json = JsonBody::::new(&req, &mut pl, None, true).await; assert_eq!( json.ok().unwrap(), MyObject { @@ -670,6 +718,21 @@ mod tests { assert!(s.is_err()) } + #[actix_rt::test] + async fn test_json_with_no_content_type() { + let (req, mut pl) = TestRequest::default() + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .set_payload(Bytes::from_static(b"{\"name\": \"test\"}")) + .app_data(JsonConfig::default().content_type_required(false)) + .to_http_parts(); + + let s = Json::::from_request(&req, &mut pl).await; + assert!(s.is_ok()) + } + #[actix_rt::test] async fn test_with_config_in_data_wrapper() { let (req, mut pl) = TestRequest::default() @@ -683,6 +746,7 @@ mod tests { assert!(s.is_err()); let err_str = s.err().unwrap().to_string(); - assert!(err_str.contains("Json payload size is bigger than allowed")); + assert!(err_str + .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).")); } } diff --git a/src/types/mod.rs b/src/types/mod.rs index a062c351e..461d771eb 100644 --- a/src/types/mod.rs +++ b/src/types/mod.rs @@ -3,6 +3,7 @@ // TODO: review visibility mod either; pub(crate) mod form; +mod header; pub(crate) mod json; mod path; pub(crate) mod payload; @@ -11,6 +12,7 @@ pub(crate) mod readlines; pub use self::either::{Either, EitherExtractError}; pub use self::form::{Form, FormConfig}; +pub use self::header::Header; pub use self::json::{Json, JsonConfig}; pub use self::path::{Path, PathConfig}; pub use self::payload::{Payload, PayloadConfig}; diff --git a/src/types/path.rs b/src/types/path.rs index 9ee5106d0..4b60d27c0 100644 --- a/src/types/path.rs +++ b/src/types/path.rs @@ -2,25 +2,28 @@ use std::{fmt, ops, sync::Arc}; -use actix_http::error::{Error, ErrorNotFound}; use actix_router::PathDeserializer; -use futures_util::future::{ready, Ready}; +use actix_utils::future::{ready, Ready}; use serde::de; -use crate::{dev::Payload, error::PathError, FromRequest, HttpRequest}; +use crate::{ + dev::Payload, + error::{Error, ErrorNotFound, PathError}, + FromRequest, HttpRequest, +}; /// Extract typed data from request path segments. /// -/// Use [`PathConfig`] to configure extraction process. +/// Use [`PathConfig`] to configure extraction option. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{get, web}; /// /// // extract path info from "/{name}/{count}/index.html" into tuple /// // {name} - deserialize a String /// // {count} - deserialize a u32 -/// #[get("/")] +/// #[get("/{name}/{count}/index.html")] /// async fn index(path: web::Path<(String, u32)>) -> String { /// let (name, count) = path.into_inner(); /// format!("Welcome {}! {}", name, count) @@ -40,12 +43,12 @@ use crate::{dev::Payload, error::PathError, FromRequest, HttpRequest}; /// } /// /// // extract `Info` from a path using serde -/// #[get("/")] +/// #[get("/{name}")] /// async fn index(info: web::Path) -> String { /// format!("Welcome {}!", info.name) /// } /// ``` -#[derive(PartialEq, Eq, PartialOrd, Ord)] +#[derive(PartialEq, Eq, PartialOrd, Ord, Debug)] pub struct Path(T); impl Path { @@ -81,48 +84,40 @@ impl From for Path { } } -impl fmt::Debug for Path { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - impl fmt::Display for Path { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Path where T: de::DeserializeOwned, { type Error = Error; - type Future = Ready>; - type Config = PathConfig; + type Future = Ready>; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req - .app_data::() - .map(|c| c.ehandler.clone()) - .unwrap_or(None); + .app_data::() + .and_then(|c| c.err_handler.clone()); ready( de::Deserialize::deserialize(PathDeserializer::new(req.match_info())) .map(Path) - .map_err(move |e| { + .map_err(move |err| { log::debug!( "Failed during Path extractor deserialization. \ Request path: {:?}", req.path() ); if let Some(error_handler) = error_handler { - let e = PathError::Deserialize(e); + let e = PathError::Deserialize(err); (error_handler)(e, req) } else { - ErrorNotFound(e) + ErrorNotFound(err) } }), ) @@ -155,7 +150,7 @@ where /// .app_data(PathConfig::default().error_handler(|err, req| { /// error::InternalError::from_response( /// err, -/// HttpResponse::Conflict().finish(), +/// HttpResponse::Conflict().into(), /// ) /// .into() /// })) @@ -163,9 +158,9 @@ where /// ); /// } /// ``` -#[derive(Clone)] +#[derive(Clone, Default)] pub struct PathConfig { - ehandler: Option Error + Send + Sync>>, + err_handler: Option Error + Send + Sync>>, } impl PathConfig { @@ -174,17 +169,11 @@ impl PathConfig { where F: Fn(PathError, &HttpRequest) -> Error + Send + Sync + 'static, { - self.ehandler = Some(Arc::new(f)); + self.err_handler = Some(Arc::new(f)); self } } -impl Default for PathConfig { - fn default() -> Self { - PathConfig { ehandler: None } - } -} - #[cfg(test)] mod tests { use actix_router::ResourceDef; @@ -213,7 +202,7 @@ mod tests { let resource = ResourceDef::new("/{value}/"); let mut req = TestRequest::with_uri("/32/").to_srv_request(); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); assert_eq!(*Path::::from_request(&req, &mut pl).await.unwrap(), 32); @@ -225,7 +214,7 @@ mod tests { let resource = ResourceDef::new("/{key}/{value}/"); let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let (Path(res),) = <(Path<(String, String)>,)>::from_request(&req, &mut pl) @@ -235,11 +224,9 @@ mod tests { assert_eq!(res.1, "user1"); let (Path(a), Path(b)) = - <(Path<(String, String)>, Path<(String, String)>)>::from_request( - &req, &mut pl, - ) - .await - .unwrap(); + <(Path<(String, String)>, Path<(String, String)>)>::from_request(&req, &mut pl) + .await + .unwrap(); assert_eq!(a.0, "name"); assert_eq!(a.1, "user1"); assert_eq!(b.0, "name"); @@ -253,7 +240,7 @@ mod tests { let mut req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let mut s = Path::::from_request(&req, &mut pl).await.unwrap(); @@ -263,7 +250,7 @@ mod tests { assert_eq!(s.value, "user2"); assert_eq!( format!("{}, {:?}", s, s), - "MyStruct(name, user2), MyStruct { key: \"name\", value: \"user2\" }" + "MyStruct(name, user2), Path(MyStruct { key: \"name\", value: \"user2\" })" ); let s = s.into_inner(); assert_eq!(s.value, "user2"); @@ -276,7 +263,7 @@ mod tests { let mut req = TestRequest::with_uri("/name/32/").to_srv_request(); let resource = ResourceDef::new("/{key}/{value}/"); - resource.match_path(req.match_info_mut()); + resource.capture_match_info(req.match_info_mut()); let (req, mut pl) = req.into_parts(); let s = Path::::from_request(&req, &mut pl).await.unwrap(); @@ -300,18 +287,15 @@ mod tests { async fn test_custom_err_handler() { let (req, mut pl) = TestRequest::with_uri("/name/user1/") .app_data(PathConfig::default().error_handler(|err, _| { - error::InternalError::from_response( - err, - HttpResponse::Conflict().finish(), - ) - .into() + error::InternalError::from_response(err, HttpResponse::Conflict().finish()) + .into() })) .to_http_parts(); let s = Path::<(usize,)>::from_request(&req, &mut pl) .await .unwrap_err(); - let res: HttpResponse = s.into(); + let res = HttpResponse::from_error(s); assert_eq!(res.status(), http::StatusCode::CONFLICT); } diff --git a/src/types/payload.rs b/src/types/payload.rs index ad0bfd14f..73987def5 100644 --- a/src/types/payload.rs +++ b/src/types/payload.rs @@ -1,32 +1,33 @@ //! Basic binary and string payload extractors. use std::{ + borrow::Cow, future::Future, pin::Pin, str, task::{Context, Poll}, }; -use actix_http::error::{ErrorBadRequest, PayloadError}; +use actix_http::error::PayloadError; +use actix_utils::future::{ready, Either, Ready}; use bytes::{Bytes, BytesMut}; use encoding_rs::{Encoding, UTF_8}; -use futures_core::stream::Stream; -use futures_util::{ - future::{ready, Either, ErrInto, Ready, TryFutureExt as _}, - ready, -}; +use futures_core::{ready, stream::Stream}; use mime::Mime; -use crate::{dev, http::header, web, Error, FromRequest, HttpMessage, HttpRequest}; +use crate::{ + dev, error::ErrorBadRequest, http::header, web, Error, FromRequest, HttpMessage, + HttpRequest, +}; /// Extract a request's raw payload stream. /// /// See [`PayloadConfig`] for important notes when using this advanced extractor. /// -/// # Usage +/// # Examples /// ``` /// use std::future::Future; -/// use futures_util::stream::{Stream, StreamExt}; +/// use futures_util::stream::StreamExt as _; /// use actix_web::{post, web}; /// /// // `body: web::Payload` parameter extracts raw payload stream from request @@ -42,11 +43,12 @@ use crate::{dev, http::header, web, Error, FromRequest, HttpMessage, HttpRequest /// Ok(format!("Request Body Bytes:\n{:?}", bytes)) /// } /// ``` -pub struct Payload(pub crate::dev::Payload); +pub struct Payload(dev::Payload); impl Payload { /// Unwrap to inner Payload type. - pub fn into_inner(self) -> crate::dev::Payload { + #[inline] + pub fn into_inner(self) -> dev::Payload { self.0 } } @@ -55,17 +57,13 @@ impl Stream for Payload { type Item = Result; #[inline] - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { Pin::new(&mut self.0).poll_next(cx) } } -/// See [here](#usage) for example of usage as an extractor. +/// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Payload { - type Config = PayloadConfig; type Error = Error; type Future = Ready>; @@ -81,7 +79,7 @@ impl FromRequest for Payload { /// /// Use [`PayloadConfig`] to configure extraction process. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{post, web}; /// @@ -92,9 +90,8 @@ impl FromRequest for Payload { /// } /// ``` impl FromRequest for Bytes { - type Config = PayloadConfig; type Error = Error; - type Future = Either, Ready>>; + type Future = Either>>; #[inline] fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { @@ -102,12 +99,25 @@ impl FromRequest for Bytes { let cfg = PayloadConfig::from_req(req); if let Err(err) = cfg.check_mimetype(req) { - return Either::Right(ready(Err(err))); + return Either::right(ready(Err(err))); } - let limit = cfg.limit; - let fut = HttpMessageBody::new(req, payload).limit(limit); - Either::Left(fut.err_into()) + Either::left(BytesExtractFut { + body_fut: HttpMessageBody::new(req, payload).limit(cfg.limit), + }) + } +} + +/// Future for `Bytes` extractor. +pub struct BytesExtractFut { + body_fut: HttpMessageBody, +} + +impl<'a> Future for BytesExtractFut { + type Output = Result; + + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + Pin::new(&mut self.body_fut).poll(cx).map_err(Into::into) } } @@ -115,10 +125,9 @@ impl FromRequest for Bytes { /// /// Text extractor automatically decode body according to the request's charset. /// -/// [**PayloadConfig**](PayloadConfig) allows to configure -/// extraction process. +/// Use [`PayloadConfig`] to configure extraction process. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{post, web, FromRequest}; /// @@ -128,7 +137,6 @@ impl FromRequest for Bytes { /// format!("Body {}!", text) /// } impl FromRequest for String { - type Config = PayloadConfig; type Error = Error; type Future = Either>>; @@ -138,21 +146,22 @@ impl FromRequest for String { // check content-type if let Err(err) = cfg.check_mimetype(req) { - return Either::Right(ready(Err(err))); + return Either::right(ready(Err(err))); } // check charset let encoding = match req.encoding() { Ok(enc) => enc, - Err(err) => return Either::Right(ready(Err(err.into()))), + Err(err) => return Either::right(ready(Err(err.into()))), }; let limit = cfg.limit; let body_fut = HttpMessageBody::new(req, payload).limit(limit); - Either::Left(StringExtractFut { body_fut, encoding }) + Either::left(StringExtractFut { body_fut, encoding }) } } +/// Future for `String` extractor. pub struct StringExtractFut { body_fut: HttpMessageBody, encoding: &'static Encoding, @@ -179,18 +188,22 @@ fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result Self { Self { limit, @@ -206,7 +219,7 @@ impl PayloadConfig { } } - /// Set maximum accepted payload size. The default limit is 256kB. + /// Set maximum accepted payload size in bytes. The default limit is 256kB. pub fn limit(mut self, limit: usize) -> Self { self.limit = limit; self @@ -268,9 +281,9 @@ impl Default for PayloadConfig { pub struct HttpMessageBody { limit: usize, length: Option, - #[cfg(feature = "compress")] + #[cfg(feature = "__compress")] stream: dev::Decompress, - #[cfg(not(feature = "compress"))] + #[cfg(not(feature = "__compress"))] stream: dev::Payload, buf: BytesMut, err: Option, @@ -286,20 +299,27 @@ impl HttpMessageBody { if let Some(l) = req.headers().get(&header::CONTENT_LENGTH) { match l.to_str() { Ok(s) => match s.parse::() { - Ok(l) if l > DEFAULT_CONFIG_LIMIT => { - err = Some(PayloadError::Overflow) + Ok(l) => { + if l > DEFAULT_CONFIG_LIMIT { + err = Some(PayloadError::Overflow); + } + length = Some(l) } - Ok(l) => length = Some(l), Err(_) => err = Some(PayloadError::UnknownLength), }, Err(_) => err = Some(PayloadError::UnknownLength), } } - #[cfg(feature = "compress")] - let stream = dev::Decompress::from_headers(payload.take(), req.headers()); - #[cfg(not(feature = "compress"))] - let stream = payload.take(); + let stream = { + cfg_if::cfg_if! { + if #[cfg(feature = "__compress")] { + dev::Decompress::from_headers(payload.take(), req.headers()) + } else { + payload.take() + } + } + }; HttpMessageBody { stream, @@ -313,9 +333,11 @@ impl HttpMessageBody { /// Change max size of payload. By default max size is 256kB pub fn limit(mut self, limit: usize) -> Self { if let Some(l) = self.length { - if l > limit { - self.err = Some(PayloadError::Overflow); - } + self.err = if l > limit { + Some(PayloadError::Overflow) + } else { + None + }; } self.limit = limit; self @@ -375,6 +397,8 @@ mod tests { assert!(cfg.check_mimetype(&req).is_ok()); } + // allow deprecated App::data + #[allow(deprecated)] #[actix_rt::test] async fn test_config_recall_locations() { async fn bytes_handler(_: Bytes) -> impl Responder { @@ -385,13 +409,11 @@ mod tests { "payload is probably json string" } - let mut srv = init_service( + let srv = init_service( App::new() .service( web::resource("/bytes-app-data") - .app_data( - PayloadConfig::default().mimetype(mime::APPLICATION_JSON), - ) + .app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(bytes_handler)), ) .service( @@ -401,9 +423,7 @@ mod tests { ) .service( web::resource("/string-app-data") - .app_data( - PayloadConfig::default().mimetype(mime::APPLICATION_JSON), - ) + .app_data(PayloadConfig::default().mimetype(mime::APPLICATION_JSON)) .route(web::get().to(string_handler)), ) .service( @@ -415,43 +435,43 @@ mod tests { .await; let req = TestRequest::with_uri("/bytes-app-data").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/bytes-data").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/string-app-data").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/string-data").to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::BAD_REQUEST); let req = TestRequest::with_uri("/bytes-app-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/bytes-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/string-app-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); let req = TestRequest::with_uri("/string-data") .insert_header(header::ContentType(mime::APPLICATION_JSON)) .to_request(); - let resp = call_service(&mut srv, req).await; + let resp = call_service(&srv, req).await; assert_eq!(resp.status(), StatusCode::OK); } diff --git a/src/types/query.rs b/src/types/query.rs index 2cfc18c36..9fac21173 100644 --- a/src/types/query.rs +++ b/src/types/query.rs @@ -2,15 +2,15 @@ use std::{fmt, ops, sync::Arc}; -use futures_util::future::{err, ok, Ready}; -use serde::de; +use actix_utils::future::{err, ok, Ready}; +use serde::de::DeserializeOwned; use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest}; /// Extract typed information from the request's query. /// /// To extract typed data from the URL query string, the inner type `T` must implement the -/// [`serde::Deserialize`] trait. +/// [`DeserializeOwned`] trait. /// /// Use [`QueryConfig`] to configure extraction process. /// @@ -18,7 +18,7 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// A query string consists of unordered `key=value` pairs, therefore it cannot be decoded into any /// type which depends upon data ordering (eg. tuples). Trying to do so will result in a panic. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{get, web}; /// use serde::Deserialize; @@ -29,7 +29,7 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// Code /// } /// -/// #[derive(Deserialize)] +/// #[derive(Debug, Deserialize)] /// pub struct AuthRequest { /// id: u64, /// response_type: ResponseType, @@ -42,17 +42,33 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ /// async fn index(info: web::Query) -> String { /// format!("Authorization request for id={} and type={:?}!", info.id, info.response_type) /// } +/// +/// // To access the entire underlying query struct, use `.into_inner()`. +/// #[get("/debug1")] +/// async fn debug1(info: web::Query) -> String { +/// dbg!("Authorization object = {:?}", info.into_inner()); +/// "OK".to_string() +/// } +/// +/// // Or use destructuring, which is equivalent to `.into_inner()`. +/// #[get("/debug2")] +/// async fn debug2(web::Query(info): web::Query) -> String { +/// dbg!("Authorization object = {:?}", info); +/// "OK".to_string() +/// } /// ``` -#[derive(Clone, PartialEq, Eq, PartialOrd, Ord)] -pub struct Query(T); +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub struct Query(pub T); impl Query { /// Unwrap into inner `T` value. pub fn into_inner(self) -> T { self.0 } +} - /// Deserialize `T` from a URL encoded query parameter string. +impl Query { + /// Deserialize a `T` from the URL encoded query parameter string. /// /// ``` /// # use std::collections::HashMap; @@ -62,10 +78,7 @@ impl Query { /// assert_eq!(numbers.get("two"), Some(&2)); /// assert!(numbers.get("three").is_none()); /// ``` - pub fn from_query(query_str: &str) -> Result - where - T: de::DeserializeOwned, - { + pub fn from_query(query_str: &str) -> Result { serde_urlencoded::from_str::(query_str) .map(Self) .map_err(QueryPayloadError::Deserialize) @@ -86,33 +99,22 @@ impl ops::DerefMut for Query { } } -impl fmt::Debug for Query { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - self.0.fmt(f) - } -} - impl fmt::Display for Query { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { self.0.fmt(f) } } -/// See [here](#usage) for example of usage as an extractor. -impl FromRequest for Query -where - T: de::DeserializeOwned, -{ +/// See [here](#Examples) for example of usage as an extractor. +impl FromRequest for Query { type Error = Error; type Future = Ready>; - type Config = QueryConfig; #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { let error_handler = req - .app_data::() - .map(|c| c.err_handler.clone()) - .unwrap_or(None); + .app_data::() + .and_then(|c| c.err_handler.clone()); serde_urlencoded::from_str::(req.query_string()) .map(|val| ok(Query(val))) @@ -138,7 +140,7 @@ where /// Query extractor configuration. /// -/// # Usage +/// # Examples /// ``` /// use actix_web::{error, get, web, App, FromRequest, HttpResponse}; /// use serde::Deserialize; @@ -165,10 +167,9 @@ where /// .app_data(query_cfg) /// .service(index); /// ``` -#[derive(Clone)] +#[derive(Clone, Default)] pub struct QueryConfig { - err_handler: - Option Error + Send + Sync>>, + err_handler: Option Error + Send + Sync>>, } impl QueryConfig { @@ -182,12 +183,6 @@ impl QueryConfig { } } -impl Default for QueryConfig { - fn default() -> Self { - QueryConfig { err_handler: None } - } -} - #[cfg(test)] mod tests { use actix_http::http::StatusCode; @@ -207,13 +202,16 @@ mod tests { #[actix_rt::test] async fn test_service_request_extract() { let req = TestRequest::with_uri("/name/user1/").to_srv_request(); - assert!(Query::::from_query(&req.query_string()).is_err()); + assert!(Query::::from_query(req.query_string()).is_err()); let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); - let mut s = Query::::from_query(&req.query_string()).unwrap(); + let mut s = Query::::from_query(req.query_string()).unwrap(); assert_eq!(s.id, "test"); - assert_eq!(format!("{}, {:?}", s, s), "test, Id { id: \"test\" }"); + assert_eq!( + format!("{}, {:?}", s, s), + "test, Query(Id { id: \"test\" })" + ); s.id = "test1".to_string(); let s = s.into_inner(); @@ -231,7 +229,10 @@ mod tests { let mut s = Query::::from_request(&req, &mut pl).await.unwrap(); assert_eq!(s.id, "test"); - assert_eq!(format!("{}, {:?}", s, s), "test, Id { id: \"test\" }"); + assert_eq!( + format!("{}, {:?}", s, s), + "test, Query(Id { id: \"test\" })" + ); s.id = "test1".to_string(); let s = s.into_inner(); diff --git a/src/types/readlines.rs b/src/types/readlines.rs index 01aab64ab..6c456e21c 100644 --- a/src/types/readlines.rs +++ b/src/types/readlines.rs @@ -74,10 +74,7 @@ where { type Item = Result; - fn poll_next( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); if let Some(err) = this.err.take() { @@ -180,7 +177,7 @@ where #[cfg(test)] mod tests { - use futures_util::stream::StreamExt; + use futures_util::stream::StreamExt as _; use super::*; use crate::test::TestRequest; diff --git a/src/web.rs b/src/web.rs index 88071f551..b58adc2f8 100644 --- a/src/web.rs +++ b/src/web.rs @@ -1,48 +1,40 @@ //! Essentials helper functions and types for application registration. -use actix_http::http::Method; -use actix_router::IntoPattern; -use std::future::Future; -pub use actix_http::Response as HttpResponse; +use std::{error::Error as StdError, future::Future}; + +use actix_http::http::Method; +use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; -use crate::error::BlockingError; -use crate::extract::FromRequest; -use crate::handler::Handler; -use crate::resource::Resource; -use crate::responder::Responder; -use crate::route::Route; -use crate::scope::Scope; -use crate::service::WebService; +use crate::{ + body::MessageBody, error::BlockingError, extract::FromRequest, handler::Handler, + resource::Resource, responder::Responder, route::Route, scope::Scope, service::WebService, +}; pub use crate::config::ServiceConfig; pub use crate::data::Data; pub use crate::request::HttpRequest; pub use crate::request_data::ReqData; +pub use crate::response::HttpResponse; pub use crate::types::*; -/// Create resource for a specific path. +/// Creates a new resource for a specific path. /// -/// Resources may have variable path segments. For example, a -/// resource with the path `/a/{name}/c` would match all incoming -/// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. +/// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c` +/// would match all incoming requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`. /// -/// A variable segment is specified in the form `{identifier}`, -/// where the identifier can be used later in a request handler to -/// access the matched value for that segment. This is done by -/// looking up the identifier in the `Params` object returned by -/// `HttpRequest.match_info()` method. +/// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used +/// later in a request handler to access the matched value for that segment. This is done by looking +/// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method. /// /// By default, each segment matches the regular expression `[^{}/]+`. /// /// You can also specify a custom regex in the form `{identifier:regex}`: /// -/// For instance, to route `GET`-requests on any route matching -/// `/users/{userid}/{friend}` and store `userid` and `friend` in -/// the exposed `Params` object: +/// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store +/// `userid` and `friend` in the exposed `Path` object: /// -/// ```rust -/// # extern crate actix_web; +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -51,16 +43,22 @@ pub use crate::types::*; /// .route(web::head().to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` -pub fn resource(path: T) -> Resource { +pub fn resource(path: T) -> Resource { Resource::new(path) } -/// Configure scope for common root path. +/// Creates scope for common path prefix. /// -/// Scopes collect multiple paths under a common path prefix. -/// Scope path can contain variable path segments as resources. +/// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic +/// path segments. /// -/// ```rust +/// # Examples +/// In this example, three routes are set up (and will handle any method): +/// * `/{project_id}/path1` +/// * `/{project_id}/path2` +/// * `/{project_id}/path3` +/// +/// ``` /// use actix_web::{web, App, HttpResponse}; /// /// let app = App::new().service( @@ -70,150 +68,52 @@ pub fn resource(path: T) -> Resource { /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// ); /// ``` -/// -/// In the above example, three routes get added: -/// * /{project_id}/path1 -/// * /{project_id}/path2 -/// * /{project_id}/path3 -/// pub fn scope(path: &str) -> Scope { Scope::new(path) } -/// Create *route* without configuration. +/// Creates a new un-configured route. pub fn route() -> Route { Route::new() } -/// Create *route* with `GET` method guard. -/// -/// ```rust -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::get().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `GET` route gets added: -/// * /{project_id} -/// -pub fn get() -> Route { - method(Method::GET) +macro_rules! method_route { + ($method_fn:ident, $method_const:ident) => { + paste::paste! { + #[doc = " Creates a new route with `" $method_const "` method guard."] + /// + /// # Examples + #[doc = " In this example, one `" $method_const " /{project_id}` route is set up:"] + /// ``` + /// use actix_web::{web, App, HttpResponse}; + /// + /// let app = App::new().service( + /// web::resource("/{project_id}") + #[doc = " .route(web::" $method_fn "().to(|| HttpResponse::Ok()))"] + /// + /// ); + /// ``` + pub fn $method_fn() -> Route { + method(Method::$method_const) + } + } + }; } -/// Create *route* with `POST` method guard. -/// -/// ```rust -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::post().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `POST` route gets added: -/// * /{project_id} -/// -pub fn post() -> Route { - method(Method::POST) -} +method_route!(get, GET); +method_route!(post, POST); +method_route!(put, PUT); +method_route!(patch, PATCH); +method_route!(delete, DELETE); +method_route!(head, HEAD); +method_route!(trace, TRACE); -/// Create *route* with `PUT` method guard. +/// Creates a new route with specified method guard. /// -/// ```rust -/// use actix_web::{web, App, HttpResponse}; +/// # Examples +/// In this example, one `GET /{project_id}` route is set up: /// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::put().to(|| HttpResponse::Ok())) -/// ); /// ``` -/// -/// In the above example, one `PUT` route gets added: -/// * /{project_id} -/// -pub fn put() -> Route { - method(Method::PUT) -} - -/// Create *route* with `PATCH` method guard. -/// -/// ```rust -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::patch().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `PATCH` route gets added: -/// * /{project_id} -/// -pub fn patch() -> Route { - method(Method::PATCH) -} - -/// Create *route* with `DELETE` method guard. -/// -/// ```rust -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::delete().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// In the above example, one `DELETE` route gets added: -/// * /{project_id} -/// -pub fn delete() -> Route { - method(Method::DELETE) -} - -/// Create *route* with `HEAD` method guard. -/// -/// ```rust -/// use actix_web::{web, App, HttpResponse}; -/// -/// let app = App::new().service( -/// web::resource("/{project_id}") -/// .route(web::head().to(|| HttpResponse::Ok())) -/// ); -/// ``` -/// -/// 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 /// use actix_web::{web, http, App, HttpResponse}; /// /// let app = App::new().service( @@ -221,17 +121,13 @@ pub fn trace() -> Route { /// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok())) /// ); /// ``` -/// -/// In the above example, one `GET` route gets added: -/// * /{project_id} -/// pub fn method(method: Method) -> Route { Route::new().method(method) } -/// Create a new route and add handler. +/// Creates a new any-method route with handler. /// -/// ```rust +/// ``` /// use actix_web::{web, App, HttpResponse, Responder}; /// /// async fn index() -> impl Responder { @@ -249,13 +145,15 @@ where I: FromRequest + 'static, R: Future + 'static, R::Output: Responder + 'static, + ::Body: MessageBody + 'static, + <::Body as MessageBody>::Error: Into>, { Route::new().to(handler) } -/// Create raw service for a specific path. +/// Creates a raw service for a specific path. /// -/// ```rust +/// ``` /// use actix_web::{dev, web, guard, App, Error, HttpResponse}; /// /// async fn my_service(req: dev::ServiceRequest) -> Result { @@ -268,20 +166,17 @@ where /// .finish(my_service) /// ); /// ``` -pub fn service(path: T) -> WebService { +pub fn service(path: T) -> WebService { WebService::new(path) } -/// Execute blocking function on a thread pool, returns future that resolves -/// to result of the function execution. -pub async fn block(f: F) -> Result> +/// Executes blocking function on a thread pool, returns future that resolves to result of the +/// function execution. +pub fn block(f: F) -> impl Future> where - F: FnOnce() -> Result + Send + 'static, - I: Send + 'static, - E: Send + std::fmt::Debug + 'static, + F: FnOnce() -> R + Send + 'static, + R: Send + 'static, { - match actix_rt::task::spawn_blocking(f).await { - Ok(res) => res.map_err(BlockingError::Error), - Err(_) => Err(BlockingError::Canceled), - } + let fut = actix_rt::task::spawn_blocking(f); + async { fut.await.map_err(|_| BlockingError) } } diff --git a/tests/cert.pem b/tests/cert.pem deleted file mode 100644 index 0eeb6721d..000000000 --- a/tests/cert.pem +++ /dev/null @@ -1,19 +0,0 @@ ------BEGIN CERTIFICATE----- -MIIDEDCCAfgCCQCQdmIZc/Ib/jANBgkqhkiG9w0BAQsFADBKMQswCQYDVQQGEwJ1 -czELMAkGA1UECAwCY2ExCzAJBgNVBAcMAnNmMSEwHwYJKoZIhvcNAQkBFhJmYWZo -cmQ5MUBnbWFpbC5jb20wHhcNMTkxMTE5MTEwNjU1WhcNMjkxMTE2MTEwNjU1WjBK -MQswCQYDVQQGEwJ1czELMAkGA1UECAwCY2ExCzAJBgNVBAcMAnNmMSEwHwYJKoZI -hvcNAQkBFhJmYWZocmQ5MUBnbWFpbC5jb20wggEiMA0GCSqGSIb3DQEBAQUAA4IB -DwAwggEKAoIBAQDcnaz12CKzUL7248V7Axhms/O9UQXfAdw0yolEfC3P5jADa/1C -+kLWKjAc2coqDSbGsrsR6KiH2g06Kunx+tSGqUO+Sct7HEehmxndiSwx/hfMWezy -XRe/olcHFTeCk/Tllz4xGEplhPua6GLhJygLOhAMiV8cwCYrgyPqsDduExLDFCqc -K2xntIPreumXpiE3QY4+MWyteiJko4IWDFf/UwwsdCY5MlFfw1F/Uv9vz7FfOfvu -GccHd/ex8cOwotUqd6emZb+0bVE24Sv8U+yLnHIVx/tOkxgMAnJEpAnf2G3Wp3zU -b2GJosbmfGaf+xTfnGGhTLLL7kCtva+NvZr5AgMBAAEwDQYJKoZIhvcNAQELBQAD -ggEBANftoL8zDGrjCwWvct8kOOqset2ukK8vjIGwfm88CKsy0IfSochNz2qeIu9R -ZuO7c0pfjmRkir9ZQdq9vXgG3ccL9UstFsferPH9W3YJ83kgXg3fa0EmCiN/0hwz -6Ij1ZBiN1j3+d6+PJPgyYFNu2nGwox5mJ9+aRAGe0/9c63PEOY8P2TI4HsiPmYSl -fFR8k/03vr6e+rTKW85BgctjvYKe/TnFxeCQ7dZ+na7vlEtch4tNmy6O/vEk2kCt -5jW0DUxhmRsv2wGmfFRI0+LotHjoXQQZi6nN5aGL3odaGF3gYwIVlZNd3AdkwDQz -BzG0ZwXuDDV9bSs3MfWEWcy4xuU= ------END CERTIFICATE----- diff --git a/tests/key.pem b/tests/key.pem deleted file mode 100644 index a6d308168..000000000 --- a/tests/key.pem +++ /dev/null @@ -1,28 +0,0 @@ ------BEGIN PRIVATE KEY----- -MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQDcnaz12CKzUL72 -48V7Axhms/O9UQXfAdw0yolEfC3P5jADa/1C+kLWKjAc2coqDSbGsrsR6KiH2g06 -Kunx+tSGqUO+Sct7HEehmxndiSwx/hfMWezyXRe/olcHFTeCk/Tllz4xGEplhPua -6GLhJygLOhAMiV8cwCYrgyPqsDduExLDFCqcK2xntIPreumXpiE3QY4+MWyteiJk -o4IWDFf/UwwsdCY5MlFfw1F/Uv9vz7FfOfvuGccHd/ex8cOwotUqd6emZb+0bVE2 -4Sv8U+yLnHIVx/tOkxgMAnJEpAnf2G3Wp3zUb2GJosbmfGaf+xTfnGGhTLLL7kCt -va+NvZr5AgMBAAECggEBAKoU0UwzVgVCQgca8Jt2dnBvWYDhnxIfYAI/BvaKedMm -1ms87OKfB7oOiksjyI0E2JklH72dzZf2jm4CuZt5UjGC+xwPzlTaJ4s6hQVbBHyC -NRyxU1BCXtW5tThbrhD4OjxqjmLRJEIB9OunLtwAEQoeuFLB8Va7+HFhR+Zd9k3f -7aVA93pC5A50NRbZlke4miJ3Q8n7ZF0+UmxkBfm3fbqLk7aMWkoEKwLLTadjRlu1 -bBp0YDStX66I/p1kujqBOdh6VpPvxFOa1sV9pq0jeiGc9YfSkzRSKzIn8GoyviFB -fHeszQdNlcnrSDSNnMABAw+ZpxUO7SCaftjwejEmKZUCgYEA+TY43VpmV95eY7eo -WKwGepiHE0fwQLuKGELmZdZI80tFi73oZMuiB5WzwmkaKGcJmm7KGE9KEvHQCo9j -xvmktBR0VEZH8pmVfun+4h6+0H7m/NKMBBeOyv/IK8jBgHjkkB6e6nmeR7CqTxCw -tf9tbajl1QN8gNzXZSjBDT/lanMCgYEA4qANOKOSiEARtgwyXQeeSJcM2uPv6zF3 -ffM7vjSedtuEOHUSVeyBP/W8KDt7zyPppO/WNbURHS+HV0maS9yyj6zpVS2HGmbs -3fetswsQ+zYVdokW89x4oc2z4XOGHd1LcSlyhRwPt0u2g1E9L0irwTQLWU0npFmG -PRf7sN9+LeMCgYAGkDUDL2ROoB6gRa/7Vdx90hKMoXJkYgwLA4gJ2pDlR3A3c/Lw -5KQJyxmG3zm/IqeQF6be6QesZA30mT4peV2rGHbP2WH/s6fKReNelSy1VQJEWk8x -tGUgV4gwDwN5nLV4TjYlOrq+bJqvpmLhCC8bmj0jVQosYqSRl3cuICasnQKBgGlV -VO/Xb1su1EyWPK5qxRIeSxZOTYw2sMB01nbgxCqge0M2fvA6/hQ5ZlwY0cIEgits -YlcSMsMq/TAAANxz1vbaupUhlSMbZcsBvNV0Nk9c4vr2Wxm7hsJF9u66IEMvQUp2 -pkjiMxfR9CHzF4orr9EcHI5EQ0Grbq5kwFKEfoRbAoGAcWoFPILeJOlp2yW/Ds3E -g2fQdI9BAamtEZEaslJmZMmsDTg5ACPcDkOSFEQIaJ7wLPXeZy74FVk/NrY5F8Gz -bjX9OD/xzwp852yW5L9r62vYJakAlXef5jI6CFdYKDDCcarU0S7W5k6kq9n+wrBR -i1NklYmUAMr2q59uJA5zsic= ------END PRIVATE KEY----- diff --git a/tests/test-macro-import-conflict.rs b/tests/test-macro-import-conflict.rs new file mode 100644 index 000000000..0d23bb41d --- /dev/null +++ b/tests/test-macro-import-conflict.rs @@ -0,0 +1,15 @@ +//! Checks that test macro does not cause problems in the presence of imports named "test" that +//! could be either a module with test items or the "test with runtime" macro itself. +//! +//! Before actix/actix-net#399 was implemented, this macro was running twice. The first run output +//! `#[test]` and it got run again and since it was in scope. +//! +//! Prevented by using the fully-qualified test marker (`#[::core::prelude::v1::test]`). + +use actix_web::test; + +#[actix_web::test] +async fn test_macro_naming_conflict() { + let _req = test::TestRequest::default(); + assert_eq!(async { 1 }.await, 1); +} diff --git a/tests/test_error_propagation.rs b/tests/test_error_propagation.rs new file mode 100644 index 000000000..958276b62 --- /dev/null +++ b/tests/test_error_propagation.rs @@ -0,0 +1,99 @@ +use std::sync::Arc; + +use actix_utils::future::{ok, Ready}; +use actix_web::{ + dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, + get, + test::{call_service, init_service, TestRequest}, + ResponseError, +}; +use futures_core::future::LocalBoxFuture; +use futures_util::lock::Mutex; + +#[derive(Debug, Clone)] +pub struct MyError; + +impl ResponseError for MyError {} + +impl std::fmt::Display for MyError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "A custom error") + } +} + +#[get("/test")] +async fn test() -> Result { + Err(MyError.into()) +} + +#[derive(Clone)] +pub struct SpyMiddleware(Arc>>); + +impl Transform for SpyMiddleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Transform = Middleware; + type InitError = (); + type Future = Ready>; + + fn new_transform(&self, service: S) -> Self::Future { + ok(Middleware { + was_error: self.0.clone(), + service, + }) + } +} + +#[doc(hidden)] +pub struct Middleware { + was_error: Arc>>, + service: S, +} + +impl Service for Middleware +where + S: Service, Error = actix_web::Error>, + S::Future: 'static, + B: 'static, +{ + type Response = ServiceResponse; + type Error = actix_web::Error; + type Future = LocalBoxFuture<'static, Result>; + + forward_ready!(service); + + fn call(&self, req: ServiceRequest) -> Self::Future { + let lock = self.was_error.clone(); + let response_future = self.service.call(req); + Box::pin(async move { + let response = response_future.await; + if let Ok(success) = &response { + *lock.lock().await = Some(success.response().error().is_some()); + } + response + }) + } +} + +#[actix_rt::test] +async fn error_cause_should_be_propagated_to_middlewares() { + let lock = Arc::new(Mutex::new(None)); + let spy_middleware = SpyMiddleware(lock.clone()); + + let app = init_service( + actix_web::App::new() + .wrap(spy_middleware.clone()) + .service(test), + ) + .await; + + call_service(&app, TestRequest::with_uri("/test").to_request()).await; + + let was_error_captured = lock.lock().await.unwrap(); + assert!(was_error_captured); +} diff --git a/tests/test_httpserver.rs b/tests/test_httpserver.rs index 5eca14931..887b51d41 100644 --- a/tests/test_httpserver.rs +++ b/tests/test_httpserver.rs @@ -1,122 +1,117 @@ -use std::sync::mpsc; -use std::{thread, time::Duration}; - #[cfg(feature = "openssl")] -use open_ssl::ssl::SslAcceptorBuilder; +extern crate tls_openssl as openssl; -use actix_web::{test, web, App, HttpResponse, HttpServer}; +#[cfg(any(unix, feature = "openssl"))] +use { + actix_web::{web, App, HttpResponse, HttpServer}, + std::{sync::mpsc, thread, time::Duration}, +}; #[cfg(unix)] #[actix_rt::test] async fn test_start() { - let addr = test::unused_addr(); + let addr = actix_test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { - let sys = actix_rt::System::new("test"); + actix_rt::System::new() + .block_on(async { + let srv = HttpServer::new(|| { + App::new().service( + web::resource("/").route(web::to(|| HttpResponse::Ok().body("test"))), + ) + }) + .workers(1) + .backlog(1) + .max_connections(10) + .max_connection_rate(10) + .keep_alive(10) + .client_timeout(5000) + .client_shutdown(0) + .server_hostname("localhost") + .system_exit() + .disable_signals() + .bind(format!("{}", addr)) + .unwrap() + .run(); - sys.block_on(async { - let srv = HttpServer::new(|| { - App::new().service( - web::resource("/") - .route(web::to(|| HttpResponse::Ok().body("test"))), - ) + tx.send(srv.handle()).unwrap(); + + srv.await }) - .workers(1) - .backlog(1) - .max_connections(10) - .max_connection_rate(10) - .keep_alive(10) - .client_timeout(5000) - .client_shutdown(0) - .server_hostname("localhost") - .system_exit() - .disable_signals() - .bind(format!("{}", addr)) - .unwrap() - .run(); - - let _ = tx.send((srv, actix_rt::System::current())); - }); - - let _ = sys.run(); + .unwrap(); }); - let (srv, sys) = rx.recv().unwrap(); - #[cfg(feature = "client")] - { - use actix_http::client; + let srv = rx.recv().unwrap(); - let client = awc::Client::builder() - .connector( - client::Connector::new() - .timeout(Duration::from_millis(100)) - .finish(), - ) - .finish(); + let client = awc::Client::builder() + .connector(awc::Connector::new().timeout(Duration::from_millis(100))) + .finish(); - let host = format!("http://{}", addr); - let response = client.get(host.clone()).send().await.unwrap(); - assert!(response.status().is_success()); - } + let host = format!("http://{}", addr); + let response = client.get(host.clone()).send().await.unwrap(); + assert!(response.status().is_success()); - // stop - let _ = srv.stop(false); - - thread::sleep(Duration::from_millis(100)); - let _ = sys.stop(); + srv.stop(false).await; } -#[allow(clippy::unnecessary_wraps)] #[cfg(feature = "openssl")] -fn ssl_acceptor() -> std::io::Result { - use open_ssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - // load ssl keys +fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { + use openssl::{ + pkey::PKey, + ssl::{SslAcceptor, SslMethod}, + x509::X509, + }; + + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); + let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder.set_certificate(&cert).unwrap(); + builder.set_private_key(&key).unwrap(); + builder - .set_private_key_file("tests/key.pem", SslFiletype::PEM) - .unwrap(); - builder - .set_certificate_chain_file("tests/cert.pem") - .unwrap(); - Ok(builder) } #[actix_rt::test] #[cfg(feature = "openssl")] async fn test_start_ssl() { use actix_web::HttpRequest; + use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; - let addr = test::unused_addr(); + let addr = actix_test::unused_addr(); let (tx, rx) = mpsc::channel(); thread::spawn(move || { - let sys = actix_rt::System::new("test"); - let builder = ssl_acceptor().unwrap(); + actix_rt::System::new() + .block_on(async { + let builder = ssl_acceptor(); - let srv = HttpServer::new(|| { - App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { - assert!(req.app_config().secure()); - HttpResponse::Ok().body("test") - }))) - }) - .workers(1) - .shutdown_timeout(1) - .system_exit() - .disable_signals() - .bind_openssl(format!("{}", addr), builder) - .unwrap(); + let srv = HttpServer::new(|| { + App::new().service(web::resource("/").route(web::to(|req: HttpRequest| { + assert!(req.app_config().secure()); + HttpResponse::Ok().body("test") + }))) + }) + .workers(1) + .shutdown_timeout(1) + .system_exit() + .disable_signals() + .bind_openssl(format!("{}", addr), builder) + .unwrap(); - sys.block_on(async { - let srv = srv.run(); - let _ = tx.send((srv, actix_rt::System::current())); - }); + let srv = srv.run(); + tx.send(srv.handle()).unwrap(); - let _ = sys.run(); + srv.await + }) + .unwrap() }); - let (srv, sys) = rx.recv().unwrap(); + let srv = rx.recv().unwrap(); - use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE); let _ = builder @@ -127,8 +122,7 @@ async fn test_start_ssl() { .connector( awc::Connector::new() .ssl(builder.build()) - .timeout(Duration::from_millis(100)) - .finish(), + .timeout(Duration::from_millis(100)), ) .finish(); @@ -136,9 +130,5 @@ async fn test_start_ssl() { let response = client.get(host.clone()).send().await.unwrap(); assert!(response.status().is_success()); - // stop - let _ = srv.stop(false); - - thread::sleep(Duration::from_millis(100)); - let _ = sys.stop(); + srv.stop(false).await; } diff --git a/tests/test_server.rs b/tests/test_server.rs index 0f51a2fdb..a850f228d 100644 --- a/tests/test_server.rs +++ b/tests/test_server.rs @@ -1,3 +1,8 @@ +#[cfg(feature = "openssl")] +extern crate tls_openssl as openssl; +#[cfg(feature = "rustls")] +extern crate tls_rustls as rustls; + use std::{ future::Future, io::{Read, Write}, @@ -6,22 +11,29 @@ use std::{ }; use actix_http::http::header::{ - ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, - TRANSFER_ENCODING, + ContentEncoding, ACCEPT_ENCODING, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING, }; use brotli2::write::{BrotliDecoder, BrotliEncoder}; use bytes::Bytes; +use cookie::{Cookie, CookieBuilder}; use flate2::{ read::GzDecoder, write::{GzEncoder, ZlibDecoder, ZlibEncoder}, Compression, }; -use futures_util::ready; +use futures_core::ready; +#[cfg(feature = "openssl")] +use openssl::{ + pkey::PKey, + ssl::{SslAcceptor, SslMethod}, + x509::X509, +}; use rand::{distributions::Alphanumeric, Rng}; +use zstd::stream::{read::Decoder as ZstdDecoder, write::Encoder as ZstdEncoder}; use actix_web::dev::BodyEncoding; use actix_web::middleware::{Compress, NormalizePath, TrailingSlash}; -use actix_web::{dev, test, web, App, Error, HttpResponse}; +use actix_web::{web, App, Error, HttpResponse}; const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ @@ -45,6 +57,31 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World"; +#[cfg(feature = "openssl")] +fn openssl_config() -> SslAcceptor { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); + let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); + + let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap(); + builder.set_certificate(&cert).unwrap(); + builder.set_private_key(&key).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(openssl::ssl::AlpnError::NOACK) + } + }); + builder.set_alpn_protos(b"\x02h2").unwrap(); + + builder.build() +} + struct TestBody { data: Bytes, chunk_size: usize, @@ -64,14 +101,10 @@ impl TestBody { impl futures_core::stream::Stream for TestBody { type Item = Result; - fn poll_next( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { ready!(Pin::new(&mut self.delay).poll(cx)); - self.delay = - Box::pin(actix_rt::time::sleep(std::time::Duration::from_millis(10))); + self.delay = Box::pin(actix_rt::time::sleep(std::time::Duration::from_millis(10))); let chunk_size = std::cmp::min(self.chunk_size, self.data.len()); let chunk = self.data.split_to(chunk_size); if chunk.is_empty() { @@ -84,9 +117,8 @@ impl futures_core::stream::Stream for TestBody { #[actix_rt::test] async fn test_body() { - let srv = test::start(|| { - App::new() - .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) + let srv = actix_test::start(|| { + App::new().service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); let mut response = srv.get("/").send().await.unwrap(); @@ -95,11 +127,13 @@ async fn test_body() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_body_gzip() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) @@ -122,16 +156,16 @@ async fn test_body_gzip() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_body_gzip2() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::new(ContentEncoding::Gzip)) - .service(web::resource("/").route(web::to(|| { - HttpResponse::Ok().body(STR).into_body::() - }))) + .service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR)))) }); let mut response = srv @@ -151,11 +185,13 @@ async fn test_body_gzip2() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_body_encoding_override() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::to(|| { @@ -164,13 +200,10 @@ async fn test_body_encoding_override() { .body(STR) }))) .service(web::resource("/raw").route(web::to(|| { - let body = actix_web::dev::Body::Bytes(STR.into()); let mut response = - HttpResponse::with_body(actix_web::http::StatusCode::OK, body); - + HttpResponse::with_body(actix_web::http::StatusCode::OK, STR); response.encoding(ContentEncoding::Deflate); - - response + response.map_into_boxed_body() }))) }); @@ -211,6 +244,8 @@ async fn test_body_encoding_override() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -218,7 +253,7 @@ async fn test_body_gzip_large() { let data = STR.repeat(10); let srv_data = data.clone(); - let srv = test::start_with(test::config().h1(), move || { + let srv = actix_test::start_with(actix_test::config().h1(), move || { let data = srv_data.clone(); App::new() .wrap(Compress::new(ContentEncoding::Gzip)) @@ -245,6 +280,8 @@ async fn test_body_gzip_large() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -256,7 +293,7 @@ async fn test_body_gzip_large_random() { .collect::(); let srv_data = data.clone(); - let srv = test::start_with(test::config().h1(), move || { + let srv = actix_test::start_with(actix_test::config().h1(), move || { let data = srv_data.clone(); App::new() .wrap(Compress::new(ContentEncoding::Gzip)) @@ -284,11 +321,13 @@ async fn test_body_gzip_large_random() { e.read_to_end(&mut dec).unwrap(); assert_eq!(dec.len(), data.len()); assert_eq!(Bytes::from(dec), Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] async fn test_body_chunked_implicit() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::new(ContentEncoding::Gzip)) .service(web::resource("/").route(web::get().to(move || { @@ -318,17 +357,19 @@ async fn test_body_chunked_implicit() { let mut dec = Vec::new(); e.read_to_end(&mut dec).unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_body_br_streaming() { - let srv = test::start_with(test::config().h1(), || { - App::new().wrap(Compress::new(ContentEncoding::Br)).service( - web::resource("/").route(web::to(move || { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::new(ContentEncoding::Br)) + .service(web::resource("/").route(web::to(move || { HttpResponse::Ok() .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) - })), - ) + }))) }); let mut response = srv @@ -350,14 +391,15 @@ async fn test_body_br_streaming() { let dec = e.finish().unwrap(); println!("T: {:?}", Bytes::copy_from_slice(&dec)); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_head_binary() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::head().to(move || HttpResponse::Ok().body(STR))), + web::resource("/").route(web::head().to(move || HttpResponse::Ok().body(STR))), ) }); @@ -372,11 +414,13 @@ async fn test_head_binary() { // read response let bytes = response.body().await.unwrap(); assert!(bytes.is_empty()); + + srv.stop().await; } #[actix_rt::test] async fn test_no_chunking() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service(web::resource("/").route(web::to(move || { HttpResponse::Ok() .no_chunking(STR.len() as u64) @@ -391,16 +435,16 @@ async fn test_no_chunking() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_body_deflate() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(Compress::new(ContentEncoding::Deflate)) - .service( - web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))), - ) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) }); // client request @@ -420,14 +464,16 @@ async fn test_body_deflate() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_body_brotli() { - let srv = test::start_with(test::config().h1(), || { - App::new().wrap(Compress::new(ContentEncoding::Br)).service( - web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR))), - ) + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::new(ContentEncoding::Br)) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) }); // client request @@ -448,14 +494,142 @@ async fn test_body_brotli() { e.write_all(bytes.as_ref()).unwrap(); let dec = e.finish().unwrap(); assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; +} + +#[actix_rt::test] +async fn test_body_zstd() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::new(ContentEncoding::Zstd)) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().body(STR)))) + }); + + // client request + let mut response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "zstd")) + .no_decompress() + .send() + .await + .unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + + // decode + let mut e = ZstdDecoder::new(&bytes[..]).unwrap(); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; +} + +#[actix_rt::test] +async fn test_body_zstd_streaming() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::new(ContentEncoding::Zstd)) + .service(web::resource("/").route(web::to(move || { + HttpResponse::Ok() + .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) + }))) + }); + + // client request + let mut response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "zstd")) + .no_decompress() + .send() + .await + .unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + + // decode + let mut e = ZstdDecoder::new(&bytes[..]).unwrap(); + let mut dec = Vec::new(); + e.read_to_end(&mut dec).unwrap(); + assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref())); + + srv.stop().await; +} + +#[actix_rt::test] +async fn test_zstd_encoding() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new().service( + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + ) + }); + + let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap(); + e.write_all(STR.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + // client request + let request = srv + .post("/") + .append_header((CONTENT_ENCODING, "zstd")) + .send_body(enc.clone()); + let mut response = request.await.unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; +} + +#[actix_rt::test] +async fn test_zstd_encoding_large() { + let data = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(320_000) + .map(char::from) + .collect::(); + + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new().service( + web::resource("/") + .app_data(web::PayloadConfig::new(320_000)) + .route(web::to(move |body: Bytes| { + HttpResponse::Ok().streaming(TestBody::new(body, 10240)) + })), + ) + }); + + let mut e = ZstdEncoder::new(Vec::new(), 5).unwrap(); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + // client request + let request = srv + .post("/") + .append_header((CONTENT_ENCODING, "zstd")) + .send_body(enc.clone()); + let mut response = request.await.unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().limit(320_000).await.unwrap(); + assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] async fn test_encoding() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().wrap(Compress::default()).service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -474,14 +648,15 @@ async fn test_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_gzip_encoding() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -500,15 +675,16 @@ async fn test_gzip_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_gzip_encoding_large() { let data = STR.repeat(10); - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -527,6 +703,8 @@ async fn test_gzip_encoding_large() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -537,10 +715,9 @@ async fn test_reading_gzip_encoding_large_random() { .map(char::from) .collect::(); - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -560,14 +737,15 @@ async fn test_reading_gzip_encoding_large_random() { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] async fn test_reading_deflate_encoding() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -586,15 +764,16 @@ async fn test_reading_deflate_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] async fn test_reading_deflate_encoding_large() { let data = STR.repeat(10); - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -613,6 +792,8 @@ async fn test_reading_deflate_encoding_large() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] @@ -623,10 +804,9 @@ async fn test_reading_deflate_encoding_large_random() { .map(char::from) .collect::(); - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -646,14 +826,15 @@ async fn test_reading_deflate_encoding_large_random() { let bytes = response.body().await.unwrap(); assert_eq!(bytes.len(), data.len()); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[actix_rt::test] async fn test_brotli_encoding() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( - web::resource("/") - .route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), + web::resource("/").route(web::to(move |body: Bytes| HttpResponse::Ok().body(body))), ) }); @@ -672,6 +853,8 @@ async fn test_brotli_encoding() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(STR.as_ref())); + + srv.stop().await; } #[actix_rt::test] @@ -682,7 +865,7 @@ async fn test_brotli_encoding_large() { .map(char::from) .collect::(); - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new().service( web::resource("/") .app_data(web::PayloadConfig::new(320_000)) @@ -707,34 +890,27 @@ async fn test_brotli_encoding_large() { // read response let bytes = response.body().limit(320_000).await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } #[cfg(feature = "openssl")] #[actix_rt::test] async fn test_brotli_encoding_large_openssl() { - // load ssl keys - use open_ssl::ssl::{SslAcceptor, SslFiletype, SslMethod}; - 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(); - let data = STR.repeat(10); - let srv = test::start_with(test::config().openssl(builder.build()), move || { - App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { - HttpResponse::Ok() - .encoding(actix_web::http::ContentEncoding::Identity) - .body(bytes) - }))) - }); + let srv = + actix_test::start_with(actix_test::config().openssl(openssl_config()), move || { + App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { + HttpResponse::Ok() + .encoding(actix_web::http::ContentEncoding::Identity) + .body(bytes) + }))) + }); // body - let mut e = BrotliEncoder::new(Vec::new(), 3); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); + let mut enc = BrotliEncoder::new(Vec::new(), 3); + enc.write_all(data.as_ref()).unwrap(); + let enc = enc.finish().unwrap(); // client request let mut response = srv @@ -748,85 +924,107 @@ async fn test_brotli_encoding_large_openssl() { // read response let bytes = response.body().await.unwrap(); assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; } -#[cfg(all(feature = "rustls", feature = "openssl"))] -#[actix_rt::test] -async fn test_reading_deflate_encoding_large_random_rustls() { - use rust_tls::internal::pemfile::{certs, pkcs8_private_keys}; - use rust_tls::{NoClientAuth, ServerConfig}; - use std::fs::File; +#[cfg(feature = "rustls")] +mod plus_rustls { use std::io::BufReader; - let data = rand::thread_rng() - .sample_iter(&Alphanumeric) - .take(160_000) - .map(char::from) - .collect::(); + use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig}; + use rustls_pemfile::{certs, pkcs8_private_keys}; - // load ssl keys - let mut config = ServerConfig::new(NoClientAuth::new()); - let cert_file = &mut BufReader::new(File::open("tests/cert.pem").unwrap()); - let key_file = &mut BufReader::new(File::open("tests/key.pem").unwrap()); - let cert_chain = certs(cert_file).unwrap(); - let mut keys = pkcs8_private_keys(key_file).unwrap(); - config.set_single_cert(cert_chain, keys.remove(0)).unwrap(); + use super::*; - let srv = test::start_with(test::config().rustls(config), || { - App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { - HttpResponse::Ok() - .encoding(actix_web::http::ContentEncoding::Identity) - .body(bytes) - }))) - }); + fn tls_config() -> RustlsServerConfig { + let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); + let cert_file = cert.serialize_pem().unwrap(); + let key_file = cert.serialize_private_key_pem(); - // encode data - let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); - e.write_all(data.as_ref()).unwrap(); - let enc = e.finish().unwrap(); + let cert_file = &mut BufReader::new(cert_file.as_bytes()); + let key_file = &mut BufReader::new(key_file.as_bytes()); - // client request - let req = srv - .post("/") - .insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate")) - .send_stream(TestBody::new(Bytes::from(enc), 1024)); + let cert_chain = certs(cert_file) + .unwrap() + .into_iter() + .map(Certificate) + .collect(); + let mut keys = pkcs8_private_keys(key_file).unwrap(); - let mut response = req.await.unwrap(); - assert!(response.status().is_success()); + RustlsServerConfig::builder() + .with_safe_defaults() + .with_no_client_auth() + .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .unwrap() + } - // read response - let bytes = response.body().await.unwrap(); - assert_eq!(bytes.len(), data.len()); - assert_eq!(bytes, Bytes::from(data)); + #[actix_rt::test] + async fn test_reading_deflate_encoding_large_random_rustls() { + let data = rand::thread_rng() + .sample_iter(&Alphanumeric) + .take(160_000) + .map(char::from) + .collect::(); + + let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || { + App::new().service(web::resource("/").route(web::to(|bytes: Bytes| { + HttpResponse::Ok() + .encoding(actix_web::http::ContentEncoding::Identity) + .body(bytes) + }))) + }); + + // encode data + let mut e = ZlibEncoder::new(Vec::new(), Compression::default()); + e.write_all(data.as_ref()).unwrap(); + let enc = e.finish().unwrap(); + + // client request + let req = srv + .post("/") + .insert_header((actix_web::http::header::CONTENT_ENCODING, "deflate")) + .send_stream(TestBody::new(Bytes::from(enc), 1024)); + + let mut response = req.await.unwrap(); + assert!(response.status().is_success()); + + // read response + let bytes = response.body().await.unwrap(); + assert_eq!(bytes.len(), data.len()); + assert_eq!(bytes, Bytes::from(data)); + + srv.stop().await; + } } #[actix_rt::test] async fn test_server_cookies() { - use actix_web::{http, HttpMessage}; + use actix_web::http; - let srv = test::start(|| { + let srv = actix_test::start(|| { App::new().default_service(web::to(|| { HttpResponse::Ok() .cookie( - http::CookieBuilder::new("first", "first_value") + CookieBuilder::new("first", "first_value") .http_only(true) .finish(), ) - .cookie(http::Cookie::new("second", "first_value")) - .cookie(http::Cookie::new("second", "second_value")) + .cookie(Cookie::new("second", "first_value")) + .cookie(Cookie::new("second", "second_value")) .finish() })) }); - let first_cookie = http::CookieBuilder::new("first", "first_value") - .http_only(true) - .finish(); - let second_cookie = http::Cookie::new("second", "second_value"); - let req = srv.get("/"); let res = req.send().await.unwrap(); assert!(res.status().is_success()); + let first_cookie = CookieBuilder::new("first", "first_value") + .http_only(true) + .finish(); + let second_cookie = Cookie::new("second", "second_value"); + let cookies = res.cookies().expect("To have cookies"); assert_eq!(cookies.len(), 2); if cookies[0] == first_cookie { @@ -851,13 +1049,15 @@ async fn test_server_cookies() { assert_eq!(cookies[0], second_cookie); assert_eq!(cookies[1], first_cookie); } + + srv.stop().await; } #[actix_rt::test] async fn test_slow_request() { use std::net; - let srv = test::start_with(test::config().client_timeout(200), || { + let srv = actix_test::start_with(actix_test::config().client_timeout(200), || { App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))) }); @@ -871,18 +1071,91 @@ async fn test_slow_request() { let mut data = String::new(); let _ = stream.read_to_string(&mut data); assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); + + srv.stop().await; } #[actix_rt::test] async fn test_normalize() { - let srv = test::start_with(test::config().h1(), || { + let srv = actix_test::start_with(actix_test::config().h1(), || { App::new() .wrap(NormalizePath::new(TrailingSlash::Trim)) - .service( - web::resource("/one").route(web::to(|| HttpResponse::Ok().finish())), - ) + .service(web::resource("/one").route(web::to(HttpResponse::Ok))) }); let response = srv.get("/one/").send().await.unwrap(); assert!(response.status().is_success()); + + srv.stop().await +} + +// allow deprecated App::data +#[allow(deprecated)] +#[actix_rt::test] +async fn test_data_drop() { + use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }; + + struct TestData(Arc); + + impl TestData { + fn new(inner: Arc) -> Self { + let _ = inner.fetch_add(1, Ordering::SeqCst); + Self(inner) + } + } + + impl Clone for TestData { + fn clone(&self) -> Self { + let inner = self.0.clone(); + let _ = inner.fetch_add(1, Ordering::SeqCst); + Self(inner) + } + } + + impl Drop for TestData { + fn drop(&mut self) { + self.0.fetch_sub(1, Ordering::SeqCst); + } + } + + let num = Arc::new(AtomicUsize::new(0)); + let data = TestData::new(num.clone()); + assert_eq!(num.load(Ordering::SeqCst), 1); + + let srv = actix_test::start(move || { + let data = data.clone(); + + App::new() + .data(data) + .service(web::resource("/").to(|_data: web::Data| async { "ok" })) + }); + + assert!(srv.get("/").send().await.unwrap().status().is_success()); + srv.stop().await; + + assert_eq!(num.load(Ordering::SeqCst), 0); +} + +#[actix_rt::test] +async fn test_accept_encoding_no_match() { + let srv = actix_test::start_with(actix_test::config().h1(), || { + App::new() + .wrap(Compress::default()) + .service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish()))) + }); + + let response = srv + .get("/") + .append_header((ACCEPT_ENCODING, "compress, identity;q=0")) + .no_decompress() + .send() + .await + .unwrap(); + + assert_eq!(response.status().as_u16(), 406); + + srv.stop().await; }