diff --git a/.cargo/config.toml b/.cargo/config.toml index deb300749..a2345e184 100644 --- a/.cargo/config.toml +++ b/.cargo/config.toml @@ -1,17 +1,10 @@ [alias] -lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo" -lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo" +lint = "clippy --workspace --all-targets -- -Dclippy::todo" +lint-all = "clippy --workspace --all-features --all-targets -- -Dclippy::todo" # lib checking ci-check-min = "hack --workspace check --no-default-features" ci-check-default = "hack --workspace check" ci-check-default-tests = "check --workspace --tests" -ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,experimental-io-uring check" -ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check" - -# testing -ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture" -ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" - -# compile docs as docs.rs would -# RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace +ci-check-all-feature-powerset="hack --workspace --feature-powerset --depth=4 --skip=__compress,experimental-io-uring check" +ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --depth=4 --skip=__compress check" diff --git a/codecov.yml b/.codecov.yml similarity index 100% rename from codecov.yml rename to .codecov.yml diff --git a/.github/dependabot.yml b/.github/dependabot.yml index 1a54090f0..c7ecf5eaa 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -1,12 +1,10 @@ version: 2 updates: - - package-ecosystem: "cargo" - directory: "/" + - package-ecosystem: cargo + directory: / schedule: - interval: "monthly" - open-pull-requests-limit: 10 - - package-ecosystem: "github-actions" - directory: "/" + interval: weekly + - package-ecosystem: github-actions + directory: / schedule: - interval: "monthly" - open-pull-requests-limit: 10 + interval: weekly diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index f0e4cc8b3..fd6bc6d73 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -5,7 +5,7 @@ on: branches: [master] permissions: - contents: read # to fetch code (actions/checkout) + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} @@ -16,7 +16,7 @@ jobs: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust run: | diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 06669e31e..1729d9a07 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -22,30 +22,36 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - nightly + - { name: nightly, version: nightly } - name: ${{ matrix.target.name }} / ${{ matrix.version }} + name: ${{ matrix.target.name }} / ${{ matrix.version.name }} runs-on: ${{ matrix.target.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Install nasm + if: matrix.target.os == 'windows-latest' + uses: ilammy/setup-nasm@v1.5.1 - name: Install OpenSSL if: matrix.target.os == 'windows-latest' - run: choco install openssl -y --forcex64 --no-progress - - name: Set OpenSSL dir in env - if: matrix.target.os == 'windows-latest' + shell: bash run: | - echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append - echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' | Out-File -FilePath $env:GITHUB_ENV -Append + set -e + choco install openssl --version=1.1.1.2100 -y --no-progress + echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV + echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV - - name: Install Rust (${{ matrix.version }}) - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Install Rust (${{ matrix.version.name }}) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: - toolchain: ${{ matrix.version }} + toolchain: ${{ matrix.version.version }} - - name: Install cargo-hack - uses: taiki-e/install-action@cargo-hack + - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean + uses: taiki-e/install-action@v2.38.0 + with: + tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean - name: check minimal run: cargo ci-check-min @@ -55,54 +61,31 @@ jobs: - 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 + run: just test - - name: Clear the cargo caches - run: | - cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean - cargo-cache + - name: CI cache clean + run: cargo-ci-cache-clean ci_feature_powerset_check: name: Verify Feature Combinations runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Free Disk Space + run: ./scripts/free-disk-space.sh - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - name: Install cargo-hack - uses: taiki-e/install-action@cargo-hack + uses: taiki-e/install-action@v2.38.0 + with: + tool: cargo-hack - name: check feature combinations run: cargo ci-check-all-feature-powerset - name: check feature combinations run: cargo ci-check-all-feature-powerset-linux - - nextest: - name: nextest - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - - - name: Install nextest - uses: taiki-e/install-action@nextest - - - name: Test with cargo-nextest - run: cargo nextest run diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f073f6afe..1b6f7b460 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,7 +16,13 @@ concurrency: cancel-in-progress: true jobs: + read_msrv: + name: Read MSRV + uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0 + build_and_test: + needs: read_msrv + strategy: fail-fast: false matrix: @@ -26,35 +32,45 @@ jobs: - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - 1.68.0 # MSRV - - stable + - { name: msrv, version: "${{ needs.read_msrv.outputs.msrv }}" } + - { name: stable, version: stable } - name: ${{ matrix.target.name }} / ${{ matrix.version }} + name: ${{ matrix.target.name }} / ${{ matrix.version.name }} runs-on: ${{ matrix.target.os }} steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 + + - name: Install nasm + if: matrix.target.os == 'windows-latest' + uses: ilammy/setup-nasm@v1.5.1 - name: Install OpenSSL if: matrix.target.os == 'windows-latest' - run: choco install openssl -y --forcex64 --no-progress - - name: Set OpenSSL dir in env - if: matrix.target.os == 'windows-latest' + shell: bash run: | - echo 'OPENSSL_DIR=C:\Program Files\OpenSSL-Win64' | Out-File -FilePath $env:GITHUB_ENV -Append - echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' | Out-File -FilePath $env:GITHUB_ENV -Append + set -e + choco install openssl --version=1.1.1.2100 -y --no-progress + echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV + echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV - - name: Install Rust (${{ matrix.version }}) - uses: actions-rust-lang/setup-rust-toolchain@v1 + - name: Setup mold linker + if: matrix.target.os == 'ubuntu-latest' + uses: rui314/setup-mold@v1 + + - name: Install Rust (${{ matrix.version.name }}) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: - toolchain: ${{ matrix.version }} + toolchain: ${{ matrix.version.version }} - - name: Install cargo-hack - uses: taiki-e/install-action@cargo-hack + - name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean + uses: taiki-e/install-action@v2.38.0 + with: + tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean - # - name: workaround MSRV issues - # if: matrix.version != 'stable' - # run: | + - name: workaround MSRV issues + if: matrix.version.name == 'msrv' + run: just downgrade-for-msrv - name: check minimal run: cargo ci-check-min @@ -64,32 +80,21 @@ jobs: - 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 + run: just test - - name: Clear the cargo caches - run: | - cargo install cargo-cache --version 0.8.3 --no-default-features --features ci-autoclean - cargo-cache + - name: CI cache clean + run: cargo-ci-cache-clean io-uring: name: io-uring tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: { toolchain: nightly } + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly - name: tests (io-uring) timeout-minutes: 60 @@ -100,12 +105,17 @@ jobs: name: doc tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Install Rust (nightly) - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: { toolchain: nightly } + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly + + - name: Install just + uses: taiki-e/install-action@v2.38.0 + with: + tool: just - name: doc tests - run: cargo ci-doctest - timeout-minutes: 60 + run: just test-docs diff --git a/.github/workflows/clippy-fmt.yml b/.github/workflows/clippy-fmt.yml deleted file mode 100644 index 109165ce0..000000000 --- a/.github/workflows/clippy-fmt.yml +++ /dev/null @@ -1,76 +0,0 @@ -name: Lint - -on: - pull_request: - types: [opened, synchronize, reopened] - -permissions: - contents: read # to fetch code (actions/checkout) - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: - toolchain: nightly - components: rustfmt - - - run: cargo fmt --all -- --check - - clippy: - permissions: - checks: write # to add clippy checks to PR diffs - - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: { components: clippy } - - - uses: giraffate/clippy-action@v1 - with: - reporter: 'github-pr-check' - github_token: ${{ secrets.GITHUB_TOKEN }} - clippy_flags: --workspace --all-features --tests --examples --bins -- -Dclippy::todo - - lint-docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: { components: rust-docs } - - - name: Check for broken intra-doc links - env: { RUSTDOCFLAGS: "-D warnings" } - run: cargo doc --no-deps --all-features --workspace - - public-api-diff: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v3 - with: - ref: ${{ github.base_ref }} - - - uses: actions/checkout@v3 - - - uses: actions-rust-lang/setup-rust-toolchain@v1 - # temp: unpin once https://github.com/rust-lang/rust/issues/113152 is fixed - with: { toolchain: nightly-2023-06-28 } - - - uses: taiki-e/cache-cargo-install-action@v1 - with: { tool: cargo-public-api } - - - name: generate API diff - run: | - for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do - cargo public-api --manifest-path "$f" diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} - done diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 42f16450d..de7fd7031 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,5 +1,3 @@ -# disabled because `cargo tarpaulin` currently segfaults - name: Coverage on: @@ -7,28 +5,35 @@ on: branches: [master] permissions: - contents: read # to fetch code (actions/checkout) + contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: - # job currently (1st Feb 2022) segfaults coverage: - name: coverage runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - - uses: actions-rust-lang/setup-rust-toolchain@v1 - with: { toolchain: nightly } - - - name: Generate coverage file - run: | - cargo install cargo-tarpaulin --vers "^0.13" - cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose - - name: Upload to Codecov - uses: codecov/codecov-action@v3.1.4 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: - file: cobertura.xml + components: llvm-tools-preview + + - name: Install just,cargo-llvm-cov + uses: taiki-e/install-action@v2.38.0 + with: + tool: just,cargo-llvm-cov + + - name: Generate code coverage + run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json + + - name: Upload coverage to Codecov + uses: codecov/codecov-action@v4.4.1 + with: + files: codecov.json + fail_ci_if_error: true + env: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml new file mode 100644 index 000000000..8fe8f59d3 --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,93 @@ +name: Lint + +on: + pull_request: + types: [opened, synchronize, reopened] + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + fmt: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust (nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly + components: rustfmt + + - name: Check with Rustfmt + run: cargo fmt --all -- --check + + clippy: + permissions: + contents: read + checks: write # to add clippy checks to PR diffs + + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + components: clippy + + - name: Check with Clippy + uses: giraffate/clippy-action@v1.0.1 + with: + reporter: github-pr-check + github_token: ${{ secrets.GITHUB_TOKEN }} + clippy_flags: >- + --workspace --all-features --tests --examples --bins -- + -A unknown_lints -D clippy::todo -D clippy::dbg_macro + + lint-docs: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust (nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly + components: rust-docs + + - name: Check for broken intra-doc links + env: + RUSTDOCFLAGS: -D warnings + run: cargo +nightly doc --no-deps --workspace --all-features + + public-api-diff: + runs-on: ubuntu-latest + steps: + - name: Checkout main branch + uses: actions/checkout@v4 + with: + ref: ${{ github.base_ref }} + + - name: Checkout PR branch + uses: actions/checkout@v4 + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly-2024-06-07 + + - name: Install cargo-public-api + uses: taiki-e/install-action@v2.38.0 + with: + tool: cargo-public-api + + - name: Generate API diff + run: | + for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do + cargo public-api --manifest-path "$f" --simplified diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }} + done diff --git a/.github/workflows/upload-doc.yml b/.github/workflows/upload-doc.yml deleted file mode 100644 index 05b81411d..000000000 --- a/.github/workflows/upload-doc.yml +++ /dev/null @@ -1,38 +0,0 @@ -name: Upload Documentation - -on: - push: - branches: [master] - -permissions: - contents: read # to fetch code (actions/checkout) - -concurrency: - group: ${{ github.workflow }}-${{ github.ref }} - cancel-in-progress: true - -jobs: - build: - permissions: - contents: write # to push changes in repo (jamesives/github-pages-deploy-action) - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v3 - - - uses: dtolnay/rust-toolchain@nightly - - - name: Build Docs - run: cargo +nightly doc --no-deps --workspace --all-features - env: - RUSTDOCFLAGS: --cfg=docsrs - - - name: Tweak HTML - run: echo '' > target/doc/index.html - - - name: Deploy to GitHub Pages - uses: JamesIves/github-pages-deploy-action@v4.4.3 - with: - folder: target/doc - single-commit: true diff --git a/.gitignore b/.gitignore index 543403267..48ccccb92 100644 --- a/.gitignore +++ b/.gitignore @@ -19,3 +19,7 @@ guide/build/ # Configuration directory generated by VSCode .vscode + +# code coverage +/lcov.info +/codecov.json diff --git a/.prettierrc.yml b/.prettierrc.yml index b61fd8974..d70303479 100644 --- a/.prettierrc.yml +++ b/.prettierrc.yml @@ -1,5 +1,5 @@ overrides: - - files: '*.md' + - files: "*.md" options: printWidth: 9999 proseWrap: never diff --git a/Cargo.toml b/Cargo.toml index 65e3c6ae8..19d5dd116 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,6 +14,13 @@ members = [ "awc", ] +[workspace.package] +homepage = "https://actix.rs" +repository = "https://github.com/actix/actix-web" +license = "MIT OR Apache-2.0" +edition = "2021" +rust-version = "1.72" + [profile.dev] # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. debug = 0 diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 31c0499ed..e94f43907 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,10 +1,22 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased +## 0.6.6 + +- Update `tokio-uring` dependency to `0.4`. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 0.6.5 + +- Fix handling of special characters in filenames. + +## 0.6.4 + +- Fix handling of newlines in filenames. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 0.6.3 - 2023-01-21 +## 0.6.3 - XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. @@ -12,14 +24,14 @@ [#2903]: https://github.com/actix/actix-web/pull/2903 -## 0.6.2 - 2022-07-23 +## 0.6.2 - Allow partial range responses for video content to start streaming sooner. [#2817] - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. [#2817]: https://github.com/actix/actix-web/pull/2817 -## 0.6.1 - 2022-06-11 +## 0.6.1 - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] - Update `tokio-uring` dependency to `0.3`. @@ -29,25 +41,25 @@ [#2021]: https://github.com/actix/actix-web/pull/2021 [#2645]: https://github.com/actix/actix-web/pull/2645 -## 0.6.0 - 2022-02-25 +## 0.6.0 - No significant changes since `0.6.0-beta.16`. -## 0.6.0-beta.16 - 2022-01-31 +## 0.6.0-beta.16 - No significant changes since `0.6.0-beta.15`. -## 0.6.0-beta.15 - 2022-01-21 +## 0.6.0-beta.15 - No significant changes since `0.6.0-beta.14`. -## 0.6.0-beta.14 - 2022-01-14 +## 0.6.0-beta.14 - The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583] [#2583]: https://github.com/actix/actix-web/pull/2583 -## 0.6.0-beta.13 - 2022-01-04 +## 0.6.0-beta.13 - The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] - The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] @@ -55,19 +67,19 @@ [#2398]: https://github.com/actix/actix-web/pull/2398 -## 0.6.0-beta.12 - 2021-12-29 +## 0.6.0-beta.12 - No significant changes since `0.6.0-beta.11`. -## 0.6.0-beta.11 - 2021-12-27 +## 0.6.0-beta.11 - No significant changes since `0.6.0-beta.10`. -## 0.6.0-beta.10 - 2021-12-11 +## 0.6.0-beta.10 - No significant changes since `0.6.0-beta.9`. -## 0.6.0-beta.9 - 2021-11-22 +## 0.6.0-beta.9 - 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] @@ -79,15 +91,15 @@ [#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 +## 0.6.0-beta.8 - Minimum supported Rust version (MSRV) is now 1.52. -## 0.6.0-beta.7 - 2021-09-09 +## 0.6.0-beta.7 - Minimum supported Rust version (MSRV) is now 1.51. -## 0.6.0-beta.6 - 2021-06-26 +## 0.6.0-beta.6 - 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] @@ -95,7 +107,7 @@ [#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 +## 0.6.0-beta.5 - `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] @@ -107,17 +119,17 @@ [#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 +## 0.6.0-beta.4 - 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 +## 0.6.0-beta.3 - No notable changes. -## 0.6.0-beta.2 - 2021-02-10 +## 0.6.0-beta.2 - Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] - Replace `v_htmlescape` with `askama_escape`. [#1953] @@ -125,39 +137,39 @@ [#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 +## 0.6.0-beta.1 - `HttpRange::parse` now has its own error type. - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 -## 0.5.0 - 2020-12-26 +## 0.5.0 - Optionally support hidden files/directories. [#1811] [#1811]: https://github.com/actix/actix-web/pull/1811 -## 0.4.1 - 2020-11-24 +## 0.4.1 - Clarify order of parameters in `Files::new` and improve docs. -## 0.4.0 - 2020-10-06 +## 0.4.0 - Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714] [#1714]: https://github.com/actix/actix-web/pull/1714 -## 0.3.0 - 2020-09-11 +## 0.3.0 - No significant changes from 0.3.0-beta.1. -## 0.3.0-beta.1 - 2020-07-15 +## 0.3.0-beta.1 - Update `v_htmlescape` to 0.10 - Update `actix-web` and `actix-http` dependencies to beta.1 -## 0.3.0-alpha.1 - 2020-05-23 +## 0.3.0-alpha.1 - Update `actix-web` and `actix-http` dependencies to alpha - Fix some typos in the docs @@ -166,73 +178,73 @@ [#1384]: https://github.com/actix/actix-web/pull/1384 -## 0.2.1 - 2019-12-22 +## 0.2.1 - Use the same format for file URLs regardless of platforms -## 0.2.0 - 2019-12-20 +## 0.2.0 - Fix BodyEncoding trait import #1220 -## 0.2.0-alpha.1 - 2019-12-07 +## 0.2.0-alpha.1 - Migrate to `std::future` -## 0.1.7 - 2019-11-06 +## 0.1.7 - Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) -## 0.1.6 - 2019-10-14 +## 0.1.6 - Add option to redirect to a slash-ended path `Files` #1132 -## 0.1.5 - 2019-10-08 +## 0.1.5 - Bump up `mime_guess` crate version to 2.0.1 - Bump up `percent-encoding` crate version to 2.1 - Allow user defined request guards for `Files` #1113 -## 0.1.4 - 2019-07-20 +## 0.1.4 - Allow to disable `Content-Disposition` header #686 -## 0.1.3 - 2019-06-28 +## 0.1.3 - Do not set `Content-Length` header, let actix-http set it #930 -## 0.1.2 - 2019-06-13 +## 0.1.2 - Content-Length is 0 for NamedFile HEAD request #914 - Fix ring dependency from actix-web default features for #741 -## 0.1.1 - 2019-06-01 +## 0.1.1 - Static files are incorrectly served as both chunked and with length #812 -## 0.1.0 - 2019-05-25 +## 0.1.0 - NamedFile last-modified check always fails due to nano-seconds in file modified date #820 -## 0.1.0-beta.4 - 2019-05-12 +## 0.1.0-beta.4 - Update actix-web to beta.4 -## 0.1.0-beta.1 - 2019-04-20 +## 0.1.0-beta.1 - Update actix-web to beta.1 -## 0.1.0-alpha.6 - 2019-04-14 +## 0.1.0-alpha.6 - Update actix-web to alpha6 -## 0.1.0-alpha.4 - 2019-04-08 +## 0.1.0-alpha.4 - Update actix-web to alpha4 -## 0.1.0-alpha.2 - 2019-04-02 +## 0.1.0-alpha.2 - Add default handler support -## 0.1.0-alpha.1 - 2019-03-28 +## 0.1.0-alpha.1 - Initial impl diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 80c609d1d..7adb8eaf5 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.3" +version = "0.6.6" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -40,11 +40,12 @@ v_htmlescape = "0.15.5" # experimental-io-uring [target.'cfg(target_os = "linux")'.dependencies] -tokio-uring = { version = "0.4", optional = true, features = ["bytes"] } -actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions +tokio-uring = { version = "0.5", optional = true, features = ["bytes"] } +actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions [dev-dependencies] actix-rt = "2.7" actix-test = "0.1" actix-web = "4" +env_logger = "0.11" tempfile = "3.2" diff --git a/actix-files/README.md b/actix-files/README.md index 3e656c431..f6d5143f5 100644 --- a/actix-files/README.md +++ b/actix-files/README.md @@ -1,18 +1,32 @@ -# actix-files +# `actix-files` -> 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.6.3)](https://docs.rs/actix-files/0.6.3) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.6)](https://docs.rs/actix-files/0.6.6) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
-[![dependency status](https://deps.rs/crate/actix-files/0.6.3/status.svg)](https://deps.rs/crate/actix-files/0.6.3) +[![dependency status](https://deps.rs/crate/actix-files/0.6.6/status.svg)](https://deps.rs/crate/actix-files/0.6.6) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![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/basics/static-files) -- Minimum Supported Rust Version (MSRV): 1.68 + + +Static file serving for Actix Web. + +Provides a non-blocking service for serving static files from disk. + +## Examples + +```rust +use actix_web::App; +use actix_files::Files; + +let app = App::new() + .service(Files::new("/static", ".").prefer_utf8(true)); +``` + + diff --git a/actix-files/examples/guarded-listing.rs b/actix-files/examples/guarded-listing.rs new file mode 100644 index 000000000..e8cde0c85 --- /dev/null +++ b/actix-files/examples/guarded-listing.rs @@ -0,0 +1,33 @@ +use actix_files::Files; +use actix_web::{get, guard, middleware, App, HttpServer, Responder}; + +const EXAMPLES_DIR: &str = concat![env!("CARGO_MANIFEST_DIR"), "/examples"]; + +#[get("/")] +async fn index() -> impl Responder { + "Hello world!" +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + log::info!("starting HTTP server at http://localhost:8080"); + + HttpServer::new(|| { + App::new() + .service(index) + .service( + Files::new("/assets", EXAMPLES_DIR) + .show_files_listing() + .guard(guard::Header("show-listing", "?1")), + ) + .service(Files::new("/assets", EXAMPLES_DIR)) + .wrap(middleware::Compress::default()) + .wrap(middleware::Logger::default()) + }) + .bind(("127.0.0.1", 8080))? + .workers(2) + .run() + .await +} diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index e34b5f26a..cfd3b9c22 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -235,7 +235,7 @@ impl Files { /// 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`]. + /// To allow `POST` requests to retrieve files, see [`Files::method_guard()`]. /// /// # Examples /// ``` diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 1d8609889..167f996c0 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -13,7 +13,6 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible, missing_docs, missing_debug_implementations)] -#![allow(clippy::uninlined_format_args)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -66,6 +65,7 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool; #[cfg(test)] mod tests { use std::{ + fmt::Write as _, fs::{self}, ops::Add, time::{Duration, SystemTime}, @@ -75,7 +75,7 @@ mod tests { dev::ServiceFactory, guard, http::{ - header::{self, ContentDisposition, DispositionParam, DispositionType}, + header::{self, ContentDisposition, DispositionParam}, Method, StatusCode, }, middleware::Compress, @@ -568,6 +568,30 @@ mod tests { assert_eq!(bytes, data); } + #[cfg(not(target_os = "windows"))] + #[actix_rt::test] + async fn test_static_files_with_special_characters() { + // Create the file we want to test against ad-hoc. We can't check it in as otherwise + // Windows can't even checkout this repository. + let temp_dir = tempfile::tempdir().unwrap(); + let file_with_newlines = temp_dir.path().join("test\n\x0B\x0C\rnewline.text"); + fs::write(&file_with_newlines, "Look at my newlines").unwrap(); + + let srv = test::init_service( + App::new().service(Files::new("/", temp_dir.path()).index_file("Cargo.toml")), + ) + .await; + let request = TestRequest::get() + .uri("/test%0A%0B%0C%0Dnewline.text") + .to_request(); + let response = test::call_service(&srv, request).await; + assert_eq!(response.status(), StatusCode::OK); + + let bytes = test::read_body(response).await; + let data = web::Bytes::from(fs::read(file_with_newlines).unwrap()); + assert_eq!(bytes, data); + } + #[actix_rt::test] async fn test_files_not_allowed() { let srv = test::init_service(App::new().service(Files::new("/", "."))).await; @@ -840,19 +864,21 @@ mod tests { #[actix_rt::test] async fn test_percent_encoding_2() { - let tmpdir = tempfile::tempdir().unwrap(); + let temp_dir = tempfile::tempdir().unwrap(); let filename = match cfg!(unix) { - true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test", + true => "ض:?#[]{}<>()@!$&'`|*+,;= %20\n.test", false => "ض#[]{}()@!$&'`+,;= %20.test", }; let filename_encoded = filename .as_bytes() .iter() - .map(|c| format!("%{:02X}", c)) - .collect::(); - std::fs::File::create(tmpdir.path().join(filename)).unwrap(); + .fold(String::new(), |mut buf, c| { + write!(&mut buf, "%{:02X}", c).unwrap(); + buf + }); + std::fs::File::create(temp_dir.path().join(filename)).unwrap(); - let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await; + let srv = test::init_service(App::new().service(Files::new("/", temp_dir.path()))).await; let req = TestRequest::get() .uri(&format!("/{}", filename_encoded)) diff --git a/actix-files/src/named.rs b/actix-files/src/named.rs index d7795ba73..9e4a37737 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -24,7 +24,6 @@ use bitflags::bitflags; use derive_more::{Deref, DerefMut}; use futures_core::future::LocalBoxFuture; use mime::Mime; -use mime_guess::from_path; use crate::{encoding::equiv_utf8_text, range::HttpRange}; @@ -128,7 +127,7 @@ impl NamedFile { } }; - let ct = from_path(&path).first_or_octet_stream(); + let ct = mime_guess::from_path(&path).first_or_octet_stream(); let disposition = match ct.type_() { mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline, @@ -140,7 +139,13 @@ impl NamedFile { _ => DispositionType::Attachment, }; - let mut parameters = vec![DispositionParam::Filename(String::from(filename.as_ref()))]; + // replace special characters in filenames which could occur on some filesystems + let filename_s = filename + .replace('\n', "%0A") // \n line break + .replace('\x0B', "%0B") // \v vertical tab + .replace('\x0C', "%0C") // \f form feed + .replace('\r', "%0D"); // \r carriage return + let mut parameters = vec![DispositionParam::Filename(filename_s)]; if !filename.is_ascii() { parameters.push(DispositionParam::FilenameExt(ExtendedValue { diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 0fc89214d..4d133e3ec 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,14 +1,18 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased + +- Minimum supported Rust version (MSRV) is now 1.72. + +## 3.2.0 - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 3.1.0 - 2023-01-21 +## 3.1.0 - Minimum supported Rust version (MSRV) is now 1.59. -## 3.0.0 - 2022-07-24 +## 3.0.0 - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - Added `TestServer::client_headers` method. [#2097] @@ -24,41 +28,41 @@
3.0.0 Pre-Releases -## 3.0.0-beta.13 - 2022-02-16 +## 3.0.0-beta.13 - No significant changes since `3.0.0-beta.12`. -## 3.0.0-beta.12 - 2022-01-31 +## 3.0.0-beta.12 - No significant changes since `3.0.0-beta.11`. -## 3.0.0-beta.11 - 2022-01-04 +## 3.0.0-beta.11 - Minimum supported Rust version (MSRV) is now 1.54. -## 3.0.0-beta.10 - 2021-12-27 +## 3.0.0-beta.10 - Update `actix-server` to `2.0.0-rc.2`. [#2550] [#2550]: https://github.com/actix/actix-web/pull/2550 -## 3.0.0-beta.9 - 2021-12-11 +## 3.0.0-beta.9 - No significant changes since `3.0.0-beta.8`. -## 3.0.0-beta.8 - 2021-11-30 +## 3.0.0-beta.8 - 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 +## 3.0.0-beta.7 - 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 +## 3.0.0-beta.6 - `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] @@ -66,25 +70,25 @@ [#2442]: https://github.com/actix/actix-web/pull/2442 -## 3.0.0-beta.5 - 2021-09-09 +## 3.0.0-beta.5 - Minimum supported Rust version (MSRV) is now 1.51. -## 3.0.0-beta.4 - 2021-04-02 +## 3.0.0-beta.4 - Added `TestServer::client_headers` method. [#2097] [#2097]: https://github.com/actix/actix-web/pull/2097 -## 3.0.0-beta.3 - 2021-03-09 +## 3.0.0-beta.3 - No notable changes. -## 3.0.0-beta.2 - 2021-02-10 +## 3.0.0-beta.2 - No notable changes. -## 3.0.0-beta.1 - 2021-01-07 +## 3.0.0-beta.1 - Update `bytes` to `1.0`. [#1813] @@ -92,7 +96,7 @@
-## 2.1.0 - 2020-11-25 +## 2.1.0 - Add ability to set address for `TestServer`. [#1645] - Upgrade `base64` to `0.13`. @@ -101,11 +105,11 @@ [#1773]: https://github.com/actix/actix-web/pull/1773 [#1645]: https://github.com/actix/actix-web/pull/1645 -## 2.0.0 - 2020-09-11 +## 2.0.0 - Update actix-codec and actix-utils dependencies. -## 2.0.0-alpha.1 - 2020-05-23 +## 2.0.0-alpha.1 - Update the `time` dependency to 0.2.7 - Update `actix-connect` dependency to 2.0.0-alpha.2 @@ -115,57 +119,57 @@ - Update `base64` dependency to 0.12 - Update `env_logger` dependency to 0.7 -## 1.0.0 - 2019-12-13 +## 1.0.0 - Replaced `TestServer::start()` with `test_server()` -## 1.0.0-alpha.3 - 2019-12-07 +## 1.0.0-alpha.3 - Migrate to `std::future` -## 0.2.5 - 2019-09-17 +## 0.2.5 - Update serde_urlencoded to "0.6.1" - Increase TestServerRuntime timeouts from 500ms to 3000ms - Do not override current `System` -## 0.2.4 - 2019-07-18 +## 0.2.4 - Update actix-server to 0.6 -## 0.2.3 - 2019-07-16 +## 0.2.3 - Add `delete`, `options`, `patch` methods to `TestServerRunner` -## 0.2.2 - 2019-06-16 +## 0.2.2 - Add .put() and .sput() methods -## 0.2.1 - 2019-06-05 +## 0.2.1 - Add license files -## 0.2.0 - 2019-05-12 +## 0.2.0 - Update awc and actix-http deps -## 0.1.1 - 2019-04-24 +## 0.1.1 - Always make new connection for http client -## 0.1.0 - 2019-04-16 +## 0.1.0 - No changes -## 0.1.0-alpha.3 - 2019-04-02 +## 0.1.0-alpha.3 - Request functions accept path #743 -## 0.1.0-alpha.2 - 2019-03-29 +## 0.1.0-alpha.2 - Added TestServerRuntime::load_body() method - Update actix-http and awc libraries -## 0.1.0-alpha.1 - 2019-03-28 +## 0.1.0-alpha.1 - Initial impl diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 7f00ba30a..bfb0a3539 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "actix-http-test" -version = "3.1.0" +version = "3.2.0" authors = ["Nikolay Kim "] description = "Various helpers for Actix applications to use during testing" keywords = ["http", "web", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" +repository = "https://github.com/actix/actix-web" categories = [ "network-programming", "asynchronous", diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 4b286e603..ee242d1d5 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -1,17 +1,16 @@ -# actix-http-test +# `actix-http-test` > 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=3.1.0)](https://docs.rs/actix-http-test/3.1.0) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.2.0)](https://docs.rs/actix-http-test/3.2.0) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![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.1.0/status.svg)](https://deps.rs/crate/actix-http-test/3.1.0) +[![Dependency Status](https://deps.rs/crate/actix-http-test/3.2.0/status.svg)](https://deps.rs/crate/actix-http-test/3.2.0) [![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) -- Minimum Supported Rust Version (MSRV): 1.68 + diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index 2f1725d1c..554af9102 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -2,7 +2,6 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::uninlined_format_args)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 0dedd2c74..85ba03100 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,23 +1,71 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased ### Added -- Add `body::to_body_limit()` function. +- Add `error::InvalidStatusCode` re-export. + +## 3.7.0 + +### Added + +- Add `rustls-0_23` crate feature +- Add `{h1::H1Service, h2::H2Service, HttpService}::rustls_0_23()` and `HttpService::rustls_0_23_with_config()` service constructors. + +### Changed + +- Update `brotli` dependency to `6`. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 3.6.0 + +### Added + +- Add `rustls-0_22` crate feature. +- Add `{h1::H1Service, h2::H2Service, HttpService}::rustls_0_22()` and `HttpService::rustls_0_22_with_config()` service constructors. +- Implement `From<&HeaderMap>` for `http::HeaderMap`. + +## 3.5.1 + +### Fixed + +- Prevent hang when returning zero-sized response bodies through compression layer. + +## 3.5.0 + +### Added + +- Implement `From` for `http::HeaderMap`. + +### Changed + +- Updated `zstd` dependency to `0.13`. + +### Fixed + +- Prevent compression of zero-sized response bodies. + +## 3.4.0 + +### Added + +- Add `rustls-0_20` crate feature. +- Add `{h1::H1Service, h2::H2Service, HttpService}::rustls_021()` and `HttpService::rustls_021_with_config()` service constructors. +- Add `body::to_bytes_limited()` function. - Add `body::BodyLimitExceeded` error type. ### Changed - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 3.3.1 - 2023-03-02 +## 3.3.1 ### Fixed - Use correct `http` version requirement to ensure support for const `HeaderName` definitions. -## 3.3.0 - 2023-01-21 +## 3.3.0 ### Added @@ -53,7 +101,7 @@ [#2956]: https://github.com/actix/actix-web/pull/2956 [#2968]: https://github.com/actix/actix-web/pull/2968 -## 3.2.2 - 2022-09-11 +## 3.2.2 ### Changed @@ -65,7 +113,7 @@ [#2369]: https://github.com/actix/actix-web/pull/2369 -## 3.2.1 - 2022-07-02 +## 3.2.1 ### Fixed @@ -73,7 +121,7 @@ [#2794]: https://github.com/actix/actix-web/pull/2794 -## 3.2.0 - 2022-06-30 +## 3.2.0 ### Changed @@ -87,7 +135,7 @@ [#2790]: https://github.com/actix/actix-web/pull/2790 [#2798]: https://github.com/actix/actix-web/pull/2798 -## 3.1.0 - 2022-06-11 +## 3.1.0 ### Changed @@ -101,13 +149,13 @@ [#2357]: https://github.com/actix/actix-web/issues/2357 [#2779]: https://github.com/actix/actix-web/pull/2779 -## 3.0.4 - 2022-03-09 +## 3.0.4 ### Fixed - Document on docs.rs with `ws` feature enabled. -## 3.0.3 - 2022-03-08 +## 3.0.3 ### Fixed @@ -115,7 +163,7 @@ [#2684]: https://github.com/actix/actix-web/pull/2684 -## 3.0.2 - 2022-03-05 +## 3.0.2 ### Fixed @@ -123,13 +171,13 @@ [#2683]: https://github.com/actix/actix-web/pull/2683 -## 3.0.1 - 2022-03-04 +## 3.0.1 - Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678] [#2678]: https://github.com/actix/actix-web/issues/2678 -## 3.0.0 - 2022-02-25 +## 3.0.0 ### Dependencies @@ -417,7 +465,7 @@
3.0.0 Pre-Releases -## 3.0.0-rc.4 - 2022-02-22 +## 3.0.0-rc.4 ### Fixed @@ -425,11 +473,11 @@ [1ce58ecb]: https://github.com/actix/actix-web/commit/1ce58ecb305c60e51db06e6c913b7a1344e229ca -## 3.0.0-rc.3 - 2022-02-16 +## 3.0.0-rc.3 - No significant changes since `3.0.0-rc.2`. -## 3.0.0-rc.2 - 2022-02-08 +## 3.0.0-rc.2 ### Added @@ -446,7 +494,7 @@ [#2624]: https://github.com/actix/actix-web/pull/2624 [#2625]: https://github.com/actix/actix-web/pull/2625 -## 3.0.0-rc.1 - 2022-01-31 +## 3.0.0-rc.1 ### Added @@ -482,7 +530,7 @@ [#2611]: https://github.com/actix/actix-web/pull/2611 [#2618]: https://github.com/actix/actix-web/pull/2618 -## 3.0.0-beta.19 - 2022-01-21 +## 3.0.0-beta.19 ### Added @@ -502,7 +550,7 @@ [#2585]: https://github.com/actix/actix-web/pull/2585 [#2587]: https://github.com/actix/actix-web/pull/2587 -## 3.0.0-beta.18 - 2022-01-04 +## 3.0.0-beta.18 ### Added @@ -533,7 +581,7 @@ [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 -## 3.0.0-beta.17 - 2021-12-27 +## 3.0.0-beta.17 ### Changed @@ -551,7 +599,7 @@ [#2527]: https://github.com/actix/actix-web/pull/2527 [#2545]: https://github.com/actix/actix-web/pull/2545 -## 3.0.0-beta.16 - 2021-12-17 +## 3.0.0-beta.16 ### Added @@ -570,7 +618,7 @@ [#2510]: https://github.com/actix/actix-web/pull/2510 [#2522]: https://github.com/actix/actix-web/pull/2522 -## 3.0.0-beta.15 - 2021-12-11 +## 3.0.0-beta.15 ### Added @@ -624,7 +672,7 @@ [#2497]: https://github.com/actix/actix-web/pull/2497 [#2520]: https://github.com/actix/actix-web/pull/2520 -## 3.0.0-beta.14 - 2021-11-30 +## 3.0.0-beta.14 ### Changed @@ -637,7 +685,7 @@ [#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 +## 3.0.0-beta.13 ### Added @@ -666,7 +714,7 @@ [#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 +## 3.0.0-beta.12 ### Changed @@ -680,7 +728,7 @@ [#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 +## 3.0.0-beta.11 ### Changed @@ -689,7 +737,7 @@ [#2414]: https://github.com/actix/actix-web/pull/2414 -## 3.0.0-beta.10 - 2021-09-09 +## 3.0.0-beta.10 ### Changed @@ -707,13 +755,13 @@ [#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 +## 3.0.0-beta.9 ### 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 +## 3.0.0-beta.8 ### Changed @@ -726,7 +774,7 @@ [#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 +## 3.0.0-beta.7 ### Added @@ -773,7 +821,7 @@ [#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 +## 3.0.0-beta.6 ### Added @@ -808,7 +856,7 @@ [#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 +## 3.0.0-beta.5 ### Added @@ -830,7 +878,7 @@ [#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 +## 3.0.0-beta.4 ### Changed @@ -848,11 +896,11 @@ [#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 +## 3.0.0-beta.3 - No notable changes. -## 3.0.0-beta.2 - 2021-02-10 +## 3.0.0-beta.2 ### Added @@ -904,7 +952,7 @@ [#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 +## 3.0.0-beta.1 ### Added @@ -932,7 +980,7 @@
-## 2.2.2 - 2022-01-21 +## 2.2.2 ### Changed @@ -940,13 +988,13 @@ [ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06 -## 2.2.1 - 2021-08-09 +## 2.2.1 ### Fixed - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) -## 2.2.0 - 2020-11-25 +## 2.2.0 ### Added @@ -968,7 +1016,7 @@ [#1793]: https://github.com/actix/actix-web/pull/1793 [#1797]: https://github.com/actix/actix-web/pull/1797 -## 2.1.0 - 2020-10-30 +## 2.1.0 ### Added @@ -985,18 +1033,18 @@ [#1733]: https://github.com/actix/actix-web/pull/1733 [#1744]: https://github.com/actix/actix-web/pull/1744 -## 2.0.0 - 2020-09-11 +## 2.0.0 - No significant changes from `2.0.0-beta.4`. -## 2.0.0-beta.4 - 2020-09-09 +## 2.0.0-beta.4 ### Changed - Update actix-codec and actix-utils dependencies. - Update actix-connect and actix-tls dependencies. -## 2.0.0-beta.3 - 2020-08-14 +## 2.0.0-beta.3 ### Fixed @@ -1004,7 +1052,7 @@ [#1626]: https://github.com/actix/actix-web/pull/1626 -## 2.0.0-beta.2 - 2020-07-21 +## 2.0.0-beta.2 ### Fixed @@ -1017,7 +1065,7 @@ [#1614]: https://github.com/actix/actix-web/pull/1614 [#1615]: https://github.com/actix/actix-web/pull/1615 -## 2.0.0-beta.1 - 2020-07-11 +## 2.0.0-beta.1 ### Changed @@ -1030,7 +1078,7 @@ [#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 ### Changed @@ -1046,7 +1094,7 @@ [#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 ### Fixed @@ -1061,7 +1109,7 @@ [#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 ### Changed @@ -1073,7 +1121,7 @@ [#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 ### Changed @@ -1086,14 +1134,14 @@ - Allow `SameSite=None` cookies to be sent in a response. -## 1.0.1 - 2019-12-20 +## 1.0.1 ### Fixed - Poll upgrade service's readiness from HTTP service handlers - Replace brotli with brotli2 #1224 -## 1.0.0 - 2019-12-13 +## 1.0.0 ### Added @@ -1103,7 +1151,7 @@ - Replace `flate2-xxx` features with `compress` -## 1.0.0-alpha.5 - 2019-12-09 +## 1.0.0-alpha.5 ### Fixed @@ -1114,7 +1162,7 @@ - Websockets: Ping and Pong should have binary data #1049 -## 1.0.0-alpha.4 - 2019-12-08 +## 1.0.0-alpha.4 ### Added @@ -1124,14 +1172,14 @@ - Use rust based brotli compression library -## 1.0.0-alpha.3 - 2019-12-07 +## 1.0.0-alpha.3 ### Changed - Migrate to tokio 0.2 - Migrate to `std::future` -## 0.2.11 - 2019-11-06 +## 0.2.11 ### Added @@ -1145,7 +1193,7 @@ [#1878]: https://github.com/actix/actix-web/pull/1878 -## 0.2.10 - 2019-09-11 +## 0.2.10 ### Added @@ -1156,7 +1204,7 @@ - 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 ### Changed @@ -1168,7 +1216,7 @@ - Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) -## 0.2.8 - 2019-08-01 +## 0.2.8 ### Added @@ -1180,20 +1228,20 @@ - awc client panic #1016 - Invalid response with compression middleware enabled, but compression-related features disabled #997 -## 0.2.7 - 2019-07-18 +## 0.2.7 ### Added - Add support for downcasting response errors #986 -## 0.2.6 - 2019-07-17 +## 0.2.6 ### Changed - Replace `ClonableService` with local copy - Upgrade `rand` dependency version to 0.7 -## 0.2.5 - 2019-06-28 +## 0.2.5 ### Added @@ -1204,13 +1252,13 @@ - 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 ### Fixed - Do not compress NoContent (204) responses #918 -## 0.2.3 - 2019-06-02 +## 0.2.3 ### Added @@ -1221,19 +1269,19 @@ - SizedStream uses u64 -## 0.2.2 - 2019-05-29 +## 0.2.2 ### Fixed - Parse incoming stream before closing stream on disconnect #868 -## 0.2.1 - 2019-05-25 +## 0.2.1 ### Fixed - Handle socket read disconnect -## 0.2.0 - 2019-05-12 +## 0.2.0 ### Changed @@ -1244,13 +1292,13 @@ - `OneRequest` service -## 0.1.5 - 2019-05-04 +## 0.1.5 ### Fixed - Clean up response extensions in response pool #817 -## 0.1.4 - 2019-04-24 +## 0.1.4 ### Added @@ -1260,20 +1308,20 @@ - Read until eof for http/1.0 responses #771 -## 0.1.3 - 2019-04-23 +## 0.1.3 ### Fixed - Fix http client pool management - Fix http client wait queue management #794 -## 0.1.2 - 2019-04-23 +## 0.1.2 ### Fixed - Fix BorrowMutError panic in client connector #793 -## 0.1.1 - 2019-04-19 +## 0.1.1 ### Changed @@ -1281,7 +1329,7 @@ - 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 ### Added @@ -1292,7 +1340,7 @@ - `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 ### Added @@ -1304,7 +1352,7 @@ - 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 ### Added @@ -1321,7 +1369,7 @@ - Removed PayloadBuffer -## 0.1.0-alpha.3 - 2019-04-02 +## 0.1.0-alpha.3 ### Added @@ -1333,7 +1381,7 @@ - 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 ### Added @@ -1343,6 +1391,6 @@ - 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 - Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 6909b785f..87e2b391d 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,26 +1,38 @@ [package] name = "actix-http" -version = "3.3.1" +version = "3.7.0" authors = [ "Nikolay Kim ", "Rob Ede ", ] -description = "HTTP primitives for the Actix ecosystem" +description = "HTTP types and services for the Actix ecosystem" keywords = ["actix", "http", "framework", "async", "futures"] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" +repository = "https://github.com/actix/actix-web" categories = [ "network-programming", "asynchronous", "web-programming::http-server", "web-programming::websocket", ] -license = "MIT OR Apache-2.0" -edition = "2021" +license.workspace = true +edition.workspace = true +rust-version.workspace = true [package.metadata.docs.rs] -# features that docs.rs will build with -features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"] +rustdoc-args = ["--cfg", "docsrs"] +features = [ + "http2", + "ws", + "openssl", + "rustls-0_20", + "rustls-0_21", + "rustls-0_22", + "rustls-0_23", + "compress-brotli", + "compress-gzip", + "compress-zstd", +] [lib] name = "actix_http" @@ -43,15 +55,27 @@ ws = [ # TLS via OpenSSL openssl = ["actix-tls/accept", "actix-tls/openssl"] -# TLS via Rustls -rustls = ["actix-tls/accept", "actix-tls/rustls"] +# TLS via Rustls v0.20 +rustls = ["rustls-0_20"] + +# TLS via Rustls v0.20 +rustls-0_20 = ["actix-tls/accept", "actix-tls/rustls-0_20"] + +# TLS via Rustls v0.21 +rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"] + +# TLS via Rustls v0.22 +rustls-0_22 = ["actix-tls/accept", "actix-tls/rustls-0_22"] + +# TLS via Rustls v0.23 +rustls-0_23 = ["actix-tls/accept", "actix-tls/rustls-0_23"] # Compression codecs compress-brotli = ["__compress", "brotli"] compress-gzip = ["__compress", "flate2"] compress-zstd = ["__compress", "zstd"] -# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. __compress = [] @@ -82,50 +106,59 @@ tokio-util = { version = "0.7", features = ["io", "codec"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] } # http2 -h2 = { version = "0.3.17", optional = true } +h2 = { version = "0.3.26", optional = true } # websockets local-channel = { version = "0.1", optional = true } -base64 = { version = "0.21", optional = true } +base64 = { version = "0.22", optional = true } rand = { version = "0.8", optional = true } sha1 = { version = "0.10", optional = true } # openssl/rustls -actix-tls = { version = "3", default-features = false, optional = true } +actix-tls = { version = "3.4", default-features = false, optional = true } # compress-* -brotli = { version = "3.3.3", optional = true } +brotli = { version = "6", optional = true } flate2 = { version = "1.0.13", optional = true } -zstd = { version = "0.12", optional = true } +zstd = { version = "0.13", optional = true } [dev-dependencies] actix-http-test = { version = "3", features = ["openssl"] } actix-server = "2" -actix-tls = { version = "3", features = ["openssl"] } +actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23-webpki-roots"] } actix-web = "4" async-stream = "0.3" criterion = { version = "0.5", features = ["html_reports"] } -env_logger = "0.10" +divan = "0.1.8" +env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } memchr = "2.4" once_cell = "1.9" -rcgen = "0.11" +rcgen = "0.13" regex = "1.3" rustversion = "1" -rustls-pemfile = "1" +rustls-pemfile = "2" serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.55" } -tls-rustls = { package = "rustls", version = "0.20" } +tls-rustls_023 = { package = "rustls", version = "0.23" } tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } [[example]] name = "ws" -required-features = ["ws", "rustls"] +required-features = ["ws", "rustls-0_23"] + +[[example]] +name = "tls_rustls" +required-features = ["http2", "rustls-0_23"] [[bench]] name = "response-body-compression" harness = false required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] + +[[bench]] +name = "date-formatting" +harness = false diff --git a/actix-http/README.md b/actix-http/README.md index ce2e5cb32..0ba3fdcac 100644 --- a/actix-http/README.md +++ b/actix-http/README.md @@ -1,22 +1,21 @@ -# actix-http +# `actix-http` -> HTTP primitives for the Actix ecosystem. +> HTTP types and services for the Actix ecosystem. + + [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) -[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.3.1)](https://docs.rs/actix-http/3.3.1) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.7.0)](https://docs.rs/actix-http/3.7.0) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
-[![dependency status](https://deps.rs/crate/actix-http/3.3.1/status.svg)](https://deps.rs/crate/actix-http/3.3.1) +[![dependency status](https://deps.rs/crate/actix-http/3.7.0/status.svg)](https://deps.rs/crate/actix-http/3.7.0) [![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) -- Minimum Supported Rust Version (MSRV): 1.68 - -## Example +## Examples ```rust use std::{env, io}; diff --git a/actix-http/benches/date-formatting.rs b/actix-http/benches/date-formatting.rs new file mode 100644 index 000000000..26d0f3daa --- /dev/null +++ b/actix-http/benches/date-formatting.rs @@ -0,0 +1,20 @@ +use std::time::SystemTime; + +use actix_http::header::HttpDate; +use divan::{black_box, AllocProfiler, Bencher}; + +#[global_allocator] +static ALLOC: AllocProfiler = AllocProfiler::system(); + +#[divan::bench] +fn date_formatting(b: Bencher<'_, '_>) { + let now = SystemTime::now(); + + b.bench(|| { + black_box(HttpDate::from(black_box(now)).to_string()); + }) +} + +fn main() { + divan::main(); +} diff --git a/actix-http/benches/response-body-compression.rs b/actix-http/benches/response-body-compression.rs index d128bf75b..53279e312 100644 --- a/actix-http/benches/response-body-compression.rs +++ b/actix-http/benches/response-body-compression.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use std::convert::Infallible; use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode}; diff --git a/actix-http/examples/h2c-detect.rs b/actix-http/examples/h2c-detect.rs index aa3dd5d31..b0bde3fe6 100644 --- a/actix-http/examples/h2c-detect.rs +++ b/actix-http/examples/h2c-detect.rs @@ -8,7 +8,7 @@ use std::{convert::Infallible, io}; -use actix_http::{HttpService, Request, Response, StatusCode}; +use actix_http::{body::BodyStream, HttpService, Request, Response, StatusCode}; use actix_server::Server; #[tokio::main(flavor = "current_thread")] @@ -19,7 +19,12 @@ async fn main() -> io::Result<()> { .bind("h2c-detect", ("127.0.0.1", 8080), || { HttpService::build() .finish(|_req: Request| async move { - Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!")) + Ok::<_, Infallible>(Response::build(StatusCode::OK).body(BodyStream::new( + futures_util::stream::iter([ + Ok::<_, String>("123".into()), + Err("wertyuikmnbvcxdfty6t".to_owned()), + ]), + ))) }) .tcp_auto_h2c() })? diff --git a/actix-http/examples/tls_rustls.rs b/actix-http/examples/tls_rustls.rs new file mode 100644 index 000000000..17303c556 --- /dev/null +++ b/actix-http/examples/tls_rustls.rs @@ -0,0 +1,76 @@ +//! Demonstrates TLS configuration (via Rustls) for HTTP/1.1 and HTTP/2 connections. +//! +//! Test using cURL: +//! +//! ```console +//! $ curl --insecure https://127.0.0.1:8443 +//! Hello World! +//! Protocol: HTTP/2.0 +//! +//! $ curl --insecure --http1.1 https://127.0.0.1:8443 +//! Hello World! +//! Protocol: HTTP/1.1 +//! ``` + +extern crate tls_rustls_023 as rustls; + +use std::io; + +use actix_http::{Error, HttpService, Request, Response}; +use actix_utils::future::ok; + +#[actix_rt::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + tracing::info!("starting HTTP server at https://127.0.0.1:8443"); + + actix_server::Server::build() + .bind("echo", ("127.0.0.1", 8443), || { + HttpService::build() + .finish(|req: Request| { + let body = format!( + "Hello World!\n\ + Protocol: {:?}", + req.head().version + ); + ok::<_, Error>(Response::ok().set_body(body)) + }) + .rustls_0_23(rustls_config()) + })? + .run() + .await +} + +fn rustls_config() -> rustls::ServerConfig { + let rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_pem(); + + let cert_file = &mut io::BufReader::new(cert_file.as_bytes()); + let key_file = &mut io::BufReader::new(key_file.as_bytes()); + + let cert_chain = rustls_pemfile::certs(cert_file) + .collect::, _>>() + .unwrap(); + let mut keys = rustls_pemfile::pkcs8_private_keys(key_file) + .collect::, _>>() + .unwrap(); + + let mut config = rustls::ServerConfig::builder() + .with_no_client_auth() + .with_single_cert( + cert_chain, + rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)), + ) + .unwrap(); + + const H1_ALPN: &[u8] = b"http/1.1"; + const H2_ALPN: &[u8] = b"h2"; + + config.alpn_protocols.push(H2_ALPN.to_vec()); + config.alpn_protocols.push(H1_ALPN.to_vec()); + + config +} diff --git a/actix-http/examples/ws.rs b/actix-http/examples/ws.rs index 6af6d5095..fb86bc5ea 100644 --- a/actix-http/examples/ws.rs +++ b/actix-http/examples/ws.rs @@ -1,7 +1,7 @@ //! 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; +extern crate tls_rustls_023 as rustls; use std::{ io, @@ -28,7 +28,9 @@ async fn main() -> io::Result<()> { HttpService::build().h1(handler).tcp() })? .bind("tls", ("127.0.0.1", 8443), || { - HttpService::build().finish(handler).rustls(tls_config()) + HttpService::build() + .finish(handler) + .rustls_0_23(tls_config()) })? .run() .await @@ -83,27 +85,27 @@ impl Stream for Heartbeat { 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_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 cert_chain = certs(cert_file).collect::, _>>().unwrap(); + let mut keys = pkcs8_private_keys(key_file) + .collect::, _>>() + .unwrap(); let mut config = rustls::ServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() - .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .with_single_cert( + cert_chain, + rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)), + ) .unwrap(); config.alpn_protocols.push(b"http/1.1".to_vec()); diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index c3f55ce7d..739fe5027 100644 --- a/actix-http/src/body/message_body.rs +++ b/actix-http/src/body/message_body.rs @@ -531,7 +531,6 @@ where mod tests { use actix_rt::pin; use actix_utils::future::poll_fn; - use bytes::{Bytes, BytesMut}; use futures_util::stream; use super::*; diff --git a/actix-http/src/date.rs b/actix-http/src/date.rs index 1358bbd8c..735dd9100 100644 --- a/actix-http/src/date.rs +++ b/actix-http/src/date.rs @@ -28,7 +28,7 @@ impl Date { fn update(&mut self) { self.pos = 0; - write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap(); + write!(self, "{}", httpdate::HttpDate::from(SystemTime::now())).unwrap(); } } diff --git a/actix-http/src/encoding/decoder.rs b/actix-http/src/encoding/decoder.rs index e9aba25e7..cda534d60 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -191,7 +191,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -205,7 +205,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -218,7 +218,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-zstd")] @@ -231,7 +231,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, } } @@ -250,7 +250,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -265,7 +265,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -280,7 +280,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-zstd")] @@ -295,7 +295,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, } } diff --git a/actix-http/src/encoding/encoder.rs b/actix-http/src/encoding/encoder.rs index 527bfebaa..180927ac6 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -50,10 +50,21 @@ impl Encoder { } } + fn empty() -> Self { + Encoder { + body: EncoderBody::Full { body: Bytes::new() }, + encoder: None, + fut: None, + eof: true, + } + } + pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { - // no need to compress an empty body - if matches!(body.size(), BodySize::None) { - return Self::none(); + // no need to compress empty bodies + match body.size() { + BodySize::None => return Self::none(), + BodySize::Sized(0) => return Self::empty(), + _ => {} } let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING) diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index fbd2eb7ae..6f332118e 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -3,7 +3,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; use derive_more::{Display, Error, From}; -pub use http::Error as HttpError; +pub use http::{status::InvalidStatusCode, Error as HttpError}; use http::{uri::InvalidUri, StatusCode}; use crate::{body::BoxBody, Response}; @@ -399,9 +399,7 @@ pub enum ContentTypeError { #[cfg(test)] mod tests { - use std::io; - - use http::{Error as HttpError, StatusCode}; + use http::Error as HttpError; use super::*; diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index 8dae2e43e..2b452f8f8 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -198,9 +198,6 @@ impl Encoder, BodySize)>> for Codec { #[cfg(test)] mod tests { - use bytes::BytesMut; - use http::Method; - use super::*; use crate::HttpMessage as _; diff --git a/actix-http/src/h1/decoder.rs b/actix-http/src/h1/decoder.rs index 9e9b253d6..af64e8802 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -532,7 +532,7 @@ impl Decoder for PayloadDecoder { *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), + Poll::Ready(Err(err)) => return Err(err), }; if *state == ChunkedState::End { @@ -563,15 +563,8 @@ impl Decoder for PayloadDecoder { #[cfg(test)] mod tests { - use bytes::{Bytes, BytesMut}; - use http::{Method, Version}; - use super::*; - use crate::{ - error::ParseError, - header::{HeaderName, SET_COOKIE}, - HttpMessage as _, - }; + use crate::{header::SET_COOKIE, HttpMessage as _}; impl PayloadType { pub(crate) fn unwrap(self) -> PayloadDecoder { diff --git a/actix-http/src/h1/dispatcher.rs b/actix-http/src/h1/dispatcher.rs index 270707807..00b51360e 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -512,8 +512,10 @@ where } Poll::Ready(Some(Err(err))) => { + let err = err.into(); + tracing::error!("Response payload stream error: {err:?}"); this.flags.insert(Flags::FINISHED); - return Err(DispatchError::Body(err.into())); + return Err(DispatchError::Body(err)); } Poll::Pending => return Ok(PollResponse::DoNothing), @@ -549,6 +551,7 @@ where } Poll::Ready(Some(Err(err))) => { + tracing::error!("Response payload stream error: {err:?}"); this.flags.insert(Flags::FINISHED); return Err(DispatchError::Body( Error::new_body().with_cause(err).into(), @@ -703,7 +706,7 @@ where req.head_mut().peer_addr = *this.peer_addr; - req.conn_data = this.conn_data.as_ref().map(Rc::clone); + req.conn_data.clone_from(this.conn_data); match this.codec.message_type() { // request has no payload diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 1ed785a1b..2ad3a14a3 100644 --- a/actix-http/src/h1/payload.rs +++ b/actix-http/src/h1/payload.rs @@ -117,6 +117,7 @@ impl PayloadSender { } } + #[allow(clippy::needless_pass_by_ref_mut)] #[inline] pub fn need_read(&self, cx: &mut Context<'_>) -> PayloadStatus { // we check need_read only if Payload (other side) is alive, @@ -174,7 +175,7 @@ impl Inner { /// Register future waiting data from payload. /// Waker would be used in `Inner::wake` - fn register(&mut self, cx: &mut Context<'_>) { + fn register(&mut self, cx: &Context<'_>) { if self .task .as_ref() @@ -186,7 +187,7 @@ impl Inner { // Register future feeding data to payload. /// Waker would be used in `Inner::wake_io` - fn register_io(&mut self, cx: &mut Context<'_>) { + fn register_io(&mut self, cx: &Context<'_>) { if self .io_task .as_ref() @@ -221,7 +222,7 @@ impl Inner { fn poll_next( mut self: Pin<&mut Self>, - cx: &mut Context<'_>, + cx: &Context<'_>, ) -> Poll>> { if let Some(data) = self.items.pop_front() { self.len -= data.len(); diff --git a/actix-http/src/h1/service.rs b/actix-http/src/h1/service.rs index fbda7138e..f2f8a0e48 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -152,13 +152,13 @@ mod openssl { } } -#[cfg(feature = "rustls")] -mod rustls { +#[cfg(feature = "rustls-0_20")] +mod rustls_0_20 { use std::io; use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - rustls::{reexports::ServerConfig, Acceptor, TlsStream}, + rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream}, TlsError, }; @@ -188,7 +188,7 @@ mod rustls { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create Rustls based service. + /// Create Rustls v0.20 based service. pub fn rustls( self, config: ServerConfig, @@ -213,6 +213,189 @@ mod rustls { } } +#[cfg(feature = "rustls-0_21")] +mod rustls_0_21 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H1Service, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into>, + S::InitError: fmt::Debug, + S::Response: Into>, + + B: MessageBody, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.21 based service. + pub fn rustls_021( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + 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)) + } + } +} + +#[cfg(feature = "rustls-0_22")] +mod rustls_0_22 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H1Service, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into>, + S::InitError: fmt::Debug, + S::Response: Into>, + + B: MessageBody, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.22 based service. + pub fn rustls_0_22( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + 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)) + } + } +} + +#[cfg(feature = "rustls-0_23")] +mod rustls_0_23 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H1Service, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into>, + S::InitError: fmt::Debug, + S::Response: Into>, + + B: MessageBody, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.23 based service. + pub fn rustls_0_23( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + 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)) + } + } +} + impl H1Service where S: ServiceFactory, diff --git a/actix-http/src/h2/dispatcher.rs b/actix-http/src/h2/dispatcher.rs index 3e618820e..400476c88 100644 --- a/actix-http/src/h2/dispatcher.rs +++ b/actix-http/src/h2/dispatcher.rs @@ -4,7 +4,7 @@ use std::{ future::Future, marker::PhantomData, net, - pin::Pin, + pin::{pin, Pin}, rc::Rc, task::{Context, Poll}, }; @@ -20,7 +20,6 @@ use h2::{ Ping, PingPong, }; use pin_project_lite::pin_project; -use tracing::{error, trace, warn}; use crate::{ body::{BodySize, BoxBody, MessageBody}, @@ -127,7 +126,7 @@ where head.headers = parts.headers.into(); head.peer_addr = this.peer_addr; - req.conn_data = this.conn_data.as_ref().map(Rc::clone); + req.conn_data.clone_from(&this.conn_data); let fut = this.flow.service.call(req); let config = this.config.clone(); @@ -147,11 +146,13 @@ where if let Err(err) = res { match err { DispatchError::SendResponse(err) => { - trace!("Error sending HTTP/2 response: {:?}", err) + tracing::trace!("Error sending response: {err:?}"); + } + DispatchError::SendData(err) => { + tracing::warn!("Send data error: {err:?}"); } - DispatchError::SendData(err) => warn!("{:?}", err), DispatchError::ResponseBody(err) => { - error!("Response payload stream error: {:?}", err) + tracing::error!("Response payload stream error: {err:?}"); } } } @@ -228,9 +229,9 @@ where return Ok(()); } - // poll response body and send chunks to client - actix_rt::pin!(body); + let mut body = pin!(body); + // poll response body and send chunks to client 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()))?; diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 3f742135a..636ac3161 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -140,8 +140,8 @@ mod openssl { } } -#[cfg(feature = "rustls")] -mod rustls { +#[cfg(feature = "rustls-0_20")] +mod rustls_0_20 { use std::io; use actix_service::ServiceFactoryExt as _; @@ -162,7 +162,7 @@ mod rustls { B: MessageBody + 'static, { - /// Create Rustls based service. + /// Create Rustls v0.20 based service. pub fn rustls( self, mut config: ServerConfig, @@ -191,6 +191,159 @@ mod rustls { } } +#[cfg(feature = "rustls-0_21")] +mod rustls_0_21 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H2Service, S, B> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + { + /// Create Rustls v0.21 based service. + pub fn rustls_021( + self, + mut config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = S::InitError, + > { + let mut protos = vec![b"h2".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); + config.alpn_protocols = protos; + + 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)) + } + } +} + +#[cfg(feature = "rustls-0_22")] +mod rustls_0_22 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H2Service, S, B> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + { + /// Create Rustls v0.22 based service. + pub fn rustls_0_22( + self, + mut config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = S::InitError, + > { + let mut protos = vec![b"h2".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); + config.alpn_protocols = protos; + + 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)) + } + } +} + +#[cfg(feature = "rustls-0_23")] +mod rustls_0_23 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl H2Service, S, B> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + { + /// Create Rustls v0.23 based service. + pub fn rustls_0_23( + self, + mut config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = S::InitError, + > { + let mut protos = vec![b"h2".to_vec()]; + protos.extend_from_slice(&config.alpn_protocols); + config.alpn_protocols = protos; + + 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)) + } + } +} + impl ServiceFactory<(T, Option)> for H2Service where T: AsyncRead + AsyncWrite + Unpin + 'static, diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index e8118be93..b86798a4c 100644 --- a/actix-http/src/header/map.rs +++ b/actix-http/src/header/map.rs @@ -636,10 +636,24 @@ impl<'a> IntoIterator for &'a HeaderMap { } } -/// Convert `http::HeaderMap` to our `HeaderMap`. +/// Convert a `http::HeaderMap` to our `HeaderMap`. impl From for HeaderMap { - fn from(mut map: http::HeaderMap) -> HeaderMap { - HeaderMap::from_drain(map.drain()) + fn from(mut map: http::HeaderMap) -> Self { + Self::from_drain(map.drain()) + } +} + +/// Convert our `HeaderMap` to a `http::HeaderMap`. +impl From for http::HeaderMap { + fn from(map: HeaderMap) -> Self { + Self::from_iter(map) + } +} + +/// Convert our `&HeaderMap` to a `http::HeaderMap`. +impl From<&HeaderMap> for http::HeaderMap { + fn from(map: &HeaderMap) -> Self { + map.to_owned().into() } } diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs index 21ed49f0c..bdfbc7051 100644 --- a/actix-http/src/header/shared/http_date.rs +++ b/actix-http/src/header/shared/http_date.rs @@ -24,8 +24,7 @@ impl FromStr for HttpDate { 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) + httpdate::HttpDate::from(self.0).fmt(f) } } @@ -37,7 +36,7 @@ impl TryIntoHeaderValue for HttpDate { 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(); + write!(wrt, "{}", self).unwrap(); HeaderValue::from_maybe_shared(buf.split().freeze()) } diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index f4f34d347..caaab3b1e 100644 --- a/actix-http/src/header/utils.rs +++ b/actix-http/src/header/utils.rs @@ -80,18 +80,18 @@ mod tests { #[test] fn comma_delimited_parsing() { - let headers = vec![]; + let headers = []; let res: Vec = from_comma_delimited(headers.iter()).unwrap(); assert_eq!(res, vec![0; 0]); - let headers = vec![ + let headers = [ 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![ + let headers = [ HeaderValue::from_static(""), HeaderValue::from_static(","), HeaderValue::from_static(" "), diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 8b755f2f4..f9697c4d5 100644 --- a/actix-http/src/lib.rs +++ b/actix-http/src/lib.rs @@ -1,11 +1,15 @@ -//! HTTP primitives for the Actix ecosystem. +//! HTTP types and services for the Actix ecosystem. //! //! ## Crate Features +//! //! | Feature | Functionality | //! | ------------------- | ------------------------------------------- | //! | `http2` | HTTP/2 support via [h2]. | //! | `openssl` | TLS support via [OpenSSL]. | -//! | `rustls` | TLS support via [rustls]. | +//! | `rustls` | TLS support via [rustls] 0.20. | +//! | `rustls-0_21` | TLS support via [rustls] 0.21. | +//! | `rustls-0_22` | TLS support via [rustls] 0.22. | +//! | `rustls-0_23` | TLS support via [rustls] 0.23. | //! | `compress-brotli` | Payload compression support: Brotli. | //! | `compress-gzip` | Payload compression support: Deflate, Gzip. | //! | `compress-zstd` | Payload compression support: Zstd. | @@ -21,14 +25,13 @@ #![allow( clippy::type_complexity, clippy::too_many_arguments, - clippy::borrow_interior_mutable_const, - clippy::uninlined_format_args + clippy::borrow_interior_mutable_const )] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] -pub use ::http::{uri, uri::Uri, Method, StatusCode, Version}; +pub use http::{uri, uri::Uri, Method, StatusCode, Version}; pub mod body; mod builder; @@ -58,7 +61,13 @@ pub mod ws; #[allow(deprecated)] pub use self::payload::PayloadStream; -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any( + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22", + feature = "rustls-0_23", +))] pub use self::service::TlsAcceptorConfig; pub use self::{ builder::HttpServiceBuilder, diff --git a/actix-http/src/notify_on_drop.rs b/actix-http/src/notify_on_drop.rs index 98544bb5d..95904b28e 100644 --- a/actix-http/src/notify_on_drop.rs +++ b/actix-http/src/notify_on_drop.rs @@ -5,7 +5,7 @@ use std::cell::RefCell; thread_local! { - static NOTIFY_DROPPED: RefCell> = RefCell::new(None); + static NOTIFY_DROPPED: RefCell> = const { RefCell::new(None) }; } /// Check if the spawned task is dropped. diff --git a/actix-http/src/requests/head.rs b/actix-http/src/requests/head.rs index 4558801f3..9ceb2a20c 100644 --- a/actix-http/src/requests/head.rs +++ b/actix-http/src/requests/head.rs @@ -16,7 +16,10 @@ pub struct RequestHead { pub uri: Uri, pub version: Version, pub headers: HeaderMap, + + /// Will only be None when called in unit tests unless set manually. pub peer_addr: Option, + flags: Flags, } diff --git a/actix-http/src/requests/request.rs b/actix-http/src/requests/request.rs index 1750fb2f7..6a267a7a6 100644 --- a/actix-http/src/requests/request.rs +++ b/actix-http/src/requests/request.rs @@ -173,7 +173,7 @@ impl

Request

{ /// 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. + /// Will only return None when called in unit tests unless set manually. #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr diff --git a/actix-http/src/responses/builder.rs b/actix-http/src/responses/builder.rs index 063af92da..91c69ba54 100644 --- a/actix-http/src/responses/builder.rs +++ b/actix-http/src/responses/builder.rs @@ -93,7 +93,7 @@ impl ResponseBuilder { Ok((key, value)) => { parts.headers.insert(key, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }; } @@ -119,7 +119,7 @@ impl ResponseBuilder { if let Some(parts) = self.inner() { match header.try_into_pair() { Ok((key, value)) => parts.headers.append(key, value), - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }; } @@ -193,7 +193,7 @@ impl ResponseBuilder { Ok(value) => { parts.headers.insert(header::CONTENT_TYPE, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }; } self diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index e118d8361..a58be93c7 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -241,13 +241,25 @@ where } /// Configuration options used when accepting TLS connection. -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any( + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22", + feature = "rustls-0_23", +))] #[derive(Debug, Default)] pub struct TlsAcceptorConfig { pub(crate) handshake_timeout: Option, } -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any( + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22", + feature = "rustls-0_23", +))] impl TlsAcceptorConfig { /// Set TLS handshake timeout duration. pub fn handshake_timeout(self, dur: std::time::Duration) -> Self { @@ -352,13 +364,13 @@ mod openssl { } } -#[cfg(feature = "rustls")] -mod rustls { +#[cfg(feature = "rustls-0_20")] +mod rustls_0_20 { use std::io; use actix_service::ServiceFactoryExt as _; use actix_tls::accept::{ - rustls::{reexports::ServerConfig, Acceptor, TlsStream}, + rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream}, TlsError, }; @@ -389,7 +401,7 @@ mod rustls { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create Rustls based service. + /// Create Rustls v0.20 based service. pub fn rustls( self, config: ServerConfig, @@ -403,7 +415,7 @@ mod rustls { self.rustls_with_config(config, TlsAcceptorConfig::default()) } - /// Create Rustls based service with custom TLS acceptor configuration. + /// Create Rustls v0.20 based service with custom TLS acceptor configuration. pub fn rustls_with_config( self, mut config: ServerConfig, @@ -448,6 +460,294 @@ mod rustls { } } +#[cfg(feature = "rustls-0_21")] +mod rustls_0_21 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl HttpService, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::InitError: fmt::Debug, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, h1::Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.21 based service. + pub fn rustls_021( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + self.rustls_021_with_config(config, TlsAcceptorConfig::default()) + } + + /// Create Rustls v0.21 based service with custom TLS acceptor configuration. + pub fn rustls_021_with_config( + self, + mut config: ServerConfig, + tls_acceptor_config: TlsAcceptorConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + 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; + + let mut acceptor = Acceptor::new(config); + + if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout { + acceptor.set_handshake_timeout(handshake_timeout); + } + + acceptor + .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 + }; + let peer_addr = io.get_ref().0.peer_addr().ok(); + Ok((io, proto, peer_addr)) + }) + .and_then(self.map_err(TlsError::Service)) + } + } +} + +#[cfg(feature = "rustls-0_22")] +mod rustls_0_22 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl HttpService, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::InitError: fmt::Debug, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, h1::Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.22 based service. + pub fn rustls_0_22( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + self.rustls_0_22_with_config(config, TlsAcceptorConfig::default()) + } + + /// Create Rustls v0.22 based service with custom TLS acceptor configuration. + pub fn rustls_0_22_with_config( + self, + mut config: ServerConfig, + tls_acceptor_config: TlsAcceptorConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + 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; + + let mut acceptor = Acceptor::new(config); + + if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout { + acceptor.set_handshake_timeout(handshake_timeout); + } + + acceptor + .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 + }; + let peer_addr = io.get_ref().0.peer_addr().ok(); + Ok((io, proto, peer_addr)) + }) + .and_then(self.map_err(TlsError::Service)) + } + } +} + +#[cfg(feature = "rustls-0_23")] +mod rustls_0_23 { + use std::io; + + use actix_service::ServiceFactoryExt as _; + use actix_tls::accept::{ + rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream}, + TlsError, + }; + + use super::*; + + impl HttpService, S, B, X, U> + where + S: ServiceFactory, + S::Future: 'static, + S::Error: Into> + 'static, + S::InitError: fmt::Debug, + S::Response: Into> + 'static, + >::Future: 'static, + + B: MessageBody + 'static, + + X: ServiceFactory, + X::Future: 'static, + X::Error: Into>, + X::InitError: fmt::Debug, + + U: ServiceFactory< + (Request, Framed, h1::Codec>), + Config = (), + Response = (), + >, + U::Future: 'static, + U::Error: fmt::Display + Into>, + U::InitError: fmt::Debug, + { + /// Create Rustls v0.23 based service. + pub fn rustls_0_23( + self, + config: ServerConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + self.rustls_0_23_with_config(config, TlsAcceptorConfig::default()) + } + + /// Create Rustls v0.23 based service with custom TLS acceptor configuration. + pub fn rustls_0_23_with_config( + self, + mut config: ServerConfig, + tls_acceptor_config: TlsAcceptorConfig, + ) -> impl ServiceFactory< + TcpStream, + Config = (), + Response = (), + Error = TlsError, + InitError = (), + > { + 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; + + let mut acceptor = Acceptor::new(config); + + if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout { + acceptor.set_handshake_timeout(handshake_timeout); + } + + acceptor + .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 + }; + let peer_addr = io.get_ref().0.peer_addr().ok(); + Ok((io, proto, peer_addr)) + }) + .and_then(self.map_err(TlsError::Service)) + } + } +} + impl ServiceFactory<(T, Protocol, Option)> for HttpService where diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 681649a7e..ad487e400 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -296,7 +296,7 @@ impl Decoder for Codec { } } Ok(None) => Ok(None), - Err(e) => Err(e), + Err(err) => Err(err), } } } diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index c9fb0cde9..35b3f8e66 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -178,14 +178,14 @@ impl Parser { }; if payload_len < 126 { - dst.reserve(p_len + 2 + if mask { 4 } else { 0 }); + dst.reserve(p_len + 2); dst.put_slice(&[one, two | payload_len as u8]); } else if payload_len <= 65_535 { - dst.reserve(p_len + 4 + if mask { 4 } else { 0 }); + dst.reserve(p_len + 4); dst.put_slice(&[one, two | 126]); dst.put_u16(payload_len as u16); } else { - dst.reserve(p_len + 10 + if mask { 4 } else { 0 }); + dst.reserve(p_len + 10); dst.put_slice(&[one, two | 127]); dst.put_u64(payload_len as u64); }; diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 87f9b38f3..3ed53b70a 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -221,7 +221,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { #[cfg(test)] mod tests { use super::*; - use crate::{header, test::TestRequest, Method}; + use crate::{header, test::TestRequest}; #[test] fn test_handshake() { diff --git a/actix-http/src/ws/proto.rs b/actix-http/src/ws/proto.rs index 0653c00b0..27815eaf2 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -1,7 +1,4 @@ -use std::{ - convert::{From, Into}, - fmt, -}; +use std::fmt; use base64::prelude::*; use tracing::error; diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index b4d8ed1a5..4dd22b585 100644 --- a/actix-http/tests/test_openssl.rs +++ b/actix-http/tests/test_openssl.rs @@ -1,5 +1,4 @@ #![cfg(feature = "openssl")] -#![allow(clippy::uninlined_format_args)] extern crate tls_openssl as openssl; @@ -43,9 +42,11 @@ where } 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 3d9a39cbd..3ca0d94c2 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -1,7 +1,6 @@ -#![cfg(feature = "rustls")] -#![allow(clippy::uninlined_format_args)] +#![cfg(feature = "rustls-0_23")] -extern crate tls_rustls as rustls; +extern crate tls_rustls_023 as rustls; use std::{ convert::Infallible, @@ -21,13 +20,13 @@ use actix_http::{ use actix_http_test::test_server; use actix_rt::pin; use actix_service::{fn_factory_with_config, fn_service}; -use actix_tls::connect::rustls::webpki_roots_cert_store; +use actix_tls::connect::rustls_0_23::webpki_roots_cert_store; use actix_utils::future::{err, ok, poll_fn}; use bytes::{Bytes, BytesMut}; use derive_more::{Display, Error}; use futures_core::{ready, Stream}; use futures_util::stream::once; -use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName}; +use rustls::{pki_types::ServerName, ServerConfig as RustlsServerConfig}; use rustls_pemfile::{certs, pkcs8_private_keys}; async fn load_body(stream: S) -> Result @@ -53,24 +52,25 @@ where } 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_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 cert_chain = certs(cert_file).collect::, _>>().unwrap(); + let mut keys = pkcs8_private_keys(key_file) + .collect::, _>>() + .unwrap(); let mut config = RustlsServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() - .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .with_single_cert( + cert_chain, + rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)), + ) .unwrap(); config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec()); @@ -84,7 +84,6 @@ pub fn get_negotiated_alpn_protocol( 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(); @@ -110,7 +109,7 @@ async fn h1() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h1(|_| ok::<_, Error>(Response::ok())) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -124,7 +123,7 @@ async fn h2() -> io::Result<()> { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -142,7 +141,7 @@ async fn h1_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_11); ok::<_, Error>(Response::ok()) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -160,7 +159,7 @@ async fn h2_1() -> io::Result<()> { assert_eq!(req.version(), Version::HTTP_2); ok::<_, Error>(Response::ok()) }) - .rustls_with_config( + .rustls_0_23_with_config( tls_config(), TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)), ) @@ -181,7 +180,7 @@ async fn h2_body1() -> io::Result<()> { let body = load_body(req.take_payload()).await?; Ok::<_, Error>(Response::ok().set_body(body)) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -207,7 +206,7 @@ async fn h2_content_length() { ]; ok::<_, Infallible>(Response::new(statuses[indx])) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -279,7 +278,7 @@ async fn h2_headers() { } ok::<_, Infallible>(config.body(data.clone())) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -318,7 +317,7 @@ async fn h2_body2() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -335,7 +334,7 @@ async fn h2_head_empty() { let mut srv = test_server(move || { HttpService::build() .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -361,7 +360,7 @@ async fn h2_head_binary() { let mut srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -386,7 +385,7 @@ async fn h2_head_binary2() { let srv = test_server(move || { HttpService::build() .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -412,7 +411,7 @@ async fn h2_body_length() { Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), ) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -436,7 +435,7 @@ async fn h2_body_chunked_explicit() { .body(BodyStream::new(body)), ) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -465,7 +464,7 @@ async fn h2_response_http_error_handling() { ) })) })) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -495,7 +494,7 @@ async fn h2_service_error() { let mut srv = test_server(move || { HttpService::build() .h2(|_| err::, _>(BadRequest)) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -512,7 +511,7 @@ async fn h1_service_error() { let mut srv = test_server(move || { HttpService::build() .h1(|_| err::, _>(BadRequest)) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -535,7 +534,7 @@ async fn alpn_h1() -> io::Result<()> { config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); HttpService::build() .h1(|_| ok::<_, Error>(Response::ok())) - .rustls(config) + .rustls_0_23(config) }) .await; @@ -557,7 +556,7 @@ async fn alpn_h2() -> io::Result<()> { config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); HttpService::build() .h2(|_| ok::<_, Error>(Response::ok())) - .rustls(config) + .rustls_0_23(config) }) .await; @@ -583,7 +582,7 @@ async fn alpn_h2_1() -> io::Result<()> { config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec()); HttpService::build() .finish(|_| ok::<_, Error>(Response::ok())) - .rustls(config) + .rustls_0_23(config) }) .await; diff --git a/actix-http/tests/test_server.rs b/actix-http/tests/test_server.rs index f55b1b36c..4ba64a53c 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -147,7 +147,7 @@ async fn chunked_payload() { .take_payload() .map(|res| match res { Ok(pl) => pl, - Err(e) => panic!("Error reading payload: {}", e), + Err(err) => panic!("Error reading payload: {err}"), }) .fold(0usize, |acc, chunk| ready(acc + chunk.len())) .map(|req_size| { diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index a2866613b..9a78074c4 100644 --- a/actix-http/tests/test_ws.rs +++ b/actix-http/tests/test_ws.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use std::{ cell::Cell, convert::Infallible, diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md index f90b9e3cf..1b44ba4b7 100644 --- a/actix-multipart-derive/CHANGES.md +++ b/actix-multipart-derive/CHANGES.md @@ -2,9 +2,13 @@ ## Unreleased +- Minimum supported Rust version (MSRV) is now 1.72. + +## 0.6.1 + - Update `syn` dependency to `2`. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 0.6.0 - 2023-02-26 +## 0.6.0 - Add `MultipartForm` derive macro. diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml index 41e687e50..e978864a3 100644 --- a/actix-multipart-derive/Cargo.toml +++ b/actix-multipart-derive/Cargo.toml @@ -1,13 +1,14 @@ [package] name = "actix-multipart-derive" -version = "0.6.0" +version = "0.6.1" authors = ["Jacob Halsey "] description = "Multipart form derive macro for Actix Web" keywords = ["http", "web", "framework", "async", "futures"] -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -license = "MIT OR Apache-2.0" -edition = "2021" +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true [package.metadata.docs.rs] rustdoc-args = ["--cfg", "docsrs"] diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md index 2737410f6..ec0afffdd 100644 --- a/actix-multipart-derive/README.md +++ b/actix-multipart-derive/README.md @@ -1,17 +1,16 @@ -# actix-multipart-derive +# `actix-multipart-derive` > The derive macro implementation for actix-multipart-derive. + + [![crates.io](https://img.shields.io/crates/v/actix-multipart-derive?label=latest)](https://crates.io/crates/actix-multipart-derive) -[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.5.0)](https://docs.rs/actix-multipart-derive/0.5.0) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-multipart-derive/badge.svg?version=0.6.1)](https://docs.rs/actix-multipart-derive/0.6.1) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart-derive.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.5.0/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.5.0) +[![dependency status](https://deps.rs/crate/actix-multipart-derive/0.6.1/status.svg)](https://deps.rs/crate/actix-multipart-derive/0.6.1) [![Download](https://img.shields.io/crates/d/actix-multipart-derive.svg)](https://crates.io/crates/actix-multipart-derive) [![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-derive) -- Minimum Supported Rust Version (MSRV): 1.68 + diff --git a/actix-multipart-derive/tests/trybuild.rs b/actix-multipart-derive/tests/trybuild.rs index 88aa619c6..6b25d78df 100644 --- a/actix-multipart-derive/tests/trybuild.rs +++ b/actix-multipart-derive/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.68)] // MSRV +#[rustversion::stable(1.72)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 1ad5865ec..a91edf9c8 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,49 +1,56 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased + +## 0.6.2 + +- Add testing utilities under new module `test`. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 0.6.1 - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 0.6.0 - 2023-02-26 +## 0.6.0 - Added `MultipartForm` typed data extractor. [#2883] [#2883]: https://github.com/actix/actix-web/pull/2883 -## 0.5.0 - 2023-01-21 +## 0.5.0 - `Field::content_type()` now returns `Option<&mime::Mime>`. [#2885] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2885]: https://github.com/actix/actix-web/pull/2885 -## 0.4.0 - 2022-02-25 +## 0.4.0 - No significant changes since `0.4.0-beta.13`. -## 0.4.0-beta.13 - 2022-01-31 +## 0.4.0-beta.13 - No significant changes since `0.4.0-beta.12`. -## 0.4.0-beta.12 - 2022-01-04 +## 0.4.0-beta.12 - Minimum supported Rust version (MSRV) is now 1.54. -## 0.4.0-beta.11 - 2021-12-27 +## 0.4.0-beta.11 - No significant changes since `0.4.0-beta.10`. -## 0.4.0-beta.10 - 2021-12-11 +## 0.4.0-beta.10 - No significant changes since `0.4.0-beta.9`. -## 0.4.0-beta.9 - 2021-12-01 +## 0.4.0-beta.9 - 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 +## 0.4.0-beta.8 - Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] - Added `MultipartError::NoContentDisposition` variant. [#2451] @@ -54,31 +61,31 @@ [#2451]: https://github.com/actix/actix-web/pull/2451 -## 0.4.0-beta.7 - 2021-10-20 +## 0.4.0-beta.7 - Minimum supported Rust version (MSRV) is now 1.52. -## 0.4.0-beta.6 - 2021-09-09 +## 0.4.0-beta.6 - Minimum supported Rust version (MSRV) is now 1.51. -## 0.4.0-beta.5 - 2021-06-17 +## 0.4.0-beta.5 - No notable changes. -## 0.4.0-beta.4 - 2021-04-02 +## 0.4.0-beta.4 - No notable changes. -## 0.4.0-beta.3 - 2021-03-09 +## 0.4.0-beta.3 - No notable changes. -## 0.4.0-beta.2 - 2021-02-10 +## 0.4.0-beta.2 - No notable changes. -## 0.4.0-beta.1 - 2021-01-07 +## 0.4.0-beta.1 - Fix multipart consuming payload before header checks. [#1513] - Update `bytes` to `1.0`. [#1813] @@ -86,19 +93,19 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 [#1513]: https://github.com/actix/actix-web/pull/1513 -## 0.3.0 - 2020-09-11 +## 0.3.0 - No significant changes from `0.3.0-beta.2`. -## 0.3.0-beta.2 - 2020-09-10 +## 0.3.0-beta.2 - Update `actix-*` dependencies to latest versions. -## 0.3.0-beta.1 - 2020-07-15 +## 0.3.0-beta.1 - Update `actix-web` to 3.0.0-beta.1 -## 0.3.0-alpha.1 - 2020-05-25 +## 0.3.0-alpha.1 - Update `actix-web` to 3.0.0-alpha.3 - Bump minimum supported Rust version to 1.40 @@ -106,45 +113,45 @@ - Remove the unused `time` dependency - Fix missing `std::error::Error` implement for `MultipartError`. -## [0.2.0] - 2019-12-20 +## 0.2.0 - Release -## [0.2.0-alpha.4] - 2019-12-xx +## 0.2.0-alpha.4 - Multipart handling now handles Pending during read of boundary #1205 -## [0.2.0-alpha.2] - 2019-12-03 +## 0.2.0-alpha.2 - Migrate to `std::future` -## [0.1.4] - 2019-09-12 +## 0.1.4 - Multipart handling now parses requests which do not end in CRLF #1038 -## [0.1.3] - 2019-08-18 +## 0.1.3 - Fix ring dependency from actix-web default features for #741. -## [0.1.2] - 2019-06-02 +## 0.1.2 - Fix boundary parsing #876 -## [0.1.1] - 2019-05-25 +## 0.1.1 - Fix disconnect handling #834 -## [0.1.0] - 2019-05-18 +## 0.1.0 - Release -## [0.1.0-beta.4] - 2019-05-12 +## 0.1.0-beta.4 - Handle cancellation of uploads #736 - Upgrade to actix-web 1.0.0-beta.4 -## [0.1.0-beta.1] - 2019-04-21 +## 0.1.0-beta.1 - Do not support nested multipart diff --git a/actix-multipart/Cargo.toml b/actix-multipart/Cargo.toml index 9220e793c..f1289d3a2 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-multipart" -version = "0.6.0" +version = "0.6.2" authors = [ "Nikolay Kim ", "Jacob Halsey ", @@ -8,7 +8,7 @@ authors = [ 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" +repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2021" @@ -22,7 +22,7 @@ derive = ["actix-multipart-derive"] tempfile = ["dep:tempfile", "tokio/fs"] [dependencies] -actix-multipart-derive = { version = "=0.6.0", optional = true } +actix-multipart-derive = { version = "=0.6.1", optional = true } actix-utils = "3" actix-web = { version = "4", default-features = false } @@ -35,6 +35,7 @@ local-waker = "0.1" log = "0.4" memchr = "2.5" mime = "0.3" +rand = "0.8" serde = "1" serde_json = "1" serde_plain = "1" @@ -46,7 +47,9 @@ actix-http = "3" actix-multipart-rfc7578 = "0.10" actix-rt = "2.2" actix-test = "0.1" +actix-web = "4" awc = "3" futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } +multer = "3" tokio = { version = "1.24.2", features = ["sync"] } tokio-stream = "0.1" diff --git a/actix-multipart/README.md b/actix-multipart/README.md index 66550668d..c7697785a 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -1,17 +1,77 @@ -# actix-multipart +# `actix-multipart` > Multipart form support for Actix Web. + + [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) -[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.0)](https://docs.rs/actix-multipart/0.6.0) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.6.2)](https://docs.rs/actix-multipart/0.6.2) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
-[![dependency status](https://deps.rs/crate/actix-multipart/0.6.0/status.svg)](https://deps.rs/crate/actix-multipart/0.6.0) +[![dependency status](https://deps.rs/crate/actix-multipart/0.6.2/status.svg)](https://deps.rs/crate/actix-multipart/0.6.2) [![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.68 +## Example + +Dependencies: + +```toml +[dependencies] +actix-multipart = "0.6" +actix-web = "4.5" +serde = { version = "1.0", features = ["derive"] } +``` + +Code: + +```rust +use actix_web::{post, App, HttpServer, Responder}; + +use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; +use serde::Deserialize; + +#[derive(Debug, Deserialize)] +struct Metadata { + name: String, +} + +#[derive(Debug, MultipartForm)] +struct UploadForm { + #[multipart(limit = "100MB")] + file: TempFile, + json: MPJson, +} + +#[post("/videos")] +pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder { + format!( + "Uploaded file {}, with size: {}", + form.json.name, form.file.size + ) +} + +#[actix_web::main] +async fn main() -> std::io::Result<()> { + HttpServer::new(move || App::new().service(post_video)) + .bind(("127.0.0.1", 8080))? + .run() + .await +} +``` + +Curl request : + +```bash +curl -v --request POST \ + --url http://localhost:8080/videos \ + -F 'json={"name": "Cargo.lock"};type=application/json' \ + -F file=@./Cargo.lock +``` + +### Examples + +https://github.com/actix/examples/tree/master/forms/multipart diff --git a/actix-multipart/src/form/json.rs b/actix-multipart/src/form/json.rs index fb90a82b9..bb4e03bf6 100644 --- a/actix-multipart/src/form/json.rs +++ b/actix-multipart/src/form/json.rs @@ -131,14 +131,13 @@ impl Default for JsonConfig { #[cfg(test)] mod tests { - use std::{collections::HashMap, io::Cursor}; + use std::collections::HashMap; - use actix_multipart_rfc7578::client::multipart; use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; + use bytes::Bytes; use crate::form::{ json::{Json, JsonConfig}, - tests::send_form, MultipartForm, }; @@ -155,6 +154,8 @@ mod tests { HttpResponse::Ok().finish() } + const TEST_JSON: &str = r#"{"key1": "value1", "key2": "value2"}"#; + #[actix_rt::test] async fn test_json_without_content_type() { let srv = actix_test::start(|| { @@ -163,10 +164,16 @@ mod tests { .app_data(JsonConfig::default().validate_content_type(false)) }); - let mut form = multipart::Form::default(); - form.add_text("json", "{\"key1\": \"value1\", \"key2\": \"value2\"}"); - let response = send_form(&srv, form, "/").await; - assert_eq!(response.status(), StatusCode::OK); + let (body, headers) = crate::test::create_form_data_payload_and_headers( + "json", + None, + None, + Bytes::from_static(TEST_JSON.as_bytes()), + ); + let mut req = srv.post("/"); + *req.headers_mut() = headers; + let res = req.send_body(body).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); } #[actix_rt::test] @@ -178,17 +185,27 @@ mod tests { }); // Deny because wrong content type - let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}"); - let mut form = multipart::Form::default(); - form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_OCTET_STREAM); - let response = send_form(&srv, form, "/").await; - assert_eq!(response.status(), StatusCode::BAD_REQUEST); + let (body, headers) = crate::test::create_form_data_payload_and_headers( + "json", + None, + Some(mime::APPLICATION_OCTET_STREAM), + Bytes::from_static(TEST_JSON.as_bytes()), + ); + let mut req = srv.post("/"); + *req.headers_mut() = headers; + let res = req.send_body(body).await.unwrap(); + assert_eq!(res.status(), StatusCode::BAD_REQUEST); // Allow because correct content type - let bytes = Cursor::new("{\"key1\": \"value1\", \"key2\": \"value2\"}"); - let mut form = multipart::Form::default(); - form.add_reader_file_with_mime("json", bytes, "", mime::APPLICATION_JSON); - let response = send_form(&srv, form, "/").await; - assert_eq!(response.status(), StatusCode::OK); + let (body, headers) = crate::test::create_form_data_payload_and_headers( + "json", + None, + Some(mime::APPLICATION_JSON), + Bytes::from_static(TEST_JSON.as_bytes()), + ); + let mut req = srv.post("/"); + *req.headers_mut() = headers; + let res = req.send_body(body).await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); } } diff --git a/actix-multipart/src/form/mod.rs b/actix-multipart/src/form/mod.rs index 67adfd4b2..451b103fd 100644 --- a/actix-multipart/src/form/mod.rs +++ b/actix-multipart/src/form/mod.rs @@ -313,7 +313,8 @@ where let entry = field_limits .entry(field.name().to_owned()) .or_insert_with(|| T::limit(field.name())); - limits.field_limit_remaining = entry.to_owned(); + + limits.field_limit_remaining.clone_from(entry); T::handle_field(&req, field, &mut limits, &mut state).await?; diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 615a8e6de..d19e951e6 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,8 +1,43 @@ //! Multipart form support for Actix Web. +//! # Examples +//! ```no_run +//! use actix_web::{post, App, HttpServer, Responder}; +//! +//! use actix_multipart::form::{json::Json as MPJson, tempfile::TempFile, MultipartForm}; +//! use serde::Deserialize; +//! +//! #[derive(Debug, Deserialize)] +//! struct Metadata { +//! name: String, +//! } +//! +//! #[derive(Debug, MultipartForm)] +//! struct UploadForm { +//! #[multipart(limit = "100MB")] +//! file: TempFile, +//! json: MPJson, +//! } +//! +//! #[post("/videos")] +//! pub async fn post_video(MultipartForm(form): MultipartForm) -> impl Responder { +//! format!( +//! "Uploaded file {}, with size: {}", +//! form.json.name, form.file.size +//! ) +//! } +//! +//! #[actix_web::main] +//! async fn main() -> std::io::Result<()> { +//! HttpServer::new(move || App::new().service(post_video)) +//! .bind(("127.0.0.1", 8080))? +//! .run() +//! .await +//! } +//! ``` #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)] +#![allow(clippy::borrow_interior_mutable_const)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -13,11 +48,14 @@ extern crate self as actix_multipart; mod error; mod extractor; -mod server; - pub mod form; +mod server; +pub mod test; pub use self::{ error::MultipartError, server::{Field, Multipart}, + test::{ + create_form_data_payload_and_headers, create_form_data_payload_and_headers_with_boundary, + }, }; diff --git a/actix-multipart/src/server.rs b/actix-multipart/src/server.rs index b20429040..d0f833318 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -252,7 +252,7 @@ impl InnerMultipart { fn poll( &mut self, safety: &Safety, - cx: &mut Context<'_>, + cx: &Context<'_>, ) -> Poll>> { if self.state == InnerState::Eof { Poll::Ready(None) @@ -740,7 +740,7 @@ impl Safety { self.clean.get() } - fn clone(&self, cx: &mut Context<'_>) -> Safety { + fn clone(&self, cx: &Context<'_>) -> Safety { let payload = Rc::clone(&self.payload); let s = Safety { task: LocalWaker::new(), @@ -863,13 +863,15 @@ mod tests { test::TestRequest, FromRequest, }; - use bytes::Bytes; + use bytes::BufMut as _; use futures_util::{future::lazy, StreamExt as _}; use tokio::sync::mpsc; use tokio_stream::wrappers::UnboundedReceiverStream; use super::*; + const BOUNDARY: &str = "abbc761f78ff4d7cb7573b5a23f96ef0"; + #[actix_rt::test] async fn test_boundary() { let headers = HeaderMap::new(); @@ -965,6 +967,26 @@ mod tests { } fn create_simple_request_with_header() -> (Bytes, HeaderMap) { + let (body, headers) = crate::test::create_form_data_payload_and_headers_with_boundary( + BOUNDARY, + "file", + Some("fn.txt".to_owned()), + Some(mime::TEXT_PLAIN_UTF_8), + Bytes::from_static(b"data"), + ); + + let mut buf = BytesMut::with_capacity(body.len() + 14); + + // add junk before form to test pre-boundary data rejection + buf.put("testasdadsad\r\n".as_bytes()); + + buf.put(body); + + (buf.freeze(), headers) + } + + // TODO: use test utility when multi-file support is introduced + fn create_double_request_with_header() -> (Bytes, HeaderMap) { let bytes = Bytes::from( "testasdadsad\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ @@ -990,7 +1012,7 @@ mod tests { #[actix_rt::test] async fn test_multipart_no_end_crlf() { let (sender, payload) = create_stream(); - let (mut bytes, headers) = create_simple_request_with_header(); + let (mut bytes, headers) = create_double_request_with_header(); let bytes_stripped = bytes.split_to(bytes.len()); // strip crlf sender.send(Ok(bytes_stripped)).unwrap(); @@ -1017,7 +1039,7 @@ mod tests { #[actix_rt::test] async fn test_multipart() { let (sender, payload) = create_stream(); - let (bytes, headers) = create_simple_request_with_header(); + let (bytes, headers) = create_double_request_with_header(); sender.send(Ok(bytes)).unwrap(); @@ -1080,7 +1102,7 @@ mod tests { #[actix_rt::test] async fn test_stream() { - let (bytes, headers) = create_simple_request_with_header(); + let (bytes, headers) = create_double_request_with_header(); let payload = SlowStream::new(bytes); let mut multipart = Multipart::new(&headers, payload); @@ -1319,7 +1341,7 @@ mod tests { #[actix_rt::test] async fn test_drop_field_awaken_multipart() { let (sender, payload) = create_stream(); - let (bytes, headers) = create_simple_request_with_header(); + let (bytes, headers) = create_double_request_with_header(); sender.send(Ok(bytes)).unwrap(); drop(sender); // eof diff --git a/actix-multipart/src/test.rs b/actix-multipart/src/test.rs new file mode 100644 index 000000000..77d918283 --- /dev/null +++ b/actix-multipart/src/test.rs @@ -0,0 +1,217 @@ +use actix_web::http::header::{self, HeaderMap}; +use bytes::{BufMut as _, Bytes, BytesMut}; +use mime::Mime; +use rand::{ + distributions::{Alphanumeric, DistString as _}, + thread_rng, +}; + +const CRLF: &[u8] = b"\r\n"; +const CRLF_CRLF: &[u8] = b"\r\n\r\n"; +const HYPHENS: &[u8] = b"--"; +const BOUNDARY_PREFIX: &str = "------------------------"; + +/// Constructs a `multipart/form-data` payload from bytes and metadata. +/// +/// Returned header map can be extended or merged with existing headers. +/// +/// Multipart boundary used is a random alphanumeric string. +/// +/// # Examples +/// +/// ``` +/// use actix_multipart::test::create_form_data_payload_and_headers; +/// use actix_web::test::TestRequest; +/// use bytes::Bytes; +/// use memchr::memmem::find; +/// +/// let (body, headers) = create_form_data_payload_and_headers( +/// "foo", +/// Some("lorem.txt".to_owned()), +/// Some(mime::TEXT_PLAIN_UTF_8), +/// Bytes::from_static(b"Lorem ipsum."), +/// ); +/// +/// assert!(find(&body, b"foo").is_some()); +/// assert!(find(&body, b"lorem.txt").is_some()); +/// assert!(find(&body, b"text/plain; charset=utf-8").is_some()); +/// assert!(find(&body, b"Lorem ipsum.").is_some()); +/// +/// let req = TestRequest::default(); +/// +/// // merge header map into existing test request and set multipart body +/// let req = headers +/// .into_iter() +/// .fold(req, |req, hdr| req.insert_header(hdr)) +/// .set_payload(body) +/// .to_http_request(); +/// +/// assert!( +/// req.headers() +/// .get("content-type") +/// .unwrap() +/// .to_str() +/// .unwrap() +/// .starts_with("multipart/form-data; boundary=\"") +/// ); +/// ``` +pub fn create_form_data_payload_and_headers( + name: &str, + filename: Option, + content_type: Option, + file: Bytes, +) -> (Bytes, HeaderMap) { + let boundary = Alphanumeric.sample_string(&mut thread_rng(), 32); + + create_form_data_payload_and_headers_with_boundary( + &boundary, + name, + filename, + content_type, + file, + ) +} + +/// Constructs a `multipart/form-data` payload from bytes and metadata with a fixed boundary. +/// +/// See [`create_form_data_payload_and_headers`] for more details. +pub fn create_form_data_payload_and_headers_with_boundary( + boundary: &str, + name: &str, + filename: Option, + content_type: Option, + file: Bytes, +) -> (Bytes, HeaderMap) { + let mut buf = BytesMut::with_capacity(file.len() + 128); + + let boundary_str = [BOUNDARY_PREFIX, boundary].concat(); + let boundary = boundary_str.as_bytes(); + + buf.put(HYPHENS); + buf.put(boundary); + buf.put(CRLF); + + buf.put(format!("Content-Disposition: form-data; name=\"{name}\"").as_bytes()); + if let Some(filename) = filename { + buf.put(format!("; filename=\"{filename}\"").as_bytes()); + } + buf.put(CRLF); + + if let Some(ct) = content_type { + buf.put(format!("Content-Type: {ct}").as_bytes()); + buf.put(CRLF); + } + + buf.put(format!("Content-Length: {}", file.len()).as_bytes()); + buf.put(CRLF_CRLF); + + buf.put(file); + buf.put(CRLF); + + buf.put(HYPHENS); + buf.put(boundary); + buf.put(HYPHENS); + buf.put(CRLF); + + let mut headers = HeaderMap::new(); + headers.insert( + header::CONTENT_TYPE, + format!("multipart/form-data; boundary=\"{boundary_str}\"") + .parse() + .unwrap(), + ); + + (buf.freeze(), headers) +} + +#[cfg(test)] +mod tests { + use std::convert::Infallible; + + use futures_util::stream; + + use super::*; + + fn find_boundary(headers: &HeaderMap) -> String { + headers + .get("content-type") + .unwrap() + .to_str() + .unwrap() + .parse::() + .unwrap() + .get_param(mime::BOUNDARY) + .unwrap() + .as_str() + .to_owned() + } + + #[test] + fn wire_format() { + let (pl, headers) = create_form_data_payload_and_headers_with_boundary( + "qWeRtYuIoP", + "foo", + None, + None, + Bytes::from_static(b"Lorem ipsum dolor\nsit ame."), + ); + + assert_eq!( + find_boundary(&headers), + "------------------------qWeRtYuIoP", + ); + + assert_eq!( + std::str::from_utf8(&pl).unwrap(), + "--------------------------qWeRtYuIoP\r\n\ + Content-Disposition: form-data; name=\"foo\"\r\n\ + Content-Length: 26\r\n\ + \r\n\ + Lorem ipsum dolor\n\ + sit ame.\r\n\ + --------------------------qWeRtYuIoP--\r\n", + ); + + let (pl, _headers) = create_form_data_payload_and_headers_with_boundary( + "qWeRtYuIoP", + "foo", + Some("Lorem.txt".to_owned()), + Some(mime::TEXT_PLAIN_UTF_8), + Bytes::from_static(b"Lorem ipsum dolor\nsit ame."), + ); + + assert_eq!( + std::str::from_utf8(&pl).unwrap(), + "--------------------------qWeRtYuIoP\r\n\ + Content-Disposition: form-data; name=\"foo\"; filename=\"Lorem.txt\"\r\n\ + Content-Type: text/plain; charset=utf-8\r\n\ + Content-Length: 26\r\n\ + \r\n\ + Lorem ipsum dolor\n\ + sit ame.\r\n\ + --------------------------qWeRtYuIoP--\r\n", + ); + } + + /// Test using an external library to prevent the two-wrongs-make-a-right class of errors. + #[actix_web::test] + async fn ecosystem_compat() { + let (pl, headers) = create_form_data_payload_and_headers( + "foo", + None, + None, + Bytes::from_static(b"Lorem ipsum dolor\nsit ame."), + ); + + let boundary = find_boundary(&headers); + + let pl = stream::once(async { Ok::<_, Infallible>(pl) }); + + let mut form = multer::Multipart::new(pl, boundary); + let field = form.next_field().await.unwrap().unwrap(); + assert_eq!(field.name().unwrap(), "foo"); + assert_eq!(field.file_name(), None); + assert_eq!(field.content_type(), None); + assert!(field.bytes().await.unwrap().starts_with(b"Lorem")); + } +} diff --git a/actix-router/CHANGES.md b/actix-router/CHANGES.md index 786ee12d9..6305b45c3 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,17 +1,24 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased + +## 0.5.3 + +- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 0.5.2 - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 0.5.1 - 2022-09-19 +## 0.5.1 - Correct typo in error string for `i32` deserialization. [#2876] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2876]: https://github.com/actix/actix-web/pull/2876 -## 0.5.0 - 2022-02-22 +## 0.5.0 ### Added @@ -86,7 +93,7 @@

0.5.0 Pre-Releases -## 0.5.0-rc.3 - 2022-01-31 +## 0.5.0-rc.3 - Remove unused `ResourceInfo`. [#2612] - Add `RouterBuilder::push`. [#2612] @@ -98,32 +105,32 @@ [#2612]: https://github.com/actix/actix-web/pull/2612 [#2613]: https://github.com/actix/actix-web/pull/2613 -## 0.5.0-rc.2 - 2022-01-21 +## 0.5.0-rc.2 - Add `Path::as_str`. [#2590] - Deprecate `Path::path`. [#2590] [#2590]: https://github.com/actix/actix-web/pull/2590 -## 0.5.0-rc.1 - 2022-01-14 +## 0.5.0-rc.1 - `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568] - `Resource` is now implemented for `&mut Path<_>` and `RefMut>`. [#2568] [#2568]: https://github.com/actix/actix-web/pull/2568 -## 0.5.0-beta.4 - 2022-01-04 +## 0.5.0-beta.4 - `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566] - Minimum supported Rust version (MSRV) is now 1.54. [#2566]: https://github.com/actix/actix-net/pull/2566 -## 0.5.0-beta.3 - 2021-12-17 +## 0.5.0-beta.3 - Minimum supported Rust version (MSRV) is now 1.52. -## 0.5.0-beta.2 - 2021-09-09 +## 0.5.0-beta.2 - Introduce `ResourceDef::join`. [#380][net#380] - Disallow prefix routes with tail segments. [#379][net#379] @@ -143,7 +150,7 @@ [#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 +## 0.5.0-beta.1 - Fix a bug in multi-patterns where static patterns are interpreted as regex. [#366][net#366] - Introduce `ResourceDef::pattern_iter` to get an iterator over all patterns in a multi-pattern resource. [#373][net#373] @@ -173,7 +180,7 @@
-## 0.4.0 - 2021-06-06 +## 0.4.0 - When matching path parameters, `%25` is now kept in the percent-encoded form; no longer decoded to `%`. [#357][net#357] - Path tail patterns now match new lines (`\n`) in request URL. [#360][net#360] @@ -185,70 +192,70 @@ [net#359]: https://github.com/actix/actix-net/pull/359 [net#360]: https://github.com/actix/actix-net/pull/360 -## 0.3.0 - 2019-12-31 +## 0.3.0 - Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 -## 0.2.7 - 2021-02-06 +## 0.2.7 - Add `Router::recognize_checked` [#247][net#247] [net#247]: https://github.com/actix/actix-net/pull/247 -## 0.2.6 - 2021-01-09 +## 0.2.6 - Use `bytestring` version range compatible with Bytes v1.0. [#246][net#246] [net#246]: https://github.com/actix/actix-net/pull/246 -## 0.2.5 - 2020-09-20 +## 0.2.5 - Fix `from_hex()` method -## 0.2.4 - 2019-12-31 +## 0.2.4 - Add `ResourceDef::resource_path_named()` path generation method -## 0.2.3 - 2019-12-25 +## 0.2.3 - Add impl `IntoPattern` for `&String` -## 0.2.2 - 2019-12-25 +## 0.2.2 - Use `IntoPattern` for `RouterBuilder::path()` -## 0.2.1 - 2019-12-25 +## 0.2.1 - Add `IntoPattern` trait - Add multi-pattern resources -## 0.2.0 - 2019-12-07 +## 0.2.0 - Update http to 0.2 - Update regex to 1.3 - Use bytestring instead of string -## 0.1.5 - 2019-05-15 +## 0.1.5 - Remove debug prints -## 0.1.4 - 2019-05-15 +## 0.1.4 - Fix checked resource match -## 0.1.3 - 2019-04-22 +## 0.1.3 - Added support for `remainder match` (i.e "/path/{tail}\*") -## 0.1.2 - 2019-04-07 +## 0.1.2 - Export `Quoter` type - Allow to reset `Path` instance -## 0.1.1 - 2019-04-03 +## 0.1.1 - Get dynamic segment by name instead of iterator. -## 0.1.0 - 2019-03-09 +## 0.1.0 - Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index adf43a086..56e4bed2f 100644 --- a/actix-router/Cargo.toml +++ b/actix-router/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-router" -version = "0.5.1" +version = "0.5.3" authors = [ "Nikolay Kim ", "Ali MJ Al-Nasrawy ", @@ -8,7 +8,7 @@ authors = [ ] description = "Resource path matching and router" keywords = ["actix", "router", "routing"] -repository = "https://github.com/actix/actix-web.git" +repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2021" @@ -17,12 +17,16 @@ name = "actix_router" path = "src/lib.rs" [features] -default = ["http"] +default = ["http", "unicode"] +http = ["dep:http"] +unicode = ["dep:regex"] [dependencies] bytestring = ">=0.1.5, <2" +cfg-if = "1" http = { version = "0.2.7", optional = true } -regex = "1.5" +regex = { version = "1.5", optional = true } +regex-lite = "0.1" serde = "1" tracing = { version = "0.1.30", default-features = false, features = ["log"] } @@ -35,6 +39,7 @@ percent-encoding = "2.1" [[bench]] name = "router" harness = false +required-features = ["unicode"] [[bench]] name = "quoter" diff --git a/actix-router/README.md b/actix-router/README.md new file mode 100644 index 000000000..12d1b0146 --- /dev/null +++ b/actix-router/README.md @@ -0,0 +1,20 @@ +# `actix-router` + + + +[![crates.io](https://img.shields.io/crates/v/actix-router?label=latest)](https://crates.io/crates/actix-router) +[![Documentation](https://docs.rs/actix-router/badge.svg?version=0.5.3)](https://docs.rs/actix-router/0.5.3) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-router.svg) +
+[![dependency status](https://deps.rs/crate/actix-router/0.5.3/status.svg)](https://deps.rs/crate/actix-router/0.5.3) +[![Download](https://img.shields.io/crates/d/actix-router.svg)](https://crates.io/crates/actix-router) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) + + + + + +Resource path matching and router. + + diff --git a/actix-router/benches/quoter.rs b/actix-router/benches/quoter.rs index 2065fd563..2428a767d 100644 --- a/actix-router/benches/quoter.rs +++ b/actix-router/benches/quoter.rs @@ -1,6 +1,4 @@ -#![allow(clippy::uninlined_format_args)] - -use std::borrow::Cow; +use std::{borrow::Cow, fmt::Write as _}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; @@ -8,9 +6,10 @@ fn compare_quoters(c: &mut Criterion) { let mut group = c.benchmark_group("Compare Quoters"); let quoter = actix_router::Quoter::new(b"", b""); - let path_quoted = (0..=0x7f) - .map(|c| format!("%{:02X}", c)) - .collect::(); + let path_quoted = (0..=0x7f).fold(String::new(), |mut buf, c| { + write!(&mut buf, "%{:02X}", c).unwrap(); + buf + }); let path_unquoted = ('\u{00}'..='\u{7f}').collect::(); group.bench_function("quoter_unquoted", |b| { diff --git a/actix-router/src/de.rs b/actix-router/src/de.rs index e8c7c658e..ce2dcf8f3 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -500,10 +500,10 @@ impl<'de> de::VariantAccess<'de> for UnitVariant { #[cfg(test)] mod tests { - use serde::{de, Deserialize}; + use serde::Deserialize; use super::*; - use crate::{path::Path, router::Router, ResourceDef}; + use crate::{router::Router, ResourceDef}; #[derive(Deserialize)] struct MyStruct { diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs index 53c0ad82a..c4d0d2c87 100644 --- a/actix-router/src/lib.rs +++ b/actix-router/src/lib.rs @@ -2,7 +2,6 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::uninlined_format_args)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -11,6 +10,7 @@ mod de; mod path; mod pattern; mod quoter; +mod regex_set; mod resource; mod resource_path; mod router; diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index dc4150ddc..9031ab763 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -3,7 +3,7 @@ use std::{ ops::{DerefMut, Index}, }; -use serde::de; +use serde::{de, Deserialize}; use crate::{de::PathDeserializer, Resource, ResourcePath}; @@ -24,8 +24,13 @@ impl Default for PathItem { /// If resource path contains variable patterns, `Path` stores them. #[derive(Debug, Clone, Default)] pub struct Path { + /// Full path representation. path: T, + + /// Number of characters in `path` that have been processed into `segments`. pub(crate) skip: u16, + + /// List of processed dynamic segments; name->value pairs. pub(crate) segments: Vec<(Cow<'static, str>, PathItem)>, } @@ -83,8 +88,8 @@ impl Path { /// Set new path. #[inline] pub fn set(&mut self, path: T) { - self.skip = 0; self.path = path; + self.skip = 0; self.segments.clear(); } @@ -103,7 +108,7 @@ impl Path { pub(crate) fn add(&mut self, name: impl Into>, value: PathItem) { match value { - PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))), + PathItem::Static(seg) => self.segments.push((name.into(), PathItem::Static(seg))), PathItem::Segment(begin, end) => self.segments.push(( name.into(), PathItem::Segment(self.skip + begin, self.skip + end), @@ -149,15 +154,11 @@ impl Path { None } - /// Get matched parameter by name. + /// Returns matched parameter by name. /// /// If keyed parameter is not available empty string is used as default value. pub fn query(&self, key: &str) -> &str { - if let Some(s) = self.get(key) { - s - } else { - "" - } + self.get(key).unwrap_or_default() } /// Return iterator to items in parameter container. @@ -168,9 +169,13 @@ impl Path { } } - /// Try to deserialize matching parameters to a specified type `U` - pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result { - de::Deserialize::deserialize(PathDeserializer::new(self)) + /// Deserializes matching parameters to a specified type `U`. + /// + /// # Errors + /// + /// Returns error when dynamic path segments cannot be deserialized into a `U` type. + pub fn load<'de, U: Deserialize<'de>>(&'de self) -> Result { + Deserialize::deserialize(PathDeserializer::new(self)) } } diff --git a/actix-router/src/regex_set.rs b/actix-router/src/regex_set.rs new file mode 100644 index 000000000..48f38df2c --- /dev/null +++ b/actix-router/src/regex_set.rs @@ -0,0 +1,66 @@ +//! Abstraction over `regex` and `regex-lite` depending on whether we have `unicode` crate feature +//! enabled. + +use cfg_if::cfg_if; +#[cfg(feature = "unicode")] +pub(crate) use regex::{escape, Regex}; +#[cfg(not(feature = "unicode"))] +pub(crate) use regex_lite::{escape, Regex}; + +#[cfg(feature = "unicode")] +#[derive(Debug, Clone)] +pub(crate) struct RegexSet(regex::RegexSet); + +#[cfg(not(feature = "unicode"))] +#[derive(Debug, Clone)] +pub(crate) struct RegexSet(Vec); + +impl RegexSet { + /// Create a new regex set. + /// + /// # Panics + /// + /// Panics if any path patterns are malformed. + pub(crate) fn new(re_set: Vec) -> Self { + cfg_if! { + if #[cfg(feature = "unicode")] { + Self(regex::RegexSet::new(re_set).unwrap()) + } else { + Self(re_set.iter().map(|re| Regex::new(re).unwrap()).collect()) + } + } + } + + /// Create a new empty regex set. + pub(crate) fn empty() -> Self { + cfg_if! { + if #[cfg(feature = "unicode")] { + Self(regex::RegexSet::empty()) + } else { + Self(Vec::new()) + } + } + } + + /// Returns true if regex set matches `path`. + pub(crate) fn is_match(&self, path: &str) -> bool { + cfg_if! { + if #[cfg(feature = "unicode")] { + self.0.is_match(path) + } else { + self.0.iter().any(|re| re.is_match(path)) + } + } + } + + /// Returns index within `path` of first match. + pub(crate) fn first_match_idx(&self, path: &str) -> Option { + cfg_if! { + if #[cfg(feature = "unicode")] { + self.0.matches(path).into_iter().next() + } else { + Some(self.0.iter().enumerate().find(|(_, re)| re.is_match(path))?.0) + } + } + } +} diff --git a/actix-router/src/resource.rs b/actix-router/src/resource.rs index 80c0a2d68..3a102945b 100644 --- a/actix-router/src/resource.rs +++ b/actix-router/src/resource.rs @@ -5,10 +5,13 @@ use std::{ mem, }; -use regex::{escape, Regex, RegexSet}; use tracing::error; -use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath}; +use crate::{ + path::PathItem, + regex_set::{escape, Regex, RegexSet}, + IntoPatterns, Patterns, Resource, ResourcePath, +}; const MAX_DYNAMIC_SEGMENTS: usize = 16; @@ -193,8 +196,8 @@ const REGEX_FLAGS: &str = "(?s-m)"; /// # 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. +/// you wish to accommodate "recoverable" path errors. Below are several examples of resource-path +/// pairs that would not be compatible. /// /// ## Examples /// ``` @@ -233,7 +236,7 @@ enum PatternSegment { Var(String), } -#[derive(Clone, Debug)] +#[derive(Debug, Clone)] #[allow(clippy::large_enum_variant)] enum PatternType { /// Single constant/literal segment. @@ -603,7 +606,7 @@ impl ResourceDef { PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()), PatternType::DynamicSet(re, params) => { - let idx = re.matches(path).into_iter().next()?; + let idx = re.first_match_idx(path)?; let (ref pattern, _) = params[idx]; Some(pattern.captures(path)?[1].len()) } @@ -706,7 +709,7 @@ impl ResourceDef { PatternType::DynamicSet(re, params) => { let path = path.unprocessed(); - let (pattern, names) = match re.matches(path).into_iter().next() { + let (pattern, names) = match re.first_match_idx(path) { Some(idx) => ¶ms[idx], _ => return false, }; @@ -870,7 +873,7 @@ impl ResourceDef { } } - let pattern_re_set = RegexSet::new(re_set).unwrap(); + let pattern_re_set = RegexSet::new(re_set); let segments = segments.unwrap_or_default(); ( diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index d31d10ce8..1dd4449da 100644 --- a/actix-router/src/router.rs +++ b/actix-router/src/router.rs @@ -97,6 +97,7 @@ impl RouterBuilder { ctx: U, ) -> (&mut ResourceDef, &mut T, &mut U) { self.routes.push((rdef, val, ctx)); + #[allow(clippy::map_identity)] // map is used to distribute &mut-ness to tuple elements self.routes .last_mut() .map(|(rdef, val, ctx)| (rdef, val, ctx)) @@ -186,11 +187,11 @@ mod tests { assert_eq!(path.get("file").unwrap(), "file"); assert_eq!(path.get("ext").unwrap(), "gz"); - let mut path = Path::new("/vtest/ttt/index.html"); + let mut path = Path::new("/v2/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("val").unwrap(), "2"); assert_eq!(path.get("val2").unwrap(), "ttt"); let mut path = Path::new("/v/blah-blah/index.html"); diff --git a/actix-router/src/url.rs b/actix-router/src/url.rs index 2920e271d..b3d9e1121 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -62,6 +62,8 @@ impl ResourcePath for Url { #[cfg(test)] mod tests { + use std::fmt::Write as _; + use http::Uri; use super::*; @@ -78,7 +80,11 @@ mod tests { } fn percent_encode(data: &[u8]) -> String { - data.iter().map(|c| format!("%{:02X}", c)).collect() + data.iter() + .fold(String::with_capacity(data.len() * 3), |mut buf, c| { + write!(&mut buf, "%{:02X}", c).unwrap(); + buf + }) } #[test] diff --git a/actix-test/CHANGES.md b/actix-test/CHANGES.md index fae8201d2..ec2dd6776 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,78 +1,97 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased -- Add `TestServerConfig::workers()` setter method. +## 0.1.5 + +- Add `TestServerConfig::listen_address()` method. + +## 0.1.4 + +- Add `TestServerConfig::rustls_0_23()` method for Rustls v0.23 support behind new `rustls-0_23` crate feature. +- Add `TestServerConfig::disable_redirects()` method. +- Various types from `awc`, such as `ClientRequest` and `ClientResponse`, are now re-exported. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 0.1.3 + +- Add `TestServerConfig::rustls_0_22()` method for Rustls v0.22 support behind new `rustls-0_22` crate feature. + +## 0.1.2 + +- Add `TestServerConfig::rustls_021()` method for Rustls v0.21 support behind new `rustls-0_21` crate feature. +- Add `TestServerConfig::workers()` method. +- Add `rustls-0_20` crate feature, which the existing `rustls` feature now aliases. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 0.1.1 - 2023-02-26 +## 0.1.1 - Add `TestServerConfig::port()` setter method. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. -## 0.1.0 - 2022-07-24 +## 0.1.0 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. -## 0.1.0-beta.13 - 2022-02-16 +## 0.1.0-beta.13 - No significant changes since `0.1.0-beta.12`. -## 0.1.0-beta.12 - 2022-01-31 +## 0.1.0-beta.12 - Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611] [#2611]: https://github.com/actix/actix-web/pull/2611 -## 0.1.0-beta.11 - 2022-01-04 +## 0.1.0-beta.11 - Minimum supported Rust version (MSRV) is now 1.54. -## 0.1.0-beta.10 - 2021-12-27 +## 0.1.0-beta.10 - No significant changes since `0.1.0-beta.9`. -## 0.1.0-beta.9 - 2021-12-17 +## 0.1.0-beta.9 - Re-export `actix_http::body::to_bytes`. [#2518] - Update `actix_web::test` re-exports. [#2518] [#2518]: https://github.com/actix/actix-web/pull/2518 -## 0.1.0-beta.8 - 2021-12-11 +## 0.1.0-beta.8 - No significant changes since `0.1.0-beta.7`. -## 0.1.0-beta.7 - 2021-11-22 +## 0.1.0-beta.7 - 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 +## 0.1.0-beta.6 - No significant changes from `0.1.0-beta.5`. -## 0.1.0-beta.5 - 2021-10-20 +## 0.1.0-beta.5 - 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 +## 0.1.0-beta.4 - Minimum supported Rust version (MSRV) is now 1.51. -## 0.1.0-beta.3 - 2021-06-20 +## 0.1.0-beta.3 - No significant changes from `0.1.0-beta.2`. -## 0.1.0-beta.2 - 2021-04-17 +## 0.1.0-beta.2 - No significant changes from `0.1.0-beta.1`. -## 0.1.0-beta.1 - 2021-04-02 +## 0.1.0-beta.1 - Move integration testing structs from `actix-web`. [#2112] diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index 9cf5aa76c..41267c969 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.1" +version = "0.1.5" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -8,7 +8,7 @@ authors = [ 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" +repository = "https://github.com/actix/actix-web" categories = [ "network-programming", "asynchronous", @@ -21,21 +21,29 @@ edition = "2021" [features] default = [] -# rustls -rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"] +# TLS via Rustls v0.20 +rustls = ["rustls-0_20"] +# TLS via Rustls v0.20 +rustls-0_20 = ["tls-rustls-0_20", "actix-http/rustls-0_20", "awc/rustls-0_20"] +# TLS via Rustls v0.21 +rustls-0_21 = ["tls-rustls-0_21", "actix-http/rustls-0_21", "awc/rustls-0_21"] +# TLS via Rustls v0.22 +rustls-0_22 = ["tls-rustls-0_22", "actix-http/rustls-0_22", "awc/rustls-0_22-webpki-roots"] +# TLS via Rustls v0.23 +rustls-0_23 = ["tls-rustls-0_23", "actix-http/rustls-0_23", "awc/rustls-0_23-webpki-roots"] -# openssl +# TLS via OpenSSL openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"] [dependencies] actix-codec = "0.5" -actix-http = "3" +actix-http = "3.7" actix-http-test = "3" actix-rt = "2.1" actix-service = "2" actix-utils = "3" -actix-web = { version = "4", default-features = false, features = ["cookies"] } -awc = { version = "3", default-features = false, features = ["cookies"] } +actix-web = { version = "4.6", default-features = false, features = ["cookies"] } +awc = { version = "3.5", default-features = false, features = ["cookies"] } futures-core = { version = "0.3.17", default-features = false, features = ["std"] } futures-util = { version = "0.3.17", default-features = false, features = [] } @@ -44,5 +52,8 @@ serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" tls-openssl = { package = "openssl", version = "0.10.55", optional = true } -tls-rustls = { package = "rustls", version = "0.20", optional = true } +tls-rustls-0_20 = { package = "rustls", version = "0.20", optional = true } +tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true } +tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } +tls-rustls-0_23 = { package = "rustls", version = "0.23", default-features = false, optional = true } tokio = { version = "1.24.2", features = ["sync"] } diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 751ab3161..803320607 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -34,8 +34,6 @@ #[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}; @@ -54,7 +52,7 @@ use actix_web::{ rt::{self, System}, web, Error, }; -use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; +pub use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector}; use futures_core::Stream; use tokio::sync::mpsc; @@ -141,14 +139,22 @@ where StreamType::Tcp => false, #[cfg(feature = "openssl")] StreamType::Openssl(_) => true, - #[cfg(feature = "rustls")] - StreamType::Rustls(_) => true, + #[cfg(feature = "rustls-0_20")] + StreamType::Rustls020(_) => true, + #[cfg(feature = "rustls-0_21")] + StreamType::Rustls021(_) => true, + #[cfg(feature = "rustls-0_22")] + StreamType::Rustls022(_) => true, + #[cfg(feature = "rustls-0_23")] + StreamType::Rustls023(_) => true, }; + let client_cfg = cfg.clone(); + // 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", cfg.port)).unwrap(); + let tcp = net::TcpListener::bind((cfg.listen_address.clone(), cfg.port)).unwrap(); let local_addr = tcp.local_addr().unwrap(); let factory = factory.clone(); let srv_cfg = cfg.clone(); @@ -243,8 +249,8 @@ where .openssl(acceptor.clone()) }), }, - #[cfg(feature = "rustls")] - StreamType::Rustls(config) => match cfg.tp { + #[cfg(feature = "rustls-0_20")] + StreamType::Rustls020(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); @@ -285,6 +291,132 @@ where .rustls(config.clone()) }), }, + #[cfg(feature = "rustls-0_21")] + StreamType::Rustls021(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_request_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .rustls_021(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_request_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .rustls_021(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_request_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .rustls_021(config.clone()) + }), + }, + #[cfg(feature = "rustls-0_22")] + StreamType::Rustls022(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_request_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .rustls_0_22(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_request_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .rustls_0_22(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_request_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .rustls_0_22(config.clone()) + }), + }, + #[cfg(feature = "rustls-0_23")] + StreamType::Rustls023(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_request_timeout(timeout) + .h1(map_config(fac, move |_| app_cfg.clone())) + .rustls_0_23(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_request_timeout(timeout) + .h2(map_config(fac, move |_| app_cfg.clone())) + .rustls_0_23(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_request_timeout(timeout) + .finish(map_config(fac, move |_| app_cfg.clone())) + .rustls_0_23(config.clone()) + }), + }, } .expect("test server could not be created"); @@ -316,7 +448,7 @@ where 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)); + .map_err(|err| log::error!("Can not set alpn protocol: {err:?}")); Connector::new() .conn_lifetime(Duration::from_secs(0)) .timeout(Duration::from_millis(30000)) @@ -330,7 +462,13 @@ where } }; - Client::builder().connector(connector).finish() + let mut client_builder = Client::builder().connector(connector); + + if client_cfg.disable_redirects { + client_builder = client_builder.disable_redirects(); + } + + client_builder.finish() }; TestServer { @@ -350,13 +488,20 @@ enum HttpVer { Both, } +#[allow(clippy::large_enum_variant)] #[derive(Clone)] enum StreamType { Tcp, #[cfg(feature = "openssl")] Openssl(openssl::ssl::SslAcceptor), - #[cfg(feature = "rustls")] - Rustls(rustls::ServerConfig), + #[cfg(feature = "rustls-0_20")] + Rustls020(tls_rustls_0_20::ServerConfig), + #[cfg(feature = "rustls-0_21")] + Rustls021(tls_rustls_0_21::ServerConfig), + #[cfg(feature = "rustls-0_22")] + Rustls022(tls_rustls_0_22::ServerConfig), + #[cfg(feature = "rustls-0_23")] + Rustls023(tls_rustls_0_23::ServerConfig), } /// Create default test server config. @@ -369,8 +514,10 @@ pub struct TestServerConfig { tp: HttpVer, stream: StreamType, client_request_timeout: Duration, + listen_address: String, port: u16, workers: usize, + disable_redirects: bool, } impl Default for TestServerConfig { @@ -380,49 +527,96 @@ impl Default for TestServerConfig { } impl TestServerConfig { - /// Create default server configuration + /// Constructs default server configuration. pub(crate) fn new() -> TestServerConfig { TestServerConfig { tp: HttpVer::Both, stream: StreamType::Tcp, client_request_timeout: Duration::from_secs(5), + listen_address: "127.0.0.1".to_string(), port: 0, workers: 1, + disable_redirects: false, } } - /// Accept HTTP/1.1 only. + /// Accepts HTTP/1.1 only. pub fn h1(mut self) -> Self { self.tp = HttpVer::Http1; self } - /// Accept HTTP/2 only. + /// Accepts HTTP/2 only. pub fn h2(mut self) -> Self { self.tp = HttpVer::Http2; self } - /// Accept secure connections via OpenSSL. + /// Accepts 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); + #[doc(hidden)] + #[deprecated(note = "Renamed to `rustls_0_20()`.")] + #[cfg(feature = "rustls-0_20")] + pub fn rustls(mut self, config: tls_rustls_0_20::ServerConfig) -> Self { + self.stream = StreamType::Rustls020(config); self } - /// Set client timeout for first request. + /// Accepts secure connections via Rustls v0.20. + #[cfg(feature = "rustls-0_20")] + pub fn rustls_0_20(mut self, config: tls_rustls_0_20::ServerConfig) -> Self { + self.stream = StreamType::Rustls020(config); + self + } + + #[doc(hidden)] + #[deprecated(note = "Renamed to `rustls_0_21()`.")] + #[cfg(feature = "rustls-0_21")] + pub fn rustls_021(mut self, config: tls_rustls_0_21::ServerConfig) -> Self { + self.stream = StreamType::Rustls021(config); + self + } + + /// Accepts secure connections via Rustls v0.21. + #[cfg(feature = "rustls-0_21")] + pub fn rustls_0_21(mut self, config: tls_rustls_0_21::ServerConfig) -> Self { + self.stream = StreamType::Rustls021(config); + self + } + + /// Accepts secure connections via Rustls v0.22. + #[cfg(feature = "rustls-0_22")] + pub fn rustls_0_22(mut self, config: tls_rustls_0_22::ServerConfig) -> Self { + self.stream = StreamType::Rustls022(config); + self + } + + /// Accepts secure connections via Rustls v0.23. + #[cfg(feature = "rustls-0_23")] + pub fn rustls_0_23(mut self, config: tls_rustls_0_23::ServerConfig) -> Self { + self.stream = StreamType::Rustls023(config); + self + } + + /// Sets client timeout for first request. pub fn client_request_timeout(mut self, dur: Duration) -> Self { self.client_request_timeout = dur; self } + /// Sets the address the server will listen on. + /// + /// By default, only listens on `127.0.0.1`. + pub fn listen_address(mut self, addr: impl Into) -> Self { + self.listen_address = addr.into(); + self + } + /// Sets test server port. /// /// By default, a random free port is determined by the OS. @@ -438,6 +632,15 @@ impl TestServerConfig { self.workers = workers; self } + + /// Instruct the client to not follow redirects. + /// + /// By default, the client will follow up to 10 consecutive redirects + /// before giving up. + pub fn disable_redirects(mut self) -> Self { + self.disable_redirects = true; + self + } } /// A basic HTTP server controller that simplifies the process of writing integration tests for @@ -464,9 +667,9 @@ impl TestServer { let scheme = if self.tls { "https" } else { "http" }; if uri.starts_with('/') { - format!("{}://localhost:{}{}", scheme, self.addr.port(), uri) + format!("{}://{}{}", scheme, self.addr, uri) } else { - format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri) + format!("{}://{}/{}", scheme, self.addr, uri) } } diff --git a/actix-web-actors/CHANGES.md b/actix-web-actors/CHANGES.md index cff27a16d..3e854c0b8 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,40 +1,45 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased + +- Take the encoded buffer when yielding bytes in the response stream rather than splitting the buffer, reducing memory use +- Minimum supported Rust version (MSRV) is now 1.72. + +## 4.3.0 - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 4.2.0 - 2023-01-21 +## 4.2.0 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. -## 4.1.0 - 2022-03-02 +## 4.1.0 - Add support for `actix` version `0.13`. [#2675] [#2675]: https://github.com/actix/actix-web/pull/2675 -## 4.0.0 - 2022-02-25 +## 4.0.0 - No significant changes since `4.0.0-beta.12`. -## 4.0.0-beta.12 - 2022-02-16 +## 4.0.0-beta.12 - No significant changes since `4.0.0-beta.11`. -## 4.0.0-beta.11 - 2022-01-31 +## 4.0.0-beta.11 - No significant changes since `4.0.0-beta.10`. -## 4.0.0-beta.10 - 2022-01-04 +## 4.0.0-beta.10 - Minimum supported Rust version (MSRV) is now 1.54. -## 4.0.0-beta.9 - 2021-12-27 +## 4.0.0-beta.9 - No significant changes since `4.0.0-beta.8`. -## 4.0.0-beta.8 - 2021-12-11 +## 4.0.0-beta.8 - Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] - Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] @@ -42,33 +47,33 @@ [#1920]: https://github.com/actix/actix-web/pull/1920 -## 4.0.0-beta.7 - 2021-09-09 +## 4.0.0-beta.7 - Minimum supported Rust version (MSRV) is now 1.51. -## 4.0.0-beta.6 - 2021-06-26 +## 4.0.0-beta.6 - Update `actix` to `0.12`. [#2277] [#2277]: https://github.com/actix/actix-web/pull/2277 -## 4.0.0-beta.5 - 2021-06-17 +## 4.0.0-beta.5 - No notable changes. -## 4.0.0-beta.4 - 2021-04-02 +## 4.0.0-beta.4 - No notable changes. -## 4.0.0-beta.3 - 2021-03-09 +## 4.0.0-beta.3 - No notable changes. -## 4.0.0-beta.2 - 2021-02-10 +## 4.0.0-beta.2 - No notable changes. -## 4.0.0-beta.1 - 2021-01-07 +## 4.0.0-beta.1 - Update `pin-project` to `1.0`. - Update `bytes` to `1.0`. [#1813] @@ -77,63 +82,63 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 [#1864]: https://github.com/actix/actix-web/pull/1864 -## 3.0.0 - 2020-09-11 +## 3.0.0 - No significant changes from `3.0.0-beta.2`. -## 3.0.0-beta.2 - 2020-09-10 +## 3.0.0-beta.2 - Update `actix-*` dependencies to latest versions. -## [3.0.0-beta.1] - 2020-xx-xx +## 3.0.0-beta.1 - Update `actix-web` & `actix-http` dependencies to beta.1 - Bump minimum supported Rust version to 1.40 -## [3.0.0-alpha.1] - 2020-05-08 +## 3.0.0-alpha.1 - Update the actix-web dependency to 3.0.0-alpha.1 - Update the actix dependency to 0.10.0-alpha.2 - Update the actix-http dependency to 2.0.0-alpha.3 -## [2.0.0] - 2019-12-20 +## 2.0.0 - Release -## [2.0.0-alpha.1] - 2019-12-15 +## 2.0.0-alpha.1 - Migrate to actix-web 2.0.0 -## [1.0.4] - 2019-12-07 +## 1.0.4 - Allow comma-separated websocket subprotocols without spaces (#1172) -## [1.0.3] - 2019-11-14 +## 1.0.3 - Update actix-web and actix-http dependencies -## [1.0.2] - 2019-07-20 +## 1.0.2 - Add `ws::start_with_addr()`, returning the address of the created actor, along with the `HttpResponse`. - Add support for specifying protocols on websocket handshake #835 -## [1.0.1] - 2019-06-28 +## 1.0.1 - Allow to use custom ws codec with `WebsocketContext` #925 -## [1.0.0] - 2019-05-29 +## 1.0.0 - Update actix-http and actix-web -## [0.1.0-alpha.3] - 2019-04-02 +## 0.1.0-alpha.3 - Update actix-http and actix-web -## [0.1.0-alpha.2] - 2019-03-29 +## 0.1.0-alpha.2 - Update actix-http and actix-web -## [0.1.0-alpha.1] - 2019-03-28 +## 0.1.0-alpha.1 - Initial impl diff --git a/actix-web-actors/Cargo.toml b/actix-web-actors/Cargo.toml index c6f14554a..114ec5a87 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-actors" -version = "4.2.0" +version = "4.3.0" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] @@ -32,6 +32,6 @@ actix-test = "0.1" awc = { version = "3", default-features = false } actix-web = { version = "4", features = ["macros"] } -env_logger = "0.10" +env_logger = "0.11" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } mime = "0.3" diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index b2c30b954..feb3d1b33 100644 --- a/actix-web-actors/README.md +++ b/actix-web-actors/README.md @@ -1,17 +1,16 @@ -# actix-web-actors +# `actix-web-actors` > Actix actors support for Actix Web. + + [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) -[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.2.0)](https://docs.rs/actix-web-actors/4.2.0) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.3.0)](https://docs.rs/actix-web-actors/4.3.0) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
-[![dependency status](https://deps.rs/crate/actix-web-actors/4.2.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.2.0) +[![dependency status](https://deps.rs/crate/actix-web-actors/4.3.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.3.0) [![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.68 + diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index be8fd387c..23e336459 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -248,13 +248,11 @@ where mod tests { use std::time::Duration; - use actix::Actor; use actix_web::{ http::StatusCode, test::{call_service, init_service, read_body, TestRequest}, web, App, HttpResponse, }; - use bytes::Bytes; use super::*; diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index cf2eb3645..d89b0ee35 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -57,7 +57,6 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::uninlined_format_args)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index 6ce3e69a1..7f7607fa9 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -710,7 +710,7 @@ where } if !this.buf.is_empty() { - Poll::Ready(Some(Ok(this.buf.split().freeze()))) + Poll::Ready(Some(Ok(std::mem::take(&mut this.buf).freeze()))) } else if this.fut.alive() && !this.closed { Poll::Pending } else { @@ -775,10 +775,10 @@ where break; } Poll::Pending => break, - Poll::Ready(Some(Err(e))) => { + Poll::Ready(Some(Err(err))) => { return Poll::Ready(Some(Err(ProtocolError::Io(io::Error::new( io::ErrorKind::Other, - format!("{}", e), + format!("{err}"), ))))); } } @@ -817,10 +817,7 @@ where #[cfg(test)] mod tests { - use actix_web::{ - http::{header, Method}, - test::TestRequest, - }; + use actix_web::test::TestRequest; use super::*; diff --git a/actix-web-codegen/CHANGES.md b/actix-web-codegen/CHANGES.md index 9ff1edf6e..d143723f4 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,52 +1,65 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased + +## 4.3.0 + +- Add `#[scope]` macro. +- Add `compat-routing-macros-force-pub` crate feature which, on-by-default, which when disabled causes handlers to inherit their attached function's visibility. +- Prevent inclusion of default `actix-router` features. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 4.2.2 + +- Fix regression when declaring `wrap` attribute using an expression. + +## 4.2.1 - Update `syn` dependency to `2`. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 4.2.0 - 2023-02-26 +## 4.2.0 - Add support for custom methods with the `#[route]` macro. [#2969] [#2969]: https://github.com/actix/actix-web/pull/2969 -## 4.1.0 - 2022-09-11 +## 4.1.0 - Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2718]: https://github.com/actix/actix-web/pull/2718 -## 4.0.1 - 2022-06-11 +## 4.0.1 - Fix support for guard paths in route handler macros. [#2771] - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. [#2771]: https://github.com/actix/actix-web/pull/2771 -## 4.0.0 - 2022-02-24 +## 4.0.0 - Version aligned with `actix-web` and will remain in sync going forward. - No significant changes since `0.5.0`. -## 0.5.0 - 2022-02-24 +## 0.5.0 - No significant changes since `0.5.0-rc.2`. -## 0.5.0-rc.2 - 2022-02-01 +## 0.5.0-rc.2 - No significant changes since `0.5.0-rc.1`. -## 0.5.0-rc.1 - 2022-01-04 +## 0.5.0-rc.1 - Minimum supported Rust version (MSRV) is now 1.54. -## 0.5.0-beta.6 - 2021-12-11 +## 0.5.0-beta.6 - No significant changes since `0.5.0-beta.5`. -## 0.5.0-beta.5 - 2021-10-20 +## 0.5.0-beta.5 - Improve error recovery potential when macro input is invalid. [#2410] - Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] @@ -55,18 +68,18 @@ [#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 +## 0.5.0-beta.4 - 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 +## 0.5.0-beta.3 - No notable changes. -## 0.5.0-beta.2 - 2021-03-09 +## 0.5.0-beta.2 - Preserve doc comments when using route macros. [#2022] - Add `name` attribute to `route` macro. [#1934] @@ -74,11 +87,11 @@ [#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 +## 0.5.0-beta.1 - Use new call signature for `System::new`. -## 0.4.0 - 2020-09-20 +## 0.4.0 - Added compile success and failure testing. [#1677] - Add `route` macro for supporting multiple HTTP methods guards. [#1674] @@ -86,23 +99,23 @@ [#1677]: https://github.com/actix/actix-web/pull/1677 [#1674]: https://github.com/actix/actix-web/pull/1674 -## 0.3.0 - 2020-09-11 +## 0.3.0 - No significant changes from `0.3.0-beta.1`. -## 0.3.0-beta.1 - 2020-07-14 +## 0.3.0-beta.1 - Add main entry-point macro that uses re-exported runtime. [#1559] [#1559]: https://github.com/actix/actix-web/pull/1559 -## 0.2.2 - 2020-05-23 +## 0.2.2 - Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 -## 0.2.1 - 2020-02-25 +## 0.2.1 - Add `#[allow(missing_docs)]` attribute to generated structs [#1368] - Allow the handler function to be named as `config` [#1290] @@ -110,35 +123,35 @@ [#1368]: https://github.com/actix/actix-web/issues/1368 [#1290]: https://github.com/actix/actix-web/issues/1290 -## 0.2.0 - 2019-12-13 +## 0.2.0 - Generate code for actix-web 2.0 -## 0.1.3 - 2019-10-14 +## 0.1.3 - Bump up `syn` & `quote` to 1.0 - Provide better error message -## 0.1.2 - 2019-06-04 +## 0.1.2 - Add macros for head, options, trace, connect and patch http methods -## 0.1.1 - 2019-06-01 +## 0.1.1 - Add syn "extra-traits" feature -## 0.1.0 - 2019-05-18 +## 0.1.0 - Release -## 0.1.0-beta.1 - 2019-04-20 +## 0.1.0-beta.1 - Gen code for actix-web 1.0.0-beta.1 -## 0.1.0-alpha.6 - 2019-04-14 +## 0.1.0-alpha.6 - Gen code for actix-web 1.0.0-alpha.6 -## 0.1.0-alpha.1 - 2019-03-28 +## 0.1.0-alpha.1 - Initial impl diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index c202c5d6e..7500807d2 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,21 +1,26 @@ [package] name = "actix-web-codegen" -version = "4.2.0" +version = "4.3.0" description = "Routing and runtime macros for Actix Web" -homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" authors = [ "Nikolay Kim ", "Rob Ede ", ] -license = "MIT OR Apache-2.0" -edition = "2021" +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true [lib] proc-macro = true +[features] +default = ["compat-routing-macros-force-pub"] +compat-routing-macros-force-pub = [] + [dependencies] -actix-router = "0.5" +actix-router = { version = "0.5", default-features = false } proc-macro2 = "1" quote = "1" syn = { version = "2", features = ["full", "extra-traits"] } diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index e6df8164e..e61bf5c74 100644 --- a/actix-web-codegen/README.md +++ b/actix-web-codegen/README.md @@ -1,20 +1,19 @@ -# actix-web-codegen +# `actix-web-codegen` > Routing and runtime macros for Actix Web. + + [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) -[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.2.0)](https://docs.rs/actix-web-codegen/4.2.0) -![Version](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) +[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=4.3.0)](https://docs.rs/actix-web-codegen/4.3.0) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
-[![dependency status](https://deps.rs/crate/actix-web-codegen/4.2.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.2.0) +[![dependency status](https://deps.rs/crate/actix-web-codegen/4.3.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.3.0) [![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) -- Minimum Supported Rust Version (MSRV): 1.68 + ## Compile Testing diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 6d6c9ab5c..c518007a0 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -83,6 +83,7 @@ use proc_macro::TokenStream; use quote::quote; mod route; +mod scope; /// Creates resource handler, allowing multiple HTTP method guards. /// @@ -197,6 +198,43 @@ method_macro!(Options, options); method_macro!(Trace, trace); method_macro!(Patch, patch); +/// Prepends a path prefix to all handlers using routing macros inside the attached module. +/// +/// # Syntax +/// +/// ``` +/// # use actix_web_codegen::scope; +/// #[scope("/prefix")] +/// mod api { +/// // ... +/// } +/// ``` +/// +/// # Arguments +/// +/// - `"/prefix"` - Raw literal string to be prefixed onto contained handlers' paths. +/// +/// # Example +/// +/// ``` +/// # use actix_web_codegen::{scope, get}; +/// # use actix_web::Responder; +/// #[scope("/api")] +/// mod api { +/// # use super::*; +/// #[get("/hello")] +/// pub async fn hello() -> impl Responder { +/// // this has path /api/hello +/// "Hello, world!" +/// } +/// } +/// # fn main() {} +/// ``` +#[proc_macro_attribute] +pub fn scope(args: TokenStream, input: TokenStream) -> TokenStream { + scope::with_scope(args, input) +} + /// Marks async main function as the Actix Web system entry-point. /// /// Note that Actix Web also works under `#[tokio::main]` since version 4.0. However, this macro is @@ -240,3 +278,15 @@ pub fn test(_: TokenStream, item: TokenStream) -> TokenStream { output.extend(item); output } + +/// 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/src/route.rs b/actix-web-codegen/src/route.rs index 525a1c8ba..e24903e3a 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -6,10 +6,12 @@ use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token}; +use crate::input_and_compile_error; + #[derive(Debug)] pub struct RouteArgs { - path: syn::LitStr, - options: Punctuated, + pub(crate) path: syn::LitStr, + pub(crate) options: Punctuated, } impl syn::parse::Parse for RouteArgs { @@ -78,7 +80,7 @@ macro_rules! standard_method_type { } } - fn from_path(method: &Path) -> Result { + pub(crate) fn from_path(method: &Path) -> Result { match () { $(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+ _ => Err(()), @@ -224,7 +226,7 @@ struct Args { path: syn::LitStr, resource_name: Option, guards: Vec, - wrappers: Vec, + wrappers: Vec, methods: HashSet, } @@ -251,7 +253,7 @@ impl Args { } else { return Err(syn::Error::new_spanned( nv.value, - "Attribute name expects literal string!", + "Attribute name expects literal string", )); } } else if nv.path.is_ident("guard") { @@ -264,7 +266,7 @@ impl Args { } else { return Err(syn::Error::new_spanned( nv.value, - "Attribute guard expects literal string!", + "Attribute guard expects literal string", )); } } else if nv.path.is_ident("wrap") { @@ -283,9 +285,9 @@ impl Args { } else if nv.path.is_ident("method") { if !is_route_macro { return Err(syn::Error::new_spanned( - &nv, - "HTTP method forbidden here. To handle multiple methods, use `route` instead", - )); + &nv, + "HTTP method forbidden here; to handle multiple methods, use `route` instead", + )); } else if let syn::Expr::Lit(syn::ExprLit { lit: syn::Lit::Str(lit), .. @@ -300,13 +302,13 @@ impl Args { } else { return Err(syn::Error::new_spanned( nv.value, - "Attribute method expects literal string!", + "Attribute method expects literal string", )); } } else { return Err(syn::Error::new_spanned( nv.path, - "Unknown attribute key is specified. Allowed: guard, method and wrap", + "Unknown attribute key is specified; allowed: guard, method and wrap", )); } } @@ -411,6 +413,13 @@ impl ToTokens for Route { doc_attributes, } = self; + #[allow(unused_variables)] // used when force-pub feature is disabled + let vis = &ast.vis; + + // TODO(breaking): remove this force-pub forwards-compatibility feature + #[cfg(feature = "compat-routing-macros-force-pub")] + let vis = syn::Visibility::Public(::default()); + let registrations: TokenStream2 = args .iter() .map(|args| { @@ -458,7 +467,7 @@ impl ToTokens for Route { let stream = quote! { #(#doc_attributes)* #[allow(non_camel_case_types, missing_docs)] - pub struct #name; + #vis struct #name; impl ::actix_web::dev::HttpServiceFactory for #name { fn register(self, __config: &mut actix_web::dev::AppService) { @@ -542,15 +551,3 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream { 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/src/scope.rs b/actix-web-codegen/src/scope.rs new file mode 100644 index 000000000..067d95a60 --- /dev/null +++ b/actix-web-codegen/src/scope.rs @@ -0,0 +1,103 @@ +use proc_macro::TokenStream; +use proc_macro2::{Span, TokenStream as TokenStream2}; +use quote::{quote, ToTokens as _}; + +use crate::{ + input_and_compile_error, + route::{MethodType, RouteArgs}, +}; + +pub fn with_scope(args: TokenStream, input: TokenStream) -> TokenStream { + match with_scope_inner(args, input.clone()) { + Ok(stream) => stream, + Err(err) => input_and_compile_error(input, err), + } +} + +fn with_scope_inner(args: TokenStream, input: TokenStream) -> syn::Result { + if args.is_empty() { + return Err(syn::Error::new( + Span::call_site(), + "missing arguments for scope macro, expected: #[scope(\"/prefix\")]", + )); + } + + let scope_prefix = syn::parse::(args.clone()).map_err(|err| { + syn::Error::new( + err.span(), + "argument to scope macro is not a string literal, expected: #[scope(\"/prefix\")]", + ) + })?; + + let scope_prefix_value = scope_prefix.value(); + + if scope_prefix_value.ends_with('/') { + // trailing slashes cause non-obvious problems + // it's better to point them out to developers rather than + + return Err(syn::Error::new( + scope_prefix.span(), + "scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes", + )); + } + + let mut module = syn::parse::(input).map_err(|err| { + syn::Error::new(err.span(), "#[scope] macro must be attached to a module") + })?; + + // modify any routing macros (method or route[s]) attached to + // functions by prefixing them with this scope macro's argument + if let Some((_, items)) = &mut module.content { + for item in items { + if let syn::Item::Fn(fun) = item { + fun.attrs = fun + .attrs + .iter() + .map(|attr| modify_attribute_with_scope(attr, &scope_prefix_value)) + .collect(); + } + } + } + + Ok(module.to_token_stream().into()) +} + +/// Checks if the attribute is a method type and has a route path, then modifies it. +fn modify_attribute_with_scope(attr: &syn::Attribute, scope_path: &str) -> syn::Attribute { + match (attr.parse_args::(), attr.clone().meta) { + (Ok(route_args), syn::Meta::List(meta_list)) if has_allowed_methods_in_scope(attr) => { + let modified_path = format!("{}{}", scope_path, route_args.path.value()); + + let options_tokens: Vec = route_args + .options + .iter() + .map(|option| { + quote! { ,#option } + }) + .collect(); + + let combined_options_tokens: TokenStream2 = + options_tokens + .into_iter() + .fold(TokenStream2::new(), |mut acc, ts| { + acc.extend(std::iter::once(ts)); + acc + }); + + syn::Attribute { + meta: syn::Meta::List(syn::MetaList { + tokens: quote! { #modified_path #combined_options_tokens }, + ..meta_list.clone() + }), + ..attr.clone() + } + } + _ => attr.clone(), + } +} + +fn has_allowed_methods_in_scope(attr: &syn::Attribute) -> bool { + MethodType::from_path(attr.path()).is_ok() + || attr.path().is_ident("route") + || attr.path().is_ident("ROUTE") +} diff --git a/actix-web-codegen/tests/test_macro.rs b/actix-web-codegen/tests/routes.rs similarity index 97% rename from actix-web-codegen/tests/test_macro.rs rename to actix-web-codegen/tests/routes.rs index f28654cd9..fb50d4ae0 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/routes.rs @@ -212,6 +212,19 @@ async fn get_wrap(_: web::Path) -> impl Responder { HttpResponse::Ok() } +/// Using expression, not just path to type, in wrap attribute. +/// +/// Regression from . +#[route( + "/catalog", + method = "GET", + method = "HEAD", + wrap = "actix_web::middleware::Compress::default()" +)] +async fn get_catalog() -> impl Responder { + HttpResponse::Ok().body("123123123") +} + #[actix_rt::test] async fn test_params() { let srv = actix_test::start(|| { diff --git a/actix-web-codegen/tests/scopes.rs b/actix-web-codegen/tests/scopes.rs new file mode 100644 index 000000000..4ee6db16f --- /dev/null +++ b/actix-web-codegen/tests/scopes.rs @@ -0,0 +1,200 @@ +use actix_web::{guard::GuardContext, http, http::header, web, App, HttpResponse, Responder}; +use actix_web_codegen::{delete, get, post, route, routes, scope}; + +pub fn image_guard(ctx: &GuardContext) -> bool { + ctx.header::() + .map(|h| h.preference() == "image/*") + .unwrap_or(false) +} + +#[scope("/test")] +mod scope_module { + // ensure that imports can be brought into the scope + use super::*; + + #[get("/test/guard", guard = "image_guard")] + pub async fn guard() -> impl Responder { + HttpResponse::Ok() + } + + #[get("/test")] + pub async fn test() -> impl Responder { + HttpResponse::Ok().finish() + } + + #[get("/twice-test/{value}")] + pub async fn twice(value: web::Path) -> impl actix_web::Responder { + let int_value: i32 = value.parse().unwrap_or(0); + let doubled = int_value * 2; + HttpResponse::Ok().body(format!("Twice value: {}", doubled)) + } + + #[post("/test")] + pub async fn post() -> impl Responder { + HttpResponse::Ok().body("post works") + } + + #[delete("/test")] + pub async fn delete() -> impl Responder { + "delete works" + } + + #[route("/test", method = "PUT", method = "PATCH", method = "CUSTOM")] + pub async fn multiple_shared_path() -> impl Responder { + HttpResponse::Ok().finish() + } + + #[routes] + #[head("/test1")] + #[connect("/test2")] + #[options("/test3")] + #[trace("/test4")] + pub async fn multiple_separate_paths() -> impl Responder { + HttpResponse::Ok().finish() + } + + // test calling this from other mod scope with scope attribute... + pub fn mod_common(message: String) -> impl actix_web::Responder { + HttpResponse::Ok().body(message) + } +} + +/// Scope doc string to check in cargo expand. +#[scope("/v1")] +mod mod_scope_v1 { + use super::*; + + /// Route doc string to check in cargo expand. + #[get("/test")] + pub async fn test() -> impl Responder { + scope_module::mod_common("version1 works".to_string()) + } +} + +#[scope("/v2")] +mod mod_scope_v2 { + use super::*; + + // check to make sure non-function tokens in the scope block are preserved... + enum TestEnum { + Works, + } + + #[get("/test")] + pub async fn test() -> impl Responder { + // make sure this type still exists... + let test_enum = TestEnum::Works; + + match test_enum { + TestEnum::Works => scope_module::mod_common("version2 works".to_string()), + } + } +} + +#[actix_rt::test] +async fn scope_get_async() { + let srv = actix_test::start(|| App::new().service(scope_module::test)); + + let request = srv.request(http::Method::GET, srv.url("/test/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} + +#[actix_rt::test] +async fn scope_get_param_async() { + let srv = actix_test::start(|| App::new().service(scope_module::twice)); + + let request = srv.request(http::Method::GET, srv.url("/test/twice-test/4")); + let mut response = request.send().await.unwrap(); + let body = response.body().await.unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, "Twice value: 8"); +} + +#[actix_rt::test] +async fn scope_post_async() { + let srv = actix_test::start(|| App::new().service(scope_module::post)); + + let request = srv.request(http::Method::POST, srv.url("/test/test")); + let mut response = request.send().await.unwrap(); + let body = response.body().await.unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, "post works"); +} + +#[actix_rt::test] +async fn multiple_shared_path_async() { + let srv = actix_test::start(|| App::new().service(scope_module::multiple_shared_path)); + + let request = srv.request(http::Method::PUT, srv.url("/test/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::PATCH, srv.url("/test/test")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} + +#[actix_rt::test] +async fn multiple_multi_path_async() { + let srv = actix_test::start(|| App::new().service(scope_module::multiple_separate_paths)); + + let request = srv.request(http::Method::HEAD, srv.url("/test/test1")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::CONNECT, srv.url("/test/test2")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::OPTIONS, srv.url("/test/test3")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(http::Method::TRACE, srv.url("/test/test4")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} + +#[actix_rt::test] +async fn scope_delete_async() { + let srv = actix_test::start(|| App::new().service(scope_module::delete)); + + let request = srv.request(http::Method::DELETE, srv.url("/test/test")); + let mut response = request.send().await.unwrap(); + let body = response.body().await.unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, "delete works"); +} + +#[actix_rt::test] +async fn scope_get_with_guard_async() { + let srv = actix_test::start(|| App::new().service(scope_module::guard)); + + let request = srv + .request(http::Method::GET, srv.url("/test/test/guard")) + .insert_header(("Accept", "image/*")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} + +#[actix_rt::test] +async fn scope_v1_v2_async() { + let srv = actix_test::start(|| { + App::new() + .service(mod_scope_v1::test) + .service(mod_scope_v2::test) + }); + + let request = srv.request(http::Method::GET, srv.url("/v1/test")); + let mut response = request.send().await.unwrap(); + let body = response.body().await.unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, "version1 works"); + + let request = srv.request(http::Method::GET, srv.url("/v2/test")); + let mut response = request.send().await.unwrap(); + let body = response.body().await.unwrap(); + let body_str = String::from_utf8(body.to_vec()).unwrap(); + assert_eq!(body_str, "version2 works"); +} diff --git a/actix-web-codegen/tests/trybuild.rs b/actix-web-codegen/tests/trybuild.rs index 8e1f58a4c..91073cf3b 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.68)] // MSRV +#[rustversion::stable(1.72)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); @@ -18,6 +18,11 @@ fn compile_macros() { t.compile_fail("tests/trybuild/routes-missing-method-fail.rs"); t.compile_fail("tests/trybuild/routes-missing-args-fail.rs"); + t.compile_fail("tests/trybuild/scope-on-handler.rs"); + t.compile_fail("tests/trybuild/scope-missing-args.rs"); + t.compile_fail("tests/trybuild/scope-invalid-args.rs"); + t.compile_fail("tests/trybuild/scope-trailing-slash.rs"); + t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/test-runtime.rs"); diff --git a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr index 88198a55d..c2a51d005 100644 --- a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr +++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr @@ -13,17 +13,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future | required by a bound introduced by this call | = help: the following other types implement trait `HttpServiceFactory`: + Resource + actix_web::Scope + Vec + Redirect + (A,) (A, B) (A, B, C) (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) - (A, B, C, D, E, F, G) - (A, B, C, D, E, F, G, H) - (A, B, C, D, E, F, G, H, I) and $N others note: required by a bound in `App::::service` --> $WORKSPACE/actix-web/src/app.rs | + | pub fn service(mut self, factory: F) -> Self + | ------- required by a bound in this associated function + | where | F: HttpServiceFactory + 'static, | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` 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 bda736348..ae18f347f 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -13,17 +13,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future | required by a bound introduced by this call | = help: the following other types implement trait `HttpServiceFactory`: + Resource + actix_web::Scope + Vec + Redirect + (A,) (A, B) (A, B, C) (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) - (A, B, C, D, E, F, G) - (A, B, C, D, E, F, G, H) - (A, B, C, D, E, F, G, H, I) and $N others note: required by a bound in `App::::service` --> $WORKSPACE/actix-web/src/app.rs | + | pub fn service(mut self, factory: F) -> Self + | ------- required by a bound in this associated function + | where | F: HttpServiceFactory + 'static, | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr index 93c510109..c1100c784 100644 --- a/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-malformed-path-fail.stderr @@ -20,10 +20,7 @@ error: custom attribute panicked 13 | #[get("/{}")] | ^^^^^^^^^^^^^ | - = help: message: Wrong path pattern: "/{}" regex parse error: - ((?s-m)^/(?P<>[^/]+))$ - ^ - error: empty capture group name + = help: message: Wrong path pattern: "/{}" empty capture group names are not allowed error: custom attribute panicked --> $DIR/route-malformed-path-fail.rs:23:1 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 9f2f788fb..37d8354c9 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -15,17 +15,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future | required by a bound introduced by this call | = help: the following other types implement trait `HttpServiceFactory`: + Resource + actix_web::Scope + Vec + Redirect + (A,) (A, B) (A, B, C) (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) - (A, B, C, D, E, F, G) - (A, B, C, D, E, F, G, H) - (A, B, C, D, E, F, G, H, I) and $N others note: required by a bound in `App::::service` --> $WORKSPACE/actix-web/src/app.rs | + | pub fn service(mut self, factory: F) -> Self + | ------- required by a bound in this associated function + | where | F: HttpServiceFactory + 'static, | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr index 2e84c296a..40b19fc77 100644 --- a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr +++ b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr @@ -29,17 +29,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future | required by a bound introduced by this call | = help: the following other types implement trait `HttpServiceFactory`: + Resource + actix_web::Scope + Vec + Redirect + (A,) (A, B) (A, B, C) (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) - (A, B, C, D, E, F, G) - (A, B, C, D, E, F, G, H) - (A, B, C, D, E, F, G, H, I) and $N others note: required by a bound in `App::::service` --> $WORKSPACE/actix-web/src/app.rs | + | pub fn service(mut self, factory: F) -> Self + | ------- required by a bound in this associated function + | where | F: HttpServiceFactory + 'static, | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr index 228dced9c..ff7f00b3b 100644 --- a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr @@ -15,17 +15,20 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future | required by a bound introduced by this call | = help: the following other types implement trait `HttpServiceFactory`: + Resource + actix_web::Scope + Vec + Redirect + (A,) (A, B) (A, B, C) (A, B, C, D) - (A, B, C, D, E) - (A, B, C, D, E, F) - (A, B, C, D, E, F, G) - (A, B, C, D, E, F, G, H) - (A, B, C, D, E, F, G, H, I) and $N others note: required by a bound in `App::::service` --> $WORKSPACE/actix-web/src/app.rs | + | pub fn service(mut self, factory: F) -> Self + | ------- required by a bound in this associated function + | where | F: HttpServiceFactory + 'static, | ^^^^^^^^^^^^^^^^^^ required by this bound in `App::::service` diff --git a/actix-web-codegen/tests/trybuild/scope-invalid-args.rs b/actix-web-codegen/tests/trybuild/scope-invalid-args.rs new file mode 100644 index 000000000..ec021d5eb --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-invalid-args.rs @@ -0,0 +1,14 @@ +use actix_web_codegen::scope; + +const PATH: &str = "/api"; + +#[scope(PATH)] +mod api_const {} + +#[scope(true)] +mod api_bool {} + +#[scope(123)] +mod api_num {} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/scope-invalid-args.stderr b/actix-web-codegen/tests/trybuild/scope-invalid-args.stderr new file mode 100644 index 000000000..0ab335966 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-invalid-args.stderr @@ -0,0 +1,17 @@ +error: argument to scope macro is not a string literal, expected: #[scope("/prefix")] + --> tests/trybuild/scope-invalid-args.rs:5:9 + | +5 | #[scope(PATH)] + | ^^^^ + +error: argument to scope macro is not a string literal, expected: #[scope("/prefix")] + --> tests/trybuild/scope-invalid-args.rs:8:9 + | +8 | #[scope(true)] + | ^^^^ + +error: argument to scope macro is not a string literal, expected: #[scope("/prefix")] + --> tests/trybuild/scope-invalid-args.rs:11:9 + | +11 | #[scope(123)] + | ^^^ diff --git a/actix-web-codegen/tests/trybuild/scope-missing-args.rs b/actix-web-codegen/tests/trybuild/scope-missing-args.rs new file mode 100644 index 000000000..39bcb9d1a --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-missing-args.rs @@ -0,0 +1,6 @@ +use actix_web_codegen::scope; + +#[scope] +mod api {} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/scope-missing-args.stderr b/actix-web-codegen/tests/trybuild/scope-missing-args.stderr new file mode 100644 index 000000000..d59842e39 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-missing-args.stderr @@ -0,0 +1,7 @@ +error: missing arguments for scope macro, expected: #[scope("/prefix")] + --> tests/trybuild/scope-missing-args.rs:3:1 + | +3 | #[scope] + | ^^^^^^^^ + | + = note: this error originates in the attribute macro `scope` (in Nightly builds, run with -Z macro-backtrace for more info) diff --git a/actix-web-codegen/tests/trybuild/scope-on-handler.rs b/actix-web-codegen/tests/trybuild/scope-on-handler.rs new file mode 100644 index 000000000..e5d478981 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-on-handler.rs @@ -0,0 +1,8 @@ +use actix_web_codegen::scope; + +#[scope("/api")] +async fn index() -> &'static str { + "Hello World!" +} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/scope-on-handler.stderr b/actix-web-codegen/tests/trybuild/scope-on-handler.stderr new file mode 100644 index 000000000..4491f42dd --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-on-handler.stderr @@ -0,0 +1,5 @@ +error: #[scope] macro must be attached to a module + --> tests/trybuild/scope-on-handler.rs:4:1 + | +4 | async fn index() -> &'static str { + | ^^^^^ diff --git a/actix-web-codegen/tests/trybuild/scope-trailing-slash.rs b/actix-web-codegen/tests/trybuild/scope-trailing-slash.rs new file mode 100644 index 000000000..84632b59f --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-trailing-slash.rs @@ -0,0 +1,6 @@ +use actix_web_codegen::scope; + +#[scope("/api/")] +mod api {} + +fn main() {} diff --git a/actix-web-codegen/tests/trybuild/scope-trailing-slash.stderr b/actix-web-codegen/tests/trybuild/scope-trailing-slash.stderr new file mode 100644 index 000000000..66933432e --- /dev/null +++ b/actix-web-codegen/tests/trybuild/scope-trailing-slash.stderr @@ -0,0 +1,5 @@ +error: scopes should not have trailing slashes; see https://docs.rs/actix-web/4/actix_web/struct.Scope.html#avoid-trailing-slashes + --> tests/trybuild/scope-trailing-slash.rs:3:9 + | +3 | #[scope("/api/")] + | ^^^^^^^ diff --git a/actix-web-codegen/tests/trybuild/simple-fail.stderr b/actix-web-codegen/tests/trybuild/simple-fail.stderr index 3b3f9d850..ab81599ed 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.stderr +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -38,7 +38,7 @@ error: Multiple paths specified! There should be only one. | = note: this error originates in the attribute macro `delete` (in Nightly builds, run with -Z macro-backtrace for more info) -error: HTTP method forbidden here. To handle multiple methods, use `route` instead +error: HTTP method forbidden here; to handle multiple methods, use `route` instead --> $DIR/simple-fail.rs:25:19 | 25 | #[delete("/five", method="GET")] diff --git a/actix-web/CHANGES.md b/actix-web/CHANGES.md index f4e2f7863..98cf54ac3 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -1,15 +1,83 @@ # Changelog -## Unreleased - 2023-xx-xx +## Unreleased ### Added -- Add `HttpServer::{bind, listen}_auto_h2c()` method behind new `http2` crate feature. +- Add `HttpRequest::full_uri()` method to get the full uri of an incoming request. + +### Fixed + +- `ConnectionInfo::realip_remote_addr()` now handles IPv6 addresses from `Forwarded` header correctly. Previously, it sometimes returned the forwarded port as well. +- The `UrlencodedError::ContentType` variant (relevant to the `Form` extractor) now uses the 415 (Media Type Unsupported) status code in it's `ResponseError` implementation. + +## 4.7.0 + +### Added + +- Add `#[scope]` macro. +- Add `middleware::Identity` type. +- Add `CustomizeResponder::add_cookie()` method. +- Add `guard::GuardContext::app_data()` method. +- Add `compat-routing-macros-force-pub` crate feature which (on-by-default) which, when disabled, causes handlers to inherit their attached function's visibility. +- Add `compat` crate feature group (on-by-default) which, when disabled, helps with transitioning to some planned v5.0 breaking changes, starting only with `compat-routing-macros-force-pub`. +- Implement `From>` for `Error`. + +## 4.6.0 + +### Added + +- Add `unicode` crate feature (on-by-default) to switch between `regex` and `regex-lite` as a trade-off between full unicode support and binary size. +- Add `rustls-0_23` crate feature. +- Add `HttpServer::{bind_rustls_0_23, listen_rustls_0_23}()` builder methods. +- Add `HttpServer::tls_handshake_timeout()` builder method for `rustls-0_22` and `rustls-0_23`. + +### Changed + +- Update `brotli` dependency to `6`. +- Minimum supported Rust version (MSRV) is now 1.72. + +### Fixed + +- Avoid type confusion with `rustls` in some circumstances. + +## 4.5.1 + +### Fixed + +- Fix missing import when using enabling Rustls v0.22 support. + +## 4.5.0 + +### Added + +- Add `rustls-0_22` crate feature. +- Add `HttpServer::{bind_rustls_0_22, listen_rustls_0_22}()` builder methods. + +## 4.4.1 + +### Changed + +- Updated `zstd` dependency to `0.13`. +- Compression middleware now prefers brotli over zstd over gzip. + +### Fixed + +- Fix validation of `Json` extractor when `JsonConfig::validate_content_type()` is set to false. + +## 4.4.0 + +### Added + +- Add `HttpServer::{bind, listen}_auto_h2c()` methods behind new `http2` crate feature. +- Add `HttpServer::{bind, listen}_rustls_021()` methods for Rustls v0.21 support behind new `rustls-0_21` crate feature. - Add `Resource::{get, post, etc...}` methods for more concisely adding routes that don't need additional guards. - Add `web::Payload::to_bytes[_limited]()` helper methods. - Add missing constructors on `HttpResponse` for several status codes. - Add `http::header::ContentLength` typed header. -- Add `HttpRequest::full_uri()` method to get the full uri of an incoming request. +- Implement `Default` for `web::Data`. +- Implement `serde::Deserialize` for `web::Data`. +- Add `rustls-0_20` crate feature, which the existing `rustls` feature now aliases. ### Changed @@ -18,7 +86,7 @@ - Hide sensitive header values in `HttpRequest`'s `Debug` output. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 4.3.1 - 2023-02-26 +## 4.3.1 ### Added @@ -26,7 +94,7 @@ [#2969]: https://github.com/actix/actix-web/pull/2969 -## 4.3.0 - 2023-01-21 +## 4.3.0 ### Added @@ -49,7 +117,7 @@ [#2949]: https://github.com/actix/actix-web/pull/2949 [#2961]: https://github.com/actix/actix-web/pull/2961 -## 4.2.1 - 2022-09-12 +## 4.2.1 ### Fixed @@ -57,7 +125,7 @@ [#2871]: https://github.com/actix/actix-web/pull/2871 -## 4.2.0 - 2022-09-11 +## 4.2.0 ### Added @@ -73,7 +141,7 @@ [#2752]: https://github.com/actix/actix-web/pull/2752 [#2786]: https://github.com/actix/actix-web/pull/2786 -## 4.1.0 - 2022-06-11 +## 4.1.0 ### Added @@ -96,13 +164,13 @@ [#2742]: https://github.com/actix/actix-web/pull/2742 [#2743]: https://github.com/actix/actix-web/pull/2743 -## 4.0.1 - 2022-02-25 +## 4.0.1 ### Fixed - Use stable version in readme example. -## 4.0.0 - 2022-02-25 +## 4.0.0 ### Dependencies @@ -380,7 +448,7 @@
4.0.0 Pre-Releases -## 4.0.0-rc.3 - 2022-02-08 +## 4.0.0-rc.3 ### Changed @@ -394,7 +462,7 @@ [#2625]: https://github.com/actix/actix-web/pull/2625 [#2635]: https://github.com/actix/actix-web/pull/2635 -## 4.0.0-rc.2 - 2022-02-02 +## 4.0.0-rc.2 ### Added @@ -406,7 +474,7 @@ [#2619]: https://github.com/actix/actix-web/pull/2619 -## 4.0.0-rc.1 - 2022-01-31 +## 4.0.0-rc.1 ### Changed @@ -420,7 +488,7 @@ [#2601]: https://github.com/actix/actix-web/pull/2601 [#2611]: https://github.com/actix/actix-web/pull/2611 -## 4.0.0-beta.21 - 2022-01-21 +## 4.0.0-beta.21 ### Added @@ -437,7 +505,7 @@ [#2591]: https://github.com/actix/actix-web/pull/2591 [#2594]: https://github.com/actix/actix-web/pull/2594 -## 4.0.0-beta.20 - 2022-01-14 +## 4.0.0-beta.20 ### Added @@ -459,7 +527,7 @@ [#2582]: https://github.com/actix/actix-web/pull/2582 [#2584]: https://github.com/actix/actix-web/pull/2584 -## 4.0.0-beta.19 - 2022-01-04 +## 4.0.0-beta.19 ### Added @@ -484,7 +552,7 @@ [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 -## 4.0.0-beta.18 - 2021-12-29 +## 4.0.0-beta.18 ### Changed @@ -498,7 +566,7 @@ [#2555]: https://github.com/actix/actix-web/pull/2555 [`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html -## 4.0.0-beta.17 - 2021-12-29 +## 4.0.0-beta.17 ### Added @@ -521,7 +589,7 @@ [#2552]: https://github.com/actix/actix-web/pull/2552 [#2554]: https://github.com/actix/actix-web/pull/2554 -## 4.0.0-beta.16 - 2021-12-27 +## 4.0.0-beta.16 ### Changed @@ -531,7 +599,7 @@ [#2523]: https://github.com/actix/actix-web/pull/2523 [#2526]: https://github.com/actix/actix-web/pull/2526 -## 4.0.0-beta.15 - 2021-12-17 +## 4.0.0-beta.15 ### Added @@ -559,7 +627,7 @@ [#2516]: https://github.com/actix/actix-web/pull/2516 [#2518]: https://github.com/actix/actix-web/pull/2518 -## 4.0.0-beta.14 - 2021-12-11 +## 4.0.0-beta.14 ### Added @@ -604,7 +672,7 @@ [#2493]: https://github.com/actix/actix-web/pull/2493 [#2499]: https://github.com/actix/actix-web/pull/2499 -## 4.0.0-beta.13 - 2021-11-30 +## 4.0.0-beta.13 ### Changed @@ -612,7 +680,7 @@ [#2474]: https://github.com/actix/actix-web/pull/2474 -## 4.0.0-beta.12 - 2021-11-22 +## 4.0.0-beta.12 ### Changed @@ -629,7 +697,7 @@ [#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 +## 4.0.0-beta.11 ### Added @@ -643,7 +711,7 @@ [#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 +## 4.0.0-beta.10 ### Added @@ -670,7 +738,7 @@ [#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 +## 4.0.0-beta.9 ### Added @@ -693,7 +761,7 @@ [#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 +## 4.0.0-beta.8 ### Added @@ -720,7 +788,7 @@ [#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 +## 4.0.0-beta.7 ### Added @@ -749,7 +817,7 @@ [#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 +## 4.0.0-beta.6 ### Added @@ -763,7 +831,7 @@ [#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 +## 4.0.0-beta.5 ### Added @@ -789,7 +857,7 @@ [#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 +## 4.0.0-beta.4 ### Changed @@ -799,11 +867,11 @@ [#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 +## 4.0.0-beta.3 - Update `actix-web-codegen` to `0.5.0-beta.1`. -## 4.0.0-beta.2 - 2021-02-10 +## 4.0.0-beta.2 ### Added @@ -841,7 +909,7 @@ [#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 +## 4.0.0-beta.1 ### Added @@ -875,7 +943,7 @@
-## 3.3.3 - 2021-12-18 +## 3.3.3 ### Changed @@ -883,7 +951,7 @@ [#2529]: https://github.com/actix/actix-web/pull/2529 -## 3.3.2 - 2020-12-01 +## 3.3.2 ### Fixed @@ -895,11 +963,11 @@ [#1798]: https://github.com/actix/actix-web/pull/1798 [#1803]: https://github.com/actix/actix-web/pull/1803 -## 3.3.1 - 2020-11-29 +## 3.3.1 - Ensure `actix-http` dependency uses same `serde_urlencoded`. -## 3.3.0 - 2020-11-25 +## 3.3.0 ### Added @@ -912,7 +980,7 @@ [#1773]: https://github.com/actix/actix-web/pull/1773 [#1788]: https://github.com/actix/actix-web/pull/1788 -## 3.2.0 - 2020-10-30 +## 3.2.0 ### Added @@ -937,7 +1005,7 @@ [#1757]: https://github.com/actix/actix-web/pull/1757 [#1749]: https://github.com/actix/actix-web/pull/1749 -## 3.1.0 - 2020-09-29 +## 3.1.0 ### Changed @@ -952,7 +1020,7 @@ [#1708]: https://github.com/actix/actix-web/pull/1708 [#1710]: https://github.com/actix/actix-web/pull/1710 -## 3.0.2 - 2020-09-15 +## 3.0.2 ### Fixed @@ -960,7 +1028,7 @@ [#1678]: https://github.com/actix/actix-web/pull/1678 -## 3.0.1 - 2020-09-13 +## 3.0.1 ### Changed @@ -968,11 +1036,11 @@ [#1673]: https://github.com/actix/actix-web/pull/1673 -## 3.0.0 - 2020-09-11 +## 3.0.0 - No significant changes from `3.0.0-beta.4`. -## 3.0.0-beta.4 - 2020-09-09 +## 3.0.0-beta.4 ### Added @@ -990,13 +1058,13 @@ [#1634]: https://github.com/actix/actix-web/pull/1634 [#1655]: https://github.com/actix/actix-web/pull/1655 -## 3.0.0-beta.3 - 2020-08-17 +## 3.0.0-beta.3 ### Changed - Update `rustls` to 0.18 -## 3.0.0-beta.2 - 2020-08-17 +## 3.0.0-beta.2 ### Changed @@ -1016,7 +1084,7 @@ [#1618]: https://github.com/actix/actix-web/pull/1618 [#1621]: https://github.com/actix/actix-web/pull/1621 -## 3.0.0-beta.1 - 2020-07-13 +## 3.0.0-beta.1 ### Added @@ -1034,7 +1102,7 @@ - `NormalizePath` improved consistency when path needs slashes added _and_ removed. -## 3.0.0-alpha.3 - 2020-05-21 +## 3.0.0-alpha.3 ### Added @@ -1050,7 +1118,7 @@ [#1485]: https://github.com/actix/actix-web/pull/1485 [#1509]: https://github.com/actix/actix-web/pull/1509 -## [3.0.0-alpha.2] - 2020-05-08 +## 3.0.0-alpha.2 ### Changed @@ -1064,7 +1132,7 @@ [#1452]: https://github.com/actix/actix-web/pull/1452 [#1486]: https://github.com/actix/actix-web/pull/1486 -## [3.0.0-alpha.1] - 2020-03-11 +## 3.0.0-alpha.1 ### Added @@ -1081,7 +1149,7 @@ [#1308]: https://github.com/actix/actix-web/pull/1308 -## [2.0.0] - 2019-12-25 +## 2.0.0 ### Changed @@ -1091,7 +1159,7 @@ - Allow to specify multi-patterns for resources -## [2.0.0-rc] - 2019-12-20 +## 2.0.0-rc ### Changed @@ -1109,31 +1177,31 @@ - Fix `AppConfig::secure()` is always false. #1202 -## [2.0.0-alpha.6] - 2019-12-15 +## 2.0.0-alpha.6 ### Fixed - Fixed compilation with default features off -## [2.0.0-alpha.5] - 2019-12-13 +## 2.0.0-alpha.5 ### Added - Add test server, `test::start()` and `test::start_with()` -## [2.0.0-alpha.4] - 2019-12-08 +## 2.0.0-alpha.4 ### Deleted - Delete HttpServer::run(), it is not useful with async/await -## [2.0.0-alpha.3] - 2019-12-07 +## 2.0.0-alpha.3 ### Changed - Migrate to tokio 0.2 -## [2.0.0-alpha.1] - 2019-11-22 +## 2.0.0-alpha.1 ### Changed @@ -1141,7 +1209,7 @@ - Remove implementation of `Responder` for `()`. (#1167) -## [1.0.9] - 2019-11-14 +## 1.0.9 ### Added @@ -1151,7 +1219,7 @@ - Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129) -## [1.0.8] - 2019-09-25 +## 1.0.8 ### Added @@ -1169,13 +1237,13 @@ - Use actix-testing for testing utils -## [1.0.7] - 2019-08-29 +## 1.0.7 ### Fixed - Request Extensions leak #1062 -## [1.0.6] - 2019-08-28 +## 1.0.6 ### Added @@ -1197,7 +1265,7 @@ - Update url to 2.1 -## [1.0.5] - 2019-07-18 +## 1.0.5 ### Added @@ -1209,7 +1277,7 @@ - Restored logging of errors through the `Logger` middleware -## [1.0.4] - 2019-07-17 +## 1.0.4 ### Added @@ -1221,7 +1289,7 @@ - Upgrade `rand` dependency version to 0.7 -## [1.0.3] - 2019-06-28 +## 1.0.3 ### Added @@ -1231,7 +1299,7 @@ - Use `encoding_rs` crate instead of unmaintained `encoding` crate -## [1.0.2] - 2019-06-17 +## 1.0.2 ### Changed @@ -1239,7 +1307,7 @@ - Move identity middleware to `actix-identity` crate. -## [1.0.1] - 2019-06-17 +## 1.0.1 ### Added @@ -1263,7 +1331,7 @@ - HttpRequest::url_for is broken with nested scopes #915 -## [1.0.0] - 2019-06-05 +## 1.0.0 ### Added @@ -1285,7 +1353,7 @@ - Clear http requests pool on app service drop #860 -## [1.0.0-rc] - 2019-05-18 +## 1.0.0-rc ### Added @@ -1300,7 +1368,7 @@ - Codegen with parameters in the path only resolves the first registered endpoint #841 -## [1.0.0-beta.4] - 2019-05-12 +## 1.0.0-beta.4 ### Added @@ -1311,7 +1379,7 @@ - `App::configure` take an `FnOnce` instead of `Fn` - Upgrade actix-net crates -## [1.0.0-beta.3] - 2019-05-04 +## 1.0.0-beta.3 ### Added @@ -1335,7 +1403,7 @@ - `App::data_factory()` is deleted. -## [1.0.0-beta.2] - 2019-04-24 +## 1.0.0-beta.2 ### Added @@ -1357,7 +1425,7 @@ - Fix async web::Data factory handling -## [1.0.0-beta.1] - 2019-04-20 +## 1.0.0-beta.1 ### Added @@ -1381,7 +1449,7 @@ - Fixed `TestRequest::app_data()` -## [1.0.0-alpha.6] - 2019-04-14 +## 1.0.0-alpha.6 ### Changed @@ -1393,7 +1461,7 @@ - Make extractor config type explicit. Add `FromRequest::Config` associated type. -## [1.0.0-alpha.5] - 2019-04-12 +## 1.0.0-alpha.5 ### Added @@ -1403,7 +1471,7 @@ - Removed native-tls support -## [1.0.0-alpha.4] - 2019-04-08 +## 1.0.0-alpha.4 ### Added @@ -1425,7 +1493,7 @@ - Fix body propagation in Response::from_error. #760 -## [1.0.0-alpha.3] - 2019-04-02 +## 1.0.0-alpha.3 ### Changed @@ -1439,7 +1507,7 @@ - Removed unused `actix_web::web::md()` -## [1.0.0-alpha.2] - 2019-03-29 +## 1.0.0-alpha.2 ### Added @@ -1451,7 +1519,7 @@ - Multipart::Field renamed to MultipartField -## [1.0.0-alpha.1] - 2019-03-28 +## 1.0.0-alpha.1 ### Changed diff --git a/actix-web/Cargo.toml b/actix-web/Cargo.toml index 4322fb871..10a507680 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "actix-web" -version = "4.3.1" +version = "4.7.0" +description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" authors = [ "Nikolay Kim ", "Rob Ede ", ] -description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" keywords = ["actix", "http", "web", "framework", "async"] categories = [ "network-programming", @@ -14,21 +14,42 @@ categories = [ "web-programming::websocket" ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -license = "MIT OR Apache-2.0" -edition = "2021" +repository = "https://github.com/actix/actix-web" +license.workspace = true +edition.workspace = true +rust-version.workspace = true [package.metadata.docs.rs] -# features that docs.rs will build with -features = ["macros", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] rustdoc-args = ["--cfg", "docsrs"] +features = [ + "macros", + "openssl", + "rustls-0_20", + "rustls-0_21", + "rustls-0_22", + "rustls-0_23", + "compress-brotli", + "compress-gzip", + "compress-zstd", + "cookies", + "secure-cookies", +] [lib] name = "actix_web" path = "src/lib.rs" [features] -default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "http2"] +default = [ + "macros", + "compress-brotli", + "compress-gzip", + "compress-zstd", + "cookies", + "http2", + "unicode", + "compat", +] # Brotli algorithm content-encoding support compress-brotli = ["actix-http/compress-brotli", "__compress"] @@ -38,21 +59,33 @@ compress-gzip = ["actix-http/compress-gzip", "__compress"] compress-zstd = ["actix-http/compress-zstd", "__compress"] # Routing and runtime proc macros -macros = ["actix-macros", "actix-web-codegen"] +macros = ["dep:actix-macros", "dep:actix-web-codegen"] # Cookies support -cookies = ["cookie"] +cookies = ["dep:cookie"] # Secure & signed cookies secure-cookies = ["cookies", "cookie/secure"] +# HTTP/2 support (including h2c). http2 = ["actix-http/http2"] # TLS via OpenSSL openssl = ["http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] -# TLS via Rustls -rustls = ["http2", "actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] +# TLS via Rustls v0.20 +rustls = ["rustls-0_20"] +# TLS via Rustls v0.20 +rustls-0_20 = ["http2", "actix-http/rustls-0_20", "actix-tls/accept", "actix-tls/rustls-0_20"] +# TLS via Rustls v0.21 +rustls-0_21 = ["http2", "actix-http/rustls-0_21", "actix-tls/accept", "actix-tls/rustls-0_21"] +# TLS via Rustls v0.22 +rustls-0_22 = ["http2", "actix-http/rustls-0_22", "actix-tls/accept", "actix-tls/rustls-0_22"] +# TLS via Rustls v0.23 +rustls-0_23 = ["http2", "actix-http/rustls-0_23", "actix-tls/accept", "actix-tls/rustls-0_23"] + +# Full unicode support +unicode = ["dep:regex", "actix-router/unicode"] # Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. @@ -61,6 +94,14 @@ __compress = [] # io-uring feature only available for Linux OSes. experimental-io-uring = ["actix-server/io-uring"] +# Feature group which, when disabled, helps migrate code to v5.0. +compat = [ + "compat-routing-macros-force-pub", +] + +# Opt-out forwards-compatibility for handler visibility inheritance fix. +compat-routing-macros-force-pub = ["actix-web-codegen?/compat-routing-macros-force-pub"] + [dependencies] actix-codec = "0.5" actix-macros = { version = "0.2.3", optional = true } @@ -68,11 +109,11 @@ actix-rt = { version = "2.6", default-features = false } actix-server = "2" actix-service = "2" actix-utils = "3" -actix-tls = { version = "3", default-features = false, optional = true } +actix-tls = { version = "3.4", default-features = false, optional = true } -actix-http = { version = "3.3", features = ["ws"] } -actix-router = "0.5" -actix-web-codegen = { version = "4.2", optional = true } +actix-http = { version = "3.7", features = ["ws"] } +actix-router = { version = "0.5.3", default-features = false, features = ["http"] } +actix-web-codegen = { version = "4.3", optional = true, default-features = false } ahash = "0.8" bytes = "1" @@ -89,7 +130,8 @@ log = "0.4" mime = "0.3" once_cell = "1.5" pin-project-lite = "0.2.7" -regex = "1.5.5" +regex = { version = "1.5.5", optional = true } +regex-lite = "0.1" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" @@ -100,24 +142,25 @@ url = "2.1" [dev-dependencies] actix-files = "0.6" -actix-test = { version = "0.1", features = ["openssl", "rustls"] } +actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] } awc = { version = "3", features = ["openssl"] } -brotli = "3.3.3" +brotli = "6" const-str = "0.5" +core_affinity = "0.8" criterion = { version = "0.5", features = ["html_reports"] } -env_logger = "0.10" +env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } rand = "0.8" -rcgen = "0.11" -rustls-pemfile = "1" +rcgen = "0.13" +rustls-pemfile = "2" serde = { version = "1.0", features = ["derive"] } static_assertions = "1" tls-openssl = { package = "openssl", version = "0.10.55" } -tls-rustls = { package = "rustls", version = "0.20" } +tls-rustls = { package = "rustls", version = "0.23" } tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } -zstd = "0.12" +zstd = "0.13" [[test]] name = "test_server" diff --git a/actix-web/MIGRATION-4.0.md b/actix-web/MIGRATION-4.0.md index 0f0bd3a53..08c89635a 100644 --- a/actix-web/MIGRATION-4.0.md +++ b/actix-web/MIGRATION-4.0.md @@ -372,13 +372,13 @@ You may need to review the [guidance on shared mutable state](https://docs.rs/ac HttpServer::new(|| { - App::new() - .data(MyState::default()) -- .service(hander) +- .service(handler) + let my_state: Data = Data::new(MyState::default()); + + App::new() + .app_data(my_state) -+ .service(hander) ++ .service(handler) }) ``` diff --git a/actix-web/README.md b/actix-web/README.md index 565f2b0f3..8b4375bdd 100644 --- a/actix-web/README.md +++ b/actix-web/README.md @@ -5,7 +5,20 @@

-[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.3.1)](https://docs.rs/actix-web/4.3.1) ![MSRV](https://img.shields.io/badge/rustc-1.68+-ab6000.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) [![Dependency Status](https://deps.rs/crate/actix-web/4.3.1/status.svg)](https://deps.rs/crate/actix-web/4.3.1)
[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) ![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) + + +[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) +[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.7.0)](https://docs.rs/actix-web/4.7.0) +![MSRV](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) +[![Dependency Status](https://deps.rs/crate/actix-web/4.7.0/status.svg)](https://deps.rs/crate/actix-web/4.7.0) +
+[![CI](https://github.com/actix/actix-web/actions/workflows/ci.yml/badge.svg)](https://github.com/actix/actix-web/actions/workflows/ci.yml) +[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) +![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) + +

@@ -24,7 +37,7 @@ - SSL support using OpenSSL or Rustls - Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) - Integrates with the [`awc` HTTP client](https://docs.rs/awc/) -- Runs on stable Rust 1.68+ +- Runs on stable Rust 1.72+ ## Documentation diff --git a/actix-web/benches/server.rs b/actix-web/benches/server.rs index 2c9f71dc5..0d45c9403 100644 --- a/actix-web/benches/server.rs +++ b/actix-web/benches/server.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use actix_web::{web, App, HttpResponse}; use awc::Client; use criterion::{criterion_group, criterion_main, Criterion}; diff --git a/actix-web/examples/macroless.rs b/actix-web/examples/macroless.rs index d3589da21..78ffd45c1 100644 --- a/actix-web/examples/macroless.rs +++ b/actix-web/examples/macroless.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; async fn index(req: HttpRequest) -> &'static str { diff --git a/actix-web/examples/uds.rs b/actix-web/examples/uds.rs index 15e28ba1d..e854bb3b1 100644 --- a/actix-web/examples/uds.rs +++ b/actix-web/examples/uds.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use actix_web::{get, web, HttpRequest}; #[cfg(unix)] use actix_web::{middleware, App, Error, HttpResponse, HttpServer}; diff --git a/actix-web/examples/worker-cpu-pin.rs b/actix-web/examples/worker-cpu-pin.rs new file mode 100644 index 000000000..58e060821 --- /dev/null +++ b/actix-web/examples/worker-cpu-pin.rs @@ -0,0 +1,41 @@ +use std::{ + io, + sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, + }, + thread, +}; + +use actix_web::{middleware, web, App, HttpServer}; + +async fn hello() -> &'static str { + "Hello world!" +} + +#[actix_web::main] +async fn main() -> io::Result<()> { + env_logger::init_from_env(env_logger::Env::new().default_filter_or("info")); + + let core_ids = core_affinity::get_core_ids().unwrap(); + let n_core_ids = core_ids.len(); + let next_core_id = Arc::new(AtomicUsize::new(0)); + + HttpServer::new(move || { + let pin = Arc::clone(&next_core_id).fetch_add(1, Ordering::AcqRel); + log::info!( + "setting CPU affinity for worker {}: pinning to core {}", + thread::current().name().unwrap(), + pin, + ); + core_affinity::set_for_current(core_ids[pin]); + + App::new() + .wrap(middleware::Logger::default()) + .service(web::resource("/").get(hello)) + }) + .bind(("127.0.0.1", 8080))? + .workers(n_core_ids) + .run() + .await +} diff --git a/actix-web/src/app.rs b/actix-web/src/app.rs index a4cd8d819..3d86d1f9b 100644 --- a/actix-web/src/app.rs +++ b/actix-web/src/app.rs @@ -112,8 +112,8 @@ where /// }) /// ``` #[doc(alias = "manage")] - pub fn app_data(mut self, ext: U) -> Self { - self.extensions.insert(ext); + pub fn app_data(mut self, data: U) -> Self { + self.extensions.insert(data); self } @@ -129,6 +129,8 @@ where /// /// Data items are constructed during application initialization, before the server starts /// accepting requests. + /// + /// The returned data value `D` is wrapped as [`Data`]. pub fn data_factory(mut self, data: F) -> Self where F: Fn() -> Out + 'static, @@ -141,8 +143,8 @@ where let fut = data(); async move { match fut.await { - Err(e) => { - log::error!("Can not construct data instance: {:?}", e); + Err(err) => { + log::error!("Can not construct data instance: {err:?}"); Err(()) } Ok(data) => { @@ -469,7 +471,6 @@ mod tests { Method, StatusCode, }, middleware::DefaultHeaders, - service::ServiceRequest, test::{call_service, init_service, read_body, try_init_service, TestRequest}, web, HttpRequest, HttpResponse, }; diff --git a/actix-web/src/app_service.rs b/actix-web/src/app_service.rs index 513e7a8a1..65a6ed87b 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -112,11 +112,7 @@ where let endpoint_fut = self.endpoint.new_service(()); // take extensions or create new one as app data container. - let mut app_data = self - .extensions - .borrow_mut() - .take() - .unwrap_or_else(Extensions::new); + let mut app_data = self.extensions.borrow_mut().take().unwrap_or_default(); Box::pin(async move { // async data factories @@ -267,8 +263,9 @@ impl ServiceFactory for AppRoutingFactory { let guards = guards.borrow_mut().take().unwrap_or_default(); let factory_fut = factory.new_service(()); async move { - let service = factory_fut.await?; - Ok((path, guards, service)) + factory_fut + .await + .map(move |service| (path, guards, service)) } })); diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index fba0c2717..5e8b056f1 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -148,7 +148,7 @@ impl AppConfig { #[cfg(test)] pub(crate) fn set_host(&mut self, host: &str) { - self.host = host.to_owned(); + host.clone_into(&mut self.host); } } diff --git a/actix-web/src/data.rs b/actix-web/src/data.rs index 423dd598c..acbb8e23a 100644 --- a/actix-web/src/data.rs +++ b/actix-web/src/data.rs @@ -3,7 +3,7 @@ use std::{any::type_name, ops::Deref, sync::Arc}; use actix_http::Extensions; use actix_utils::future::{err, ok, Ready}; use futures_core::future::LocalBoxFuture; -use serde::Serialize; +use serde::{de, Serialize}; use crate::{dev::Payload, error, Error, FromRequest, HttpRequest}; @@ -32,8 +32,8 @@ pub(crate) type FnDataFactory = /// Since the Actix Web router layers application data, the returned object will reference the /// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope` /// also stores a `u32`, and the delegated request handler falls within that `Scope`, then -/// extracting a `web::>` for that handler will return the `Scope`'s instance. -/// However, using the same router set up and a request that does not get captured by the `Scope`, +/// extracting a `web::Data` for that handler will return the `Scope`'s instance. However, +/// using the same router set up and a request that does not get captured by the `Scope`, /// `web::>` would return the `App`'s instance. /// /// If route data is not set for a handler, using `Data` extractor would cause a `500 Internal @@ -69,7 +69,7 @@ pub(crate) type FnDataFactory = /// HttpResponse::Ok() /// } /// -/// /// Alteratively, use the `HttpRequest::app_data` method to access data in a handler. +/// /// Alternatively, use the `HttpRequest::app_data` method to access data in a handler. /// async fn index_alt(req: HttpRequest) -> impl Responder { /// let data = req.app_data::>>().unwrap(); /// let mut my_data = data.lock().unwrap(); @@ -128,6 +128,12 @@ impl From> for Data { } } +impl Default for Data { + fn default() -> Self { + Data::new(T::default()) + } +} + impl Serialize for Data where T: Serialize, @@ -139,6 +145,17 @@ where self.0.serialize(serializer) } } +impl<'de, T> de::Deserialize<'de> for Data +where + T: de::Deserialize<'de>, +{ + fn deserialize(deserializer: D) -> Result + where + D: de::Deserializer<'de>, + { + Ok(Data::new(T::deserialize(deserializer)?)) + } +} impl FromRequest for Data { type Error = Error; diff --git a/actix-web/src/error/error.rs b/actix-web/src/error/error.rs index 3a5a128f6..670a58a00 100644 --- a/actix-web/src/error/error.rs +++ b/actix-web/src/error/error.rs @@ -60,6 +60,12 @@ impl From for Error { } } +impl From> for Error { + fn from(value: Box) -> Self { + Error { cause: value } + } +} + impl From for Response { fn from(err: Error) -> Response { err.error_response().into() diff --git a/actix-web/src/error/mod.rs b/actix-web/src/error/mod.rs index 91a6bcc3f..25535332c 100644 --- a/actix-web/src/error/mod.rs +++ b/actix-web/src/error/mod.rs @@ -100,6 +100,7 @@ impl ResponseError for UrlencodedError { match self { Self::Overflow { .. } => StatusCode::PAYLOAD_TOO_LARGE, Self::UnknownLength => StatusCode::LENGTH_REQUIRED, + Self::ContentType => StatusCode::UNSUPPORTED_MEDIA_TYPE, Self::Payload(err) => err.status_code(), _ => StatusCode::BAD_REQUEST, } @@ -232,7 +233,7 @@ mod tests { let resp = UrlencodedError::UnknownLength.error_response(); assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); let resp = UrlencodedError::ContentType.error_response(); - assert_eq!(resp.status(), StatusCode::BAD_REQUEST); + assert_eq!(resp.status(), StatusCode::UNSUPPORTED_MEDIA_TYPE); } #[test] diff --git a/actix-web/src/extract.rs b/actix-web/src/extract.rs index a2afd3827..249b56114 100644 --- a/actix-web/src/extract.rs +++ b/actix-web/src/extract.rs @@ -175,8 +175,8 @@ where let res = ready!(this.fut.poll(cx)); match res { Ok(t) => Poll::Ready(Ok(Some(t))), - Err(e) => { - log::debug!("Error for Option extractor: {}", e.into()); + Err(err) => { + log::debug!("Error for Option extractor: {}", err.into()); Poll::Ready(Ok(None)) } } @@ -217,8 +217,8 @@ where /// /// extract `Thing` from request /// async fn index(supplied_thing: Result) -> String { /// match supplied_thing { -/// Ok(thing) => format!("Got thing: {:?}", thing), -/// Err(e) => format!("Error extracting thing: {}", e) +/// Ok(thing) => format!("Got thing: {thing:?}"), +/// Err(err) => format!("Error extracting thing: {err}"), /// } /// } /// @@ -355,7 +355,7 @@ mod tuple_from_req { 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::Ready(Err(err)) => return Poll::Ready(Err(err.into())), Poll::Pending => ready = false, }, ExtractProj::Done { .. } => {}, diff --git a/actix-web/src/guard/acceptable.rs b/actix-web/src/guard/acceptable.rs index a31494a18..8fa7165c8 100644 --- a/actix-web/src/guard/acceptable.rs +++ b/actix-web/src/guard/acceptable.rs @@ -20,7 +20,7 @@ use crate::http::header::Accept; pub struct Acceptable { mime: mime::Mime, - /// Wether to match `*/*` mime type. + /// Whether to match `*/*` mime type. /// /// Defaults to false because it's not very useful otherwise. match_star_star: bool, diff --git a/actix-web/src/guard/host.rs b/actix-web/src/guard/host.rs index f05c81183..a971a3e30 100644 --- a/actix-web/src/guard/host.rs +++ b/actix-web/src/guard/host.rs @@ -2,7 +2,7 @@ use actix_http::{header, uri::Uri, RequestHead}; use super::{Guard, GuardContext}; -/// Creates a guard that matches requests targetting a specific host. +/// Creates a guard that matches requests targeting a specific host. /// /// # Matching Host /// This guard will: diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index 35294a3c4..41609953a 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -110,6 +110,12 @@ impl<'a> GuardContext<'a> { pub fn header(&self) -> Option { H::parse(self.req).ok() } + + /// Counterpart to [HttpRequest::app_data](crate::HttpRequest::app_data). + #[inline] + pub fn app_data(&self) -> Option<&T> { + self.req.app_data() + } } /// Interface for routing guards. @@ -380,7 +386,7 @@ impl Guard for HeaderGuard { #[cfg(test)] mod tests { - use actix_http::{header, Method}; + use actix_http::Method; use super::*; use crate::test::TestRequest; @@ -512,4 +518,18 @@ mod tests { .to_srv_request(); assert!(guard.check(&req.guard_ctx())); } + + #[test] + fn app_data() { + const TEST_VALUE: u32 = 42; + let guard = fn_guard(|ctx| dbg!(ctx.app_data::()) == Some(&TEST_VALUE)); + + let req = TestRequest::default().app_data(TEST_VALUE).to_srv_request(); + assert!(guard.check(&req.guard_ctx())); + + let req = TestRequest::default() + .app_data(TEST_VALUE * 2) + .to_srv_request(); + assert!(!guard.check(&req.guard_ctx())); + } } diff --git a/actix-web/src/handler.rs b/actix-web/src/handler.rs index 0c5e58e28..6e4e2250a 100644 --- a/actix-web/src/handler.rs +++ b/actix-web/src/handler.rs @@ -10,10 +10,12 @@ use crate::{ /// The interface for request handlers. /// /// # What Is A Request Handler +/// /// In short, a handler is just an async function that receives request-based arguments, in any /// order, and returns something that can be converted to a response. /// /// In particular, a request handler has three requirements: +/// /// 1. It is an async function (or a function/closure that returns an appropriate future); /// 1. The function parameters (up to 12) implement [`FromRequest`]; /// 1. The async function (or future) resolves to a type that can be converted into an @@ -21,11 +23,15 @@ use crate::{ /// /// /// # Compiler Errors +/// /// If you get the error `the trait Handler<_> is not implemented`, then your handler does not -/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on -/// implementing [`FromRequest`] and [`Responder`], respectively. +/// fulfill the _first_ of the above requirements. (It could also mean that you're attempting to use +/// a macro-routed handler in a manual routing context like `web::get().to(handler)`, which is not +/// supported). Breaking the other requirements manifests as errors on implementing [`FromRequest`] +/// and [`Responder`], respectively. /// /// # How Do Handlers Receive Variable Numbers Of Arguments +/// /// Rest assured there is no macro magic here; it's just traits. /// /// The first thing to note is that [`FromRequest`] is implemented for tuples (up to 12 in length). @@ -40,6 +46,7 @@ use crate::{ /// destructures the tuple into its component types and calls your handler function with them. /// /// In pseudo-code the process looks something like this: +/// /// ```ignore /// async fn my_handler(body: String, state: web::Data) -> impl Responder { /// ... @@ -167,7 +174,7 @@ mod tests { async fn handler_min() {} #[rustfmt::skip] - #[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)] + #[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits, clippy::let_unit_value)] async fn handler_max( _01: (), _02: (), _03: (), _04: (), _05: (), _06: (), _07: (), _08: (), _09: (), _10: (), _11: (), _12: (), diff --git a/actix-web/src/http/header/accept_encoding.rs b/actix-web/src/http/header/accept_encoding.rs index cc80e7bb0..19d649926 100644 --- a/actix-web/src/http/header/accept_encoding.rs +++ b/actix-web/src/http/header/accept_encoding.rs @@ -149,7 +149,7 @@ impl AcceptEncoding { /// Extracts the most preferable encoding, accounting for [q-factor weighting]. /// - /// If no q-factors are provided, the first encoding is chosen. Note that items without + /// If no q-factors are provided, we prefer brotli > zstd > gzip. Note that items without /// q-factors are given the maximum preference value. /// /// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is @@ -167,6 +167,7 @@ impl AcceptEncoding { let mut max_item = None; let mut max_pref = Quality::ZERO; + let mut max_rank = 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 @@ -174,9 +175,13 @@ impl AcceptEncoding { 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 { + + let rank = encoding_rank(pref); + + if (pref.quality, rank) > (max_pref, max_rank) { max_pref = pref.quality; max_item = Some(pref.item.clone()); + max_rank = rank; } } @@ -203,6 +208,8 @@ impl AcceptEncoding { /// Returns a sorted list of encodings from highest to lowest precedence, accounting /// for [q-factor weighting]. /// + /// If no q-factors are provided, we prefer brotli > zstd > gzip. + /// /// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2 pub fn ranked(&self) -> Vec> { self.ranked_items().map(|q| q.item).collect() @@ -210,21 +217,44 @@ impl AcceptEncoding { fn ranked_items(&self) -> impl Iterator>> { if self.0.is_empty() { - return vec![].into_iter(); + return Vec::new().into_iter(); } 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) + // sort by q-factor descending then server ranking descending + + b.quality + .cmp(&a.quality) + .then(encoding_rank(b).cmp(&encoding_rank(a))) }); types.into_iter() } } +/// Returns server-defined encoding ranking. +fn encoding_rank(qv: &QualityItem>) -> u8 { + // ensure that q=0 items are never sorted above identity encoding + // invariant: sorting methods calling this fn use first-on-equal approach + if qv.quality == Quality::ZERO { + return 0; + } + + match qv.item { + Preference::Specific(Encoding::Known(ContentEncoding::Brotli)) => 5, + Preference::Specific(Encoding::Known(ContentEncoding::Zstd)) => 4, + Preference::Specific(Encoding::Known(ContentEncoding::Gzip)) => 3, + Preference::Specific(Encoding::Known(ContentEncoding::Deflate)) => 2, + Preference::Any => 0, + Preference::Specific(Encoding::Known(ContentEncoding::Identity)) => 0, + Preference::Specific(Encoding::Known(_)) => 1, + Preference::Specific(Encoding::Unknown(_)) => 1, + } +} + /// Returns true if "identity" is an acceptable encoding. /// /// Internal algorithm relies on item list being in descending order of quality. @@ -377,11 +407,11 @@ mod tests { ); assert_eq!( test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()), - Some(Encoding::gzip()) + Some(Encoding::brotli()) ); assert_eq!( test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()), - Some(Encoding::gzip()) + Some(Encoding::brotli()) ); } @@ -398,6 +428,9 @@ mod tests { let test = accept_encoding!("br", "gzip", "*"); assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); + + let test = accept_encoding!("gzip", "br", "*"); + assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]); } #[test] @@ -420,5 +453,8 @@ mod tests { let test = accept_encoding!("br", "gzip", "*"); assert_eq!(test.preference().unwrap(), enc("br")); + + let test = accept_encoding!("gzip", "br", "*"); + assert_eq!(test.preference().unwrap(), enc("br")); } } diff --git a/actix-web/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs index 0606f5aef..9725cd19b 100644 --- a/actix-web/src/http/header/content_disposition.rs +++ b/actix-web/src/http/header/content_disposition.rs @@ -13,7 +13,10 @@ use std::fmt::{self, Write}; use once_cell::sync::Lazy; +#[cfg(feature = "unicode")] use regex::Regex; +#[cfg(not(feature = "unicode"))] +use regex_lite::Regex; use super::{ExtendedValue, Header, TryIntoHeaderValue, Writer}; use crate::http::header; diff --git a/actix-web/src/http/header/content_length.rs b/actix-web/src/http/header/content_length.rs index ad16dc409..557c7c9f5 100644 --- a/actix-web/src/http/header/content_length.rs +++ b/actix-web/src/http/header/content_length.rs @@ -126,7 +126,7 @@ mod tests { use std::fmt; use super::*; - use crate::{http::header::Header, test::TestRequest, HttpRequest}; + use crate::{test::TestRequest, HttpRequest}; fn req_from_raw_headers, V: AsRef<[u8]>>( header_lines: I, diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index c5d9638f4..1b2e554f9 100644 --- a/actix-web/src/info.rs +++ b/actix-web/src/info.rs @@ -21,6 +21,20 @@ fn unquote(val: &str) -> &str { val.trim().trim_start_matches('"').trim_end_matches('"') } +/// Remove port and IPv6 square brackets from a peer specification. +fn bare_address(val: &str) -> &str { + if val.starts_with('[') { + val.split("]:") + .next() + .map(|s| s.trim_start_matches('[').trim_end_matches(']')) + // this indicates that the IPv6 address is malformed so shouldn't + // usually happen, but if it does, just return the original input + .unwrap_or(val) + } else { + val.split(':').next().unwrap_or(val) + } +} + /// 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()?; @@ -100,7 +114,7 @@ impl ConnectionInfo { // --- 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)), + "for" => realip_remote_addr.get_or_insert_with(|| bare_address(unquote(val))), "proto" => scheme.get_or_insert_with(|| unquote(val)), "host" => host.get_or_insert_with(|| unquote(val)), "by" => { @@ -368,16 +382,25 @@ mod tests { .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")); + assert_eq!(info.realip_remote_addr(), Some("192.0.2.60")); } #[test] fn forwarded_for_ipv6() { + let req = TestRequest::default() + .insert_header((header::FORWARDED, r#"for="[2001:db8:cafe::17]""#)) + .to_http_request(); + let info = req.connection_info(); + assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17")); + } + + #[test] + fn forwarded_for_ipv6_with_port() { 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")); + assert_eq!(info.realip_remote_addr(), Some("2001:db8:cafe::17")); } #[test] diff --git a/actix-web/src/lib.rs b/actix-web/src/lib.rs index e982a43b1..205391388 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -64,12 +64,14 @@ //! - `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` +//! - `rustls` - HTTPS support via `rustls` 0.20 crate, supports `HTTP/2` +//! - `rustls-0_21` - HTTPS support via `rustls` 0.21 crate, supports `HTTP/2` +//! - `rustls-0_22` - HTTPS support via `rustls` 0.22 crate, supports `HTTP/2` +//! - `rustls-0_23` - HTTPS support via `rustls` 0.23 crate, supports `HTTP/2` //! - `secure-cookies` - secure cookies support #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] -#![allow(clippy::uninlined_format_args)] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] #![cfg_attr(docsrs, feature(doc_auto_cfg))] @@ -143,5 +145,6 @@ codegen_reexport!(delete); codegen_reexport!(trace); codegen_reexport!(connect); codegen_reexport!(options); +codegen_reexport!(scope); pub(crate) type BoxError = Box; diff --git a/actix-web/src/middleware/compat.rs b/actix-web/src/middleware/compat.rs index 7df510a5c..963dfdabb 100644 --- a/actix-web/src/middleware/compat.rs +++ b/actix-web/src/middleware/compat.rs @@ -38,15 +38,6 @@ pub struct Compat { transform: T, } -#[cfg(test)] -impl Compat { - pub(crate) fn noop() -> Self { - Self { - transform: super::Noop, - } - } -} - impl Compat { /// Wrap a middleware to give it broader compatibility. pub fn new(middleware: T) -> Self { @@ -152,7 +143,7 @@ mod tests { use crate::{ dev::ServiceRequest, http::StatusCode, - middleware::{self, Condition, Logger}, + middleware::{self, Condition, Identity, Logger}, test::{self, call_service, init_service, TestRequest}, web, App, HttpResponse, }; @@ -225,7 +216,7 @@ mod tests { async fn compat_noop_is_noop() { let srv = test::ok_service(); - let mw = Compat::noop() + let mw = Compat::new(Identity) .new_transform(srv.into_service()) .await .unwrap(); diff --git a/actix-web/src/middleware/compress.rs b/actix-web/src/middleware/compress.rs index a55b46264..943868d21 100644 --- a/actix-web/src/middleware/compress.rs +++ b/actix-web/src/middleware/compress.rs @@ -33,7 +33,7 @@ use crate::{ /// considered in this selection process. /// /// # Pre-compressed Payload -/// If you are serving some data is already using a compressed representation (e.g., a gzip +/// If you are serving some data that is already using a compressed representation (e.g., a gzip /// compressed HTML file from disk) you can signal this to `Compress` by setting an appropriate /// `Content-Encoding` header. In addition to preventing double compressing the payload, this header /// is required by the spec when using compressed representations and will inform the client that @@ -373,7 +373,7 @@ mod tests { .default_service(web::to(move || { HttpResponse::Ok() .insert_header((header::VARY, "x-test")) - .finish() + .body(TEXT_DATA) })) }) .await; @@ -429,4 +429,47 @@ mod tests { assert_successful_identity_res_with_content_type(&res, "image/jpeg"); assert_eq!(test::read_body(res).await, TEXT_DATA.as_bytes()); } + + #[actix_rt::test] + async fn prevents_compression_empty() { + let app = test::init_service({ + App::new() + .wrap(Compress::default()) + .default_service(web::to(move || HttpResponse::Ok().finish())) + }) + .await; + + let req = test::TestRequest::default() + .insert_header((header::ACCEPT_ENCODING, "gzip")) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + assert!(test::read_body(res).await.is_empty()); + } +} + +#[cfg(feature = "compress-brotli")] +#[cfg(test)] +mod tests_brotli { + use super::*; + use crate::{test, web, App}; + + #[actix_rt::test] + async fn prevents_compression_empty() { + let app = test::init_service({ + App::new() + .wrap(Compress::default()) + .default_service(web::to(move || HttpResponse::Ok().finish())) + }) + .await; + + let req = test::TestRequest::default() + .insert_header((header::ACCEPT_ENCODING, "br")) + .to_request(); + let res = test::call_service(&app, req).await; + assert_eq!(res.status(), StatusCode::OK); + assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); + assert!(test::read_body(res).await.is_empty()); + } } diff --git a/actix-web/src/middleware/condition.rs b/actix-web/src/middleware/condition.rs index 5e106c11f..5ee4467d9 100644 --- a/actix-web/src/middleware/condition.rs +++ b/actix-web/src/middleware/condition.rs @@ -135,13 +135,13 @@ mod tests { use super::*; use crate::{ body::BoxBody, - dev::{ServiceRequest, ServiceResponse}, + dev::ServiceRequest, error::Result, http::{ header::{HeaderValue, CONTENT_TYPE}, StatusCode, }, - middleware::{self, ErrorHandlerResponse, ErrorHandlers}, + middleware::{self, ErrorHandlerResponse, ErrorHandlers, Identity}, test::{self, TestRequest}, web::Bytes, HttpResponse, @@ -158,7 +158,7 @@ mod tests { #[test] fn compat_with_builtin_middleware() { - let _ = Condition::new(true, middleware::Compat::noop()); + let _ = Condition::new(true, middleware::Compat::new(Identity)); let _ = Condition::new(true, middleware::Logger::default()); let _ = Condition::new(true, middleware::Compress::default()); let _ = Condition::new(true, middleware::NormalizePath::trim()); diff --git a/actix-web/src/middleware/default_headers.rs b/actix-web/src/middleware/default_headers.rs index b5a5a6998..f21afe6eb 100644 --- a/actix-web/src/middleware/default_headers.rs +++ b/actix-web/src/middleware/default_headers.rs @@ -190,8 +190,6 @@ mod tests { use super::*; use crate::{ - dev::ServiceRequest, - http::header::CONTENT_TYPE, test::{self, TestRequest}, HttpResponse, }; diff --git a/actix-web/src/middleware/err_handlers.rs b/actix-web/src/middleware/err_handlers.rs index e640bba08..aa6d1c8a4 100644 --- a/actix-web/src/middleware/err_handlers.rs +++ b/actix-web/src/middleware/err_handlers.rs @@ -407,10 +407,7 @@ mod tests { use super::*; use crate::{ body, - http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + http::header::{HeaderValue, CONTENT_TYPE}, test::{self, TestRequest}, }; diff --git a/actix-web/src/middleware/noop.rs b/actix-web/src/middleware/identity.rs similarity index 57% rename from actix-web/src/middleware/noop.rs rename to actix-web/src/middleware/identity.rs index ae7da1d81..de374a57b 100644 --- a/actix-web/src/middleware/noop.rs +++ b/actix-web/src/middleware/identity.rs @@ -2,35 +2,39 @@ use actix_utils::future::{ready, Ready}; -use crate::dev::{Service, Transform}; +use crate::dev::{forward_ready, Service, Transform}; /// A no-op middleware that passes through request and response untouched. -pub(crate) struct Noop; +#[derive(Debug, Clone, Default)] +#[non_exhaustive] +pub struct Identity; -impl, Req> Transform for Noop { +impl, Req> Transform for Identity { type Response = S::Response; type Error = S::Error; - type Transform = NoopService; + type Transform = IdentityMiddleware; type InitError = (); type Future = Ready>; + #[inline] fn new_transform(&self, service: S) -> Self::Future { - ready(Ok(NoopService { service })) + ready(Ok(IdentityMiddleware { service })) } } #[doc(hidden)] -pub(crate) struct NoopService { +pub struct IdentityMiddleware { service: S, } -impl, Req> Service for NoopService { +impl, Req> Service for IdentityMiddleware { type Response = S::Response; type Error = S::Error; type Future = S::Future; - crate::dev::forward_ready!(service); + forward_ready!(service); + #[inline] fn call(&self, req: Req) -> Self::Future { self.service.call(req) } diff --git a/actix-web/src/middleware/logger.rs b/actix-web/src/middleware/logger.rs index 06d26617a..dc1b02399 100644 --- a/actix-web/src/middleware/logger.rs +++ b/actix-web/src/middleware/logger.rs @@ -18,7 +18,10 @@ use bytes::Bytes; use futures_core::ready; use log::{debug, warn}; use pin_project_lite::pin_project; -use regex::{Regex, RegexSet}; +#[cfg(feature = "unicode")] +use regex::Regex; +#[cfg(not(feature = "unicode"))] +use regex_lite::Regex; use time::{format_description::well_known::Rfc3339, OffsetDateTime}; use crate::{ @@ -87,7 +90,7 @@ pub struct Logger(Rc); struct Inner { format: Format, exclude: HashSet, - exclude_regex: RegexSet, + exclude_regex: Vec, log_target: Cow<'static, str>, } @@ -97,7 +100,7 @@ impl Logger { Logger(Rc::new(Inner { format: Format::new(format), exclude: HashSet::new(), - exclude_regex: RegexSet::empty(), + exclude_regex: Vec::new(), log_target: Cow::Borrowed(module_path!()), })) } @@ -114,10 +117,7 @@ impl Logger { /// Ignore and do not log access info for paths that match regex. pub fn exclude_regex>(mut self, path: T) -> Self { let inner = Rc::get_mut(&mut self.0).unwrap(); - let mut patterns = inner.exclude_regex.patterns().to_vec(); - patterns.push(path.into()); - let regex_set = RegexSet::new(patterns).unwrap(); - inner.exclude_regex = regex_set; + inner.exclude_regex.push(Regex::new(&path.into()).unwrap()); self } @@ -240,7 +240,7 @@ impl Default for Logger { Logger(Rc::new(Inner { format: Format::default(), exclude: HashSet::new(), - exclude_regex: RegexSet::empty(), + exclude_regex: Vec::new(), log_target: Cow::Borrowed(module_path!()), })) } @@ -300,7 +300,11 @@ where fn call(&self, req: ServiceRequest) -> Self::Future { let excluded = self.inner.exclude.contains(req.path()) - || self.inner.exclude_regex.is_match(req.path()); + || self + .inner + .exclude_regex + .iter() + .any(|r| r.is_match(req.path())); if excluded { LoggerResponse { @@ -356,7 +360,7 @@ where let res = match ready!(this.fut.poll(cx)) { Ok(res) => res, - Err(e) => return Poll::Ready(Err(e)), + Err(err) => return Poll::Ready(Err(err)), }; if let Some(error) = res.response().error() { @@ -716,7 +720,7 @@ impl<'a> fmt::Display for FormatDisplay<'a> { #[cfg(test)] mod tests { - use actix_service::{IntoService, Service, Transform}; + use actix_service::IntoService; use actix_utils::future::ok; use super::*; diff --git a/actix-web/src/middleware/mod.rs b/actix-web/src/middleware/mod.rs index 8dbd1ff2c..1c27b1110 100644 --- a/actix-web/src/middleware/mod.rs +++ b/actix-web/src/middleware/mod.rs @@ -33,13 +33,13 @@ //! //! # fn main() { //! # // These aren't snake_case, because they are supposed to be unit structs. -//! # let MiddlewareA = middleware::Compress::default(); -//! # let MiddlewareB = middleware::Compress::default(); -//! # let MiddlewareC = middleware::Compress::default(); +//! # type MiddlewareA = middleware::Compress; +//! # type MiddlewareB = middleware::Compress; +//! # type MiddlewareC = middleware::Compress; //! let app = App::new() -//! .wrap(MiddlewareA) -//! .wrap(MiddlewareB) -//! .wrap(MiddlewareC) +//! .wrap(MiddlewareA::default()) +//! .wrap(MiddlewareB::default()) +//! .wrap(MiddlewareC::default()) //! .service(service); //! # } //! ``` @@ -72,7 +72,7 @@ //! processes the request as well and passes it to `MiddlewareA`, which then passes it to the //! [`Service`]. In the [`Service`], the extractors will run first. They don't pass the request on, //! but only view it (see [`FromRequest`]). After the [`Service`] responds to the request, the -//! response it passed back through `MiddlewareA`, `MiddlewareB`, and `MiddlewareC`. +//! response is passed back through `MiddlewareA`, `MiddlewareB`, and `MiddlewareC`. //! //! As you register middleware using [`wrap`][crate::App::wrap] and [`wrap_fn`][crate::App::wrap_fn] //! in the [`App`] builder, imagine wrapping layers around an inner [`App`]. The first middleware @@ -218,31 +218,27 @@ //! [lab_from_fn]: https://docs.rs/actix-web-lab/latest/actix_web_lab/middleware/fn.from_fn.html mod compat; +#[cfg(feature = "__compress")] +mod compress; mod condition; mod default_headers; mod err_handlers; +mod identity; mod logger; -#[cfg(test)] -mod noop; mod normalize; -#[cfg(test)] -pub(crate) use self::noop::Noop; +#[cfg(feature = "__compress")] +pub use self::compress::Compress; pub use self::{ compat::Compat, condition::Condition, default_headers::DefaultHeaders, err_handlers::{ErrorHandlerResponse, ErrorHandlers}, + identity::Identity, logger::Logger, normalize::{NormalizePath, TrailingSlash}, }; -#[cfg(feature = "__compress")] -mod compress; - -#[cfg(feature = "__compress")] -pub use self::compress::Compress; - #[cfg(test)] mod tests { use super::*; diff --git a/actix-web/src/middleware/normalize.rs b/actix-web/src/middleware/normalize.rs index afcc0faac..482107ecb 100644 --- a/actix-web/src/middleware/normalize.rs +++ b/actix-web/src/middleware/normalize.rs @@ -4,7 +4,10 @@ use actix_http::uri::{PathAndQuery, Uri}; use actix_service::{Service, Transform}; use actix_utils::future::{ready, Ready}; use bytes::Bytes; +#[cfg(feature = "unicode")] use regex::Regex; +#[cfg(not(feature = "unicode"))] +use regex_lite::Regex; use crate::{ service::{ServiceRequest, ServiceResponse}, @@ -205,7 +208,6 @@ mod tests { use super::*; use crate::{ - dev::ServiceRequest, guard::fn_guard, test::{call_service, init_service, TestRequest}, web, App, HttpResponse, diff --git a/actix-web/src/redirect.rs b/actix-web/src/redirect.rs index f9e9f2d7a..bd29a1403 100644 --- a/actix-web/src/redirect.rs +++ b/actix-web/src/redirect.rs @@ -171,7 +171,7 @@ impl Responder for Redirect { } else { log::error!( "redirect target location can not be converted to header value: {:?}", - self.to + self.to, ); } @@ -182,7 +182,7 @@ impl Responder for Redirect { #[cfg(test)] mod tests { use super::*; - use crate::{dev::Service, http::StatusCode, test, App}; + use crate::{dev::Service, test, App}; #[actix_rt::test] async fn absolute_redirects() { diff --git a/actix-web/src/request.rs b/actix-web/src/request.rs index efb84b2a9..22841746d 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -535,7 +535,7 @@ mod tests { use super::*; use crate::{ - dev::{ResourceDef, ResourceMap, Service}, + dev::{ResourceDef, Service}, http::{header, StatusCode}, test::{self, call_service, init_service, read_body, TestRequest}, web, App, HttpResponse, diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index 95185b80a..00555b7b2 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -540,20 +540,14 @@ mod tests { use std::time::Duration; use actix_rt::time::sleep; - use actix_service::Service; use actix_utils::future::ok; use super::*; use crate::{ - guard, - http::{ - header::{self, HeaderValue}, - Method, StatusCode, - }, + http::{header::HeaderValue, Method, StatusCode}, middleware::DefaultHeaders, - service::{ServiceRequest, ServiceResponse}, test::{call_service, init_service, TestRequest}, - web, App, Error, HttpMessage, HttpResponse, + App, HttpMessage, }; #[test] @@ -777,7 +771,7 @@ mod tests { data3: web::Data| { assert_eq!(**data1, 10); assert_eq!(**data2, '*'); - let error = std::f64::EPSILON; + let error = f64::EPSILON; assert!((**data3 - 1.0).abs() < error); HttpResponse::Ok() }, diff --git a/actix-web/src/response/builder.rs b/actix-web/src/response/builder.rs index 2c06941cd..023842ee5 100644 --- a/actix-web/src/response/builder.rs +++ b/actix-web/src/response/builder.rs @@ -64,7 +64,7 @@ impl HttpResponseBuilder { Ok((key, value)) => { parts.headers.insert(key, value); } - Err(e) => self.error = Some(e.into()), + Err(err) => self.error = Some(err.into()), }; } @@ -86,7 +86,7 @@ impl HttpResponseBuilder { if let Some(parts) = self.inner() { match header.try_into_pair() { Ok((key, value)) => parts.headers.append(key, value), - Err(e) => self.error = Some(e.into()), + Err(err) => self.error = Some(err.into()), }; } @@ -210,7 +210,7 @@ impl HttpResponseBuilder { Ok(value) => { parts.headers.insert(header::CONTENT_TYPE, value); } - Err(e) => self.error = Some(e.into()), + Err(err) => self.error = Some(err.into()), }; } self @@ -408,10 +408,7 @@ mod tests { use super::*; use crate::{ body, - http::{ - header::{self, HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + http::header::{HeaderValue, CONTENT_TYPE}, test::assert_body_eq, }; diff --git a/actix-web/src/response/customize_responder.rs b/actix-web/src/response/customize_responder.rs index aad0039e0..6a43ac5e6 100644 --- a/actix-web/src/response/customize_responder.rs +++ b/actix-web/src/response/customize_responder.rs @@ -7,7 +7,7 @@ use actix_http::{ use crate::{HttpRequest, HttpResponse, Responder}; -/// Allows overriding status code and headers for a [`Responder`]. +/// Allows overriding status code and headers (including cookies) for a [`Responder`]. /// /// Created by calling the [`customize`](Responder::customize) method on a [`Responder`] type. pub struct CustomizeResponder { @@ -137,6 +137,29 @@ impl CustomizeResponder { Some(&mut self.inner) } } + + /// Appends a `cookie` to the final response. + /// + /// # Errors + /// + /// Final response will be an error if `cookie` cannot be converted into a valid header value. + #[cfg(feature = "cookies")] + pub fn add_cookie(mut self, cookie: &crate::cookie::Cookie<'_>) -> Self { + use actix_http::header::{TryIntoHeaderValue as _, SET_COOKIE}; + + if let Some(inner) = self.inner() { + match cookie.to_string().try_into_value() { + Ok(val) => { + inner.append_headers.append(SET_COOKIE, val); + } + Err(err) => { + self.error = Some(err.into()); + } + } + } + + self + } } impl Responder for CustomizeResponder @@ -175,10 +198,8 @@ mod tests { use super::*; use crate::{ - http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + cookie::Cookie, + http::header::{HeaderValue, CONTENT_TYPE}, test::TestRequest, }; @@ -212,6 +233,22 @@ mod tests { to_bytes(res.into_body()).await.unwrap(), Bytes::from_static(b"test"), ); + + let res = "test" + .to_string() + .customize() + .add_cookie(&Cookie::new("name", "value")) + .respond_to(&req); + + assert!(res.status().is_success()); + assert_eq!( + res.cookies().collect::>>(), + vec![Cookie::new("name", "value")], + ); + assert_eq!( + to_bytes(res.into_body()).await.unwrap(), + Bytes::from_static(b"test"), + ); } #[actix_rt::test] diff --git a/actix-web/src/response/mod.rs b/actix-web/src/response/mod.rs index 11fd28301..16bdc619c 100644 --- a/actix-web/src/response/mod.rs +++ b/actix-web/src/response/mod.rs @@ -5,8 +5,6 @@ mod responder; #[allow(clippy::module_inception)] mod response; -#[cfg(feature = "cookies")] -pub use self::response::CookieIter; pub use self::{ builder::HttpResponseBuilder, customize_responder::CustomizeResponder, responder::Responder, response::HttpResponse, diff --git a/actix-web/src/response/responder.rs b/actix-web/src/response/responder.rs index 7d0b0e585..90d8f6e52 100644 --- a/actix-web/src/response/responder.rs +++ b/actix-web/src/response/responder.rs @@ -188,15 +188,11 @@ impl_into_string_responder!(Cow<'_, str>); pub(crate) mod tests { use actix_http::body::to_bytes; use actix_service::Service; - use bytes::{Bytes, BytesMut}; use super::*; use crate::{ error, - http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + http::header::{HeaderValue, CONTENT_TYPE}, test::{assert_body_eq, init_service, TestRequest}, web, App, }; diff --git a/actix-web/src/response/response.rs b/actix-web/src/response/response.rs index fbd87e10c..e16dc0cd9 100644 --- a/actix-web/src/response/response.rs +++ b/actix-web/src/response/response.rs @@ -399,7 +399,7 @@ mod tests { use static_assertions::assert_impl_all; use super::*; - use crate::http::header::{HeaderValue, COOKIE}; + use crate::http::header::COOKIE; assert_impl_all!(HttpResponse: Responder); assert_impl_all!(HttpResponse: Responder); diff --git a/actix-web/src/route.rs b/actix-web/src/route.rs index 674d0082a..261e6b9ae 100644 --- a/actix-web/src/route.rs +++ b/actix-web/src/route.rs @@ -92,7 +92,8 @@ pub struct RouteService { } impl RouteService { - // TODO: does this need to take &mut ? + // TODO(breaking): remove pass by ref mut + #[allow(clippy::needless_pass_by_ref_mut)] pub fn check(&self, req: &mut ServiceRequest) -> bool { let guard_ctx = req.guard_ctx(); diff --git a/actix-web/src/rt.rs b/actix-web/src/rt.rs index 629ffe3a0..e370e2c0b 100644 --- a/actix-web/src/rt.rs +++ b/actix-web/src/rt.rs @@ -5,6 +5,7 @@ //! architecture in [`actix-rt`]'s docs. //! //! # Running Actix Web Without Macros +//! //! ```no_run //! use actix_web::{middleware, rt, web, App, HttpRequest, HttpServer}; //! @@ -25,6 +26,7 @@ //! ``` //! //! # Running Actix Web Using `#[tokio::main]` +//! //! If you need to run something that uses Tokio's work stealing functionality alongside Actix Web, //! you can run Actix Web under `#[tokio::main]`. The [`Server`](crate::dev::Server) object returned //! from [`HttpServer::run`](crate::HttpServer::run) can also be [`spawn`]ed, if preferred. @@ -32,6 +34,10 @@ //! Note that `actix` actor support (and therefore WebSocket support through `actix-web-actors`) //! still require `#[actix_web::main]` since they require a [`System`] to be set up. //! +//! Also note that calls to this module's [`spawn()`] re-export require an `#[actix_web::main]` +//! runtime (or a manually configured `LocalSet`) since it makes calls into to the current thread's +//! `LocalSet`, which `#[tokio::main]` does not set up. +//! //! ```no_run //! use actix_web::{get, middleware, rt, web, App, HttpRequest, HttpServer}; //! diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs index e7c4e047a..adc9f75d3 100644 --- a/actix-web/src/scope.rs +++ b/actix-web/src/scope.rs @@ -470,8 +470,9 @@ impl ServiceFactory for ScopeFactory { let guards = guards.borrow_mut().take().unwrap_or_default(); let factory_fut = factory.new_service(()); async move { - let service = factory_fut.await?; - Ok((path, guards, service)) + factory_fut + .await + .map(move |service| (path, guards, service)) } })); @@ -547,7 +548,6 @@ impl ServiceFactory for ScopeEndpoint { #[cfg(test)] mod tests { - use actix_service::Service; use actix_utils::future::ok; use bytes::Bytes; @@ -559,7 +559,6 @@ mod tests { Method, StatusCode, }, middleware::DefaultHeaders, - service::{ServiceRequest, ServiceResponse}, test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, web, App, HttpMessage, HttpRequest, HttpResponse, }; diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index a540da7c8..33b1e1894 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -7,7 +7,13 @@ use std::{ time::Duration, }; -#[cfg(any(feature = "openssl", feature = "rustls"))] +#[cfg(any( + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22", + feature = "rustls-0_23", +))] use actix_http::TlsAcceptorConfig; use actix_http::{body::MessageBody, Extensions, HttpService, KeepAlive, Request, Response}; use actix_server::{Server, ServerBuilder}; @@ -16,8 +22,6 @@ use actix_service::{ }; #[cfg(feature = "openssl")] use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; -#[cfg(feature = "rustls")] -use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; use crate::{config::AppConfig, Error}; @@ -31,7 +35,7 @@ struct Config { keep_alive: KeepAlive, client_request_timeout: Duration, client_disconnect_timeout: Duration, - #[cfg(any(feature = "openssl", feature = "rustls"))] + #[allow(dead_code)] // only dead when no TLS features are enabled tls_handshake_timeout: Option, } @@ -101,6 +105,12 @@ where B: MessageBody + 'static, { /// Create new HTTP server with application factory + /// + /// # Worker Count + /// + /// The `factory` will be instantiated multiple times in most configurations. See + /// [`bind()`](Self::bind()) docs for more on how worker count and bind address resolution + /// causes multiple server factory instantiations. pub fn new(factory: F) -> Self { HttpServer { factory, @@ -109,7 +119,6 @@ where keep_alive: KeepAlive::default(), client_request_timeout: Duration::from_secs(5), client_disconnect_timeout: Duration::from_secs(1), - #[cfg(any(feature = "rustls", feature = "openssl"))] tls_handshake_timeout: None, })), backlog: 1024, @@ -122,7 +131,18 @@ where /// Sets number of workers to start (per bind address). /// - /// By default, the number of available physical CPUs is used as the worker count. + /// The default worker count is the determined by [`std::thread::available_parallelism()`]. See + /// its documentation to determine what behavior you should expect when server is run. + /// + /// Note that the server factory passed to [`new`](Self::new()) will be instantiated **at least + /// once per worker**. See [`bind()`](Self::bind()) docs for more on how worker count and bind + /// address resolution causes multiple server factory instantiations. + /// + /// `num` must be greater than 0. + /// + /// # Panics + /// + /// Panics if `num` is 0. pub fn workers(mut self, num: usize) -> Self { self.builder = self.builder.workers(num); self @@ -170,7 +190,7 @@ where /// By default max connections is set to a 256. #[allow(unused_variables)] pub fn max_connection_rate(self, num: usize) -> Self { - #[cfg(any(feature = "rustls", feature = "openssl"))] + #[cfg(any(feature = "rustls-0_20", feature = "rustls-0_21", feature = "openssl"))] actix_tls::accept::max_concurrent_tls_connect(num); self } @@ -222,8 +242,14 @@ where /// Defines a timeout for TLS handshake. If the TLS handshake does not complete within this /// time, the connection is closed. /// - /// By default handshake timeout is set to 3000 milliseconds. - #[cfg(any(feature = "openssl", feature = "rustls"))] + /// By default, the handshake timeout is 3 seconds. + #[cfg(any( + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22", + feature = "rustls-0_23", + ))] pub fn tls_handshake_timeout(self, dur: Duration) -> Self { self.config .lock() @@ -247,7 +273,14 @@ where /// /// # Connection Types /// - `actix_tls::accept::openssl::TlsStream` when using OpenSSL. - /// - `actix_tls::accept::rustls::TlsStream` when using Rustls. + /// - `actix_tls::accept::rustls_0_20::TlsStream` when using + /// Rustls v0.20. + /// - `actix_tls::accept::rustls_0_21::TlsStream` when using + /// Rustls v0.21. + /// - `actix_tls::accept::rustls_0_22::TlsStream` when using + /// Rustls v0.22. + /// - `actix_tls::accept::rustls_0_23::TlsStream` when using + /// Rustls v0.23. /// - `actix_web::rt::net::TcpStream` when no encryption is used. /// /// See the `on_connect` example for additional details. @@ -319,23 +352,41 @@ where /// Resolves socket address(es) and binds server to created listener(s). /// /// # Hostname Resolution - /// When `addr` includes a hostname, it is possible for this method to bind to both the IPv4 and - /// IPv6 addresses that result from a DNS lookup. You can test this by passing `localhost:8080` - /// and noting that the server binds to `127.0.0.1:8080` _and_ `[::1]:8080`. To bind additional - /// addresses, call this method multiple times. + /// + /// When `addrs` includes a hostname, it is possible for this method to bind to both the IPv4 + /// and IPv6 addresses that result from a DNS lookup. You can test this by passing + /// `localhost:8080` and noting that the server binds to `127.0.0.1:8080` _and_ `[::1]:8080`. To + /// bind additional addresses, call this method multiple times. /// /// Note that, if a DNS lookup is required, resolving hostnames is a blocking operation. /// + /// # Worker Count + /// + /// The `factory` will be instantiated multiple times in most scenarios. The number of + /// instantiations is number of [`workers`](Self::workers()) × number of sockets resolved by + /// `addrs`. + /// + /// For example, if you've manually set [`workers`](Self::workers()) to 2, and use `127.0.0.1` + /// as the bind `addrs`, then `factory` will be instantiated twice. However, using `localhost` + /// as the bind `addrs` can often resolve to both `127.0.0.1` (IPv4) _and_ `::1` (IPv6), causing + /// the `factory` to be instantiated 4 times (2 workers × 2 bind addresses). + /// + /// Using a bind address of `0.0.0.0`, which signals to use all interfaces, may also multiple + /// the number of instantiations in a similar way. + /// /// # Typical Usage + /// /// In general, use `127.0.0.1:` when testing locally and `0.0.0.0:` when deploying /// (with or without a reverse proxy or load balancer) so that the server is accessible. /// /// # Errors + /// /// Returns an `io::Error` if: /// - `addrs` cannot be resolved into one or more socket addresses; /// - all the resolved socket addresses are already bound. /// /// # Example + /// /// ``` /// # use actix_web::{App, HttpServer}; /// # fn inner() -> std::io::Result<()> { @@ -356,6 +407,8 @@ where /// Resolves socket address(es) and binds server to created listener(s) for plaintext HTTP/1.x /// or HTTP/2 connections. + /// + /// See [`bind()`](Self::bind()) for more details on `addrs` argument. #[cfg(feature = "http2")] pub fn bind_auto_h2c(mut self, addrs: A) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; @@ -368,20 +421,77 @@ where } /// Resolves socket address(es) and binds server to created listener(s) for TLS connections - /// using Rustls. + /// using Rustls v0.20. /// - /// See [`bind()`](Self::bind) for more details on `addrs` argument. + /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. - #[cfg(feature = "rustls")] + #[cfg(feature = "rustls-0_20")] pub fn bind_rustls( mut self, addrs: A, - config: RustlsServerConfig, + config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { - self = self.listen_rustls_inner(lst, config.clone())?; + self = self.listen_rustls_0_20_inner(lst, config.clone())?; + } + Ok(self) + } + + /// Resolves socket address(es) and binds server to created listener(s) for TLS connections + /// using Rustls v0.21. + /// + /// See [`bind()`](Self::bind()) for more details on `addrs` argument. + /// + /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. + #[cfg(feature = "rustls-0_21")] + pub fn bind_rustls_021( + mut self, + addrs: A, + config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, + ) -> io::Result { + let sockets = bind_addrs(addrs, self.backlog)?; + for lst in sockets { + self = self.listen_rustls_0_21_inner(lst, config.clone())?; + } + Ok(self) + } + + /// Resolves socket address(es) and binds server to created listener(s) for TLS connections + /// using Rustls v0.22. + /// + /// See [`bind()`](Self::bind()) for more details on `addrs` argument. + /// + /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. + #[cfg(feature = "rustls-0_22")] + pub fn bind_rustls_0_22( + mut self, + addrs: A, + config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, + ) -> io::Result { + let sockets = bind_addrs(addrs, self.backlog)?; + for lst in sockets { + self = self.listen_rustls_0_22_inner(lst, config.clone())?; + } + Ok(self) + } + + /// Resolves socket address(es) and binds server to created listener(s) for TLS connections + /// using Rustls v0.23. + /// + /// See [`bind()`](Self::bind()) for more details on `addrs` argument. + /// + /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. + #[cfg(feature = "rustls-0_23")] + pub fn bind_rustls_0_23( + mut self, + addrs: A, + config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, + ) -> io::Result { + let sockets = bind_addrs(addrs, self.backlog)?; + for lst in sockets { + self = self.listen_rustls_0_23_inner(lst, config.clone())?; } Ok(self) } @@ -389,7 +499,7 @@ where /// Resolves socket address(es) and binds server to created listener(s) for TLS connections /// using OpenSSL. /// - /// See [`bind()`](Self::bind) for more details on `addrs` argument. + /// See [`bind()`](Self::bind()) for more details on `addrs` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "openssl")] @@ -497,25 +607,41 @@ where Ok(self) } - /// Binds to existing listener for accepting incoming TLS connection requests using Rustls. + /// Binds to existing listener for accepting incoming TLS connection requests using Rustls + /// v0.20. /// /// See [`listen()`](Self::listen) for more details on the `lst` argument. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. - #[cfg(feature = "rustls")] + #[cfg(feature = "rustls-0_20")] pub fn listen_rustls( self, lst: net::TcpListener, - config: RustlsServerConfig, + config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { - self.listen_rustls_inner(lst, config) + self.listen_rustls_0_20_inner(lst, config) } - #[cfg(feature = "rustls")] - fn listen_rustls_inner( + /// Binds to existing listener for accepting incoming TLS connection requests using Rustls + /// v0.21. + /// + /// See [`listen()`](Self::listen()) for more details on the `lst` argument. + /// + /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. + #[cfg(feature = "rustls-0_21")] + pub fn listen_rustls_0_21( + self, + lst: net::TcpListener, + config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, + ) -> io::Result { + self.listen_rustls_0_21_inner(lst, config) + } + + #[cfg(feature = "rustls-0_20")] + fn listen_rustls_0_20_inner( mut self, lst: net::TcpListener, - config: RustlsServerConfig, + config: actix_tls::accept::rustls_0_20::reexports::ServerConfig, ) -> io::Result { let factory = self.factory.clone(); let cfg = self.config.clone(); @@ -562,6 +688,189 @@ where Ok(self) } + #[cfg(feature = "rustls-0_21")] + fn listen_rustls_0_21_inner( + mut self, + lst: net::TcpListener, + config: actix_tls::accept::rustls_0_21::reexports::ServerConfig, + ) -> io::Result { + let factory = self.factory.clone(); + let cfg = self.config.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "https", + }); + + 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)); + + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout); + + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + } else { + svc + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + let acceptor_config = match c.tls_handshake_timeout { + Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), + None => TlsAcceptorConfig::default(), + }; + + svc.finish(map_config(fac, move |_| { + AppConfig::new(true, host.clone(), addr) + })) + .rustls_021_with_config(config.clone(), acceptor_config) + })?; + + Ok(self) + } + + /// Binds to existing listener for accepting incoming TLS connection requests using Rustls + /// v0.22. + /// + /// See [`listen()`](Self::listen()) for more details on the `lst` argument. + /// + /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. + #[cfg(feature = "rustls-0_22")] + pub fn listen_rustls_0_22( + self, + lst: net::TcpListener, + config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, + ) -> io::Result { + self.listen_rustls_0_22_inner(lst, config) + } + + #[cfg(feature = "rustls-0_22")] + fn listen_rustls_0_22_inner( + mut self, + lst: net::TcpListener, + config: actix_tls::accept::rustls_0_22::reexports::ServerConfig, + ) -> io::Result { + let factory = self.factory.clone(); + let cfg = self.config.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "https", + }); + + 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)); + + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout); + + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + } else { + svc + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + let acceptor_config = match c.tls_handshake_timeout { + Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), + None => TlsAcceptorConfig::default(), + }; + + svc.finish(map_config(fac, move |_| { + AppConfig::new(true, host.clone(), addr) + })) + .rustls_0_22_with_config(config.clone(), acceptor_config) + })?; + + Ok(self) + } + + /// Binds to existing listener for accepting incoming TLS connection requests using Rustls + /// v0.23. + /// + /// See [`listen()`](Self::listen()) for more details on the `lst` argument. + /// + /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. + #[cfg(feature = "rustls-0_23")] + pub fn listen_rustls_0_23( + self, + lst: net::TcpListener, + config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, + ) -> io::Result { + self.listen_rustls_0_23_inner(lst, config) + } + + #[cfg(feature = "rustls-0_23")] + fn listen_rustls_0_23_inner( + mut self, + lst: net::TcpListener, + config: actix_tls::accept::rustls_0_23::reexports::ServerConfig, + ) -> io::Result { + let factory = self.factory.clone(); + let cfg = self.config.clone(); + let addr = lst.local_addr().unwrap(); + self.sockets.push(Socket { + addr, + scheme: "https", + }); + + 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)); + + let svc = HttpService::build() + .keep_alive(c.keep_alive) + .client_request_timeout(c.client_request_timeout) + .client_disconnect_timeout(c.client_disconnect_timeout); + + let svc = if let Some(handler) = on_connect_fn.clone() { + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + } else { + svc + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + let acceptor_config = match c.tls_handshake_timeout { + Some(dur) => TlsAcceptorConfig::default().handshake_timeout(dur), + None => TlsAcceptorConfig::default(), + }; + + svc.finish(map_config(fac, move |_| { + AppConfig::new(true, host.clone(), addr) + })) + .rustls_0_23_with_config(config.clone(), acceptor_config) + })?; + + Ok(self) + } + /// Binds to existing listener for accepting incoming TLS connection requests using OpenSSL. /// /// See [`listen()`](Self::listen) for more details on the `lst` argument. @@ -767,7 +1076,7 @@ fn bind_addrs(addrs: impl net::ToSocketAddrs, backlog: u32) -> io::Result err = Some(e), + Err(error) => err = Some(error), } } diff --git a/actix-web/src/service.rs b/actix-web/src/service.rs index 0e17c9949..a1672eba2 100644 --- a/actix-web/src/service.rs +++ b/actix-web/src/service.rs @@ -221,12 +221,9 @@ impl ServiceRequest { /// Returns peer's socket address. /// - /// 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. + /// See [`HttpRequest::peer_addr`] for more details. /// - /// To get client connection information `ConnectionInfo` should be used. - /// - /// Will only return None when called in unit tests. + /// [`HttpRequest::peer_addr`]: crate::HttpRequest::peer_addr #[inline] pub fn peer_addr(&self) -> Option { self.head().peer_addr @@ -703,7 +700,7 @@ mod tests { use crate::{ guard, http, test::{self, init_service, TestRequest}, - web, App, HttpResponse, + web, App, }; #[actix_rt::test] diff --git a/actix-web/src/test/test_request.rs b/actix-web/src/test/test_request.rs index 5491af0ac..f178d6f43 100644 --- a/actix-web/src/test/test_request.rs +++ b/actix-web/src/test/test_request.rs @@ -86,76 +86,77 @@ impl Default for TestRequest { #[allow(clippy::wrong_self_convention)] impl TestRequest { - /// Create TestRequest and set request uri - pub fn with_uri(path: &str) -> TestRequest { - TestRequest::default().uri(path) + /// Constructs test request and sets request URI. + pub fn with_uri(uri: &str) -> TestRequest { + TestRequest::default().uri(uri) } - /// Create TestRequest and set method to `Method::GET` + /// Constructs test request with GET method. pub fn get() -> TestRequest { TestRequest::default().method(Method::GET) } - /// Create TestRequest and set method to `Method::POST` + /// Constructs test request with POST method. pub fn post() -> TestRequest { TestRequest::default().method(Method::POST) } - /// Create TestRequest and set method to `Method::PUT` + /// Constructs test request with PUT method. pub fn put() -> TestRequest { TestRequest::default().method(Method::PUT) } - /// Create TestRequest and set method to `Method::PATCH` + /// Constructs test request with PATCH method. pub fn patch() -> TestRequest { TestRequest::default().method(Method::PATCH) } - /// Create TestRequest and set method to `Method::DELETE` + /// Constructs test request with DELETE method. pub fn delete() -> TestRequest { TestRequest::default().method(Method::DELETE) } - /// Set HTTP version of this request + /// Sets HTTP version of this request. pub fn version(mut self, ver: Version) -> Self { self.req.version(ver); self } - /// Set HTTP method of this request + /// Sets method of this request. pub fn method(mut self, meth: Method) -> Self { self.req.method(meth); self } - /// Set HTTP URI of this request + /// Sets URI of this request. pub fn uri(mut self, path: &str) -> Self { self.req.uri(path); self } - /// Insert a header, replacing any that were set with an equivalent field name. + /// Inserts a header, replacing any that were set with an equivalent field name. pub fn insert_header(mut self, header: impl TryIntoHeaderPair) -> Self { self.req.insert_header(header); self } - /// Append a header, keeping any that were set with an equivalent field name. + /// Appends a header, keeping any that were set with an equivalent field name. pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { self.req.append_header(header); self } - /// Set cookie for this request. + /// Sets cookie for this request. #[cfg(feature = "cookies")] pub fn cookie(mut self, cookie: Cookie<'_>) -> Self { self.cookies.add(cookie.into_owned()); self } - /// Set request path pattern parameter. + /// Sets request path pattern parameter. /// /// # Examples + /// /// ``` /// use actix_web::test::TestRequest; /// @@ -171,19 +172,19 @@ impl TestRequest { self } - /// Set peer addr. + /// Sets peer address. pub fn peer_addr(mut self, addr: SocketAddr) -> Self { self.peer_addr = Some(addr); self } - /// Set request payload. + /// Sets request payload. pub fn set_payload(mut self, data: impl Into) -> Self { self.req.set_payload(data); self } - /// Serialize `data` to a URL encoded form and set it as the request payload. + /// Serializes `data` to a URL encoded form and set it as the request payload. /// /// The `Content-Type` header is set to `application/x-www-form-urlencoded`. pub fn set_form(mut self, data: impl Serialize) -> Self { @@ -194,7 +195,7 @@ impl TestRequest { self } - /// Serialize `data` to JSON and set it as the request payload. + /// Serializes `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: impl Serialize) -> Self { @@ -204,27 +205,33 @@ impl TestRequest { self } - /// Set application data. This is equivalent of `App::data()` method - /// for testing purpose. - pub fn data(mut self, data: T) -> Self { - self.app_data.insert(Data::new(data)); - self - } - - /// Set application data. This is equivalent of `App::app_data()` method - /// for testing purpose. + /// Inserts application data. + /// + /// This is equivalent of `App::app_data()` method for testing purpose. pub fn app_data(mut self, data: T) -> Self { self.app_data.insert(data); self } + /// Inserts application data. + /// + /// This is equivalent of `App::data()` method for testing purpose. + #[doc(hidden)] + pub fn data(mut self, data: T) -> Self { + self.app_data.insert(Data::new(data)); + self + } + + /// Sets resource map. #[cfg(test)] - /// Set request config pub(crate) fn rmap(mut self, rmap: ResourceMap) -> Self { self.rmap = rmap; self } + /// Finalizes test request. + /// + /// This request builder will be useless after calling `finish()`. fn finish(&mut self) -> Request { // mut used when cookie feature is enabled #[allow(unused_mut)] @@ -251,14 +258,14 @@ impl TestRequest { req } - /// Complete request creation and generate `Request` instance + /// Finalizes request creation and returns `Request` instance. pub fn to_request(mut self) -> Request { let mut req = self.finish(); req.head_mut().peer_addr = self.peer_addr; req } - /// Complete request creation and generate `ServiceRequest` instance + /// Finalizes request creation and returns `ServiceRequest` instance. pub fn to_srv_request(mut self) -> ServiceRequest { let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; @@ -279,12 +286,12 @@ impl TestRequest { ) } - /// Complete request creation and generate `ServiceResponse` instance + /// Finalizes request creation and returns `ServiceResponse` instance. pub fn to_srv_response(self, res: HttpResponse) -> ServiceResponse { self.to_srv_request().into_response(res) } - /// Complete request creation and generate `HttpRequest` instance + /// Finalizes request creation and returns `HttpRequest` instance. pub fn to_http_request(mut self) -> HttpRequest { let (mut head, _) = self.finish().into_parts(); head.peer_addr = self.peer_addr; @@ -302,7 +309,7 @@ impl TestRequest { ) } - /// Complete request creation and generate `HttpRequest` and `Payload` instances + /// Finalizes request creation and returns `HttpRequest` and `Payload` pair. pub fn to_http_parts(mut self) -> (HttpRequest, Payload) { let (mut head, payload) = self.finish().into_parts(); head.peer_addr = self.peer_addr; @@ -322,7 +329,7 @@ impl TestRequest { (req, payload) } - /// Complete request creation, calls service and waits for response future completion. + /// Finalizes request creation, calls service, and waits for response future completion. pub async fn send_request(self, app: &S) -> S::Response where S: Service, Error = E>, @@ -343,7 +350,7 @@ mod tests { use std::time::SystemTime; use super::*; - use crate::{http::header, test::init_service, web, App, Error, HttpResponse, Responder}; + use crate::{http::header, test::init_service, web, App, Error, Responder}; #[actix_rt::test] async fn test_basics() { diff --git a/actix-web/src/types/either.rs b/actix-web/src/types/either.rs index db244fd9a..7883e89f6 100644 --- a/actix-web/src/types/either.rs +++ b/actix-web/src/types/either.rs @@ -287,10 +287,7 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::{ - test::TestRequest, - web::{Form, Json}, - }; + use crate::test::TestRequest; #[derive(Debug, Clone, Serialize, Deserialize)] struct TestForm { diff --git a/actix-web/src/types/form.rs b/actix-web/src/types/form.rs index 7096b1e9c..d6381b990 100644 --- a/actix-web/src/types/form.rs +++ b/actix-web/src/types/form.rs @@ -418,7 +418,7 @@ mod tests { use super::*; use crate::{ http::{ - header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, + header::{HeaderValue, CONTENT_TYPE}, StatusCode, }, test::{assert_body_eq, TestRequest}, diff --git a/actix-web/src/types/header.rs b/actix-web/src/types/header.rs index 8b1740e6c..977dc032e 100644 --- a/actix-web/src/types/header.rs +++ b/actix-web/src/types/header.rs @@ -2,7 +2,7 @@ use std::{fmt, ops}; -use actix_utils::future::{err, ok, Ready}; +use actix_utils::future::{ready, Ready}; use crate::{ dev::Payload, error::ParseError, extract::FromRequest, http::header::Header as ParseHeader, @@ -66,8 +66,8 @@ where #[inline] fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { match ParseHeader::parse(req) { - Ok(header) => ok(Header(header)), - Err(e) => err(e), + Ok(header) => ready(Ok(Header(header))), + Err(err) => ready(Err(err)), } } } diff --git a/actix-web/src/types/json.rs b/actix-web/src/types/json.rs index 59c95da4c..6b75c0cfe 100644 --- a/actix-web/src/types/json.rs +++ b/actix-web/src/types/json.rs @@ -328,14 +328,19 @@ impl JsonBody { ctype_required: bool, ) -> Self { // check content-type - let can_parse_json = if let Ok(Some(mime)) = req.mime_type() { - mime.subtype() == mime::JSON - || mime.suffix() == Some(mime::JSON) - || ctype_fn.map_or(false, |predicate| predicate(mime)) - } else { - // if `ctype_required` is false, assume payload is - // json even when content-type header is missing - !ctype_required + let can_parse_json = match (ctype_required, req.mime_type()) { + (true, Ok(Some(mime))) => { + mime.subtype() == mime::JSON + || mime.suffix() == Some(mime::JSON) + || ctype_fn.map_or(false, |predicate| predicate(mime)) + } + + // if content-type is expected but not parsable as mime type, bail + (true, _) => false, + + // if content-type validation is disabled, assume payload is JSON + // even when content-type header is missing or invalid mime type + (false, _) => true, }; if !can_parse_json { @@ -725,6 +730,25 @@ mod tests { assert!(s.is_ok()) } + #[actix_rt::test] + async fn test_json_ignoring_content_type() { + let (req, mut pl) = TestRequest::default() + .insert_header(( + header::CONTENT_LENGTH, + header::HeaderValue::from_static("16"), + )) + .insert_header(( + header::CONTENT_TYPE, + header::HeaderValue::from_static("invalid/value"), + )) + .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() diff --git a/actix-web/src/types/payload.rs b/actix-web/src/types/payload.rs index abb4e6b7f..e4db37d0b 100644 --- a/actix-web/src/types/payload.rs +++ b/actix-web/src/types/payload.rs @@ -440,13 +440,11 @@ impl Future for HttpMessageBody { #[cfg(test)] mod tests { - use bytes::Bytes; - use super::*; use crate::{ - http::{header, StatusCode}, + http::StatusCode, test::{call_service, init_service, read_body, TestRequest}, - web, App, Responder, + App, Responder, }; #[actix_rt::test] diff --git a/actix-web/tests/compression.rs b/actix-web/tests/compression.rs index b911b9d1f..61ff1bff5 100644 --- a/actix-web/tests/compression.rs +++ b/actix-web/tests/compression.rs @@ -96,7 +96,7 @@ async fn negotiate_encoding_gzip() { let req = srv .post("/static") - .insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd")) + .insert_header((header::ACCEPT_ENCODING, "gzip, br;q=0.8, zstd;q=0.5")) .send(); let mut res = req.await.unwrap(); @@ -109,7 +109,7 @@ async fn negotiate_encoding_gzip() { let mut res = srv .post("/static") .no_decompress() - .insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd")) + .insert_header((header::ACCEPT_ENCODING, "gzip, br;q=0.8, zstd;q=0.5")) .send() .await .unwrap(); @@ -123,9 +123,11 @@ async fn negotiate_encoding_gzip() { async fn negotiate_encoding_br() { let srv = test_server!(); + // check that brotli content-encoding header is returned + let req = srv .post("/static") - .insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip")) + .insert_header((header::ACCEPT_ENCODING, "br, zstd, gzip")) .send(); let mut res = req.await.unwrap(); @@ -135,10 +137,26 @@ async fn negotiate_encoding_br() { let bytes = res.body().await.unwrap(); assert_eq!(bytes, Bytes::from_static(LOREM)); + // check that brotli is preferred even when later in (q-less) list + + let req = srv + .post("/static") + .insert_header((header::ACCEPT_ENCODING, "gzip, zstd, br")) + .send(); + + let mut res = req.await.unwrap(); + assert_eq!(res.status(), StatusCode::OK); + assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "br"); + + let bytes = res.body().await.unwrap(); + assert_eq!(bytes, Bytes::from_static(LOREM)); + + // check that returned content is actually brotli encoded + let mut res = srv .post("/static") .no_decompress() - .insert_header((header::ACCEPT_ENCODING, "br,zstd,gzip")) + .insert_header((header::ACCEPT_ENCODING, "br, zstd, gzip")) .send() .await .unwrap(); @@ -154,7 +172,7 @@ async fn negotiate_encoding_zstd() { let req = srv .post("/static") - .insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br")) + .insert_header((header::ACCEPT_ENCODING, "zstd, gzip, br;q=0.8")) .send(); let mut res = req.await.unwrap(); @@ -167,7 +185,7 @@ async fn negotiate_encoding_zstd() { let mut res = srv .post("/static") .no_decompress() - .insert_header((header::ACCEPT_ENCODING, "zstd,gzip,br")) + .insert_header((header::ACCEPT_ENCODING, "zstd, gzip, br;q=0.8")) .send() .await .unwrap(); @@ -207,7 +225,7 @@ async fn gzip_no_decompress() { // don't decompress response body .no_decompress() // signal that we want a compressed body - .insert_header((header::ACCEPT_ENCODING, "gzip,br,zstd")) + .insert_header((header::ACCEPT_ENCODING, "gzip, br;q=0.8, zstd;q=0.5")) .send(); let mut res = req.await.unwrap(); diff --git a/actix-web/tests/test_httpserver.rs b/actix-web/tests/test_httpserver.rs index 861d76d93..039c0ffbc 100644 --- a/actix-web/tests/test_httpserver.rs +++ b/actix-web/tests/test_httpserver.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; @@ -66,9 +64,11 @@ fn ssl_acceptor() -> openssl::ssl::SslAcceptorBuilder { 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); diff --git a/actix-web/tests/test_server.rs b/actix-web/tests/test_server.rs index 8ce889396..960cf1e2b 100644 --- a/actix-web/tests/test_server.rs +++ b/actix-web/tests/test_server.rs @@ -1,6 +1,6 @@ #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; -#[cfg(feature = "rustls")] +#[cfg(feature = "rustls-0_23")] extern crate tls_rustls as rustls; use std::{ @@ -34,9 +34,11 @@ const STR: &str = const_str::repeat!(S, 100); #[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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); @@ -704,34 +706,32 @@ async fn test_brotli_encoding_large_openssl() { srv.stop().await; } -#[cfg(feature = "rustls")] +#[cfg(feature = "rustls-0_23")] mod plus_rustls { use std::io::BufReader; - use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig}; + use rustls::{pki_types::PrivateKeyDer, ServerConfig as RustlsServerConfig}; use rustls_pemfile::{certs, pkcs8_private_keys}; use super::*; 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_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 cert_chain = certs(cert_file).collect::, _>>().unwrap(); + let mut keys = pkcs8_private_keys(key_file) + .collect::, _>>() + .unwrap(); RustlsServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() - .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .with_single_cert(cert_chain, PrivateKeyDer::Pkcs8(keys.remove(0))) .unwrap() } @@ -743,7 +743,7 @@ mod plus_rustls { .map(char::from) .collect::(); - let srv = actix_test::start_with(actix_test::config().rustls(tls_config()), || { + let srv = actix_test::start_with(actix_test::config().rustls_0_23(tls_config()), || { App::new().service(web::resource("/").route(web::to(|bytes: Bytes| async { // echo decompressed request body back in response HttpResponse::Ok() diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 3429a84d7..54c5e9869 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,22 +1,44 @@ # Changes -## Unreleased - 2023-xx-xx +## Unreleased +## 3.5.0 + +- Add `rustls-0_23`, `rustls-0_23-webpki-roots`, and `rustls-0_23-native-roots` crate features. +- Add `awc::Connector::rustls_0_23()` constructor. +- Fix `rustls-0_22-native-roots` root store lookup +- Update `brotli` dependency to `6`. +- Minimum supported Rust version (MSRV) is now 1.72. + +## 3.4.0 + +- Add `rustls-0_22-webpki-roots` and `rustls-0_22-native-roots` crate feature. +- Add `awc::Connector::rustls_0_22()` method. + +## 3.3.0 + +- Update `trust-dns-resolver` dependency to `0.23`. +- Updated `zstd` dependency to `0.13`. + +## 3.2.0 + +- Add `awc::Connector::rustls_021()` method for Rustls v0.21 support behind new `rustls-0_21` crate feature. +- Add `rustls-0_20` crate feature, which the existing `rustls` feature now aliases. - Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency. -## 3.1.1 - 2023-02-26 +## 3.1.1 ### Changed - `client::Connect` is now public to allow tunneling connection with `client::Connector`. -## 3.1.0 - 2023-01-21 +## 3.1.0 ### Changed - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. -## 3.0.1 - 2022-08-25 +## 3.0.1 ### Changed @@ -28,7 +50,7 @@ [#2840]: https://github.com/actix/actix-web/pull/2840 -## 3.0.0 - 2022-03-07 +## 3.0.0 ### Dependencies @@ -130,23 +152,23 @@
3.0.0 Pre-Releases -## 3.0.0-beta.21 - 2022-02-16 +## 3.0.0-beta.21 - No significant changes since `3.0.0-beta.20`. -## 3.0.0-beta.20 - 2022-01-31 +## 3.0.0-beta.20 - No significant changes since `3.0.0-beta.19`. -## 3.0.0-beta.19 - 2022-01-21 +## 3.0.0-beta.19 - No significant changes since `3.0.0-beta.18`. -## 3.0.0-beta.18 - 2022-01-04 +## 3.0.0-beta.18 - Minimum supported Rust version (MSRV) is now 1.54. -## 3.0.0-beta.17 - 2021-12-29 +## 3.0.0-beta.17 ### Changed @@ -159,7 +181,7 @@ [#2555]: https://github.com/actix/actix-web/pull/2555 [`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html -## 3.0.0-beta.16 - 2021-12-29 +## 3.0.0-beta.16 - `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] - `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] @@ -167,7 +189,7 @@ [#2553]: https://github.com/actix/actix-web/pull/2553 -## 3.0.0-beta.15 - 2021-12-27 +## 3.0.0-beta.15 - Rename `Connector::{ssl => openssl}`. [#2503] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] @@ -180,37 +202,37 @@ [#2503]: https://github.com/actix/actix-web/pull/2503 [#2546]: https://github.com/actix/actix-web/pull/2546 -## 3.0.0-beta.14 - 2021-12-17 +## 3.0.0-beta.14 - Add `ClientBuilder::add_default_header` and deprecate `ClientBuilder::header`. [#2510] [#2510]: https://github.com/actix/actix-web/pull/2510 -## 3.0.0-beta.13 - 2021-12-11 +## 3.0.0-beta.13 - No significant changes since `3.0.0-beta.12`. -## 3.0.0-beta.12 - 2021-11-30 +## 3.0.0-beta.12 - 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 +## 3.0.0-beta.11 - No significant changes from `3.0.0-beta.10`. -## 3.0.0-beta.10 - 2021-11-15 +## 3.0.0-beta.10 - No significant changes from `3.0.0-beta.9`. -## 3.0.0-beta.9 - 2021-10-20 +## 3.0.0-beta.9 - Updated rustls to v0.20. [#2414] [#2414]: https://github.com/actix/actix-web/pull/2414 -## 3.0.0-beta.8 - 2021-09-09 +## 3.0.0-beta.8 ### Changed @@ -218,7 +240,7 @@ [#2310]: https://github.com/actix/actix-web/pull/2310 -## 3.0.0-beta.7 - 2021-06-26 +## 3.0.0-beta.7 ### Changed @@ -226,11 +248,11 @@ [#2250]: https://github.com/actix/actix-web/pull/2250 -## 3.0.0-beta.6 - 2021-06-17 +## 3.0.0-beta.6 - No significant changes since 3.0.0-beta.5. -## 3.0.0-beta.5 - 2021-04-17 +## 3.0.0-beta.5 ### Removed @@ -238,7 +260,7 @@ [#2148]: https://github.com/actix/actix-web/pull/2148 -## 3.0.0-beta.4 - 2021-04-02 +## 3.0.0-beta.4 ### Added @@ -255,7 +277,7 @@ [#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 +## 3.0.0-beta.3 ### Added @@ -278,7 +300,7 @@ [#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 +## 3.0.0-beta.2 ### Added @@ -301,7 +323,7 @@ [#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 +## 3.0.0-beta.1 ### Changed @@ -313,13 +335,13 @@
-## 2.0.3 - 2020-11-29 +## 2.0.3 ### Fixed - Ensure `actix-http` dependency uses same `serde_urlencoded`. -## 2.0.2 - 2020-11-25 +## 2.0.2 ### Changed @@ -327,7 +349,7 @@ [#1773]: https://github.com/actix/actix-web/pull/1773 -## 2.0.1 - 2020-10-30 +## 2.0.1 ### Changed @@ -342,37 +364,37 @@ [#1760]: https://github.com/actix/actix-web/pull/1760 [#1744]: https://github.com/actix/actix-web/pull/1744 -## 2.0.0 - 2020-09-11 +## 2.0.0 ### Changed - `Client::build` was renamed to `Client::builder`. -## 2.0.0-beta.4 - 2020-09-09 +## 2.0.0-beta.4 ### Changed - Update actix-codec & actix-tls dependencies. -## 2.0.0-beta.3 - 2020-08-17 +## 2.0.0-beta.3 ### Changed - Update `rustls` to 0.18 -## 2.0.0-beta.2 - 2020-07-21 +## 2.0.0-beta.2 ### Changed - Update `actix-http` dependency to 2.0.0-beta.2 -## [2.0.0-beta.1] - 2020-07-14 +## 2.0.0-beta.1 ### Changed - Update `actix-http` dependency to 2.0.0-beta.1 -## [2.0.0-alpha.2] - 2020-05-21 +## 2.0.0-alpha.2 ### Changed @@ -382,42 +404,42 @@ [#1422]: https://github.com/actix/actix-web/pull/1422 -## [2.0.0-alpha.1] - 2020-03-11 +## 2.0.0-alpha.1 - Update `actix-http` dependency to 2.0.0-alpha.2 - Update `rustls` dependency to 0.17 - ClientBuilder accepts initial_window_size and initial_connection_window_size HTTP2 configuration - ClientBuilder allowing to set max_http_version to limit HTTP version to be used -## [1.0.1] - 2019-12-15 +## 1.0.1 - Fix compilation with default features off -## [1.0.0] - 2019-12-13 +## 1.0.0 - Release -## [1.0.0-alpha.3] +## 1.0.0-alpha.3 - Migrate to `std::future` -## [0.2.8] - 2019-11-06 +## 0.2.8 - Add support for setting query from Serialize type for client request. -## [0.2.7] - 2019-09-25 +## 0.2.7 ### Added - Remaining getter methods for `ClientRequest`'s private `head` field #1101 -## [0.2.6] - 2019-09-12 +## 0.2.6 ### Added - Export frozen request related types. -## [0.2.5] - 2019-09-11 +## 0.2.5 ### Added @@ -427,7 +449,7 @@ - Ensure that the `Host` header is set when initiating a WebSocket client connection. -## [0.2.4] - 2019-08-13 +## 0.2.4 ### Changed @@ -435,13 +457,13 @@ - Update serde_urlencoded to "0.6.1" -## [0.2.3] - 2019-08-01 +## 0.2.3 ### Added - Add `rustls` support -## [0.2.2] - 2019-07-01 +## 0.2.2 ### Changed @@ -449,13 +471,13 @@ - Upgrade `rand` dependency version to 0.7 -## [0.2.1] - 2019-06-05 +## 0.2.1 ### Added - Add license files -## [0.2.0] - 2019-05-12 +## 0.2.0 ### Added @@ -465,7 +487,7 @@ - Upgrade actix-http dependency. -## [0.1.1] - 2019-04-19 +## 0.1.1 ### Added @@ -475,17 +497,17 @@ - `ClientRequest::if_true()` and `ClientRequest::if_some()` use instance instead of ref -## [0.1.0] - 2019-04-16 +## 0.1.0 - No changes -## [0.1.0-alpha.6] - 2019-04-14 +## 0.1.0-alpha.6 ### Changed - Do not set default headers for websocket request -## [0.1.0-alpha.5] - 2019-04-12 +## 0.1.0-alpha.5 ### Changed @@ -495,13 +517,13 @@ - Add Debug impl for BoxedSocket -## [0.1.0-alpha.4] - 2019-04-08 +## 0.1.0-alpha.4 ### Changed - Update actix-http dependency -## [0.1.0-alpha.3] - 2019-04-02 +## 0.1.0-alpha.3 ### Added @@ -517,7 +539,7 @@ - Renamed `ClientRequest::close_connection()` to `ClientRequest::force_close()` -## [0.1.0-alpha.2] - 2019-03-29 +## 0.1.0-alpha.2 ### Added @@ -535,6 +557,6 @@ - Export `ws` sub-module with websockets related types -## [0.1.0-alpha.1] - 2019-03-28 +## 0.1.0-alpha.1 - Initial impl diff --git a/awc/Cargo.toml b/awc/Cargo.toml index daec84ab9..6ab408ea6 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.1.1" +version = "3.5.0" authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] @@ -11,7 +11,7 @@ categories = [ "web-programming::websocket", ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" +repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" edition = "2021" @@ -20,17 +20,41 @@ name = "awc" path = "src/lib.rs" [package.metadata.docs.rs] -# features that docs.rs will build with -features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] +rustdoc-args = ["--cfg", "docsrs"] +features = [ + "cookies", + "openssl", + "rustls-0_20", + "rustls-0_21", + "rustls-0_22-webpki-roots", + "rustls-0_23-webpki-roots", + "compress-brotli", + "compress-gzip", + "compress-zstd", +] [features] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] -# openssl +# TLS via OpenSSL openssl = ["tls-openssl", "actix-tls/openssl"] -# rustls -rustls = ["tls-rustls", "actix-tls/rustls"] +# TLS via Rustls v0.20 +rustls = ["rustls-0_20"] +# TLS via Rustls v0.20 +rustls-0_20 = ["tls-rustls-0_20", "actix-tls/rustls-0_20"] +# TLS via Rustls v0.21 +rustls-0_21 = ["tls-rustls-0_21", "actix-tls/rustls-0_21"] +# TLS via Rustls v0.22 (WebPKI roots) +rustls-0_22-webpki-roots = ["tls-rustls-0_22", "actix-tls/rustls-0_22-webpki-roots"] +# TLS via Rustls v0.22 (Native roots) +rustls-0_22-native-roots = ["tls-rustls-0_22", "actix-tls/rustls-0_22-native-roots"] +# TLS via Rustls v0.23 +rustls-0_23 = ["tls-rustls-0_23", "actix-tls/rustls-0_23"] +# TLS via Rustls v0.23 (WebPKI roots) +rustls-0_23-webpki-roots = ["rustls-0_23", "actix-tls/rustls-0_23-webpki-roots"] +# TLS via Rustls v0.23 (Native roots) +rustls-0_23-native-roots = ["rustls-0_23", "actix-tls/rustls-0_23-native-roots"] # Brotli algorithm content-encoding support compress-brotli = ["actix-http/compress-brotli", "__compress"] @@ -39,13 +63,13 @@ 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"] +# Cookie parsing and cookie jar +cookies = ["dep:cookie"] -# trust-dns as dns resolver +# Use `trust-dns-resolver` crate as DNS resolver trust-dns = ["trust-dns-resolver"] -# Internal (PRIVATE!) features used to aid testing and cheking feature status. +# Internal (PRIVATE!) features used to aid testing and checking feature status. # Don't rely on these whatsoever. They may disappear at anytime. __compress = [] @@ -57,18 +81,18 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.5" actix-service = "2" -actix-http = { version = "3.3", features = ["http2", "ws"] } +actix-http = { version = "3.7", features = ["http2", "ws"] } actix-rt = { version = "2.1", default-features = false } -actix-tls = { version = "3", features = ["connect", "uri"] } +actix-tls = { version = "3.4", features = ["connect", "uri"] } actix-utils = "3" -base64 = "0.21" +base64 = "0.22" bytes = "1" cfg-if = "1" derive_more = "0.99.5" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc", "sink"] } -h2 = "0.3.17" +h2 = "0.3.26" http = "0.2.7" itoa = "1" log =" 0.4" @@ -84,30 +108,34 @@ tokio = { version = "1.24.2", features = ["sync"] } cookie = { version = "0.16", features = ["percent-encode"], optional = true } tls-openssl = { package = "openssl", version = "0.10.55", optional = true } -tls-rustls = { package = "rustls", version = "0.20", optional = true, features = ["dangerous_configuration"] } +tls-rustls-0_20 = { package = "rustls", version = "0.20", optional = true, features = ["dangerous_configuration"] } +tls-rustls-0_21 = { package = "rustls", version = "0.21", optional = true, features = ["dangerous_configuration"] } +tls-rustls-0_22 = { package = "rustls", version = "0.22", optional = true } +tls-rustls-0_23 = { package = "rustls", version = "0.23", optional = true, default-features = false } -trust-dns-resolver = { version = "0.22", optional = true } +trust-dns-resolver = { version = "0.23", optional = true } [dev-dependencies] -actix-http = { version = "3", features = ["openssl"] } +actix-http = { version = "3.7", features = ["openssl"] } actix-http-test = { version = "3", features = ["openssl"] } actix-server = "2" -actix-test = { version = "0.1", features = ["openssl", "rustls"] } -actix-tls = { version = "3", features = ["openssl", "rustls"] } +actix-test = { version = "0.1", features = ["openssl", "rustls-0_23"] } +actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23"] } actix-utils = "3" actix-web = { version = "4", features = ["openssl"] } -brotli = "3.3.3" +brotli = "6" const-str = "0.5" -env_logger = "0.10" +env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false } static_assertions = "1.1" -rcgen = "0.11" -rustls-pemfile = "1" +rcgen = "0.13" +rustls-pemfile = "2" tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } -zstd = "0.12" +zstd = "0.13" +tls-rustls-0_23 = { package = "rustls", version = "0.23" } # add rustls 0.23 with default features to make aws_lc_rs work in tests [[example]] name = "client" -required-features = ["rustls"] +required-features = ["rustls-0_23-webpki-roots"] diff --git a/awc/README.md b/awc/README.md index f2a9c7559..8e7b42812 100644 --- a/awc/README.md +++ b/awc/README.md @@ -1,20 +1,22 @@ -# awc (Actix Web Client) +# `awc` (Actix Web Client) > Async HTTP and WebSocket client library. + + [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) -[![Documentation](https://docs.rs/awc/badge.svg?version=3.1.1)](https://docs.rs/awc/3.1.1) +[![Documentation](https://docs.rs/awc/badge.svg?version=3.5.0)](https://docs.rs/awc/3.5.0) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) -[![Dependency Status](https://deps.rs/crate/awc/3.1.1/status.svg)](https://deps.rs/crate/awc/3.1.1) +[![Dependency Status](https://deps.rs/crate/awc/3.5.0/status.svg)](https://deps.rs/crate/awc/3.5.0) [![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/master/https-tls/awc-https) -- Minimum Supported Rust Version (MSRV): 1.68 +## Examples -## Example +[Example project using TLS-enabled client →](https://github.com/actix/examples/tree/master/https-tls/awc-https) + +Basic usage: ```rust use actix_rt::System; diff --git a/awc/examples/client.rs b/awc/examples/client.rs index 26edcfd62..16ad330b8 100644 --- a/awc/examples/client.rs +++ b/awc/examples/client.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use std::error::Error as StdError; #[tokio::main] diff --git a/awc/src/builder.rs b/awc/src/builder.rs index a54960382..5aae394f8 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -37,6 +37,12 @@ pub struct ClientBuilder { } impl ClientBuilder { + /// Create a new ClientBuilder with default settings + /// + /// Note: If the `rustls-0_23` feature is enabled and neither `rustls-0_23-native-roots` nor + /// `rustls-0_23-webpki-roots` are enabled, this ClientBuilder will build without TLS. In order + /// to enable TLS in this scenario, a custom `Connector` _must_ be added to the builder before + /// finishing construction. #[allow(clippy::new_ret_no_self)] pub fn new() -> ClientBuilder< impl Service< diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 64075eae8..8164e2b59 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -380,8 +380,6 @@ mod test { use std::{ future::Future, net, - pin::Pin, - task::{Context, Poll}, time::{Duration, Instant}, }; @@ -394,6 +392,8 @@ mod test { #[actix_rt::test] async fn test_h2_connection_drop() { + env_logger::try_init().ok(); + let addr = "127.0.0.1:0".parse::().unwrap(); let listener = net::TcpListener::bind(addr).unwrap(); let local = listener.local_addr().unwrap(); @@ -428,8 +428,15 @@ mod test { 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 + match this.interval.poll_tick(cx) { + Poll::Ready(_) => { + // prevents spurious test hang + this.interval.reset(); + + Poll::Pending + } + Poll::Pending => Poll::Pending, + } } } Err(_) => Poll::Ready(()), diff --git a/awc/src/client/connector.rs b/awc/src/client/connector.rs index 1ecaf9947..f3d443070 100644 --- a/awc/src/client/connector.rs +++ b/awc/src/client/connector.rs @@ -40,23 +40,38 @@ enum OurTlsConnector { /// Provided because building the OpenSSL context on newer versions can be very slow. /// This prevents unnecessary calls to `.build()` while constructing the client connector. #[cfg(feature = "openssl")] - #[allow(dead_code)] // false positive; used in build_ssl + #[allow(dead_code)] // false positive; used in build_tls OpensslBuilder(actix_tls::connect::openssl::reexports::SslConnectorBuilder), - #[cfg(feature = "rustls")] - Rustls(std::sync::Arc), + #[cfg(feature = "rustls-0_20")] + #[allow(dead_code)] // false positive; used in build_tls + Rustls020(std::sync::Arc), + + #[cfg(feature = "rustls-0_21")] + #[allow(dead_code)] // false positive; used in build_tls + Rustls021(std::sync::Arc), + + #[cfg(any( + feature = "rustls-0_22-webpki-roots", + feature = "rustls-0_22-native-roots", + ))] + #[allow(dead_code)] // false positive; used in build_tls + Rustls022(std::sync::Arc), + + #[cfg(feature = "rustls-0_23")] + #[allow(dead_code)] // false positive; used in build_tls + Rustls023(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. +/// The `Connector` type uses a builder-like combinator pattern for service construction that +/// finishes by calling the `.finish()` method. /// -/// ```ignore +/// ```no_run /// use std::time::Duration; -/// use actix_http::client::Connector; /// -/// let connector = Connector::new() +/// let connector = awc::Connector::new() /// .timeout(Duration::from_secs(5)) /// .finish(); /// ``` @@ -69,6 +84,14 @@ pub struct Connector { } impl Connector<()> { + /// Create a new connector with default TLS settings + /// + /// # Panics + /// + /// - When the `rustls-0_23-webpki-roots` or `rustls-0_23-native-roots` features are enabled + /// and no default crypto provider has been loaded, this method will panic. + /// - When the `rustls-0_23-native-roots` or `rustls-0_22-native-roots` features are enabled + /// and the runtime system has no native root certificates, this method will panic. #[allow(clippy::new_ret_no_self, clippy::let_unit_value)] pub fn new() -> Connector< impl Service< @@ -80,56 +103,114 @@ impl Connector<()> { Connector { connector: TcpConnector::new(resolver::resolver()).service(), config: ConnectorConfig::default(), - tls: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), + tls: Self::build_tls(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 - } + cfg_if::cfg_if! { + if #[cfg(any(feature = "rustls-0_23-webpki-roots", feature = "rustls-0_23-native-roots"))] { + /// Build TLS connector with Rustls v0.23, based on supplied ALPN protocols. + /// + /// Note that if other TLS crate features are enabled, Rustls v0.23 will be used. + fn build_tls(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::rustls_0_23::{self, reexports::ClientConfig}; - /// 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}; + cfg_if::cfg_if! { + if #[cfg(feature = "rustls-0_23-webpki-roots")] { + let certs = rustls_0_23::webpki_roots_cert_store(); + } else if #[cfg(feature = "rustls-0_23-native-roots")] { + let certs = rustls_0_23::native_roots_cert_store().expect("Failed to find native root certificates"); + } + } - let mut config = ClientConfig::builder() - .with_safe_defaults() - .with_root_certificates(webpki_roots_cert_store()) - .with_no_client_auth(); + let mut config = ClientConfig::builder() + .with_root_certificates(certs) + .with_no_client_auth(); - config.alpn_protocols = protocols; + config.alpn_protocols = protocols; - OurTlsConnector::Rustls(std::sync::Arc::new(config)) - } + OurTlsConnector::Rustls023(std::sync::Arc::new(config)) + } + } else if #[cfg(any(feature = "rustls-0_22-webpki-roots", feature = "rustls-0_22-native-roots"))] { + /// Build TLS connector with Rustls v0.22, based on supplied ALPN protocols. + fn build_tls(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::rustls_0_22::{self, reexports::ClientConfig}; - /// 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}; + cfg_if::cfg_if! { + if #[cfg(feature = "rustls-0_22-webpki-roots")] { + let certs = rustls_0_22::webpki_roots_cert_store(); + } else if #[cfg(feature = "rustls-0_22-native-roots")] { + let certs = rustls_0_22::native_roots_cert_store().expect("Failed to find native root certificates"); + } + } - 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 config = ClientConfig::builder() + .with_root_certificates(certs) + .with_no_client_auth(); + + config.alpn_protocols = protocols; + + OurTlsConnector::Rustls022(std::sync::Arc::new(config)) + } + } else if #[cfg(feature = "rustls-0_21")] { + /// Build TLS connector with Rustls v0.21, based on supplied ALPN protocols. + fn build_tls(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::rustls_0_21::{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::Rustls021(std::sync::Arc::new(config)) + } + } else if #[cfg(feature = "rustls-0_20")] { + /// Build TLS connector with Rustls v0.20, based on supplied ALPN protocols. + fn build_tls(protocols: Vec>) -> OurTlsConnector { + use actix_tls::connect::rustls_0_20::{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::Rustls020(std::sync::Arc::new(config)) + } + } else if #[cfg(feature = "openssl")] { + /// Build TLS connector with OpenSSL, based on supplied ALPN protocols. + fn build_tls(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::OpensslBuilder(ssl) + } + } else { + /// Provides an empty TLS connector when no TLS feature is enabled, or when only the + /// `rustls-0_23` crate feature is enabled. + fn build_tls(_: Vec>) -> OurTlsConnector { + OurTlsConnector::None + } } - - 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::OpensslBuilder(ssl) } } impl Connector { - /// Use custom connector. + /// Sets custom connector. pub fn connector(self, connector: S1) -> Connector where Io1: ActixStream + fmt::Debug + 'static, @@ -158,21 +239,28 @@ where + Clone + 'static, { - /// Tcp connection timeout, i.e. max time to connect to remote host including dns name - /// resolution. Set to 5 second by default. + /// Sets TCP connection timeout. + /// + /// This is the max time allowed to connect to remote host, including DNS name resolution. + /// + /// By default, the timeout is 5 seconds. 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. + /// Sets TLS handshake timeout. + /// + /// This is the max time allowed to perform the TLS handshake with remote host after TCP + /// connection is established. + /// + /// By default, the timeout is 5 seconds. pub fn handshake_timeout(mut self, timeout: Duration) -> Self { self.config.handshake_timeout = timeout; self } - /// Use custom OpenSSL `SslConnector` instance. + /// Sets custom OpenSSL `SslConnector` instance. #[cfg(feature = "openssl")] pub fn openssl( mut self, @@ -191,13 +279,54 @@ where self } - /// Use custom Rustls `ClientConfig` instance. - #[cfg(feature = "rustls")] + /// Sets custom Rustls v0.20 `ClientConfig` instance. + #[cfg(feature = "rustls-0_20")] pub fn rustls( mut self, - connector: std::sync::Arc, + connector: std::sync::Arc, ) -> Self { - self.tls = OurTlsConnector::Rustls(connector); + self.tls = OurTlsConnector::Rustls020(connector); + self + } + + /// Sets custom Rustls v0.21 `ClientConfig` instance. + #[cfg(feature = "rustls-0_21")] + pub fn rustls_021( + mut self, + connector: std::sync::Arc, + ) -> Self { + self.tls = OurTlsConnector::Rustls021(connector); + self + } + + /// Sets custom Rustls v0.22 `ClientConfig` instance. + #[cfg(any( + feature = "rustls-0_22-webpki-roots", + feature = "rustls-0_22-native-roots", + ))] + pub fn rustls_0_22( + mut self, + connector: std::sync::Arc, + ) -> Self { + self.tls = OurTlsConnector::Rustls022(connector); + self + } + + /// Sets custom Rustls v0.23 `ClientConfig` instance. + /// + /// In order to enable ALPN, set the `.alpn_protocols` field on the ClientConfig to the + /// following: + /// + /// ```no_run + /// vec![b"h2".to_vec(), b"http/1.1".to_vec()] + /// # ; + /// ``` + #[cfg(feature = "rustls-0_23")] + pub fn rustls_0_23( + mut self, + connector: std::sync::Arc, + ) -> Self { + self.tls = OurTlsConnector::Rustls023(connector); self } @@ -212,12 +341,12 @@ where unimplemented!("actix-http client only supports versions http/1.1 & http/2") } }; - self.tls = Connector::build_ssl(versions); + self.tls = Connector::build_tls(versions); self } - /// Sets the initial window size (in octets) for HTTP/2 stream-level flow control for - /// received data. + /// Sets the initial window size (in bytes) for HTTP/2 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 { @@ -225,7 +354,7 @@ where self } - /// Sets the initial window size (in octets) for HTTP/2 connection-level flow control for + /// Sets the initial window size (in bytes) for HTTP/2 connection-level flow control for /// received data. /// /// The default value is 65,535 and is good for APIs, but not for big objects. @@ -324,6 +453,7 @@ where use actix_tls::connect::Connection; use actix_utils::future::{ready, Ready}; + #[allow(non_local_definitions)] impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let io = self.into_parts().0; @@ -374,6 +504,7 @@ where use actix_tls::connect::openssl::{reexports::AsyncSslStream, TlsConnector}; + #[allow(non_local_definitions)] impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; @@ -405,12 +536,118 @@ where unreachable!("OpenSSL builder is built before this match."); } - #[cfg(feature = "rustls")] - OurTlsConnector::Rustls(tls) => { + #[cfg(feature = "rustls-0_20")] + OurTlsConnector::Rustls020(tls) => { const H2: &[u8] = b"h2"; - use actix_tls::connect::rustls::{reexports::AsyncTlsStream, TlsConnector}; + use actix_tls::connect::rustls_0_20::{reexports::AsyncTlsStream, TlsConnector}; + #[allow(non_local_definitions)] + 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)) + } + + #[cfg(feature = "rustls-0_21")] + OurTlsConnector::Rustls021(tls) => { + const H2: &[u8] = b"h2"; + + use actix_tls::connect::rustls_0_21::{reexports::AsyncTlsStream, TlsConnector}; + + #[allow(non_local_definitions)] + 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)) + } + + #[cfg(any( + feature = "rustls-0_22-webpki-roots", + feature = "rustls-0_22-native-roots", + ))] + OurTlsConnector::Rustls022(tls) => { + const H2: &[u8] = b"h2"; + + use actix_tls::connect::rustls_0_22::{reexports::AsyncTlsStream, TlsConnector}; + + #[allow(non_local_definitions)] + 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)) + } + + #[cfg(feature = "rustls-0_23")] + OurTlsConnector::Rustls023(tls) => { + const H2: &[u8] = b"h2"; + + use actix_tls::connect::rustls_0_23::{reexports::AsyncTlsStream, TlsConnector}; + + #[allow(non_local_definitions)] impl IntoConnectionIo for TcpConnection> { fn into_connection_io(self) -> (Box, Protocol) { let sock = self.into_parts().0; @@ -498,6 +735,17 @@ where /// service for establish tcp connection and do client tls handshake. /// operation is canceled when timeout limit reached. +#[cfg(any( + feature = "dangerous-h2c", + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22-webpki-roots", + feature = "rustls-0_22-native-roots", + feature = "rustls-0_23", + feature = "rustls-0_23-webpki-roots", + feature = "rustls-0_23-native-roots" +))] struct TlsConnectorService { /// TCP connection is canceled on `TcpConnectorInnerService`'s timeout setting. tcp_service: Tcp, @@ -508,6 +756,15 @@ struct TlsConnectorService { timeout: Duration, } +#[cfg(any( + feature = "dangerous-h2c", + feature = "openssl", + feature = "rustls-0_20", + feature = "rustls-0_21", + feature = "rustls-0_22-webpki-roots", + feature = "rustls-0_22-native-roots", + feature = "rustls-0_23", +))] impl Service for TlsConnectorService where Tcp: @@ -789,7 +1046,6 @@ 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, @@ -824,7 +1080,7 @@ mod resolver { // 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); + static TRUST_DNS_RESOLVER: RefCell> = const { RefCell::new(None) }; } // get from thread local or construct a new trust-dns resolver. @@ -837,13 +1093,13 @@ mod 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); + Err(err) => { + log::error!("Trust-DNS can not load system config: {err}"); (ResolverConfig::default(), ResolverOpts::default()) } }; - let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap(); + let resolver = TokioAsyncResolver::tokio(cfg, opts); // box trust dns resolver and put it in thread local. let resolver = Resolver::custom(TrustDnsResolver(resolver)); diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index c756179a4..3f4c9f979 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -56,7 +56,7 @@ where headers.insert(HOST, value); } }, - Err(e) => log::error!("Can not set HOST header {}", e), + Err(err) => log::error!("Can not set HOST header {err}"), } } } diff --git a/awc/src/client/h2proto.rs b/awc/src/client/h2proto.rs index d18db1410..c3f801f20 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -106,9 +106,9 @@ where } let res = poll_fn(|cx| io.poll_ready(cx)).await; - if let Err(e) = res { - io.on_release(e.is_io()); - return Err(SendRequestError::from(e)); + if let Err(err) = res { + io.on_release(err.is_io()); + return Err(SendRequestError::from(err)); } let resp = match io.send_request(req, eof) { @@ -120,9 +120,9 @@ where } fut.await.map_err(SendRequestError::from)? } - Err(e) => { - io.on_release(e.is_io()); - return Err(e.into()); + Err(err) => { + io.on_release(err.is_io()); + return Err(err.into()); } }; @@ -169,8 +169,8 @@ where let len = b.len(); let bytes = b.split_to(std::cmp::min(cap, len)); - if let Err(e) = send.send_data(bytes, false) { - return Err(e.into()); + if let Err(err) = send.send_data(bytes, false) { + return Err(err.into()); } if !b.is_empty() { send.reserve_capacity(b.len()); @@ -179,7 +179,7 @@ where } continue; } - Some(Err(e)) => return Err(e.into()), + Some(Err(err)) => return Err(err.into()), } } } diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 2cf1f3ace..2938353fd 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -374,12 +374,11 @@ impl Acquired { #[cfg(test)] mod test { - use std::{cell::Cell, io}; + use std::cell::Cell; use http::Uri; use super::*; - use crate::client::connection::ConnectionType; /// A stream type that always returns pending on async read. /// diff --git a/awc/src/lib.rs b/awc/src/lib.rs index ce2dfb34f..460480994 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -102,11 +102,11 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] +#![allow(unknown_lints)] // temp: #[allow(non_local_definitions)] #![allow( clippy::type_complexity, clippy::borrow_interior_mutable_const, - clippy::needless_doctest_main, - clippy::uninlined_format_args + clippy::needless_doctest_main )] #![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![doc(html_favicon_url = "https://actix.rs/favicon.ico")] diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index c38d6ad92..0ea5f174e 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -303,10 +303,7 @@ mod tests { use actix_web::{web, App, Error, HttpRequest, HttpResponse}; use super::*; - use crate::{ - http::{header::HeaderValue, StatusCode}, - ClientBuilder, - }; + use crate::{http::header::HeaderValue, ClientBuilder}; #[actix_rt::test] async fn basic_redirect() { diff --git a/awc/src/request.rs b/awc/src/request.rs index 4e34e5771..28ed8b5f5 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -83,7 +83,7 @@ impl ClientRequest { { match Uri::try_from(uri) { Ok(uri) => self.head.uri = uri, - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } self } @@ -152,7 +152,7 @@ impl ClientRequest { Ok((key, value)) => { self.head.headers.insert(key, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }; self @@ -166,7 +166,7 @@ impl ClientRequest { self.head.headers.insert(key, value); } } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }; self @@ -185,7 +185,7 @@ impl ClientRequest { pub fn append_header(mut self, header: impl TryIntoHeaderPair) -> Self { match header.try_into_pair() { Ok((key, value)) => self.head.headers.append(key, value), - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }; self @@ -217,7 +217,7 @@ impl ClientRequest { Ok(value) => { self.head.headers.insert(header::CONTENT_TYPE, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } self } @@ -299,7 +299,7 @@ impl ClientRequest { match Uri::from_parts(parts) { Ok(uri) => self.head.uri = uri, - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } } @@ -311,7 +311,7 @@ impl ClientRequest { pub fn freeze(self) -> Result { let slf = match self.prep_for_sending() { Ok(slf) => slf, - Err(e) => return Err(e.into()), + Err(err) => return Err(err.into()), }; let request = FrozenClientRequest { @@ -332,7 +332,7 @@ impl ClientRequest { { let slf = match self.prep_for_sending() { Ok(slf) => slf, - Err(e) => return e.into(), + Err(err) => return err.into(), }; RequestSender::Owned(slf.head).send_body( @@ -348,7 +348,7 @@ impl ClientRequest { pub fn send_json(self, value: &T) -> SendClientRequest { let slf = match self.prep_for_sending() { Ok(slf) => slf, - Err(e) => return e.into(), + Err(err) => return err.into(), }; RequestSender::Owned(slf.head).send_json( @@ -366,7 +366,7 @@ impl ClientRequest { pub fn send_form(self, value: &T) -> SendClientRequest { let slf = match self.prep_for_sending() { Ok(slf) => slf, - Err(e) => return e.into(), + Err(err) => return err.into(), }; RequestSender::Owned(slf.head).send_form( @@ -386,7 +386,7 @@ impl ClientRequest { { let slf = match self.prep_for_sending() { Ok(slf) => slf, - Err(e) => return e.into(), + Err(err) => return err.into(), }; RequestSender::Owned(slf.head).send_stream( @@ -402,7 +402,7 @@ impl ClientRequest { pub fn send(self) -> SendClientRequest { let slf = match self.prep_for_sending() { Ok(slf) => slf, - Err(e) => return e.into(), + Err(err) => return err.into(), }; RequestSender::Owned(slf.head).send( diff --git a/awc/src/responses/json_body.rs b/awc/src/responses/json_body.rs index 3912324b6..e9c03d81a 100644 --- a/awc/src/responses/json_body.rs +++ b/awc/src/responses/json_body.rs @@ -118,7 +118,7 @@ mod tests { use static_assertions::assert_impl_all; use super::*; - use crate::{http::header, test::TestResponse}; + use crate::test::TestResponse; assert_impl_all!(JsonBody: Unpin); diff --git a/awc/src/responses/response_body.rs b/awc/src/responses/response_body.rs index 8d9d1274a..0ff58341f 100644 --- a/awc/src/responses/response_body.rs +++ b/awc/src/responses/response_body.rs @@ -110,7 +110,7 @@ mod tests { use static_assertions::assert_impl_all; use super::*; - use crate::{http::header, test::TestResponse}; + use crate::test::TestResponse; assert_impl_all!(ResponseBody<()>: Unpin); diff --git a/awc/src/sender.rs b/awc/src/sender.rs index c2191d11b..8de1033a3 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -122,8 +122,8 @@ impl Future for SendClientRequest { Poll::Ready(res) } - SendClientRequest::Err(ref mut e) => match e.take() { - Some(e) => Poll::Ready(Err(e)), + SendClientRequest::Err(ref mut err) => match err.take() { + Some(err) => Poll::Ready(Err(err)), None => panic!("Attempting to call completed future"), }, } @@ -147,8 +147,8 @@ impl Future for SendClientRequest { .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)), + SendClientRequest::Err(ref mut err) => match err.take() { + Some(err) => Poll::Ready(Err(err)), None => panic!("Attempting to call completed future"), }, } @@ -219,8 +219,8 @@ impl RequestSender { Err(err) => return PrepForSendingError::Json(err).into(), }; - if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") { - return e.into(); + if let Err(err) = self.set_header_if_none(header::CONTENT_TYPE, "application/json") { + return err.into(); } self.send_body(addr, response_decompress, timeout, config, body) @@ -291,7 +291,7 @@ impl RequestSender { Ok(value) => { head.headers.insert(key, value); } - Err(e) => return Err(e.into()), + Err(err) => return Err(err.into()), } } } @@ -304,7 +304,7 @@ impl RequestSender { let h = extra_headers.get_or_insert(HeaderMap::new()); h.insert(key, v) } - Err(e) => return Err(e.into()), + Err(err) => return Err(err.into()), }; } } diff --git a/awc/src/test.rs b/awc/src/test.rs index 96ae1f0a1..126583179 100644 --- a/awc/src/test.rs +++ b/awc/src/test.rs @@ -103,7 +103,7 @@ mod tests { use actix_http::header::HttpDate; use super::*; - use crate::{cookie, http::header}; + use crate::http::header; #[test] fn test_basics() { diff --git a/awc/src/ws.rs b/awc/src/ws.rs index 836ff2024..c3340206d 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -64,7 +64,7 @@ pub struct WebsocketsRequest { } impl WebsocketsRequest { - /// Create new WebSocket connection + /// Create new WebSocket connection. pub(crate) fn new(uri: U, config: ClientConfig) -> Self where Uri: TryFrom, @@ -82,7 +82,7 @@ impl WebsocketsRequest { match Uri::try_from(uri) { Ok(uri) => head.uri = uri, - Err(e) => err = Some(e.into()), + Err(error) => err = Some(error.into()), } WebsocketsRequest { @@ -143,7 +143,7 @@ impl WebsocketsRequest { { match HeaderValue::try_from(origin) { Ok(value) => self.origin = Some(value), - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } self } @@ -177,9 +177,9 @@ impl WebsocketsRequest { Ok(value) => { self.head.headers.append(key, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }, - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } self } @@ -196,9 +196,9 @@ impl WebsocketsRequest { Ok(value) => { self.head.headers.insert(key, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), }, - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } self } @@ -217,11 +217,11 @@ impl WebsocketsRequest { Ok(value) => { self.head.headers.insert(key, value); } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } } } - Err(e) => self.err = Some(e.into()), + Err(err) => self.err = Some(err.into()), } self } diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 6d1459ac0..76915630f 100644 --- a/awc/tests/test_client.rs +++ b/awc/tests/test_client.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use std::{ collections::HashMap, convert::Infallible, diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index b3eb97367..a8b7e98c1 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -13,9 +13,11 @@ use openssl::{ }; 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); diff --git a/awc/tests/test_rustls_client.rs b/awc/tests/test_rustls_client.rs index 652997de6..7e832f67d 100644 --- a/awc/tests/test_rustls_client.rs +++ b/awc/tests/test_rustls_client.rs @@ -1,6 +1,6 @@ -#![cfg(feature = "rustls")] +#![cfg(feature = "rustls-0_23-webpki-roots")] -extern crate tls_rustls as rustls; +extern crate tls_rustls_0_23 as rustls; use std::{ io::BufReader, @@ -8,59 +8,85 @@ use std::{ atomic::{AtomicUsize, Ordering}, Arc, }, - time::SystemTime, }; use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; -use actix_tls::connect::rustls::webpki_roots_cert_store; +use actix_tls::connect::rustls_0_23::webpki_roots_cert_store; use actix_utils::future::ok; use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use rustls::{ - client::{ServerCertVerified, ServerCertVerifier}, - Certificate, ClientConfig, PrivateKey, ServerConfig, ServerName, + pki_types::{CertificateDer, PrivateKeyDer, ServerName}, + ClientConfig, ServerConfig, }; 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_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 cert_chain = certs(cert_file).collect::, _>>().unwrap(); + let mut keys = pkcs8_private_keys(key_file) + .collect::, _>>() + .unwrap(); ServerConfig::builder() - .with_safe_defaults() .with_no_client_auth() - .with_single_cert(cert_chain, PrivateKey(keys.remove(0))) + .with_single_cert(cert_chain, PrivateKeyDer::Pkcs8(keys.remove(0))) .unwrap() } mod danger { + use rustls::{ + client::danger::{ServerCertVerified, ServerCertVerifier}, + pki_types::UnixTime, + }; + use super::*; + #[derive(Debug)] pub struct NoCertificateVerification; impl ServerCertVerifier for NoCertificateVerification { fn verify_server_cert( &self, - _end_entity: &Certificate, - _intermediates: &[Certificate], - _server_name: &ServerName, - _scts: &mut dyn Iterator, + _end_entity: &CertificateDer<'_>, + _intermediates: &[CertificateDer<'_>], + _server_name: &ServerName<'_>, _ocsp_response: &[u8], - _now: SystemTime, + _now: UnixTime, ) -> Result { - Ok(ServerCertVerified::assertion()) + Ok(rustls::client::danger::ServerCertVerified::assertion()) + } + + fn verify_tls12_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn verify_tls13_signature( + &self, + _message: &[u8], + _cert: &CertificateDer<'_>, + _dss: &rustls::DigitallySignedStruct, + ) -> Result { + Ok(rustls::client::danger::HandshakeSignatureValid::assertion()) + } + + fn supported_verify_schemes(&self) -> Vec { + rustls::crypto::aws_lc_rs::default_provider() + .signature_verification_algorithms + .supported_schemes() } } } @@ -82,14 +108,13 @@ async fn test_connection_reuse_h2() { App::new().service(web::resource("/").route(web::to(HttpResponse::Ok))), |_| AppConfig::default(), )) - .rustls(tls_config()) + .rustls_0_23(tls_config()) .map_err(|_| ()), ) }) .await; let mut config = ClientConfig::builder() - .with_safe_defaults() .with_root_certificates(webpki_roots_cert_store()) .with_no_client_auth(); @@ -102,7 +127,7 @@ async fn test_connection_reuse_h2() { .set_certificate_verifier(Arc::new(danger::NoCertificateVerification)); let client = awc::Client::builder() - .connector(awc::Connector::new().rustls(Arc::new(config))) + .connector(awc::Connector::new().rustls_0_23(Arc::new(config))) .finish(); // req 1 diff --git a/awc/tests/test_ssl_client.rs b/awc/tests/test_ssl_client.rs index 5273c3fff..95d4c15f1 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -19,9 +19,11 @@ use openssl::{ }; 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 rcgen::CertifiedKey { cert, key_pair } = + rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); + let cert_file = cert.pem(); + let key_file = key_pair.serialize_pem(); + let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); diff --git a/justfile b/justfile new file mode 100644 index 000000000..7f6dbb61e --- /dev/null +++ b/justfile @@ -0,0 +1,76 @@ +_list: + @just --list + +# Format workspace. +fmt: + cargo +nightly fmt + fd --hidden --type=file --extension=md --extension=yml --exec-batch npx -y prettier --write + +# Downgrade dev-dependencies necessary to run MSRV checks/tests. +[private] +downgrade-for-msrv: + cargo update -p=clap --precise=4.4.18 + +msrv := ``` + cargo metadata --format-version=1 \ + | jq -r 'first(.packages[] | select(.source == null and .rust_version)) | .rust_version' \ + | sed -E 's/^1\.([0-9]{2})$/1\.\1\.0/' +``` +msrv_rustup := "+" + msrv + +non_linux_all_features_list := ``` + cargo metadata --format-version=1 \ + | jq '.packages[] | select(.source == null) | .features | keys' \ + | jq -r --slurp \ + --arg exclusions "tokio-uring,io-uring,experimental-io-uring" \ + 'add | unique | . - ($exclusions | split(",")) | join(",")' +``` + +all_crate_features := if os() == "linux" { + "--all-features" +} else { + "--features='" + non_linux_all_features_list + "'" +} + +# Run Clippy over workspace. +clippy toolchain="": + cargo {{ toolchain }} clippy --workspace --all-targets {{ all_crate_features }} + +# Test workspace using MSRV. +test-msrv: downgrade-for-msrv (test msrv_rustup) + +# Test workspace code. +test toolchain="": + cargo {{ toolchain }} test --lib --tests -p=actix-web-codegen --all-features + cargo {{ toolchain }} test --lib --tests -p=actix-multipart-derive --all-features + cargo {{ toolchain }} nextest run -p=actix-router --no-default-features + cargo {{ toolchain }} nextest run --workspace --exclude=actix-web-codegen --exclude=actix-multipart-derive {{ all_crate_features }} --filter-expr="not test(test_reading_deflate_encoding_large_random_rustls)" + +# Test workspace docs. +test-docs toolchain="": && doc + cargo {{ toolchain }} test --doc --workspace {{ all_crate_features }} --no-fail-fast -- --nocapture + +# Test workspace. +test-all toolchain="": (test toolchain) (test-docs toolchain) + +# Test workspace and generate Codecov coverage file. +test-coverage-codecov toolchain="": + cargo {{ toolchain }} llvm-cov --workspace {{ all_crate_features }} --codecov --output-path codecov.json + +# Test workspace and generate LCOV coverage file. +test-coverage-lcov toolchain="": + cargo {{ toolchain }} llvm-cov --workspace {{ all_crate_features }} --lcov --output-path lcov.info + +# Document crates in workspace. +doc *args: + RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --no-deps --workspace {{ all_crate_features }} {{ args }} + +# Document crates in workspace and watch for changes. +doc-watch: + @just doc --open + cargo watch -- just doc + +# Update READMEs from crate root documentation. +update-readmes: && fmt + cd ./actix-files && cargo rdme --force + cd ./actix-router && cargo rdme --force diff --git a/scripts/bump b/scripts/bump index b09d9d196..6fd879eae 100755 --- a/scripts/bump +++ b/scripts/bump @@ -5,7 +5,7 @@ # requires github cli tool for automatic release draft creation -set -euo pipefail +set -eEuo pipefail DIR=$1 @@ -93,9 +93,12 @@ fi # done; remove backup files rm -f $CARGO_MANIFEST.bak -rm -f $CHANGELOG_FILE.bak rm -f $README_FILE.bak +if [ -n "${CHANGELOG_FILE-}" ]; then + rm -f $CHANGELOG_FILE.bak +fi + echo "manifest, changelog, and readme updated" echo echo "check other references:" @@ -110,16 +113,23 @@ read -p "Update all references: (y/N) " UPDATE_REFERENCES UPDATE_REFERENCES="${UPDATE_REFERENCES:-n}" if [ "$UPDATE_REFERENCES" = 'y' ] || [ "$UPDATE_REFERENCES" = 'Y' ]; then + if [[ $NEW_VERSION == *".0.0" ]]; then + NEW_VERSION_SPEC="${NEW_VERSION%.0.0}" + elif [[ $NEW_VERSION == *".0" ]]; then + NEW_VERSION_SPEC="${NEW_VERSION%.0}" + else + NEW_VERSION_SPEC="$NEW_VERSION" + fi for f in $(fd Cargo.toml); do sed -i.bak -E \ - "s/^(${PACKAGE_NAME} ?= ?\")[^\"]+(\")$/\1${NEW_VERSION}\2/g" $f + "s/^(${PACKAGE_NAME} ?= ?\")[^\"]+(\")$/\1${NEW_VERSION_SPEC}\2/g" $f sed -i.bak -E \ - "s/^(${PACKAGE_NAME} ?=.*version ?= ?\")[^\"]+(\".*)$/\1${NEW_VERSION}\2/g" $f + "s/^(${PACKAGE_NAME} ?=.*version ?= ?\")[^\"]+(\".*)$/\1${NEW_VERSION_SPEC}\2/g" $f sed -i.bak -E \ - "s/^(.*package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\")[^\"]+(\".*)$/\1${NEW_VERSION}\2/g" $f + "s/^(.*package ?= ?\"${PACKAGE_NAME}\".*version ?= ?\")[^\"]+(\".*)$/\1${NEW_VERSION_SPEC}\2/g" $f sed -i.bak -E \ - "s/^(.*version ?= ?\")[^\"]+(\".*package ?= ?\"${PACKAGE_NAME}\".*)$/\1${NEW_VERSION}\2/g" $f + "s/^(.*version ?= ?\")[^\"]+(\".*package ?= ?\"${PACKAGE_NAME}\".*)$/\1${NEW_VERSION_SPEC}\2/g" $f # remove backup file rm -f $f.bak @@ -128,11 +138,12 @@ if [ "$UPDATE_REFERENCES" = 'y' ] || [ "$UPDATE_REFERENCES" = 'Y' ]; then fi if [ $MACOS ]; then - printf "prepare $PACKAGE_NAME release $NEW_VERSION" | pbcopy + printf "chore($PACKAGE_NAME): prepare release $NEW_VERSION" | pbcopy + echo "placed the recommended commit message on the clipboard" else echo echo "commit message:" - echo "prepare $PACKAGE_NAME release $NEW_VERSION" + echo "chore($PACKAGE_NAME): prepare release $NEW_VERSION" fi SHORT_PACKAGE_NAME="$(echo $PACKAGE_NAME | sed 's/^actix-web-//' | sed 's/^actix-//')" diff --git a/scripts/free-disk-space.sh b/scripts/free-disk-space.sh new file mode 100755 index 000000000..2946cfcf6 --- /dev/null +++ b/scripts/free-disk-space.sh @@ -0,0 +1,53 @@ +#!/usr/bin/env bash + +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You under the Apache License, Version 2.0 +# (the "License"); you may not use this file except in compliance with +# the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +# The Azure provided machines typically have the following disk allocation: +# Total space: 85GB +# Allocated: 67 GB +# Free: 17 GB +# This script frees up 28 GB of disk space by deleting unneeded packages and +# large directories. +# The Flink end to end tests download and generate more than 17 GB of files, +# causing unpredictable behavior and build failures. + +echo "==============================================================================" +echo "Freeing up disk space on CI system" +echo "==============================================================================" + +echo "Listing 100 largest packages" +dpkg-query -Wf '${Installed-Size}\t${Package}\n' | sort -n | tail -n 100 +df -h + +echo "Removing large packages" +sudo apt-get remove -y '^dotnet-.*' +sudo apt-get remove -y 'php.*' +sudo apt-get remove -y '^mongodb-.*' +sudo apt-get remove -y '^mysql-.*' +sudo apt-get remove -y azure-cli google-cloud-sdk hhvm google-chrome-stable firefox powershell mono-devel libgl1-mesa-dri +sudo apt-get autoremove -y +sudo apt-get clean +df -h + +echo "Removing large directories" +sudo rm -rf /usr/share/dotnet/ +sudo rm -rf /usr/local/graalvm/ +sudo rm -rf /usr/local/.ghcup/ +sudo rm -rf /usr/local/share/powershell +sudo rm -rf /usr/local/share/chromium +sudo rm -rf /usr/local/lib/android +sudo rm -rf /usr/local/lib/node_modules +df -h