diff --git a/.cargo/config.toml b/.cargo/config.toml index deb30074..a2345e18 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/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md index fa06a137..a2684224 100644 --- a/.github/ISSUE_TEMPLATE/bug_report.md +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -3,34 +3,40 @@ name: Bug Report about: Create a bug report. --- -Your issue may already be reported! -Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one. +Your issue may already be reported! Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one. ## Expected Behavior + ## Current Behavior + ## Possible Solution + ## Steps to Reproduce (for bugs) + + 1. 2. 3. 4. ## Context + ## Your Environment + - Rust Version (I.e, output of `rustc -V`): diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index d617cf70..e0d17fb2 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -2,12 +2,14 @@ ## PR Type + + PR_TYPE - ## PR Checklist + @@ -17,11 +19,10 @@ PR_TYPE - [ ] Format code with the latest stable rustfmt. - [ ] (Team) Label with affected crates and semver status. - ## Overview + - diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..c7ecf5ea --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,10 @@ +version: 2 +updates: + - package-ecosystem: cargo + directory: / + schedule: + interval: weekly + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 008c33f8..fd6bc6d7 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -2,28 +2,27 @@ name: Benchmark on: push: - branches: - - master + branches: [master] permissions: - contents: read # to fetch code (actions/checkout) + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: check_benchmark: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - name: Install Rust - uses: actions-rs/toolchain@v1 - with: - toolchain: nightly - profile: minimal - override: true + run: | + rustup set profile minimal + rustup install nightly + rustup override set nightly - name: Check benchmark - uses: actions-rs/cargo@v1 - with: - command: bench - args: --bench=server -- --sample-size=15 + run: cargo bench --bench=server -- --sample-size=15 diff --git a/.github/workflows/ci-post-merge.yml b/.github/workflows/ci-post-merge.yml index 7ac6388d..1729d9a0 100644 --- a/.github/workflows/ci-post-merge.yml +++ b/.github/workflows/ci-post-merge.yml @@ -5,132 +5,87 @@ on: branches: [master] permissions: - contents: read # to fetch code (actions/checkout) + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true jobs: build_and_test_nightly: strategy: fail-fast: false matrix: + # prettier-ignore target: - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } + - { 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 }} - env: - CI: 1 - CARGO_INCREMENTAL: 0 - VCPKGRS_DYNAMIC: 1 - CARGO_UNSTABLE_SPARSE_REGISTRY: true - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Install nasm + if: matrix.target.os == 'windows-latest' + uses: ilammy/setup-nasm@v1.5.1 - # install OpenSSL on Windows - # TODO: GitHub actions docs state that OpenSSL is - # already installed on these Windows machines somewhere - - name: Set vcpkg root - if: matrix.target.triple == 'x86_64-pc-windows-msvc' - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append - name: Install OpenSSL - if: matrix.target.triple == 'x86_64-pc-windows-msvc' - run: vcpkg install openssl:x64-windows + if: matrix.target.os == 'windows-latest' + shell: bash + run: | + 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 ${{ matrix.version }} - uses: actions-rs/toolchain@v1 + - name: Install Rust (${{ matrix.version.name }}) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: - toolchain: ${{ matrix.version }}-${{ matrix.target.triple }} - profile: minimal - override: true + toolchain: ${{ matrix.version.version }} - - name: Install cargo-hack - uses: taiki-e/install-action@cargo-hack - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 + - name: Install 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 - uses: actions-rs/cargo@v1 - with: { command: ci-check-min } + run: cargo ci-check-min - name: check default - uses: actions-rs/cargo@v1 - with: { command: ci-check-default } + run: cargo ci-check-default - name: tests timeout-minutes: 60 - run: | - cargo test --lib --tests -p=actix-router --all-features - cargo test --lib --tests -p=actix-http --all-features - cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls - cargo test --lib --tests -p=actix-web-codegen --all-features - cargo test --lib --tests -p=awc --all-features - cargo test --lib --tests -p=actix-http-test --all-features - cargo test --lib --tests -p=actix-test --all-features - cargo test --lib --tests -p=actix-files - cargo test --lib --tests -p=actix-multipart --all-features - cargo test --lib --tests -p=actix-web-actors --all-features + run: just test - - name: Clear the cargo caches - run: | - cargo install cargo-cache --version 0.8.2 --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 - env: - CI: 1 - CARGO_INCREMENTAL: 0 - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable + - name: Free Disk Space + run: ./scripts/free-disk-space.sh + + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 - name: Install cargo-hack - uses: taiki-e/install-action@cargo-hack - - - name: Generate Cargo.lock - run: cargo generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 + 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 - - env: - CI: 1 - CARGO_INCREMENTAL: 0 - - steps: - - uses: actions/checkout@v2 - - - uses: dtolnay/rust-toolchain@stable - - - name: Install nextest - uses: taiki-e/install-action@nextest - - - name: Generate Cargo.lock - run: cargo generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.3.0 - - - name: Test with cargo-nextest - run: cargo nextest run diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 421becc6..1b6f7b46 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -3,135 +3,119 @@ name: CI on: pull_request: types: [opened, synchronize, reopened] + merge_group: + types: [checks_requested] push: branches: [master] permissions: - contents: read # to fetch code (actions/checkout) + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + 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: + # prettier-ignore target: - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc } + - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } version: - - 1.59.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 }} - env: - CI: 1 - CARGO_INCREMENTAL: 0 - VCPKGRS_DYNAMIC: 1 - steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 + + - name: Install nasm + if: matrix.target.os == 'windows-latest' + uses: ilammy/setup-nasm@v1.5.1 - # install OpenSSL on Windows - # TODO: GitHub actions docs state that OpenSSL is - # already installed on these Windows machines somewhere - - name: Set vcpkg root - if: matrix.target.triple == 'x86_64-pc-windows-msvc' - run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append - name: Install OpenSSL - if: matrix.target.triple == 'x86_64-pc-windows-msvc' - run: vcpkg install openssl:x64-windows + if: matrix.target.os == 'windows-latest' + shell: bash + run: | + 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 ${{ matrix.version }} - uses: actions-rs/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 }}-${{ matrix.target.triple }} - profile: minimal - override: true + 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: | - cargo install cargo-edit --version=0.8.0 - cargo add const-str@0.3 --dev -p=actix-web - cargo add const-str@0.3 --dev -p=awc - - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: workaround MSRV issues - if: matrix.version != 'stable' - run: | - cargo update -p=zstd-sys --precise=2.0.1+zstd.1.5.2 + if: matrix.version.name == 'msrv' + run: just downgrade-for-msrv - name: check minimal - uses: actions-rs/cargo@v1 - with: { command: ci-check-min } + run: cargo ci-check-min - name: check default - uses: actions-rs/cargo@v1 - with: { command: ci-check-default } + run: cargo ci-check-default - name: tests timeout-minutes: 60 - run: | - cargo test --lib --tests -p=actix-router --all-features - cargo test --lib --tests -p=actix-http --all-features - cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls - cargo test --lib --tests -p=actix-web-codegen --all-features - cargo test --lib --tests -p=awc --all-features - cargo test --lib --tests -p=actix-http-test --all-features - cargo test --lib --tests -p=actix-test --all-features - cargo test --lib --tests -p=actix-files - cargo test --lib --tests -p=actix-multipart --all-features - cargo test --lib --tests -p=actix-web-actors --all-features + run: just test - - name: Clear the cargo caches - run: | - cargo install cargo-cache --version 0.8.2 --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@v2 + - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@stable - - - name: Generate Cargo.lock - run: cargo generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.3.0 + - name: Install Rust + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly - name: tests (io-uring) timeout-minutes: 60 run: > - sudo bash -c "ulimit -Sl 512 - && ulimit -Hl 512 - && PATH=$PATH:/usr/share/rust/.cargo/bin - && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features" + sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features" rustdoc: name: doc tests runs-on: ubuntu-latest steps: - - uses: actions/checkout@v2 + - uses: actions/checkout@v4 - - uses: dtolnay/rust-toolchain@nightly + - name: Install Rust (nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly - - name: Generate Cargo.lock - run: cargo generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.3.0 + - 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 e94c4d1a..00000000 --- a/.github/workflows/clippy-fmt.yml +++ /dev/null @@ -1,49 +0,0 @@ -name: Lint - -on: - pull_request: - types: [opened, synchronize, reopened] - -jobs: - fmt: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: dtolnay/rust-toolchain@nightly - with: { components: rustfmt } - - run: cargo fmt --all -- --check - - clippy: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: dtolnay/rust-toolchain@stable - with: { components: clippy } - - - name: Generate Cargo.lock - run: cargo generate-lockfile - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 - - - name: Check with Clippy - uses: actions-rs/clippy-check@v1 - with: - args: --workspace --tests --examples --all-features - token: ${{ secrets.GITHUB_TOKEN }} - - lint-docs: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - - uses: dtolnay/rust-toolchain@stable - with: { components: rust-docs } - - - name: Check for broken intra-doc links - uses: actions-rs/cargo@v1 - env: - RUSTDOCFLAGS: "-D warnings" - with: - command: doc - args: --no-deps --all-features --workspace diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 137a413d..ca311571 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -1,36 +1,40 @@ -# disabled because `cargo tarpaulin` currently segfaults - name: Coverage on: push: branches: [master] +permissions: + 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@v2 + - uses: actions/checkout@v4 - - name: Install stable - uses: actions-rs/toolchain@v1 + - name: Install Rust (nightly) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 with: - toolchain: stable-x86_64-unknown-linux-gnu - profile: minimal - override: true + toolchain: nightly + components: llvm-tools - - name: Generate Cargo.lock - uses: actions-rs/cargo@v1 - with: { command: generate-lockfile } - - name: Cache Dependencies - uses: Swatinem/rust-cache@v1.2.0 + - name: Install just, cargo-llvm-cov, cargo-nextest + uses: taiki-e/install-action@v2.38.0 + with: + tool: just,cargo-llvm-cov,cargo-nextest - - 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@v1 - with: { file: cobertura.xml } + - name: Generate code coverage + run: just test-coverage-codecov + + - 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 00000000..ca9d2bbe --- /dev/null +++ b/.github/workflows/lint.yml @@ -0,0 +1,116 @@ +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 + + check-external-types: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + + - name: Install Rust (nightly-2024-05-01) + uses: actions-rust-lang/setup-rust-toolchain@v1.9.0 + with: + toolchain: nightly-2024-05-01 + + - name: Install just + uses: taiki-e/install-action@v2.38.0 + with: + tool: just + + - name: Install cargo-check-external-types + uses: taiki-e/cache-cargo-install-action@v1.2.2 + with: + tool: cargo-check-external-types + + - name: check external types + run: just check-external-types-all +nightly-2024-05-01 + + 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 (nightly-2024-06-07) + 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 9aadafaf..00000000 --- a/.github/workflows/upload-doc.yml +++ /dev/null @@ -1,32 +0,0 @@ -name: Upload Documentation - -on: - push: - branches: [master] - -permissions: {} -jobs: - build: - permissions: - contents: write # to push changes in repo (jamesives/github-pages-deploy-action) - - runs-on: ubuntu-latest - - steps: - - uses: actions/checkout@v2 - - - 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.1 - with: - folder: target/doc - single-commit: true diff --git a/.gitignore b/.gitignore index 54340326..48ccccb9 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.json b/.prettierrc.json deleted file mode 100644 index 677ba8ef..00000000 --- a/.prettierrc.json +++ /dev/null @@ -1,3 +0,0 @@ -{ - "proseWrap": "never" -} diff --git a/.prettierrc.yml b/.prettierrc.yml new file mode 100644 index 00000000..d7030347 --- /dev/null +++ b/.prettierrc.yml @@ -0,0 +1,5 @@ +overrides: + - files: "*.md" + options: + printWidth: 9999 + proseWrap: never diff --git a/.rustfmt.toml b/.rustfmt.toml new file mode 100644 index 00000000..71b9be3a --- /dev/null +++ b/.rustfmt.toml @@ -0,0 +1,3 @@ +group_imports = "StdExternalCrate" +imports_granularity = "Crate" +use_field_init_shorthand = true diff --git a/Cargo.toml b/Cargo.toml index 26b5b91b..19d5dd11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "actix-http-test", "actix-http", "actix-multipart", + "actix-multipart-derive", "actix-router", "actix-test", "actix-web-actors", @@ -13,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 @@ -27,6 +35,7 @@ actix-files = { path = "actix-files" } actix-http = { path = "actix-http" } actix-http-test = { path = "actix-http-test" } actix-multipart = { path = "actix-multipart" } +actix-multipart-derive = { path = "actix-multipart-derive" } actix-router = { path = "actix-router" } actix-test = { path = "actix-test" } actix-web = { path = "actix-web" } diff --git a/actix-files/CHANGES.md b/actix-files/CHANGES.md index 6e57bf7a..e94f4390 100644 --- a/actix-files/CHANGES.md +++ b/actix-files/CHANGES.md @@ -1,19 +1,38 @@ # Changes -## Unreleased - 2022-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 + - 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. +- Update `tokio-uring` dependency to `0.4`. [#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 -## 0.6.1 - 2022-06-11 - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] - Update `tokio-uring` dependency to `0.3`. - Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645] @@ -22,46 +41,46 @@ [#2021]: https://github.com/actix/actix-web/pull/2021 [#2645]: https://github.com/actix/actix-web/pull/2645 +## 0.6.0 -## 0.6.0 - 2022-02-25 - No significant changes since `0.6.0-beta.16`. +## 0.6.0-beta.16 -## 0.6.0-beta.16 - 2022-01-31 - No significant changes since `0.6.0-beta.15`. +## 0.6.0-beta.15 -## 0.6.0-beta.15 - 2022-01-21 - No significant changes since `0.6.0-beta.14`. +## 0.6.0-beta.14 -## 0.6.0-beta.14 - 2022-01-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 -## 0.6.0-beta.13 - 2022-01-04 - 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] - Minimum supported Rust version (MSRV) is now 1.54. [#2398]: https://github.com/actix/actix-web/pull/2398 +## 0.6.0-beta.12 -## 0.6.0-beta.12 - 2021-12-29 - No significant changes since `0.6.0-beta.11`. +## 0.6.0-beta.11 -## 0.6.0-beta.11 - 2021-12-27 - No significant changes since `0.6.0-beta.10`. +## 0.6.0-beta.10 -## 0.6.0-beta.10 - 2021-12-11 - No significant changes since `0.6.0-beta.9`. +## 0.6.0-beta.9 -## 0.6.0-beta.9 - 2021-11-22 - Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] - Add `NamedFile::open_async`. [#2408] - Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] @@ -72,24 +91,24 @@ [#2408]: https://github.com/actix/actix-web/pull/2408 [#2453]: https://github.com/actix/actix-web/pull/2453 +## 0.6.0-beta.8 -## 0.6.0-beta.8 - 2021-10-20 - Minimum supported Rust version (MSRV) is now 1.52. +## 0.6.0-beta.7 -## 0.6.0-beta.7 - 2021-09-09 - Minimum supported Rust version (MSRV) is now 1.51. +## 0.6.0-beta.6 -## 0.6.0-beta.6 - 2021-06-26 - Added `Files::path_filter()`. [#2274] - `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] [#2274]: https://github.com/actix/actix-web/pull/2274 [#2228]: https://github.com/actix/actix-web/pull/2228 +## 0.6.0-beta.5 -## 0.6.0-beta.5 - 2021-06-17 - `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] - For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] - `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] @@ -100,58 +119,58 @@ [#2225]: https://github.com/actix/actix-web/pull/2225 [#2257]: https://github.com/actix/actix-web/pull/2257 +## 0.6.0-beta.4 -## 0.6.0-beta.4 - 2021-04-02 - Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] [#2046]: https://github.com/actix/actix-web/pull/2046 +## 0.6.0-beta.3 -## 0.6.0-beta.3 - 2021-03-09 - No notable changes. +## 0.6.0-beta.2 -## 0.6.0-beta.2 - 2021-02-10 - Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] - Replace `v_htmlescape` with `askama_escape`. [#1953] [#1887]: https://github.com/actix/actix-web/pull/1887 [#1953]: https://github.com/actix/actix-web/pull/1953 +## 0.6.0-beta.1 -## 0.6.0-beta.1 - 2021-01-07 - `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 -## 0.5.0 - 2020-12-26 - Optionally support hidden files/directories. [#1811] [#1811]: https://github.com/actix/actix-web/pull/1811 +## 0.4.1 -## 0.4.1 - 2020-11-24 - Clarify order of parameters in `Files::new` and improve docs. +## 0.4.0 -## 0.4.0 - 2020-10-06 - 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 -## 0.3.0 - 2020-09-11 - No significant changes from 0.3.0-beta.1. +## 0.3.0-beta.1 -## 0.3.0-beta.1 - 2020-07-15 - Update `v_htmlescape` to 0.10 - Update `actix-web` and `actix-http` dependencies to beta.1 +## 0.3.0-alpha.1 -## 0.3.0-alpha.1 - 2020-05-23 - Update `actix-web` and `actix-http` dependencies to alpha - Fix some typos in the docs - Bump minimum supported Rust version to 1.40 @@ -159,73 +178,73 @@ [#1384]: https://github.com/actix/actix-web/pull/1384 +## 0.2.1 -## 0.2.1 - 2019-12-22 - Use the same format for file URLs regardless of platforms +## 0.2.0 -## 0.2.0 - 2019-12-20 - Fix BodyEncoding trait import #1220 +## 0.2.0-alpha.1 -## 0.2.0-alpha.1 - 2019-12-07 - Migrate to `std::future` +## 0.1.7 -## 0.1.7 - 2019-11-06 -- Add an additional `filename*` param in the `Content-Disposition` header of - `actix_files::NamedFile` to be more compatible. (#1151) +- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) + +## 0.1.6 -## 0.1.6 - 2019-10-14 - Add option to redirect to a slash-ended path `Files` #1132 +## 0.1.5 -## 0.1.5 - 2019-10-08 - 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 -## 0.1.4 - 2019-07-20 - Allow to disable `Content-Disposition` header #686 +## 0.1.3 -## 0.1.3 - 2019-06-28 - Do not set `Content-Length` header, let actix-http set it #930 +## 0.1.2 -## 0.1.2 - 2019-06-13 - Content-Length is 0 for NamedFile HEAD request #914 - Fix ring dependency from actix-web default features for #741 +## 0.1.1 -## 0.1.1 - 2019-06-01 - Static files are incorrectly served as both chunked and with length #812 +## 0.1.0 -## 0.1.0 - 2019-05-25 - NamedFile last-modified check always fails due to nano-seconds in file modified date #820 +## 0.1.0-beta.4 -## 0.1.0-beta.4 - 2019-05-12 - Update actix-web to beta.4 +## 0.1.0-beta.1 -## 0.1.0-beta.1 - 2019-04-20 - Update actix-web to beta.1 +## 0.1.0-alpha.6 -## 0.1.0-alpha.6 - 2019-04-14 - Update actix-web to alpha6 +## 0.1.0-alpha.4 -## 0.1.0-alpha.4 - 2019-04-08 - Update actix-web to alpha4 +## 0.1.0-alpha.2 -## 0.1.0-alpha.2 - 2019-04-02 - Add default handler support +## 0.1.0-alpha.1 -## 0.1.0-alpha.1 - 2019-03-28 - Initial impl diff --git a/actix-files/Cargo.toml b/actix-files/Cargo.toml index 01dc2928..57cd4e91 100644 --- a/actix-files/Cargo.toml +++ b/actix-files/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-files" -version = "0.6.2" +version = "0.6.6" authors = [ "Nikolay Kim ", "Rob Ede ", @@ -11,11 +11,16 @@ homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" categories = ["asynchronous", "web-programming::http-server"] license = "MIT OR Apache-2.0" -edition = "2018" +edition = "2021" -[lib] -name = "actix_files" -path = "src/lib.rs" +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_http::*", + "actix_service::*", + "actix_web::*", + "http::*", + "mime::*", +] [features] experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"] @@ -26,25 +31,26 @@ actix-service = "2" actix-utils = "3" actix-web = { version = "4", default-features = false } -bitflags = "1" +bitflags = "2" bytes = "1" derive_more = "0.99.5" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } http-range = "0.1.4" log = "0.4" -mime = "0.3" +mime = "0.3.9" mime_guess = "2.0.1" percent-encoding = "2.1" pin-project-lite = "0.2.7" -v_htmlescape= "0.15" +v_htmlescape = "0.15.5" # experimental-io-uring [target.'cfg(target_os = "linux")'.dependencies] -tokio-uring = { version = "0.3", optional = true, features = ["bytes"] } -actix-server = { version = "2.1", 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 a5078c8d..f6d5143f 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.2)](https://docs.rs/actix-files/0.6.2) -![Version](https://img.shields.io/badge/rustc-1.59+-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.2/status.svg)](https://deps.rs/crate/actix-files/0.6.2) +[![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.54 + + +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 00000000..e8cde0c8 --- /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/chunked.rs b/actix-files/src/chunked.rs index 241b4dcc..c6c01903 100644 --- a/actix-files/src/chunked.rs +++ b/actix-files/src/chunked.rs @@ -7,11 +7,10 @@ use std::{ }; use actix_web::{error::Error, web::Bytes}; -use futures_core::{ready, Stream}; -use pin_project_lite::pin_project; - #[cfg(feature = "experimental-io-uring")] use bytes::BytesMut; +use futures_core::{ready, Stream}; +use pin_project_lite::pin_project; use super::named::File; diff --git a/actix-files/src/directory.rs b/actix-files/src/directory.rs index 3af53a31..6ade424b 100644 --- a/actix-files/src/directory.rs +++ b/actix-files/src/directory.rs @@ -1,4 +1,9 @@ -use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf}; +use std::{ + fmt::Write, + fs::DirEntry, + io, + path::{Path, PathBuf}, +}; use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; use percent_encoding::{utf8_percent_encode, CONTROLS}; diff --git a/actix-files/src/error.rs b/actix-files/src/error.rs index 2f3a36cd..d614651f 100644 --- a/actix-files/src/error.rs +++ b/actix-files/src/error.rs @@ -4,46 +4,45 @@ use derive_more::Display; /// Errors which can occur when serving static files. #[derive(Debug, PartialEq, Eq, Display)] pub enum FilesError { - /// Path is not a directory + /// Path is not a directory. #[allow(dead_code)] - #[display(fmt = "Path is not a directory. Unable to serve static files")] + #[display(fmt = "path is not a directory. Unable to serve static files")] IsNotDirectory, - /// Cannot render directory - #[display(fmt = "Unable to render directory without index file")] + /// Cannot render directory. + #[display(fmt = "unable to render directory without index file")] IsDirectory, } -/// Return `NotFound` for `FilesError` impl ResponseError for FilesError { + /// Returns `404 Not Found`. fn status_code(&self) -> StatusCode { StatusCode::NOT_FOUND } } -#[allow(clippy::enum_variant_names)] #[derive(Debug, PartialEq, Eq, Display)] #[non_exhaustive] pub enum UriSegmentError { - /// The segment started with the wrapped invalid character. - #[display(fmt = "The segment started with the wrapped invalid character")] + /// Segment started with the wrapped invalid character. + #[display(fmt = "segment started with invalid character: ('{_0}')")] BadStart(char), - /// The segment contained the wrapped invalid character. - #[display(fmt = "The segment contained the wrapped invalid character")] + /// Segment contained the wrapped invalid character. + #[display(fmt = "segment contained invalid character ('{_0}')")] BadChar(char), - /// The segment ended with the wrapped invalid character. - #[display(fmt = "The segment ended with the wrapped invalid character")] + /// Segment ended with the wrapped invalid character. + #[display(fmt = "segment ended with invalid character: ('{_0}')")] BadEnd(char), - /// The path is not a valid UTF-8 string after doing percent decoding. - #[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")] + /// Path is not a valid UTF-8 string after percent-decoding. + #[display(fmt = "path is not a valid UTF-8 string after percent-decoding")] NotValidUtf8, } -/// Return `BadRequest` for `UriSegmentError` impl ResponseError for UriSegmentError { + /// Returns `400 Bad Request`. fn status_code(&self) -> StatusCode { StatusCode::BAD_REQUEST } diff --git a/actix-files/src/files.rs b/actix-files/src/files.rs index a30ce6fd..cfd3b9c2 100644 --- a/actix-files/src/files.rs +++ b/actix-files/src/files.rs @@ -8,8 +8,7 @@ use std::{ use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_web::{ dev::{ - AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, - ServiceResponse, + AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse, }, error::Error, guard::Guard, @@ -142,7 +141,7 @@ impl Files { self } - /// Set custom directory renderer + /// Set custom directory renderer. pub fn files_listing_renderer(mut self, f: F) -> Self where for<'r, 's> F: @@ -152,7 +151,7 @@ impl Files { self } - /// Specifies mime override callback + /// Specifies MIME override callback. pub fn mime_override(mut self, f: F) -> Self where F: Fn(&mime::Name<'_>) -> DispositionType + 'static, @@ -236,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 /// ``` @@ -301,12 +300,8 @@ impl Files { pub fn default_handler(mut self, f: F) -> Self where F: IntoServiceFactory, - U: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - > + 'static, + U: ServiceFactory + + 'static, { // create and configure default resource self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( @@ -390,3 +385,46 @@ impl ServiceFactory for Files { } } } + +#[cfg(test)] +mod tests { + use actix_web::{ + http::StatusCode, + test::{self, TestRequest}, + App, HttpResponse, + }; + + use super::*; + + #[actix_web::test] + async fn custom_files_listing_renderer() { + let srv = test::init_service( + App::new().service( + Files::new("/", "./tests") + .show_files_listing() + .files_listing_renderer(|dir, req| { + Ok(ServiceResponse::new( + req.clone(), + HttpResponse::Ok().body(dir.path.to_str().unwrap().to_owned()), + )) + }), + ), + ) + .await; + + let req = TestRequest::with_uri("/").to_request(); + let res = test::call_service(&srv, req).await; + + assert_eq!(res.status(), StatusCode::OK); + let body = test::read_body(res).await; + let body_str = std::str::from_utf8(&body).unwrap(); + let actual_path = Path::new(&body_str); + let expected_path = Path::new("actix-files/tests"); + assert!( + actual_path.ends_with(expected_path), + "body {:?} does not end with {:?}", + actual_path, + expected_path + ); + } +} diff --git a/actix-files/src/lib.rs b/actix-files/src/lib.rs index 0fbe39a8..167f996c 100644 --- a/actix-files/src/lib.rs +++ b/actix-files/src/lib.rs @@ -13,7 +13,11 @@ #![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))] + +use std::path::Path; use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_web::{ @@ -22,7 +26,6 @@ use actix_web::{ http::header::DispositionType, }; use mime_guess::from_ext; -use std::path::Path; mod chunked; mod directory; @@ -34,16 +37,15 @@ mod path_buf; mod range; mod service; -pub use self::chunked::ChunkedReadFile; -pub use self::directory::Directory; -pub use self::files::Files; -pub use self::named::NamedFile; -pub use self::range::HttpRange; -pub use self::service::FilesService; - -use self::directory::{directory_listing, DirectoryRenderer}; -use self::error::FilesError; -use self::path_buf::PathBufWrap; +pub use self::{ + chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile, + range::HttpRange, service::FilesService, +}; +use self::{ + directory::{directory_listing, DirectoryRenderer}, + error::FilesError, + path_buf::PathBufWrap, +}; type HttpService = BoxService; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; @@ -63,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}, @@ -72,7 +75,7 @@ mod tests { dev::ServiceFactory, guard, http::{ - header::{self, ContentDisposition, DispositionParam, DispositionType}, + header::{self, ContentDisposition, DispositionParam}, Method, StatusCode, }, middleware::Compress, @@ -551,10 +554,9 @@ mod tests { #[actix_rt::test] async fn test_static_files_with_spaces() { - let srv = test::init_service( - App::new().service(Files::new("/", ".").index_file("Cargo.toml")), - ) - .await; + let srv = + test::init_service(App::new().service(Files::new("/", ".").index_file("Cargo.toml"))) + .await; let request = TestRequest::get() .uri("/tests/test%20space.binary") .to_request(); @@ -566,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; @@ -664,8 +690,7 @@ mod tests { #[actix_rt::test] async fn test_static_files() { let srv = - test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) - .await; + test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await; let req = TestRequest::with_uri("/missing").to_request(); let resp = test::call_service(&srv, req).await; @@ -678,8 +703,7 @@ mod tests { assert_eq!(resp.status(), StatusCode::NOT_FOUND); let srv = - test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) - .await; + test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await; let req = TestRequest::with_uri("/tests").to_request(); let resp = test::call_service(&srv, req).await; assert_eq!( @@ -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 23d3093d..9e4a3773 100644 --- a/actix-files/src/named.rs +++ b/actix-files/src/named.rs @@ -8,13 +8,13 @@ use std::{ use actix_web::{ body::{self, BoxBody, SizedStream}, dev::{ - self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, - ServiceRequest, ServiceResponse, + self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, + ServiceResponse, }, http::{ header::{ - self, Charset, ContentDisposition, ContentEncoding, DispositionParam, - DispositionType, ExtendedValue, HeaderValue, + self, Charset, ContentDisposition, ContentEncoding, DispositionParam, DispositionType, + ExtendedValue, HeaderValue, }, StatusCode, }, @@ -24,11 +24,11 @@ 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}; bitflags! { + #[derive(Debug, Clone, Copy)] pub(crate) struct Flags: u8 { const ETAG = 0b0000_0001; const LAST_MD = 0b0000_0010; @@ -84,6 +84,7 @@ pub struct NamedFile { #[cfg(not(feature = "experimental-io-uring"))] pub(crate) use std::fs::File; + #[cfg(feature = "experimental-io-uring")] pub(crate) use tokio_uring::fs::File; @@ -126,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, @@ -138,8 +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-files/src/path_buf.rs b/actix-files/src/path_buf.rs index 9ee1338c..c1983279 100644 --- a/actix-files/src/path_buf.rs +++ b/actix-files/src/path_buf.rs @@ -30,7 +30,7 @@ impl PathBufWrap { let mut segment_count = path.matches('/').count() + 1; // we can decode the whole path here (instead of per-segment decoding) - // because we will reject `%2F` in paths using `segement_count`. + // because we will reject `%2F` in paths using `segment_count`. let path = percent_encoding::percent_decode_str(path) .decode_utf8() .map_err(|_| UriSegmentError::NotValidUtf8)?; @@ -97,8 +97,6 @@ impl FromRequest for PathBufWrap { #[cfg(test)] mod tests { - use std::iter::FromIterator; - use super::*; #[test] diff --git a/actix-files/src/range.rs b/actix-files/src/range.rs index 8d9fe944..528911ae 100644 --- a/actix-files/src/range.rs +++ b/actix-files/src/range.rs @@ -1,4 +1,36 @@ -use derive_more::{Display, Error}; +use std::fmt; + +use derive_more::Error; + +/// Copy of `http_range::HttpRangeParseError`. +#[derive(Debug, Clone)] +enum HttpRangeParseError { + InvalidRange, + NoOverlap, +} + +impl From for HttpRangeParseError { + fn from(err: http_range::HttpRangeParseError) -> Self { + match err { + http_range::HttpRangeParseError::InvalidRange => Self::InvalidRange, + http_range::HttpRangeParseError::NoOverlap => Self::NoOverlap, + } + } +} + +#[derive(Debug, Clone, Error)] +#[non_exhaustive] +pub struct ParseRangeErr(#[error(not(source))] HttpRangeParseError); + +impl fmt::Display for ParseRangeErr { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("invalid Range header: ")?; + f.write_str(match self.0 { + HttpRangeParseError::InvalidRange => "invalid syntax", + HttpRangeParseError::NoOverlap => "range starts after end of content", + }) + } +} /// HTTP Range header representation. #[derive(Debug, Clone, Copy)] @@ -10,26 +42,22 @@ pub struct HttpRange { pub length: u64, } -#[derive(Debug, Clone, Display, Error)] -#[display(fmt = "Parse HTTP Range failed")] -pub struct ParseRangeErr(#[error(not(source))] ()); - impl HttpRange { /// Parses Range HTTP header string as per RFC 2616. /// /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `size` is full size of response (file). pub fn parse(header: &str, size: u64) -> Result, ParseRangeErr> { - match http_range::HttpRange::parse(header, size) { - Ok(ranges) => Ok(ranges - .iter() - .map(|range| HttpRange { - start: range.start, - length: range.length, - }) - .collect()), - Err(_) => Err(ParseRangeErr(())), - } + let ranges = + http_range::HttpRange::parse(header, size).map_err(|err| ParseRangeErr(err.into()))?; + + Ok(ranges + .iter() + .map(|range| HttpRange { + start: range.start, + length: range.length, + }) + .collect()) } } diff --git a/actix-files/src/service.rs b/actix-files/src/service.rs index d94fd585..3d3b36c4 100644 --- a/actix-files/src/service.rs +++ b/actix-files/src/service.rs @@ -62,11 +62,7 @@ impl FilesService { } } - fn serve_named_file( - &self, - req: ServiceRequest, - mut named_file: NamedFile, - ) -> ServiceResponse { + fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse { if let Some(ref mime_override) = self.mime_override { let new_disposition = mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; @@ -120,13 +116,11 @@ impl Service for FilesService { )); } - let path_on_disk = match PathBufWrap::parse_path( - req.match_info().unprocessed(), - this.hidden_files, - ) { - Ok(item) => item, - Err(err) => return Ok(req.error_response(err)), - }; + let path_on_disk = + match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) { + Ok(item) => item, + Err(err) => return Ok(req.error_response(err)), + }; if let Some(filter) = &this.path_filter { if !filter(path_on_disk.as_ref(), req.head()) { @@ -177,8 +171,7 @@ impl Service for FilesService { match NamedFile::open_async(&path).await { Ok(mut named_file) => { if let Some(ref mime_override) = this.mime_override { - let new_disposition = - mime_override(&named_file.content_type.type_()); + let new_disposition = mime_override(&named_file.content_type.type_()); named_file.content_disposition.disposition = new_disposition; } named_file.flags = this.file_flags; diff --git a/actix-files/tests/encoding.rs b/actix-files/tests/encoding.rs index 7aec25ff..3c8bdb59 100644 --- a/actix-files/tests/encoding.rs +++ b/actix-files/tests/encoding.rs @@ -24,8 +24,7 @@ async fn test_utf8_file_contents() { // disable UTF-8 attribute let srv = - test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))) - .await; + test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))).await; let req = TestRequest::with_uri("/utf8.txt").to_request(); let res = test::call_service(&srv, req).await; diff --git a/actix-files/tests/guard.rs b/actix-files/tests/guard.rs index d053f3fd..5a97f75d 100644 --- a/actix-files/tests/guard.rs +++ b/actix-files/tests/guard.rs @@ -12,9 +12,7 @@ async fn test_guard_filter() { let srv = test::init_service( App::new() .service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com"))) - .service( - Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")), - ), + .service(Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com"))), ) .await; diff --git a/actix-files/tests/traversal.rs b/actix-files/tests/traversal.rs index c890b3fe..4eecb8dd 100644 --- a/actix-files/tests/traversal.rs +++ b/actix-files/tests/traversal.rs @@ -9,8 +9,7 @@ use actix_web::{ async fn test_directory_traversal_prevention() { let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; - let req = - TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request(); + let req = TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request(); let res = test::call_service(&srv, req).await; assert_eq!(res.status(), StatusCode::NOT_FOUND); diff --git a/actix-http-test/CHANGES.md b/actix-http-test/CHANGES.md index 028fe3dd..4d133e3e 100644 --- a/actix-http-test/CHANGES.md +++ b/actix-http-test/CHANGES.md @@ -1,10 +1,19 @@ # Changes -## Unreleased - 2022-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 + - Minimum supported Rust version (MSRV) is now 1.59. +## 3.0.0 -## 3.0.0 - 2022-07-24 - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - Added `TestServer::client_headers` method. [#2097] - Update `actix-server` dependency to `2`. @@ -16,78 +25,79 @@ [#2097]: https://github.com/actix/actix-web/pull/2097 [#1813]: https://github.com/actix/actix-web/pull/1813 -
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 -## 3.0.0-beta.12 - 2022-01-31 - No significant changes since `3.0.0-beta.11`. +## 3.0.0-beta.11 -## 3.0.0-beta.11 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. +## 3.0.0-beta.10 -## 3.0.0-beta.10 - 2021-12-27 - Update `actix-server` to `2.0.0-rc.2`. [#2550] [#2550]: https://github.com/actix/actix-web/pull/2550 +## 3.0.0-beta.9 -## 3.0.0-beta.9 - 2021-12-11 - No significant changes since `3.0.0-beta.8`. +## 3.0.0-beta.8 -## 3.0.0-beta.8 - 2021-11-30 - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 +## 3.0.0-beta.7 -## 3.0.0-beta.7 - 2021-11-22 - Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 +## 3.0.0-beta.6 -## 3.0.0-beta.6 - 2021-11-15 - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - Update `actix-server` to `2.0.0-beta.9`. [#2442] - Minimum supported Rust version (MSRV) is now 1.52. [#2442]: https://github.com/actix/actix-web/pull/2442 +## 3.0.0-beta.5 -## 3.0.0-beta.5 - 2021-09-09 - Minimum supported Rust version (MSRV) is now 1.51. +## 3.0.0-beta.4 -## 3.0.0-beta.4 - 2021-04-02 - Added `TestServer::client_headers` method. [#2097] [#2097]: https://github.com/actix/actix-web/pull/2097 +## 3.0.0-beta.3 -## 3.0.0-beta.3 - 2021-03-09 - No notable changes. +## 3.0.0-beta.2 -## 3.0.0-beta.2 - 2021-02-10 - No notable changes. +## 3.0.0-beta.1 -## 3.0.0-beta.1 - 2021-01-07 - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813
-## 2.1.0 - 2020-11-25 +## 2.1.0 + - Add ability to set address for `TestServer`. [#1645] - Upgrade `base64` to `0.13`. - Upgrade `serde_urlencoded` to `0.7`. [#1773] @@ -95,12 +105,12 @@ [#1773]: https://github.com/actix/actix-web/pull/1773 [#1645]: https://github.com/actix/actix-web/pull/1645 +## 2.0.0 -## 2.0.0 - 2020-09-11 - Update actix-codec and actix-utils dependencies. +## 2.0.0-alpha.1 -## 2.0.0-alpha.1 - 2020-05-23 - Update the `time` dependency to 0.2.7 - Update `actix-connect` dependency to 2.0.0-alpha.2 - Make `test_server` `async` fn. @@ -109,56 +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 -## 1.0.0-alpha.3 - 2019-12-07 - Migrate to `std::future` +## 0.2.5 -## 0.2.5 - 2019-09-17 - Update serde_urlencoded to "0.6.1" - Increase TestServerRuntime timeouts from 500ms to 3000ms - Do not override current `System` +## 0.2.4 -## 0.2.4 - 2019-07-18 - Update actix-server to 0.6 +## 0.2.3 -## 0.2.3 - 2019-07-16 - Add `delete`, `options`, `patch` methods to `TestServerRunner` +## 0.2.2 -## 0.2.2 - 2019-06-16 - Add .put() and .sput() methods +## 0.2.1 -## 0.2.1 - 2019-06-05 - Add license files +## 0.2.0 -## 0.2.0 - 2019-05-12 - Update awc and actix-http deps +## 0.1.1 -## 0.1.1 - 2019-04-24 - Always make new connection for http client +## 0.1.0 -## 0.1.0 - 2019-04-16 - No changes +## 0.1.0-alpha.3 -## 0.1.0-alpha.3 - 2019-04-02 - Request functions accept path #743 +## 0.1.0-alpha.2 -## 0.1.0-alpha.2 - 2019-03-29 - Added TestServerRuntime::load_body() method - Update actix-http and awc libraries +## 0.1.0-alpha.1 -## 0.1.0-alpha.1 - 2019-03-28 - Initial impl diff --git a/actix-http-test/Cargo.toml b/actix-http-test/Cargo.toml index 1162c0a3..0947579a 100644 --- a/actix-http-test/Cargo.toml +++ b/actix-http-test/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "actix-http-test" -version = "3.0.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", @@ -13,14 +13,22 @@ categories = [ "web-programming::websocket", ] license = "MIT OR Apache-2.0" -edition = "2018" +edition = "2021" [package.metadata.docs.rs] features = [] -[lib] -name = "actix_http_test" -path = "src/lib.rs" +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_codec::*", + "actix_http::*", + "actix_server::*", + "awc::*", + "bytes::*", + "futures_core::*", + "http::*", + "tokio::*", +] [features] default = [] @@ -37,19 +45,17 @@ actix-rt = "2.2" actix-server = "2" awc = { version = "3", default-features = false } -base64 = "0.13" bytes = "1" futures-core = { version = "0.3.17", default-features = false } -http = "0.2.5" +http = "0.2.7" log = "0.4" -socket2 = "0.4" -serde = "1.0" -serde_json = "1.0" +socket2 = "0.5" +serde = "1" +serde_json = "1" slab = "0.4" serde_urlencoded = "0.7" -tls-openssl = { version = "0.10.9", package = "openssl", optional = true } -tokio = { version = "1.18.4", features = ["sync"] } +tls-openssl = { version = "0.10.55", package = "openssl", optional = true } +tokio = { version = "1.24.2", features = ["sync"] } [dev-dependencies] -actix-web = { version = "4", default-features = false, features = ["cookies"] } actix-http = "3" diff --git a/actix-http-test/README.md b/actix-http-test/README.md index 25e7c684..93902812 100644 --- a/actix-http-test/README.md +++ b/actix-http-test/README.md @@ -1,17 +1,20 @@ -# 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.0.0)](https://docs.rs/actix-http-test/3.0.0) -![Version](https://img.shields.io/badge/rustc-1.59+-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.0.0/status.svg)](https://deps.rs/crate/actix-http-test/3.0.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.54 + + +Various helpers for Actix applications to use during testing. + + diff --git a/actix-http-test/src/lib.rs b/actix-http-test/src/lib.rs index a66f7b48..554af910 100644 --- a/actix-http-test/src/lib.rs +++ b/actix-http-test/src/lib.rs @@ -2,9 +2,9 @@ #![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))] #[cfg(feature = "openssl")] extern crate tls_openssl as openssl; @@ -30,27 +30,31 @@ use tokio::sync::mpsc; /// for HTTP applications. /// /// # Examples -/// ```no_run -/// use actix_http::HttpService; +/// +/// ``` +/// use actix_http::{HttpService, Response, Error, StatusCode}; /// use actix_http_test::test_server; -/// use actix_web::{web, App, HttpResponse, Error}; +/// use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; /// -/// async fn my_handler() -> Result { -/// Ok(HttpResponse::Ok().into()) -/// } -/// -/// #[actix_web::test] +/// #[actix_rt::test] +/// # async fn hidden_test() {} /// async fn test_example() { -/// let mut srv = TestServer::start(|| -/// HttpService::new( -/// App::new().service(web::resource("/").to(my_handler)) -/// ) -/// ); +/// let srv = test_server(|| { +/// HttpService::build() +/// .h1(fn_service(|req| async move { +/// Ok::<_, Error>(Response::ok()) +/// })) +/// .tcp() +/// .map_err(|_| ()) +/// }) +/// .await; /// /// let req = srv.get("/"); /// let response = req.send().await.unwrap(); -/// assert!(response.status().is_success()); +/// +/// assert_eq!(response.status(), StatusCode::OK); /// } +/// # actix_rt::System::new().block_on(test_example()); /// ``` pub async fn test_server>(factory: F) -> TestServer { let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); diff --git a/actix-http/CHANGES.md b/actix-http/CHANGES.md index 708af896..bf08065a 100644 --- a/actix-http/CHANGES.md +++ b/actix-http/CHANGES.md @@ -1,100 +1,187 @@ # Changes -## Unreleased - 2022-xx-xx +## Unreleased + ### Added + +- Add `error::InvalidStatusCode` re-export. +- New method `response_with_level` for `Encoder` for setup compress level. [#2948] + +## 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 + +### Fixed + +- Use correct `http` version requirement to ensure support for const `HeaderName` definitions. + +## 3.3.0 + +### Added + - Implement `MessageBody` for `Cow<'static, str>` and `Cow<'static, [u8]>`. [#2959] - Implement `MessageBody` for `&mut B` where `B: MessageBody + Unpin`. [#2868] - Implement `MessageBody` for `Pin` where `B::Target: MessageBody`. [#2868] - Automatic h2c detection via new service finalizer `HttpService::tcp_auto_h2c()`. [#2957] -- `HeaderMap::retain()` [#2955]. -- Header name constants in `header` module. [#2956] +- `HeaderMap::retain()`. [#2955] +- Header name constants in `header` module. [#2956] [#2968] + - `CACHE_STATUS` + - `CDN_CACHE_CONTROL` - `CROSS_ORIGIN_EMBEDDER_POLICY` - `CROSS_ORIGIN_OPENER_POLICY` - `PERMISSIONS_POLICY` - `X_FORWARDED_FOR` - `X_FORWARDED_HOST` - `X_FORWARDED_PROTO` -- New method `response_with_level` for `Encoder` for setup compress level. [#2948] + +### Fixed + +- Fix non-empty body of HTTP/2 HEAD responses. [#2920] ### Performance + - Improve overall performance of operations on `Extensions`. [#2890] [#2959]: https://github.com/actix/actix-web/pull/2959 [#2868]: https://github.com/actix/actix-web/pull/2868 [#2890]: https://github.com/actix/actix-web/pull/2890 +[#2920]: https://github.com/actix/actix-web/pull/2920 [#2957]: https://github.com/actix/actix-web/pull/2957 [#2955]: https://github.com/actix/actix-web/pull/2955 [#2956]: https://github.com/actix/actix-web/pull/2956 -[#2948]: https://github.com/actix/actix-web/pull/2948 +[#2968]: https://github.com/actix/actix-web/pull/2968 +## 3.2.2 -## 3.2.2 - 2022-09-11 ### Changed + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. ### Fixed + - Avoid possibility of dispatcher getting stuck while back-pressuring I/O. [#2369] [#2369]: https://github.com/actix/actix-web/pull/2369 +## 3.2.1 -## 3.2.1 - 2022-07-02 ### Fixed + - Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794] [#2794]: https://github.com/actix/actix-web/pull/2794 +## 3.2.0 -## 3.2.0 - 2022-06-30 ### Changed + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. ### Fixed + - Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790] - Retain previously set Vary headers when using compression encoder. [#2798] [#2790]: https://github.com/actix/actix-web/pull/2790 [#2798]: https://github.com/actix/actix-web/pull/2798 +## 3.1.0 -## 3.1.0 - 2022-06-11 ### Changed + - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. ### Fixed + - Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779] [#2624]: https://github.com/actix/actix-web/pull/2624 [#2357]: https://github.com/actix/actix-web/issues/2357 [#2779]: https://github.com/actix/actix-web/pull/2779 +## 3.0.4 -## 3.0.4 - 2022-03-09 ### Fixed + - Document on docs.rs with `ws` feature enabled. +## 3.0.3 -## 3.0.3 - 2022-03-08 ### Fixed + - Allow spaces between header name and colon when parsing responses. [#2684] [#2684]: https://github.com/actix/actix-web/pull/2684 +## 3.0.2 -## 3.0.2 - 2022-03-05 ### Fixed + - Fix encoding camel-case header names with more than one hyphen. [#2683] [#2683]: https://github.com/actix/actix-web/pull/2683 +## 3.0.1 -## 3.0.1 - 2022-03-04 - 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 + - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `bytes` to `1.0`. [#1813] - Updated `h2` to `0.3`. [#1813] @@ -103,6 +190,7 @@ - Updated `tokio` to `1`. ### Added + - Crate Features: - `ws`; disabled by default. [#2618] - `http2`; disabled by default. [#2618] @@ -171,6 +259,7 @@ - `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920] ### Changed + - Traits: - Rename `IntoHeaderValue => TryIntoHeaderValue`. [#2510] - `MessageBody` now has an associated `Error` type. [#2183] @@ -226,6 +315,7 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - A `Vary` header is now correctly sent along with compressed content. [#2501] - HTTP/1.1 dispatcher correctly uses client request timeout. [#2611] - Fixed issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624] @@ -238,6 +328,7 @@ - Fixed quality parse error in Accept-Encoding header. [#2344] ### Removed + - Crate Features: - `compress` feature. [#2065] - `cookies` feature. [#2065] @@ -300,7 +391,6 @@ - `downcast` and `downcast_get_type_id` macros. [#2291] - Down-casting for `MessageBody` types; use standard `Any` trait. [#2183] - [#1813]: https://github.com/actix/actix-web/pull/1813 [#1845]: https://github.com/actix/actix-web/pull/1845 [#1857]: https://github.com/actix/actix-web/pull/1857 @@ -373,37 +463,42 @@ [#2660]: https://github.com/actix/actix-web/pull/2660 [00ba8d55]: https://github.com/actix/actix-web/commit/00ba8d55492284581695d824648590715a8bd386 -
3.0.0 Pre-Releases -## 3.0.0-rc.4 - 2022-02-22 +## 3.0.0-rc.4 + ### Fixed + - Fix h1 dispatcher panic. [1ce58ecb] [1ce58ecb]: https://github.com/actix/actix-web/commit/1ce58ecb305c60e51db06e6c913b7a1344e229ca +## 3.0.0-rc.3 -## 3.0.0-rc.3 - 2022-02-16 - No significant changes since `3.0.0-rc.2`. +## 3.0.0-rc.2 -## 3.0.0-rc.2 - 2022-02-08 ### Added + - Implement `From>` for `Response>`. [#2625] ### Changed + - `error::DispatcherError` enum is now marked `#[non_exhaustive]`. [#2624] ### Fixed + - Issue where handlers that took payload but then dropped without reading it to EOF it would cause keep-alive connections to become stuck. [#2624] [#2624]: https://github.com/actix/actix-web/pull/2624 [#2625]: https://github.com/actix/actix-web/pull/2625 +## 3.0.0-rc.1 -## 3.0.0-rc.1 - 2022-01-31 ### Added + - Implement `Default` for `KeepAlive`. [#2611] - Implement `From` for `KeepAlive`. [#2611] - Implement `From>` for `KeepAlive`. [#2611] @@ -412,6 +507,7 @@ - Crate `http2` feature flag, disabled by default. [#2618] ### Changed + - Rename `ServiceConfig::{client_timer_expire => client_request_deadline}`. [#2611] - Rename `ServiceConfig::{client_disconnect_timer => client_disconnect_deadline}`. [#2611] - Deadline methods in `ServiceConfig` now return `std::time::Instant`s instead of Tokio's wrapper type. [#2611] @@ -422,9 +518,11 @@ - `ServiceConfig::keep_alive` now returns a `KeepAlive`. [#2611] ### Fixed + - HTTP/1.1 dispatcher correctly uses client request timeout. [#2611] ### Removed + - `ServiceConfig::{client_timer, keep_alive_timer}`. [#2611] - `impl From for KeepAlive`; use `Duration`s instead. [#2611] - `impl From> for KeepAlive`; use `Duration`s instead. [#2611] @@ -433,16 +531,19 @@ [#2611]: https://github.com/actix/actix-web/pull/2611 [#2618]: https://github.com/actix/actix-web/pull/2618 +## 3.0.0-beta.19 -## 3.0.0-beta.19 - 2022-01-21 ### Added + - Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587] - `ResponseHead` now implements `Clone`. [#2585] ### Changed + - Brotli (de)compression support is now provided by the `brotli` crate. [#2538] ### Removed + - `ResponseHead::extensions[_mut]()`. [#2585] - `ResponseBuilder::extensions[_mut]()`. [#2585] @@ -450,9 +551,10 @@ [#2585]: https://github.com/actix/actix-web/pull/2585 [#2587]: https://github.com/actix/actix-web/pull/2587 +## 3.0.0-beta.18 -## 3.0.0-beta.18 - 2022-01-04 ### Added + - `impl Eq` for `header::ContentEncoding`. [#2501] - `impl Copy` for `QualityItem` where `T: Copy`. [#2501] - `Quality::ZERO` equivalent to `q=0`. [#2501] @@ -460,6 +562,7 @@ - `ContentEncoding::to_header_value()`. [#2501] ### Changed + - `Quality::MIN` is now the smallest non-zero value. [#2501] - `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501] - Rename `ContentEncoding::{Br => Brotli}`. [#2501] @@ -467,19 +570,22 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - `ContentEncoding::Identity` can now be parsed from a string. [#2501] - A `Vary` header is now correctly sent along with compressed content. [#2501] ### Removed + - `ContentEncoding::Auto` variant. [#2501] - `ContentEncoding::is_compression()`. [#2501] [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 +## 3.0.0-beta.17 -## 3.0.0-beta.17 - 2021-12-27 ### Changed + - `HeaderMap::get_all` now returns a `std::slice::Iter`. [#2527] - `Payload` inner fields are now named. [#2545] - `impl Stream` for `Payload` no longer requires the `Stream` variant be `Unpin`. [#2545] @@ -488,30 +594,35 @@ - Rename `PayloadStream` to `BoxedPayloadStream`. [#2545] ### Removed + - `h1::Payload::readany`. [#2545] [#2527]: https://github.com/actix/actix-web/pull/2527 [#2545]: https://github.com/actix/actix-web/pull/2545 +## 3.0.0-beta.16 -## 3.0.0-beta.16 - 2021-12-17 ### Added + - New method on `MessageBody` trait, `try_into_bytes`, with default implementation, for optimizations on body types that complete in exactly one poll. Replaces `is_complete_body` and `take_complete_body`. [#2522] ### Changed + - Rename trait `IntoHeaderPair => TryIntoHeaderPair`. [#2510] - Rename `TryIntoHeaderPair::{try_into_header_pair => try_into_pair}`. [#2510] - Rename trait `IntoHeaderValue => TryIntoHeaderValue`. [#2510] ### Removed + - `MessageBody::{is_complete_body,take_complete_body}`. [#2522] [#2510]: https://github.com/actix/actix-web/pull/2510 [#2522]: https://github.com/actix/actix-web/pull/2522 +## 3.0.0-beta.15 -## 3.0.0-beta.15 - 2021-12-11 ### Added + - Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483] - HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483] - `Response::map_into_boxed_body`. [#2468] @@ -532,6 +643,7 @@ - New `boxed` method on `MessageBody` trait for wrapping body type. [#2520] ### Changed + - Rename `body::BoxBody::{from_body => new}`. [#2468] - Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468] - The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468] @@ -541,6 +653,7 @@ - `ResponseBuilder::finish()` now returns `Response>`. [#2468] ### Removed + - `ResponseBuilder::streaming`. [#2468] - `impl Future` for `ResponseBuilder`. [#2468] - Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468] @@ -560,9 +673,10 @@ [#2497]: https://github.com/actix/actix-web/pull/2497 [#2520]: https://github.com/actix/actix-web/pull/2520 +## 3.0.0-beta.14 -## 3.0.0-beta.14 - 2021-11-30 ### Changed + - Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467] - Expose `header::map` module. [#2467] - Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470] @@ -572,15 +686,17 @@ [#2470]: https://github.com/actix/actix-web/pull/2470 [#2474]: https://github.com/actix/actix-web/pull/2474 +## 3.0.0-beta.13 -## 3.0.0-beta.13 - 2021-11-22 ### Added + - `body::AnyBody::empty` for quickly creating an empty body. [#2446] - `body::AnyBody::none` for quickly creating a "none" body. [#2456] - `impl Clone` for `body::AnyBody where S: Clone`. [#2448] - `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448] ### Changed + - Rename `body::AnyBody::{Message => Body}`. [#2446] - Rename `body::AnyBody::{from_message => new_boxed}`. [#2448] - Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448] @@ -589,6 +705,7 @@ - `Encoder::response` now returns `AnyBody>`. [#2448] ### Removed + - `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446] - `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446] - `EncoderError::Boxed`; it is no longer required. [#2446] @@ -598,33 +715,38 @@ [#2448]: https://github.com/actix/actix-web/pull/2448 [#2456]: https://github.com/actix/actix-web/pull/2456 +## 3.0.0-beta.12 -## 3.0.0-beta.12 - 2021-11-15 ### Changed + - Update `actix-server` to `2.0.0-beta.9`. [#2442] ### Removed + - `client` module. [#2425] - `trust-dns` feature. [#2425] [#2425]: https://github.com/actix/actix-web/pull/2425 [#2442]: https://github.com/actix/actix-web/pull/2442 +## 3.0.0-beta.11 -## 3.0.0-beta.11 - 2021-10-20 ### Changed + - Updated rustls to v0.20. [#2414] - Minimum supported Rust version (MSRV) is now 1.52. [#2414]: https://github.com/actix/actix-web/pull/2414 +## 3.0.0-beta.10 -## 3.0.0-beta.10 - 2021-09-09 ### Changed + - `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377] - Minimum supported Rust version (MSRV) is now 1.51. ### Fixed + - Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364] - Remove `Into` bound on `Encoder` body types. [#2375] - Fix quality parse error in Accept-Encoding header. [#2344] @@ -634,25 +756,29 @@ [#2344]: https://github.com/actix/actix-web/pull/2344 [#2377]: https://github.com/actix/actix-web/pull/2377 +## 3.0.0-beta.9 -## 3.0.0-beta.9 - 2021-08-09 ### Fixed + - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) +## 3.0.0-beta.8 -## 3.0.0-beta.8 - 2021-06-26 ### Changed + - Change compression algorithm features flags. [#2250] ### Removed + - `downcast` and `downcast_get_type_id` macros. [#2291] [#2291]: https://github.com/actix/actix-web/pull/2291 [#2250]: https://github.com/actix/actix-web/pull/2250 +## 3.0.0-beta.7 -## 3.0.0-beta.7 - 2021-06-17 ### Added + - Alias `body::Body` as `body::AnyBody`. [#2215] - `BoxAnyBody`: a boxed message body with boxed errors. [#2183] - Re-export `http` crate's `Error` type as `error::HttpError`. [#2171] @@ -663,6 +789,7 @@ - Add zstd support for `ContentEncoding`. [#2244] ### Changed + - The `MessageBody` trait now has an associated `Error` type. [#2183] - All error trait bounds in server service builders have changed from `Into` to `Into>`. [#2253] - All error trait bounds in message body and stream impls changed from `Into` to `Into>`. [#2253] @@ -673,9 +800,10 @@ - Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201] - `ResponseBuilder::message_body` now returns a `Result`. [#2201] - Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253] -- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226] +- `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuration parameter. [#2226] ### Removed + - Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171] - Down-casting for `MessageBody` types. [#2183] - `error::Result` alias. [#2201] @@ -694,21 +822,23 @@ [#2253]: https://github.com/actix/actix-web/pull/2253 [#2244]: https://github.com/actix/actix-web/pull/2244 +## 3.0.0-beta.6 - -## 3.0.0-beta.6 - 2021-04-17 ### Added + - `impl MessageBody for Pin>`. [#2152] - `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159] - Helper `body::to_bytes` for async collecting message body into Bytes. [#2158] ### Changed + - The type parameter of `Response` no longer has a default. [#2152] - The `Message` variant of `body::Body` is now `Pin>`. [#2152] - `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152] - Error enum types are marked `#[non_exhaustive]`. [#2161] ### Removed + - `cookies` feature flag. [#2065] - Top-level `cookies` mod (re-export). [#2065] - `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065] @@ -727,17 +857,20 @@ [#2158]: https://github.com/actix/actix-web/pull/2158 [#2161]: https://github.com/actix/actix-web/pull/2161 +## 3.0.0-beta.5 -## 3.0.0-beta.5 - 2021-04-02 ### Added + - `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081] - `client::ConnectorService` as `client::Connector::finish` method's return type [#2081] - `client::ConnectionIo` trait alias [#2081] ### Changed + - `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] ### Removed + - Common typed HTTP headers were moved to actix-web. [2094] - `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127] @@ -746,14 +879,16 @@ [#2094]: https://github.com/actix/actix-web/pull/2094 [#2127]: https://github.com/actix/actix-web/pull/2127 +## 3.0.0-beta.4 -## 3.0.0-beta.4 - 2021-03-08 ### Changed + - Feature `cookies` is now optional and disabled by default. [#1981] - `ws::hash_key` now returns array. [#2035] - `ResponseBuilder::json` now takes `impl Serialize`. [#2052] ### Removed + - Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994] - `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994] @@ -762,13 +897,14 @@ [#2035]: https://github.com/actix/actix-web/pull/2035 [#2052]: https://github.com/actix/actix-web/pull/2052 +## 3.0.0-beta.3 -## 3.0.0-beta.3 - 2021-02-10 - No notable changes. +## 3.0.0-beta.2 -## 3.0.0-beta.2 - 2021-02-10 ### Added + - `TryIntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869] - `ResponseBuilder::insert_header` method which allows using typed headers. [#1869] - `ResponseBuilder::append_header` method which allows using typed headers. [#1869] @@ -780,22 +916,21 @@ - `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed -- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed - `mime` types. [#1894] -- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std - `TryInto` trait. [#1894] + +- `ResponseBuilder::content_type` now takes an `impl TryIntoHeaderValue` to support using typed `mime` types. [#1894] +- Renamed `TryIntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std `TryInto` trait. [#1894] - `Extensions::insert` returns Option of replaced item. [#1904] - Remove `HttpResponseBuilder::json2()`. [#1903] - Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903] - `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] - `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] -- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool - is dead. [#1957] +- Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool is dead. [#1957] - `HeaderMap::len` now returns number of values instead of number of keys. [#1964] - `HeaderMap::insert` now returns iterator of removed values. [#1964] - `HeaderMap::remove` now returns iterator of removed values. [#1964] ### Removed + - `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869] - `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869] - `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869] @@ -805,6 +940,7 @@ - `ResponseError` impl for `actix::MailboxError`. [#1969] ### Documentation + - Vastly improve docs and add examples for `HeaderMap`. [#1964] [#1869]: https://github.com/actix/actix-web/pull/1869 @@ -817,12 +953,14 @@ [#1964]: https://github.com/actix/actix-web/pull/1964 [#1969]: https://github.com/actix/actix-web/pull/1969 +## 3.0.0-beta.1 -## 3.0.0-beta.1 - 2021-01-07 ### Added + - Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`. ### Changed + - Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] - Bumped `rand` to `0.8`. - Update `bytes` to `1.0`. [#1813] @@ -830,16 +968,11 @@ - The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864] ### Removed -- Deprecated `on_connect` methods have been removed. Prefer the new - `on_connect_ext` technique. [#1857] -- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` - due to deprecate of resolver actor. [#1813] -- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. - due to the removal of this type from `tokio-openssl` crate. openssl handshake - error would return as `ConnectError::SslError`. [#1813] -- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. - Due to this change `actix_threadpool::BlockingError` type is moved into - `actix_http::error` module. [#1878] + +- Deprecated `on_connect` methods have been removed. Prefer the new `on_connect_ext` technique. [#1857] +- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError` due to deprecate of resolver actor. [#1813] +- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`. due to the removal of this type from `tokio-openssl` crate. openssl handshake error would return as `ConnectError::SslError`. [#1813] +- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`. Due to this change `actix_threadpool::BlockingError` type is moved into `actix_http::error` module. [#1878] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1857]: https://github.com/actix/actix-web/pull/1857 @@ -848,29 +981,34 @@
+## 2.2.2 -## 2.2.2 - 2022-01-21 ### Changed + - Migrate to `brotli` crate. [ad7e3c06] [ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06 +## 2.2.1 -## 2.2.1 - 2021-08-09 ### Fixed + - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977) +## 2.2.0 -## 2.2.0 - 2020-11-25 ### Added + - HttpResponse builders for 1xx status codes. [#1768] - `Accept::mime_precedence` and `Accept::mime_preference`. [#1793] - `TryFrom` and `TryFrom` for `http::header::Quality`. [#1797] ### Fixed + - Started dropping `transfer-encoding: chunked` and `Content-Length` for 1XX and 204 responses. [#1767] ### Changed + - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 @@ -879,12 +1017,14 @@ [#1793]: https://github.com/actix/actix-web/pull/1793 [#1797]: https://github.com/actix/actix-web/pull/1797 +## 2.1.0 -## 2.1.0 - 2020-10-30 ### Added + - Added more flexible `on_connect_ext` methods for on-connect handling. [#1754] ### Changed + - Upgrade `base64` to `0.13`. [#1744] - Upgrade `pin-project` to `1.0`. [#1733] - Deprecate `ResponseBuilder::{if_some, if_true}`. [#1760] @@ -894,37 +1034,42 @@ [#1733]: https://github.com/actix/actix-web/pull/1733 [#1744]: https://github.com/actix/actix-web/pull/1744 +## 2.0.0 -## 2.0.0 - 2020-09-11 - No significant changes from `2.0.0-beta.4`. +## 2.0.0-beta.4 -## 2.0.0-beta.4 - 2020-09-09 ### Changed + - Update actix-codec and actix-utils dependencies. - Update actix-connect and actix-tls dependencies. +## 2.0.0-beta.3 -## 2.0.0-beta.3 - 2020-08-14 ### Fixed + - Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] [#1626]: https://github.com/actix/actix-web/pull/1626 +## 2.0.0-beta.2 -## 2.0.0-beta.2 - 2020-07-21 ### Fixed + - Potential UB in h1 decoder using uninitialized memory. [#1614] ### Changed + - Fix illegal chunked encoding. [#1615] [#1614]: https://github.com/actix/actix-web/pull/1614 [#1615]: https://github.com/actix/actix-web/pull/1615 +## 2.0.0-beta.1 -## 2.0.0-beta.1 - 2020-07-11 ### Changed + - Migrate cookie handling to `cookie` crate. [#1558] - Update `sha-1` to 0.9. [#1586] - Fix leak in client pool. [#1580] @@ -934,278 +1079,319 @@ [#1586]: https://github.com/actix/actix-web/pull/1586 [#1580]: https://github.com/actix/actix-web/pull/1580 +## 2.0.0-alpha.4 -## 2.0.0-alpha.4 - 2020-05-21 ### Changed + - Bump minimum supported Rust version to 1.40 -- content_length function is removed, and you can set Content-Length by calling - no_chunking function [#1439] -- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a - `u64` instead of a `usize`. +- content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439] +- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. - Update `base64` dependency to 0.12 ### Fixed + - Support parsing of `SameSite=None` [#1503] [#1439]: https://github.com/actix/actix-web/pull/1439 [#1503]: https://github.com/actix/actix-web/pull/1503 +## 2.0.0-alpha.3 -## 2.0.0-alpha.3 - 2020-05-08 ### Fixed + - Correct spelling of ConnectError::Unresolved [#1487] -- Fix a mistake in the encoding of websocket continuation messages wherein - Item::FirstText and Item::FirstBinary are each encoded as the other. +- Fix a mistake in the encoding of websocket continuation messages wherein Item::FirstText and Item::FirstBinary are each encoded as the other. ### Changed + - Implement `std::error::Error` for our custom errors [#1422] -- Remove `failure` support for `ResponseError` since that crate - will be deprecated in the near future. +- Remove `failure` support for `ResponseError` since that crate will be deprecated in the near future. [#1422]: https://github.com/actix/actix-web/pull/1422 [#1487]: https://github.com/actix/actix-web/pull/1487 +## 2.0.0-alpha.2 -## 2.0.0-alpha.2 - 2020-03-07 ### Changed + - Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] -- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB - respectively to improve download speed for awc when downloading large objects. [#1394] -- client::Connector accepts initial_window_size and initial_connection_window_size - HTTP2 configuration. [#1394] +- Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively to improve download speed for awc when downloading large objects. [#1394] +- client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394] - client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394] [#1394]: https://github.com/actix/actix-web/pull/1394 [#1395]: https://github.com/actix/actix-web/pull/1395 +## 2.0.0-alpha.1 -## 2.0.0-alpha.1 - 2020-02-27 ### Changed + - Update the `time` dependency to 0.2.7. - Moved actors messages support from actix crate, enabled with feature `actors`. -- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of - `&mut self` in the poll_next(). +- Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of `&mut self` in the poll_next(). - MessageBody is not implemented for &'static [u8] anymore. ### Fixed + - Allow `SameSite=None` cookies to be sent in a response. +## 1.0.1 -## 1.0.1 - 2019-12-20 ### Fixed + - Poll upgrade service's readiness from HTTP service handlers - Replace brotli with brotli2 #1224 +## 1.0.0 -## 1.0.0 - 2019-12-13 ### Added + - Add websockets continuation frame support ### Changed + - Replace `flate2-xxx` features with `compress` +## 1.0.0-alpha.5 -## 1.0.0-alpha.5 - 2019-12-09 ### Fixed + - Check `Upgrade` service readiness before calling it - Fix buffer remaining capacity calculation ### Changed + - Websockets: Ping and Pong should have binary data #1049 +## 1.0.0-alpha.4 -## 1.0.0-alpha.4 - 2019-12-08 ### Added + - Add impl ResponseBuilder for Error ### Changed + - Use rust based brotli compression library -## 1.0.0-alpha.3 - 2019-12-07 +## 1.0.0-alpha.3 + ### Changed + - Migrate to tokio 0.2 - Migrate to `std::future` +## 0.2.11 -## 0.2.11 - 2019-11-06 ### Added + - Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() -- Add an additional `filename*` param in the `Content-Disposition` header of - `actix_files::NamedFile` to be more compatible. (#1151) +- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) - Allow to use `std::convert::Infallible` as `actix_http::error::Error` ### Fixed -- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; - charset=utf-8` header [#1118] + +- To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header [#1118] [#1878]: https://github.com/actix/actix-web/pull/1878 +## 0.2.10 -## 0.2.10 - 2019-09-11 ### Added -- Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests - with `RequestHead` + +- Add support for sending HTTP requests with `Rc` in addition to sending HTTP requests with `RequestHead` ### Fixed + - h2 will use error response #1080 - on_connect result isn't added to request extensions for http2 requests #1009 +## 0.2.9 -## 0.2.9 - 2019-08-13 ### Changed + - Dropped the `byteorder`-dependency in favor of `stdlib`-implementation - Update percent-encoding to 2.1 - Update serde_urlencoded to 0.6.1 ### Fixed + - Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) +## 0.2.8 -## 0.2.8 - 2019-08-01 ### Added + - Add `rustls` support - Add `Clone` impl for `HeaderMap` ### Fixed + - awc client panic #1016 -- Invalid response with compression middleware enabled, but compression-related features - disabled #997 +- Invalid response with compression middleware enabled, but compression-related features disabled #997 +## 0.2.7 -## 0.2.7 - 2019-07-18 ### Added + - Add support for downcasting response errors #986 +## 0.2.6 -## 0.2.6 - 2019-07-17 ### Changed + - Replace `ClonableService` with local copy - Upgrade `rand` dependency version to 0.7 +## 0.2.5 -## 0.2.5 - 2019-06-28 ### Added + - Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 ### Changed + - Use `encoding_rs` crate instead of unmaintained `encoding` crate - Add `Copy` and `Clone` impls for `ws::Codec` +## 0.2.4 -## 0.2.4 - 2019-06-16 ### Fixed + - Do not compress NoContent (204) responses #918 +## 0.2.3 -## 0.2.3 - 2019-06-02 ### Added + - Debug impl for ResponseBuilder - From SizedStream and BodyStream for Body ### Changed + - SizedStream uses u64 +## 0.2.2 -## 0.2.2 - 2019-05-29 ### Fixed + - Parse incoming stream before closing stream on disconnect #868 +## 0.2.1 -## 0.2.1 - 2019-05-25 ### Fixed + - Handle socket read disconnect +## 0.2.0 -## 0.2.0 - 2019-05-12 ### Changed + - Update actix-service to 0.4 - Expect and upgrade services accept `ServerConfig` config. ### Deleted + - `OneRequest` service +## 0.1.5 -## 0.1.5 - 2019-05-04 ### Fixed + - Clean up response extensions in response pool #817 +## 0.1.4 -## 0.1.4 - 2019-04-24 ### Added + - Allow to render h1 request headers in `Camel-Case` ### Fixed + - Read until eof for http/1.0 responses #771 +## 0.1.3 -## 0.1.3 - 2019-04-23 ### Fixed + - Fix http client pool management - Fix http client wait queue management #794 +## 0.1.2 -## 0.1.2 - 2019-04-23 ### Fixed + - Fix BorrowMutError panic in client connector #793 +## 0.1.1 -## 0.1.1 - 2019-04-19 ### Changed + - Cookie::max_age() accepts value in seconds - Cookie::max_age_time() accepts value in time::Duration - Allow to specify server address for client connector +## 0.1.0 -## 0.1.0 - 2019-04-16 ### Added + - Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` ### Changed + - `actix_http::encoding` always available - use trust-dns-resolver 0.11.0 +## 0.1.0-alpha.5 -## 0.1.0-alpha.5 - 2019-04-12 ### Added + - Allow to use custom service for upgrade requests - Added `h1::SendResponse` future. ### Changed + - MessageBody::length() renamed to MessageBody::size() for consistency - ws handshake verification functions take RequestHead instead of Request +## 0.1.0-alpha.4 -## 0.1.0-alpha.4 - 2019-04-08 ### Added + - Allow to use custom `Expect` handler - Add minimal `std::error::Error` impl for `Error` ### Changed + - Export IntoHeaderValue - Render error and return as response body - Use thread pool for response body compression ### Deleted + - Removed PayloadBuffer +## 0.1.0-alpha.3 -## 0.1.0-alpha.3 - 2019-04-02 ### Added + - Warn when an unsealed private cookie isn't valid UTF-8 ### Fixed + - Rust 1.31.0 compatibility - Preallocate read buffer for h1 codec - Detect socket disconnection during protocol selection +## 0.1.0-alpha.2 -## 0.1.0-alpha.2 - 2019-03-29 ### Added + - Added ws::Message::Nop, no-op websockets message ### Changed + - Do not use thread pool for decompression if chunk size is smaller than 2048. +## 0.1.0-alpha.1 -## 0.1.0-alpha.1 - 2019-03-28 - Initial impl diff --git a/actix-http/Cargo.toml b/actix-http/Cargo.toml index 9939089b..4dc0f0bd 100644 --- a/actix-http/Cargo.toml +++ b/actix-http/Cargo.toml @@ -1,30 +1,59 @@ [package] name = "actix-http" -version = "3.2.2" +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 = "2018" +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" -path = "src/lib.rs" +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_codec::*", + "actix_service::*", + "actix_tls::*", + "actix_utils::*", + "bytes::*", + "bytestring::*", + "encoding_rs::*", + "futures_core::*", + "h2::*", + "http::*", + "httparse::*", + "language_tags::*", + "mime::*", + "openssl::*", + "rustls::*", + "tokio_util::*", + "tokio::*", +] [features] default = [] @@ -43,15 +72,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 = [] @@ -61,82 +102,80 @@ actix-codec = "0.5" actix-utils = "3" actix-rt = { version = "2.2", default-features = false } -ahash = "0.7" -bitflags = "1.2" +ahash = "0.8" +bitflags = "2" bytes = "1" bytestring = "1" derive_more = "0.99.5" encoding_rs = "0.8" futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] } -http = "0.2.5" +http = "0.2.7" httparse = "1.5.1" httpdate = "1.0.1" itoa = "1" language-tags = "0.3" -mime = "0.3" +mime = "0.3.4" percent-encoding = "2.1" pin-project-lite = "0.2" smallvec = "1.6.1" -tokio = { version = "1.18.4", features = [] } +tokio = { version = "1.24.2", features = [] } tokio-util = { version = "0.7", features = ["io", "codec"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] } # http2 -h2 = { version = "0.3.9", optional = true } +h2 = { version = "0.3.26", optional = true } # websockets local-channel = { version = "0.1", optional = true } -base64 = { version = "0.13", 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.4", features = ["html_reports"] } -env_logger = "0.9" +criterion = { version = "0.5", features = ["html_reports"] } +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.9" +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.9" } -tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.18.4", features = ["net", "rt", "macros"] } +tls-openssl = { package = "openssl", version = "0.10.55" } +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 = "write-camel-case" +name = "response-body-compression" harness = false +required-features = ["compress-brotli", "compress-gzip", "compress-zstd"] [[bench]] -name = "status-line" -harness = false - -[[bench]] -name = "uninit-headers" -harness = false - -[[bench]] -name = "quality-value" +name = "date-formatting" harness = false diff --git a/actix-http/README.md b/actix-http/README.md index 994cf97c..0ba3fdca 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.2.2)](https://docs.rs/actix-http/3.2.2) -![Version](https://img.shields.io/badge/rustc-1.59+-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.2.2/status.svg)](https://deps.rs/crate/actix-http/3.2.2) +[![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.54 - -## Example +## Examples ```rust use std::{env, io}; @@ -49,18 +48,3 @@ async fn main() -> io::Result<()> { .await } ``` - -## License - -This project is licensed under either of - -- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0)) -- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT)) - -at your option. - -## Code of Conduct - -Contribution to the actix-http crate is organized under the terms of the -Contributor Covenant, the maintainer of actix-http, @fafhrd91, promises to -intervene to uphold that code of conduct. diff --git a/actix-http/benches/date-formatting.rs b/actix-http/benches/date-formatting.rs new file mode 100644 index 00000000..26d0f3da --- /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/quality-value.rs b/actix-http/benches/quality-value.rs deleted file mode 100644 index 0ed274de..00000000 --- a/actix-http/benches/quality-value.rs +++ /dev/null @@ -1,97 +0,0 @@ -#![allow(clippy::uninlined_format_args)] - -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; - -const CODES: &[u16] = &[0, 1000, 201, 800, 550]; - -fn bench_quality_display_impls(c: &mut Criterion) { - let mut group = c.benchmark_group("quality value display impls"); - - for i in CODES.iter() { - group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| { - b.iter(|| _new::Quality(i).to_string()) - }); - - group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { - b.iter(|| _naive::Quality(i).to_string()) - }); - } - - group.finish(); -} - -criterion_group!(benches, bench_quality_display_impls); -criterion_main!(benches); - -mod _new { - use std::fmt; - - pub struct Quality(pub(crate) u16); - - impl fmt::Display for Quality { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0 { - 0 => f.write_str("0"), - 1000 => f.write_str("1"), - - // some number in the range 1–999 - x => { - f.write_str("0.")?; - - // this implementation avoids string allocation otherwise required - // for `.trim_end_matches('0')` - - if x < 10 { - f.write_str("00")?; - // 0 is handled so it's not possible to have a trailing 0, we can just return - itoa_fmt(f, x) - } else if x < 100 { - f.write_str("0")?; - if x % 10 == 0 { - // trailing 0, divide by 10 and write - itoa_fmt(f, x / 10) - } else { - itoa_fmt(f, x) - } - } else { - // x is in range 101–999 - - if x % 100 == 0 { - // two trailing 0s, divide by 100 and write - itoa_fmt(f, x / 100) - } else if x % 10 == 0 { - // one trailing 0, divide by 10 and write - itoa_fmt(f, x / 10) - } else { - itoa_fmt(f, x) - } - } - } - } - } - } - - pub fn itoa_fmt(mut wr: W, value: V) -> fmt::Result { - let mut buf = itoa::Buffer::new(); - wr.write_str(buf.format(value)) - } -} - -mod _naive { - use std::fmt; - - pub struct Quality(pub(crate) u16); - - impl fmt::Display for Quality { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - match self.0 { - 0 => f.write_str("0"), - 1000 => f.write_str("1"), - - x => { - write!(f, "{}", format!("{:03}", x).trim_end_matches('0')) - } - } - } - } -} diff --git a/actix-http/benches/response-body-compression.rs b/actix-http/benches/response-body-compression.rs new file mode 100644 index 00000000..53279e31 --- /dev/null +++ b/actix-http/benches/response-body-compression.rs @@ -0,0 +1,88 @@ +use std::convert::Infallible; + +use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode}; +use actix_service::{fn_service, Service as _}; +use criterion::{black_box, criterion_group, criterion_main, Criterion}; + +static BODY: &[u8] = include_bytes!("../Cargo.toml"); + +fn compression_responses(c: &mut Criterion) { + let mut group = c.benchmark_group("compression responses"); + + group.bench_function("identity", |b| { + let rt = actix_rt::Runtime::new().unwrap(); + + let identity_svc = fn_service(|_: Request| async move { + let mut res = Response::with_body(StatusCode::OK, ()); + let body = black_box(Encoder::response( + ContentEncoding::Identity, + res.head_mut(), + BODY, + )); + Ok::<_, Infallible>(black_box(res.set_body(black_box(body)))) + }); + + b.iter(|| { + rt.block_on(identity_svc.call(Request::new())).unwrap(); + }); + }); + + group.bench_function("gzip", |b| { + let rt = actix_rt::Runtime::new().unwrap(); + + let identity_svc = fn_service(|_: Request| async move { + let mut res = Response::with_body(StatusCode::OK, ()); + let body = black_box(Encoder::response( + ContentEncoding::Gzip, + res.head_mut(), + BODY, + )); + Ok::<_, Infallible>(black_box(res.set_body(black_box(body)))) + }); + + b.iter(|| { + rt.block_on(identity_svc.call(Request::new())).unwrap(); + }); + }); + + group.bench_function("br", |b| { + let rt = actix_rt::Runtime::new().unwrap(); + + let identity_svc = fn_service(|_: Request| async move { + let mut res = Response::with_body(StatusCode::OK, ()); + let body = black_box(Encoder::response( + ContentEncoding::Brotli, + res.head_mut(), + BODY, + )); + Ok::<_, Infallible>(black_box(res.set_body(black_box(body)))) + }); + + b.iter(|| { + rt.block_on(identity_svc.call(Request::new())).unwrap(); + }); + }); + + group.bench_function("zstd", |b| { + let rt = actix_rt::Runtime::new().unwrap(); + + let identity_svc = fn_service(|_: Request| async move { + let mut res = Response::with_body(StatusCode::OK, ()); + let body = black_box(Encoder::response( + ContentEncoding::Zstd, + res.head_mut(), + BODY, + )); + Ok::<_, Infallible>(black_box(res.set_body(black_box(body)))) + }); + + b.iter(|| { + rt.block_on(identity_svc.call(Request::new())).unwrap(); + }); + }); + + group.finish(); +} + +criterion_group!(benches, compression_responses); +criterion_main!(benches); diff --git a/actix-http/benches/status-line.rs b/actix-http/benches/status-line.rs deleted file mode 100644 index 9fe09947..00000000 --- a/actix-http/benches/status-line.rs +++ /dev/null @@ -1,214 +0,0 @@ -use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion}; - -use bytes::BytesMut; -use http::Version; - -const CODES: &[u16] = &[201, 303, 404, 515]; - -fn bench_write_status_line_11(c: &mut Criterion) { - let mut group = c.benchmark_group("write_status_line v1.1"); - - let version = Version::HTTP_11; - - for i in CODES.iter() { - group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _original::write_status_line(version, i, &mut b); - }) - }); - - group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _new::write_status_line(version, i, &mut b); - }) - }); - - group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _naive::write_status_line(version, i, &mut b); - }) - }); - } - - group.finish(); -} - -fn bench_write_status_line_10(c: &mut Criterion) { - let mut group = c.benchmark_group("write_status_line v1.0"); - - let version = Version::HTTP_10; - - for i in CODES.iter() { - group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _original::write_status_line(version, i, &mut b); - }) - }); - - group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _new::write_status_line(version, i, &mut b); - }) - }); - - group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _naive::write_status_line(version, i, &mut b); - }) - }); - } - - group.finish(); -} - -fn bench_write_status_line_09(c: &mut Criterion) { - let mut group = c.benchmark_group("write_status_line v0.9"); - - let version = Version::HTTP_09; - - for i in CODES.iter() { - group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _original::write_status_line(version, i, &mut b); - }) - }); - - group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _new::write_status_line(version, i, &mut b); - }) - }); - - group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| { - b.iter(|| { - let mut b = BytesMut::with_capacity(35); - _naive::write_status_line(version, i, &mut b); - }) - }); - } - - group.finish(); -} - -criterion_group!( - benches, - bench_write_status_line_11, - bench_write_status_line_10, - bench_write_status_line_09 -); -criterion_main!(benches); - -mod _naive { - use bytes::{BufMut, BytesMut}; - use http::Version; - - pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { - match version { - Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), - Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), - Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), - _ => { - // other HTTP version handlers do not use this method - } - } - - bytes.put_slice(n.to_string().as_bytes()); - } -} - -mod _new { - use bytes::{BufMut, BytesMut}; - use http::Version; - - const DIGITS_START: u8 = b'0'; - - pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { - match version { - Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), - Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), - Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), - _ => { - // other HTTP version handlers do not use this method - } - } - - let d100 = (n / 100) as u8; - let d10 = ((n / 10) % 10) as u8; - let d1 = (n % 10) as u8; - - bytes.put_u8(DIGITS_START + d100); - bytes.put_u8(DIGITS_START + d10); - bytes.put_u8(DIGITS_START + d1); - - bytes.put_u8(b' '); - } -} - -mod _original { - use std::ptr; - - use bytes::{BufMut, BytesMut}; - use http::Version; - - const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\ - 2021222324252627282930313233343536373839\ - 4041424344454647484950515253545556575859\ - 6061626364656667686970717273747576777879\ - 8081828384858687888990919293949596979899"; - - pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13; - - pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) { - let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 "; - - match version { - Version::HTTP_2 => buf[5] = b'2', - Version::HTTP_10 => buf[7] = b'0', - Version::HTTP_09 => { - buf[5] = b'0'; - buf[7] = b'9'; - } - _ => {} - } - - let mut curr: isize = 12; - let buf_ptr = buf.as_mut_ptr(); - let lut_ptr = DEC_DIGITS_LUT.as_ptr(); - let four = n > 999; - - // decode 2 more chars, if > 2 chars - let d1 = (n % 100) << 1; - n /= 100; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); - } - - // decode last 1 or 2 chars - if n < 10 { - curr -= 1; - unsafe { - *buf_ptr.offset(curr) = (n as u8) + b'0'; - } - } else { - let d1 = n << 1; - curr -= 2; - unsafe { - ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2); - } - } - - bytes.put_slice(&buf); - if four { - bytes.put_u8(b' '); - } - } -} diff --git a/actix-http/benches/uninit-headers.rs b/actix-http/benches/uninit-headers.rs deleted file mode 100644 index 688c64d6..00000000 --- a/actix-http/benches/uninit-headers.rs +++ /dev/null @@ -1,135 +0,0 @@ -use criterion::{criterion_group, criterion_main, Criterion}; - -use bytes::BytesMut; - -// A Miri run detects UB, seen on this playground: -// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915 - -fn bench_header_parsing(c: &mut Criterion) { - c.bench_function("Original (Unsound) [short]", |b| { - b.iter(|| { - let mut buf = BytesMut::from(REQ_SHORT); - _original::parse_headers(&mut buf); - }) - }); - - c.bench_function("New (safe) [short]", |b| { - b.iter(|| { - let mut buf = BytesMut::from(REQ_SHORT); - _new::parse_headers(&mut buf); - }) - }); - - c.bench_function("Original (Unsound) [realistic]", |b| { - b.iter(|| { - let mut buf = BytesMut::from(REQ); - _original::parse_headers(&mut buf); - }) - }); - - c.bench_function("New (safe) [realistic]", |b| { - b.iter(|| { - let mut buf = BytesMut::from(REQ); - _new::parse_headers(&mut buf); - }) - }); -} - -criterion_group!(benches, bench_header_parsing); -criterion_main!(benches); - -const MAX_HEADERS: usize = 96; - -const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] = - [httparse::EMPTY_HEADER; MAX_HEADERS]; - -#[derive(Clone, Copy)] -struct HeaderIndex { - name: (usize, usize), - value: (usize, usize), -} - -const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex { - name: (0, 0), - value: (0, 0), -}; - -const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS]; - -impl HeaderIndex { - fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) { - let bytes_ptr = bytes.as_ptr() as usize; - for (header, indices) in headers.iter().zip(indices.iter_mut()) { - let name_start = header.name.as_ptr() as usize - bytes_ptr; - let name_end = name_start + header.name.len(); - indices.name = (name_start, name_end); - let value_start = header.value.as_ptr() as usize - bytes_ptr; - let value_end = value_start + header.value.len(); - indices.value = (value_start, value_end); - } - } -} - -// test cases taken from: -// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs - -const REQ_SHORT: &[u8] = b"\ -GET / HTTP/1.0\r\n\ -Host: example.com\r\n\ -Cookie: session=60; user_id=1\r\n\r\n"; - -const REQ: &[u8] = b"\ -GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\ -Host: www.kittyhell.com\r\n\ -User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\ -Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\ -Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\ -Accept-Encoding: gzip,deflate\r\n\ -Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\ -Keep-Alive: 115\r\n\ -Connection: keep-alive\r\n\ -Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n"; - -mod _new { - use super::*; - - pub fn parse_headers(src: &mut BytesMut) -> usize { - let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; - - let mut req = httparse::Request::new(&mut parsed); - match req.parse(src).unwrap() { - httparse::Status::Complete(_len) => { - HeaderIndex::record(src, req.headers, &mut headers); - req.headers.len() - } - _ => unreachable!(), - } - } -} - -mod _original { - use super::*; - - use std::mem::MaybeUninit; - - pub fn parse_headers(src: &mut BytesMut) -> usize { - #![allow(invalid_value, clippy::uninit_assumed_init)] - - let mut headers: [HeaderIndex; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; - - #[allow(invalid_value)] - let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = - unsafe { MaybeUninit::uninit().assume_init() }; - - let mut req = httparse::Request::new(&mut parsed); - match req.parse(src).unwrap() { - httparse::Status::Complete(_len) => { - HeaderIndex::record(src, req.headers, &mut headers); - req.headers.len() - } - _ => unreachable!(), - } - } -} diff --git a/actix-http/benches/write-camel-case.rs b/actix-http/benches/write-camel-case.rs deleted file mode 100644 index ccf09b37..00000000 --- a/actix-http/benches/write-camel-case.rs +++ /dev/null @@ -1,93 +0,0 @@ -use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion}; - -fn bench_write_camel_case(c: &mut Criterion) { - let mut group = c.benchmark_group("write_camel_case"); - - let names = ["connection", "Transfer-Encoding", "transfer-encoding"]; - - for &i in &names { - let bts = i.as_bytes(); - - group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| { - b.iter(|| { - let mut buf = black_box([0; 24]); - _original::write_camel_case(black_box(bts), &mut buf) - }); - }); - - group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| { - b.iter(|| { - let mut buf = black_box([0; 24]); - let len = black_box(bts.len()); - _new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len) - }); - }); - } - - group.finish(); -} - -criterion_group!(benches, bench_write_camel_case); -criterion_main!(benches); - -mod _new { - pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) { - // first copy entire (potentially wrong) slice to output - let buffer = unsafe { - std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len); - std::slice::from_raw_parts_mut(buf, len) - }; - - let mut iter = value.iter(); - - // first character should be uppercase - if let Some(c @ b'a'..=b'z') = iter.next() { - buffer[0] = c & 0b1101_1111; - } - - // track 1 ahead of the current position since that's the location being assigned to - let mut index = 2; - - // remaining characters after hyphens should also be uppercase - while let Some(&c) = iter.next() { - if c == b'-' { - // advance iter by one and uppercase if needed - if let Some(c @ b'a'..=b'z') = iter.next() { - buffer[index] = c & 0b1101_1111; - } - } - - index += 1; - } - } -} - -mod _original { - pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) { - let mut index = 0; - let key = value; - let mut key_iter = key.iter(); - - if let Some(c) = key_iter.next() { - if *c >= b'a' && *c <= b'z' { - buffer[index] = *c ^ b' '; - index += 1; - } - } else { - return; - } - - while let Some(c) = key_iter.next() { - buffer[index] = *c; - index += 1; - if *c == b'-' { - if let Some(c) = key_iter.next() { - if *c >= b'a' && *c <= b'z' { - buffer[index] = *c ^ b' '; - index += 1; - } - } - } - } - } -} diff --git a/actix-http/examples/h2c-detect.rs b/actix-http/examples/h2c-detect.rs index aa3dd5d3..b0bde3fe 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/hello-world.rs b/actix-http/examples/hello-world.rs index c749cdd0..cf10bedd 100644 --- a/actix-http/examples/hello-world.rs +++ b/actix-http/examples/hello-world.rs @@ -23,10 +23,7 @@ async fn main() -> io::Result<()> { res.insert_header(("x-head", HeaderValue::from_static("dummy value!"))); let forty_two = req.conn_data::().unwrap().to_string(); - res.insert_header(( - "x-forty-two", - HeaderValue::from_str(&forty_two).unwrap(), - )); + res.insert_header(("x-forty-two", HeaderValue::from_str(&forty_two).unwrap())); Ok::<_, Infallible>(res.body("Hello world!")) }) diff --git a/actix-http/examples/tls_rustls.rs b/actix-http/examples/tls_rustls.rs new file mode 100644 index 00000000..17303c55 --- /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 6af6d509..fb86bc5e 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/body_stream.rs b/actix-http/src/body/body_stream.rs index 5a12c1e4..4574b251 100644 --- a/actix-http/src/body/body_stream.rs +++ b/actix-http/src/body/body_stream.rs @@ -47,9 +47,8 @@ where /// Attempts to pull out the next value of the underlying [`Stream`]. /// - /// Empty values are skipped to prevent [`BodyStream`]'s transmission being - /// ended on a zero-length chunk, but rather proceed until the underlying - /// [`Stream`] ends. + /// Empty values are skipped to prevent [`BodyStream`]'s transmission being ended on a + /// zero-length chunk, but rather proceed until the underlying [`Stream`] ends. fn poll_next( mut self: Pin<&mut Self>, cx: &mut Context<'_>, diff --git a/actix-http/src/body/boxed.rs b/actix-http/src/body/boxed.rs index 5fcc42f5..0151ff3a 100644 --- a/actix-http/src/body/boxed.rs +++ b/actix-http/src/body/boxed.rs @@ -77,12 +77,8 @@ impl MessageBody for BoxBody { cx: &mut Context<'_>, ) -> Poll>> { match &mut self.0 { - BoxBodyInner::None(body) => { - Pin::new(body).poll_next(cx).map_err(|err| match err {}) - } - BoxBodyInner::Bytes(body) => { - Pin::new(body).poll_next(cx).map_err(|err| match err {}) - } + BoxBodyInner::None(body) => Pin::new(body).poll_next(cx).map_err(|err| match err {}), + BoxBodyInner::Bytes(body) => Pin::new(body).poll_next(cx).map_err(|err| match err {}), BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx), } } @@ -104,7 +100,6 @@ impl MessageBody for BoxBody { #[cfg(test)] mod tests { - use static_assertions::{assert_impl_all, assert_not_impl_any}; use super::*; diff --git a/actix-http/src/body/message_body.rs b/actix-http/src/body/message_body.rs index e274cf8a..739fe502 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::*; @@ -555,6 +554,7 @@ mod tests { }; } + #[allow(unused_allocation)] // triggered by `Box::new(()).size()` #[actix_rt::test] async fn boxing_equivalence() { assert_eq!(().size(), BodySize::Sized(0)); diff --git a/actix-http/src/body/mod.rs b/actix-http/src/body/mod.rs index 0fb090eb..1a12f333 100644 --- a/actix-http/src/body/mod.rs +++ b/actix-http/src/body/mod.rs @@ -14,12 +14,14 @@ mod size; mod sized_stream; mod utils; -pub use self::body_stream::BodyStream; -pub use self::boxed::BoxBody; -pub use self::either::EitherBody; -pub use self::message_body::MessageBody; pub(crate) use self::message_body::MessageBodyMapErr; -pub use self::none::None; -pub use self::size::BodySize; -pub use self::sized_stream::SizedStream; -pub use self::utils::to_bytes; +pub use self::{ + body_stream::BodyStream, + boxed::BoxBody, + either::EitherBody, + message_body::MessageBody, + none::None, + size::BodySize, + sized_stream::SizedStream, + utils::{to_bytes, to_bytes_limited, BodyLimitExceeded}, +}; diff --git a/actix-http/src/body/utils.rs b/actix-http/src/body/utils.rs index 0a6fb0c1..d1449179 100644 --- a/actix-http/src/body/utils.rs +++ b/actix-http/src/body/utils.rs @@ -3,75 +3,196 @@ use std::task::Poll; use actix_rt::pin; use actix_utils::future::poll_fn; use bytes::{Bytes, BytesMut}; +use derive_more::{Display, Error}; use futures_core::ready; use super::{BodySize, MessageBody}; -/// Collects the body produced by a `MessageBody` implementation into `Bytes`. +/// Collects all the bytes produced by `body`. /// /// Any errors produced by the body stream are returned immediately. /// +/// Consider using [`to_bytes_limited`] instead to protect against memory exhaustion. +/// /// # Examples +/// /// ``` /// use actix_http::body::{self, to_bytes}; /// use bytes::Bytes; /// -/// # async fn test_to_bytes() { +/// # actix_rt::System::new().block_on(async { /// let body = body::None::new(); /// let bytes = to_bytes(body).await.unwrap(); /// assert!(bytes.is_empty()); /// /// let body = Bytes::from_static(b"123"); /// let bytes = to_bytes(body).await.unwrap(); -/// assert_eq!(bytes, b"123"[..]); -/// # } +/// assert_eq!(bytes, "123"); +/// # }); /// ``` pub async fn to_bytes(body: B) -> Result { + to_bytes_limited(body, usize::MAX) + .await + .expect("body should never yield more than usize::MAX bytes") +} + +/// Error type returned from [`to_bytes_limited`] when body produced exceeds limit. +#[derive(Debug, Display, Error)] +#[display(fmt = "limit exceeded while collecting body bytes")] +#[non_exhaustive] +pub struct BodyLimitExceeded; + +/// Collects the bytes produced by `body`, up to `limit` bytes. +/// +/// If a chunk read from `poll_next` causes the total number of bytes read to exceed `limit`, an +/// `Err(BodyLimitExceeded)` is returned. +/// +/// Any errors produced by the body stream are returned immediately as `Ok(Err(B::Error))`. +/// +/// # Examples +/// +/// ``` +/// use actix_http::body::{self, to_bytes_limited}; +/// use bytes::Bytes; +/// +/// # actix_rt::System::new().block_on(async { +/// let body = body::None::new(); +/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap(); +/// assert!(bytes.is_empty()); +/// +/// let body = Bytes::from_static(b"123"); +/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap(); +/// assert_eq!(bytes, "123"); +/// +/// let body = Bytes::from_static(b"123"); +/// assert!(to_bytes_limited(body, 2).await.is_err()); +/// # }); +/// ``` +pub async fn to_bytes_limited( + body: B, + limit: usize, +) -> Result, BodyLimitExceeded> { + /// Sensible default (32kB) for initial, bounded allocation when collecting body bytes. + const INITIAL_ALLOC_BYTES: usize = 32 * 1024; + let cap = match body.size() { - BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()), - BodySize::Sized(size) => size as usize, - // good enough first guess for chunk size - BodySize::Stream => 32_768, + BodySize::None | BodySize::Sized(0) => return Ok(Ok(Bytes::new())), + BodySize::Sized(size) if size as usize > limit => return Err(BodyLimitExceeded), + BodySize::Sized(size) => (size as usize).min(INITIAL_ALLOC_BYTES), + BodySize::Stream => INITIAL_ALLOC_BYTES, }; + let mut exceeded_limit = false; let mut buf = BytesMut::with_capacity(cap); pin!(body); - poll_fn(|cx| loop { + match poll_fn(|cx| loop { let body = body.as_mut(); match ready!(body.poll_next(cx)) { - Some(Ok(bytes)) => buf.extend_from_slice(&bytes), + Some(Ok(bytes)) => { + // if limit is exceeded... + if buf.len() + bytes.len() > limit { + // ...set flag to true and break out of poll_fn + exceeded_limit = true; + return Poll::Ready(Ok(())); + } + + buf.extend_from_slice(&bytes) + } None => return Poll::Ready(Ok(())), Some(Err(err)) => return Poll::Ready(Err(err)), } }) - .await?; + .await + { + // propagate error returned from body poll + Err(err) => Ok(Err(err)), - Ok(buf.freeze()) + // limit was exceeded while reading body + Ok(()) if exceeded_limit => Err(BodyLimitExceeded), + + // otherwise return body buffer + Ok(()) => Ok(Ok(buf.freeze())), + } } #[cfg(test)] -mod test { +mod tests { + use std::io; + use futures_util::{stream, StreamExt as _}; use super::*; - use crate::{body::BodyStream, Error}; + use crate::{ + body::{BodyStream, SizedStream}, + Error, + }; #[actix_rt::test] - async fn test_to_bytes() { + async fn to_bytes_complete() { let bytes = to_bytes(()).await.unwrap(); assert!(bytes.is_empty()); let body = Bytes::from_static(b"123"); let bytes = to_bytes(body).await.unwrap(); assert_eq!(bytes, b"123"[..]); + } + #[actix_rt::test] + async fn to_bytes_streams() { let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) .map(Ok::<_, Error>); let body = BodyStream::new(stream); let bytes = to_bytes(body).await.unwrap(); assert_eq!(bytes, b"123abc"[..]); } + + #[actix_rt::test] + async fn to_bytes_limited_complete() { + let bytes = to_bytes_limited((), 0).await.unwrap().unwrap(); + assert!(bytes.is_empty()); + + let bytes = to_bytes_limited((), 1).await.unwrap().unwrap(); + assert!(bytes.is_empty()); + + assert!(to_bytes_limited(Bytes::from_static(b"12"), 0) + .await + .is_err()); + assert!(to_bytes_limited(Bytes::from_static(b"12"), 1) + .await + .is_err()); + assert!(to_bytes_limited(Bytes::from_static(b"12"), 2).await.is_ok()); + assert!(to_bytes_limited(Bytes::from_static(b"12"), 3).await.is_ok()); + } + + #[actix_rt::test] + async fn to_bytes_limited_streams() { + // hinting a larger body fails + let body = SizedStream::new(8, stream::empty().map(Ok::<_, Error>)); + assert!(to_bytes_limited(body, 3).await.is_err()); + + // hinting a smaller body is okay + let body = SizedStream::new(3, stream::empty().map(Ok::<_, Error>)); + assert!(to_bytes_limited(body, 3).await.unwrap().unwrap().is_empty()); + + // hinting a smaller body then returning a larger one fails + let stream = stream::iter(vec![Bytes::from_static(b"1234")]).map(Ok::<_, Error>); + let body = SizedStream::new(3, stream); + assert!(to_bytes_limited(body, 3).await.is_err()); + + let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) + .map(Ok::<_, Error>); + let body = BodyStream::new(stream); + assert!(to_bytes_limited(body, 3).await.is_err()); + } + + #[actix_rt::test] + async fn to_body_limit_error() { + let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) }); + let body = SizedStream::new(8, err_stream); + // not too big, but propagates error from body stream + assert!(to_bytes_limited(body, 10).await.unwrap().is_err()); + } } diff --git a/actix-http/src/builder.rs b/actix-http/src/builder.rs index e2693aca..916083a9 100644 --- a/actix-http/src/builder.rs +++ b/actix-http/src/builder.rs @@ -211,7 +211,6 @@ where /// Finish service configuration and create a service for the HTTP/2 protocol. #[cfg(feature = "http2")] - #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn h2(self, service: F) -> crate::h2::H2Service where F: IntoServiceFactory, diff --git a/actix-http/src/config.rs b/actix-http/src/config.rs index c0d297a2..b3b215da 100644 --- a/actix-http/src/config.rs +++ b/actix-http/src/config.rs @@ -132,15 +132,15 @@ impl ServiceConfig { #[cfg(test)] mod tests { - use super::*; - use crate::{date::DATE_VALUE_LENGTH, notify_on_drop}; - use actix_rt::{ task::yield_now, time::{sleep, sleep_until}, }; use memchr::memmem; + use super::*; + use crate::{date::DATE_VALUE_LENGTH, notify_on_drop}; + #[actix_rt::test] async fn test_date_service_update() { let settings = diff --git a/actix-http/src/date.rs b/actix-http/src/date.rs index 1358bbd8..735dd910 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 06b672fd..cda534d6 100644 --- a/actix-http/src/encoding/decoder.rs +++ b/actix-http/src/encoding/decoder.rs @@ -9,11 +9,9 @@ use std::{ use actix_rt::task::{spawn_blocking, JoinHandle}; use bytes::Bytes; -use futures_core::{ready, Stream}; - #[cfg(feature = "compress-gzip")] use flate2::write::{GzDecoder, ZlibDecoder}; - +use futures_core::{ready, Stream}; #[cfg(feature = "compress-zstd")] use zstd::stream::write::Decoder as ZstdDecoder; @@ -49,9 +47,9 @@ where ))), #[cfg(feature = "compress-gzip")] - ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( - ZlibDecoder::new(Writer::new()), - ))), + ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(ZlibDecoder::new( + Writer::new(), + )))), #[cfg(feature = "compress-gzip")] ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new( @@ -193,7 +191,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -207,7 +205,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -220,7 +218,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-zstd")] @@ -233,7 +231,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, } } @@ -252,7 +250,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -267,7 +265,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-gzip")] @@ -282,7 +280,7 @@ impl ContentDecoder { Ok(None) } } - Err(e) => Err(e), + Err(err) => Err(err), }, #[cfg(feature = "compress-zstd")] @@ -297,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 8b20d818..6d991951 100644 --- a/actix-http/src/encoding/encoder.rs +++ b/actix-http/src/encoding/encoder.rs @@ -11,12 +11,10 @@ use std::{ use actix_rt::task::{spawn_blocking, JoinHandle}; use bytes::Bytes; use derive_more::Display; -use futures_core::ready; -use pin_project_lite::pin_project; - #[cfg(feature = "compress-gzip")] use flate2::write::{GzEncoder, ZlibEncoder}; - +use futures_core::ready; +use pin_project_lite::pin_project; use tracing::trace; #[cfg(feature = "compress-zstd")] use zstd::stream::write::Encoder as ZstdEncoder; @@ -76,6 +74,15 @@ 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 { Encoder::response_with_level(encoding, head, body, None) } @@ -86,9 +93,11 @@ impl Encoder { body: B, level: Option, ) -> 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) @@ -362,9 +371,10 @@ impl ContentEncoder { )), #[cfg(feature = "compress-gzip")] - ContentEncodingWithLevel::Gzip(level) => Some(ContentEncoder::Gzip( - GzEncoder::new(Writer::new(), flate2::Compression::new(level)), - )), + ContentEncodingWithLevel::Gzip(level) => Some(ContentEncoder::Gzip(GzEncoder::new( + Writer::new(), + flate2::Compression::new(level), + ))), #[cfg(feature = "compress-brotli")] ContentEncodingWithLevel::Brotli(level) => Some(ContentEncoder::Brotli(Box::new( diff --git a/actix-http/src/encoding/mod.rs b/actix-http/src/encoding/mod.rs index d51dd66c..6801b5fb 100644 --- a/actix-http/src/encoding/mod.rs +++ b/actix-http/src/encoding/mod.rs @@ -7,13 +7,12 @@ use bytes::{Bytes, BytesMut}; mod decoder; mod encoder; -pub use self::decoder::Decoder; -pub use self::encoder::Encoder; +pub use self::{decoder::Decoder, encoder::Encoder}; /// Special-purpose writer for streaming (de-)compression. /// /// Pre-allocates 8KiB of capacity. -pub(self) struct Writer { +struct Writer { buf: BytesMut, } diff --git a/actix-http/src/error.rs b/actix-http/src/error.rs index 2d443369..6f332118 100644 --- a/actix-http/src/error.rs +++ b/actix-http/src/error.rs @@ -3,12 +3,11 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; use derive_more::{Display, Error, From}; +pub use http::{status::InvalidStatusCode, Error as HttpError}; use http::{uri::InvalidUri, StatusCode}; use crate::{body::BoxBody, Response}; -pub use http::Error as HttpError; - pub struct Error { inner: Box, } @@ -161,44 +160,44 @@ impl From for Error { #[non_exhaustive] pub enum ParseError { /// An invalid `Method`, such as `GE.T`. - #[display(fmt = "Invalid Method specified")] + #[display(fmt = "invalid method specified")] Method, /// An invalid `Uri`, such as `exam ple.domain`. - #[display(fmt = "Uri error: {}", _0)] + #[display(fmt = "URI error: {}", _0)] Uri(InvalidUri), /// An invalid `HttpVersion`, such as `HTP/1.1` - #[display(fmt = "Invalid HTTP version specified")] + #[display(fmt = "invalid HTTP version specified")] Version, /// An invalid `Header`. - #[display(fmt = "Invalid Header provided")] + #[display(fmt = "invalid Header provided")] Header, /// A message head is too large to be reasonable. - #[display(fmt = "Message head is too large")] + #[display(fmt = "message head is too large")] TooLarge, /// A message reached EOF, but is not complete. - #[display(fmt = "Message is incomplete")] + #[display(fmt = "message is incomplete")] Incomplete, /// An invalid `Status`, such as `1337 ELITE`. - #[display(fmt = "Invalid Status provided")] + #[display(fmt = "invalid status provided")] Status, /// A timeout occurred waiting for an IO event. #[allow(dead_code)] - #[display(fmt = "Timeout")] + #[display(fmt = "timeout")] Timeout, - /// An `io::Error` that occurred while trying to read or write to a network stream. - #[display(fmt = "IO error: {}", _0)] + /// An I/O error that occurred while trying to read or write to a network stream. + #[display(fmt = "I/O error: {}", _0)] Io(io::Error), /// Parsing a field as string failed. - #[display(fmt = "UTF8 error: {}", _0)] + #[display(fmt = "UTF-8 error: {}", _0)] Utf8(Utf8Error), } @@ -257,22 +256,19 @@ impl From for Response { #[non_exhaustive] pub enum PayloadError { /// A payload reached EOF, but is not complete. - #[display( - fmt = "A payload reached EOF, but is not complete. Inner error: {:?}", - _0 - )] + #[display(fmt = "payload reached EOF before completing: {:?}", _0)] Incomplete(Option), /// Content encoding stream corruption. - #[display(fmt = "Can not decode content-encoding.")] + #[display(fmt = "can not decode content-encoding")] EncodingCorrupted, /// Payload reached size limit. - #[display(fmt = "Payload reached size limit.")] + #[display(fmt = "payload reached size limit")] Overflow, /// Payload length is unknown. - #[display(fmt = "Payload length is unknown.")] + #[display(fmt = "payload length is unknown")] UnknownLength, /// HTTP/2 payload error. @@ -294,7 +290,6 @@ impl std::error::Error for PayloadError { PayloadError::Overflow => None, PayloadError::UnknownLength => None, #[cfg(feature = "http2")] - #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] PayloadError::Http2Payload(err) => Some(err), PayloadError::Io(err) => Some(err), } @@ -331,44 +326,44 @@ impl From for Error { #[non_exhaustive] pub enum DispatchError { /// Service error. - #[display(fmt = "Service Error")] + #[display(fmt = "service error")] Service(Response), /// Body streaming error. - #[display(fmt = "Body error: {}", _0)] + #[display(fmt = "body error: {}", _0)] Body(Box), /// Upgrade service error. + #[display(fmt = "upgrade error")] Upgrade, /// An `io::Error` that occurred while trying to read or write to a network stream. - #[display(fmt = "IO error: {}", _0)] + #[display(fmt = "I/O error: {}", _0)] Io(io::Error), /// Request parse error. - #[display(fmt = "Request parse error: {}", _0)] + #[display(fmt = "request parse error: {}", _0)] Parse(ParseError), /// HTTP/2 error. #[display(fmt = "{}", _0)] #[cfg(feature = "http2")] - #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] H2(h2::Error), /// The first request did not complete within the specified timeout. - #[display(fmt = "The first request did not complete within the specified timeout")] + #[display(fmt = "request did not complete within the specified timeout")] SlowRequestTimeout, - /// Disconnect timeout. Makes sense for ssl streams. - #[display(fmt = "Connection shutdown timeout")] + /// Disconnect timeout. Makes sense for TLS streams. + #[display(fmt = "connection shutdown timeout")] DisconnectTimeout, /// Handler dropped payload before reading EOF. - #[display(fmt = "Handler dropped payload before reading EOF")] + #[display(fmt = "handler dropped payload before reading EOF")] HandlerDroppedPayload, /// Internal error. - #[display(fmt = "Internal error")] + #[display(fmt = "internal error")] InternalError, } @@ -393,20 +388,18 @@ impl StdError for DispatchError { #[cfg_attr(test, derive(PartialEq, Eq))] #[non_exhaustive] pub enum ContentTypeError { - /// Can not parse content type - #[display(fmt = "Can not parse content type")] + /// Can not parse content type. + #[display(fmt = "could not parse content type")] ParseError, - /// Unknown content encoding - #[display(fmt = "Unknown content encoding")] + /// Unknown content encoding. + #[display(fmt = "unknown content encoding")] UnknownEncoding, } #[cfg(test)] mod tests { - use std::io; - - use http::{Error as HttpError, StatusCode}; + use http::Error as HttpError; use super::*; @@ -426,7 +419,7 @@ mod tests { let err: Error = ParseError::Io(orig).into(); assert_eq!( format!("{}", err), - "error parsing HTTP message: IO error: other" + "error parsing HTTP message: I/O error: other" ); } @@ -453,7 +446,7 @@ mod tests { let err = PayloadError::Incomplete(None); assert_eq!( err.to_string(), - "A payload reached EOF, but is not complete. Inner error: None" + "payload reached EOF before completing: None" ); } @@ -473,7 +466,7 @@ mod tests { match ParseError::from($from) { e @ $error => { let desc = format!("{}", e); - assert_eq!(desc, format!("IO error: {}", $from)); + assert_eq!(desc, format!("I/O error: {}", $from)); } _ => unreachable!("{:?}", $from), } diff --git a/actix-http/src/h1/client.rs b/actix-http/src/h1/client.rs index 6a0d531d..f3947dd1 100644 --- a/actix-http/src/h1/client.rs +++ b/actix-http/src/h1/client.rs @@ -16,6 +16,7 @@ use crate::{ }; bitflags! { + #[derive(Debug, Clone, Copy)] struct Flags: u8 { const HEAD = 0b0000_0001; const KEEP_ALIVE_ENABLED = 0b0000_1000; diff --git a/actix-http/src/h1/codec.rs b/actix-http/src/h1/codec.rs index e11f175c..2b452f8f 100644 --- a/actix-http/src/h1/codec.rs +++ b/actix-http/src/h1/codec.rs @@ -9,11 +9,10 @@ use super::{ decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, encoder, Message, MessageType, }; -use crate::{ - body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig, -}; +use crate::{body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig}; bitflags! { + #[derive(Debug, Clone, Copy)] struct Flags: u8 { const HEAD = 0b0000_0001; const KEEP_ALIVE_ENABLED = 0b0000_0010; @@ -199,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 0b06bfe2..af64e880 100644 --- a/actix-http/src/h1/decoder.rs +++ b/actix-http/src/h1/decoder.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll}; +use std::{io, marker::PhantomData, mem::MaybeUninit, task::Poll}; use actix_codec::Decoder; use bytes::{Bytes, BytesMut}; @@ -94,9 +94,7 @@ pub(crate) trait MessageType: Sized { // SAFETY: httparse already checks header value is only visible ASCII bytes // from_maybe_shared_unchecked contains debug assertions so they are omitted here let value = unsafe { - HeaderValue::from_maybe_shared_unchecked( - slice.slice(idx.value.0..idx.value.1), - ) + HeaderValue::from_maybe_shared_unchecked(slice.slice(idx.value.0..idx.value.1)) }; match name { @@ -275,8 +273,7 @@ impl MessageType for Request { let mut msg = Request::new(); // convert headers - let mut length = - msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; + let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; // disallow HTTP/1.0 POST requests that do not contain a Content-Length headers // see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2 @@ -356,8 +353,8 @@ impl MessageType for ResponseHead { Version::HTTP_10 }; - let status = StatusCode::from_u16(res.code.unwrap()) - .map_err(|_| ParseError::Status)?; + let status = + StatusCode::from_u16(res.code.unwrap()).map_err(|_| ParseError::Status)?; HeaderIndex::record(src, res.headers, &mut headers); (len, version, status, res.headers.len()) @@ -378,8 +375,7 @@ impl MessageType for ResponseHead { msg.version = ver; // convert headers - let mut length = - msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; + let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; // Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed. // Protects against some request smuggling attacks. @@ -536,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 { @@ -567,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 60660b85..00b51360 100644 --- a/actix-http/src/h1/dispatcher.rs +++ b/actix-http/src/h1/dispatcher.rs @@ -19,6 +19,13 @@ use tokio::io::{AsyncRead, AsyncWrite}; use tokio_util::codec::{Decoder as _, Encoder as _}; use tracing::{error, trace}; +use super::{ + codec::Codec, + decoder::MAX_BUFFER_SIZE, + payload::{Payload, PayloadSender, PayloadStatus}, + timer::TimerState, + Message, MessageType, +}; use crate::{ body::{BodySize, BoxBody, MessageBody}, config::ServiceConfig, @@ -27,19 +34,12 @@ use crate::{ Error, Extensions, OnConnectData, Request, Response, StatusCode, }; -use super::{ - codec::Codec, - decoder::MAX_BUFFER_SIZE, - payload::{Payload, PayloadSender, PayloadStatus}, - timer::TimerState, - Message, MessageType, -}; - const LW_BUFFER_SIZE: usize = 1024; const HW_BUFFER_SIZE: usize = 1024 * 8; const MAX_PIPELINED_MESSAGES: usize = 16; bitflags! { + #[derive(Debug, Clone, Copy)] pub struct Flags: u8 { /// Set when stream is read for first time. const STARTED = 0b0000_0001; @@ -212,9 +212,7 @@ where fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::None => write!(f, "State::None"), - Self::ExpectCall { .. } => { - f.debug_struct("State::ExpectCall").finish_non_exhaustive() - } + Self::ExpectCall { .. } => f.debug_struct("State::ExpectCall").finish_non_exhaustive(), Self::ServiceCall { .. } => { f.debug_struct("State::ServiceCall").finish_non_exhaustive() } @@ -275,9 +273,7 @@ where head_timer: TimerState::new(config.client_request_deadline().is_some()), ka_timer: TimerState::new(config.keep_alive().enabled()), - shutdown_timer: TimerState::new( - config.client_disconnect_deadline().is_some(), - ), + shutdown_timer: TimerState::new(config.client_disconnect_deadline().is_some()), io: Some(io), read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), @@ -455,9 +451,7 @@ where } // return with upgrade request and poll it exclusively - Some(DispatcherMessage::Upgrade(req)) => { - return Ok(PollResponse::Upgrade(req)) - } + Some(DispatcherMessage::Upgrade(req)) => return Ok(PollResponse::Upgrade(req)), // all messages are dealt with None => { @@ -518,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), @@ -555,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(), @@ -674,9 +671,7 @@ where } _ => { - unreachable!( - "State must be set to ServiceCall or ExceptCall in handle_request" - ) + unreachable!("State must be set to ServiceCall or ExceptCall in handle_request") } } } @@ -685,10 +680,7 @@ where /// Process one incoming request. /// /// Returns true if any meaningful work was done. - fn poll_request( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Result { + fn poll_request(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result { let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES; let can_not_read = !self.can_read(cx); @@ -714,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 @@ -858,10 +850,7 @@ where Ok(()) } - fn poll_ka_timer( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Result<(), DispatchError> { + fn poll_ka_timer(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), DispatchError> { let this = self.as_mut().project(); if let TimerState::Active { timer } = this.ka_timer { debug_assert!( @@ -926,10 +915,7 @@ where } /// Poll head, keep-alive, and disconnect timer. - fn poll_timers( - mut self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Result<(), DispatchError> { + fn poll_timers(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), DispatchError> { self.as_mut().poll_head_timer(cx)?; self.as_mut().poll_ka_timer(cx)?; self.as_mut().poll_shutdown_timer(cx)?; @@ -943,10 +929,7 @@ where /// - `std::io::ErrorKind::ConnectionReset` after partial read; /// - all data read done. #[inline(always)] // TODO: bench this inline - fn read_available( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Result { + fn read_available(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result { let this = self.project(); if this.flags.contains(Flags::READ_DISCONNECT) { diff --git a/actix-http/src/h1/dispatcher_tests.rs b/actix-http/src/h1/dispatcher_tests.rs index d39c5bd6..50259e6c 100644 --- a/actix-http/src/h1/dispatcher_tests.rs +++ b/actix-http/src/h1/dispatcher_tests.rs @@ -1,14 +1,11 @@ use std::{future::Future, str, task::Poll, time::Duration}; -use actix_rt::{pin, time::sleep}; -use actix_service::fn_service; -use actix_utils::future::{ready, Ready}; -use bytes::Bytes; -use futures_util::future::lazy; - use actix_codec::Framed; -use actix_service::Service; -use bytes::{Buf, BytesMut}; +use actix_rt::{pin, time::sleep}; +use actix_service::{fn_service, Service}; +use actix_utils::future::{ready, Ready}; +use bytes::{Buf, Bytes, BytesMut}; +use futures_util::future::lazy; use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags}; use crate::{ @@ -43,8 +40,8 @@ fn status_service( fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status)))) } -fn echo_path_service( -) -> impl Service, Error = Error> { +fn echo_path_service() -> impl Service, Error = Error> +{ fn_service(|req: Request| { let path = req.path().as_bytes(); ready(Ok::<_, Error>( @@ -53,8 +50,8 @@ fn echo_path_service( }) } -fn drop_payload_service( -) -> impl Service, Error = Error> { +fn drop_payload_service() -> impl Service, Error = Error> +{ fn_service(|mut req: Request| async move { let _ = req.take_payload(); Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped")) @@ -932,7 +929,6 @@ fn http_msg(msg: impl AsRef) -> BytesMut { .as_ref() .trim() .split('\n') - .into_iter() .map(|line| [line.trim_start(), "\r"].concat()) .collect::>() .join("\n"); diff --git a/actix-http/src/h1/mod.rs b/actix-http/src/h1/mod.rs index 858cf542..9e44608d 100644 --- a/actix-http/src/h1/mod.rs +++ b/actix-http/src/h1/mod.rs @@ -17,14 +17,16 @@ mod timer; mod upgrade; mod utils; -pub use self::client::{ClientCodec, ClientPayloadCodec}; -pub use self::codec::Codec; -pub use self::dispatcher::Dispatcher; -pub use self::expect::ExpectHandler; -pub use self::payload::Payload; -pub use self::service::{H1Service, H1ServiceHandler}; -pub use self::upgrade::UpgradeHandler; -pub use self::utils::SendResponse; +pub use self::{ + client::{ClientCodec, ClientPayloadCodec}, + codec::Codec, + dispatcher::Dispatcher, + expect::ExpectHandler, + payload::Payload, + service::{H1Service, H1ServiceHandler}, + upgrade::UpgradeHandler, + utils::SendResponse, +}; #[derive(Debug)] /// Codec message diff --git a/actix-http/src/h1/payload.rs b/actix-http/src/h1/payload.rs index 1ed785a1..2ad3a14a 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 e4d90424..f2f8a0e4 100644 --- a/actix-http/src/h1/service.rs +++ b/actix-http/src/h1/service.rs @@ -15,6 +15,7 @@ use actix_utils::future::ready; use futures_core::future::LocalBoxFuture; use tracing::error; +use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler}; use crate::{ body::{BoxBody, MessageBody}, config::ServiceConfig, @@ -23,8 +24,6 @@ use crate::{ ConnectCallback, OnConnectData, Request, Response, }; -use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler}; - /// `ServiceFactory` implementation for HTTP1 transport pub struct H1Service { srv: S, @@ -82,13 +81,8 @@ where /// Create simple tcp stream service pub fn tcp( self, - ) -> impl ServiceFactory< - TcpStream, - Config = (), - Response = (), - Error = DispatchError, - InitError = (), - > { + ) -> impl ServiceFactory + { fn_service(|io: TcpStream| { let peer_addr = io.peer_addr().ok(); ready(Ok((io, peer_addr))) @@ -99,8 +93,6 @@ where #[cfg(feature = "openssl")] mod openssl { - use super::*; - use actix_tls::accept::{ openssl::{ reexports::{Error as SslError, SslAcceptor}, @@ -109,6 +101,8 @@ mod openssl { TlsError, }; + use super::*; + impl H1Service, S, B, X, U> where S: ServiceFactory, @@ -134,7 +128,6 @@ mod openssl { U::InitError: fmt::Debug, { /// Create OpenSSL based service. - #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] pub fn openssl( self, acceptor: SslAcceptor, @@ -159,14 +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, }; @@ -196,8 +188,7 @@ mod rustls { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create Rustls based service. - #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))] + /// Create Rustls v0.20 based service. pub fn rustls( self, config: ServerConfig, @@ -222,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 680936f0..400476c8 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}, @@ -29,7 +28,7 @@ use crate::{ HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE, }, service::HttpFlow, - Extensions, OnConnectData, Payload, Request, Response, ResponseHead, + Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead, }; const CHUNK_SIZE: usize = 16_384; @@ -118,6 +117,7 @@ where let payload = crate::h2::Payload::new(body); let pl = Payload::H2 { payload }; let mut req = Request::with_payload(pl); + let head_req = parts.method == Method::HEAD; let head = req.head_mut(); head.uri = parts.uri; @@ -126,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(); @@ -135,10 +135,10 @@ where actix_rt::spawn(async move { // resolve service call and send response. let res = match fut.await { - Ok(res) => handle_response(res.into(), tx, config).await, + Ok(res) => handle_response(res.into(), tx, config, head_req).await, Err(err) => { let res: Response = err.into(); - handle_response(res, tx, config).await + handle_response(res, tx, config, head_req).await } }; @@ -146,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:?}"); } } } @@ -206,6 +208,7 @@ async fn handle_response( res: Response, mut tx: SendResponse, config: ServiceConfig, + head_req: bool, ) -> Result<(), DispatchError> where B: MessageBody, @@ -215,20 +218,20 @@ where // prepare response. let mut size = body.size(); let res = prepare_response(config, res.head(), &mut size); - let eof = size.is_eof(); + let eof_or_head = size.is_eof() || head_req; // send response head and return on eof. let mut stream = tx - .send_response(res, eof) + .send_response(res, eof_or_head) .map_err(DispatchError::SendResponse)?; - if eof { + if eof_or_head { 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/mod.rs b/actix-http/src/h2/mod.rs index 39198e0f..e47099ca 100644 --- a/actix-http/src/h2/mod.rs +++ b/actix-http/src/h2/mod.rs @@ -23,8 +23,7 @@ use crate::{ mod dispatcher; mod service; -pub use self::dispatcher::Dispatcher; -pub use self::service::H2Service; +pub use self::{dispatcher::Dispatcher, service::H2Service}; /// HTTP/2 peer stream. pub struct Payload { @@ -58,10 +57,7 @@ impl Stream for Payload { } } -pub(crate) fn handshake_with_timeout( - io: T, - config: &ServiceConfig, -) -> HandshakeWithTimeout +pub(crate) fn handshake_with_timeout(io: T, config: &ServiceConfig) -> HandshakeWithTimeout where T: AsyncRead + AsyncWrite + Unpin, { diff --git a/actix-http/src/h2/service.rs b/actix-http/src/h2/service.rs index 2a45fc1d..636ac316 100644 --- a/actix-http/src/h2/service.rs +++ b/actix-http/src/h2/service.rs @@ -16,6 +16,7 @@ use actix_utils::future::ready; use futures_core::{future::LocalBoxFuture, ready}; use tracing::{error, trace}; +use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout}; use crate::{ body::{BoxBody, MessageBody}, config::ServiceConfig, @@ -24,8 +25,6 @@ use crate::{ ConnectCallback, OnConnectData, Request, Response, }; -use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout}; - /// `ServiceFactory` implementation for HTTP/2 transport pub struct H2Service { srv: S, @@ -117,7 +116,6 @@ mod openssl { B: MessageBody + 'static, { /// Create OpenSSL based service. - #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] pub fn openssl( self, acceptor: SslAcceptor, @@ -142,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 _; @@ -164,8 +162,7 @@ mod rustls { B: MessageBody + 'static, { - /// Create Rustls based service. - #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))] + /// Create Rustls v0.20 based service. pub fn rustls( self, mut config: ServerConfig, @@ -194,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/common.rs b/actix-http/src/header/common.rs index 52909099..6942dc26 100644 --- a/actix-http/src/header/common.rs +++ b/actix-http/src/header/common.rs @@ -4,6 +4,20 @@ use http::header::HeaderName; +/// Response header field that indicates how caches have handled that response and its corresponding +/// request. +/// +/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics. +// TODO(breaking): replace with http's version +pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status"); + +/// Response header field that allows origin servers to control the behavior of CDN caches +/// interposed between them and clients separately from other caches that might handle the response. +/// +/// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics. +// TODO(breaking): replace with http's version +pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control"); + /// Response header that prevents a document from loading any cross-origin resources that don't /// explicitly grant the document permission (using [CORP] or [CORS]). /// diff --git a/actix-http/src/header/into_pair.rs b/actix-http/src/header/into_pair.rs index 91c3e664..0a71f857 100644 --- a/actix-http/src/header/into_pair.rs +++ b/actix-http/src/header/into_pair.rs @@ -1,7 +1,5 @@ //! [`TryIntoHeaderPair`] trait and implementations. -use std::convert::TryFrom as _; - use super::{ Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue, }; diff --git a/actix-http/src/header/into_value.rs b/actix-http/src/header/into_value.rs index 6d369ee6..25390063 100644 --- a/actix-http/src/header/into_value.rs +++ b/actix-http/src/header/into_value.rs @@ -1,7 +1,5 @@ //! [`TryIntoHeaderValue`] trait and implementations. -use std::convert::TryFrom as _; - use bytes::Bytes; use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use mime::Mime; diff --git a/actix-http/src/header/map.rs b/actix-http/src/header/map.rs index d7b4e6dd..b86798a4 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() } } @@ -1120,9 +1134,7 @@ mod tests { assert!(vals.next().is_none()); } - fn owned_pair<'a>( - (name, val): (&'a HeaderName, &'a HeaderValue), - ) -> (HeaderName, HeaderValue) { + fn owned_pair<'a>((name, val): (&'a HeaderName, &'a HeaderValue)) -> (HeaderName, HeaderValue) { (name.clone(), val.clone()) } } diff --git a/actix-http/src/header/mod.rs b/actix-http/src/header/mod.rs index e2c2fe91..79f91afe 100644 --- a/actix-http/src/header/mod.rs +++ b/actix-http/src/header/mod.rs @@ -3,33 +3,30 @@ // declaring new header consts will yield this error #![allow(clippy::declare_interior_mutable_const)] -use percent_encoding::{AsciiSet, CONTROLS}; - // re-export from http except header map related items pub use ::http::header::{ HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError, }; - // re-export const header names, list is explicit so that any updates to `common` module do not // conflict with this set pub use ::http::header::{ ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES, - ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, - ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, - ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, - ALLOW, ALT_SVC, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, - CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, - CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE, - DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, - IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS, - ORIGIN, PRAGMA, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, - PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER, - SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, - SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER, - TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, WARNING, - WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS, - X_XSS_PROTECTION, + ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS, ACCESS_CONTROL_ALLOW_METHODS, + ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE, + ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC, + AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING, + CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, CONTENT_SECURITY_POLICY, + CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE, DNT, ETAG, EXPECT, EXPIRES, + FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, + IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, + PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, + REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, + SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, + STRICT_TRANSPORT_SECURITY, TE, TRAILER, TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, + USER_AGENT, VARY, VIA, WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, + X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS, X_XSS_PROTECTION, }; +use percent_encoding::{AsciiSet, CONTROLS}; use crate::{error::ParseError, HttpMessage}; @@ -43,22 +40,22 @@ mod utils; pub use self::{ as_name::AsHeaderName, + // re-export list is explicit so that any updates to `http` do not conflict with this set + common::{ + CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY, + CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST, + X_FORWARDED_PROTO, + }, into_pair::TryIntoHeaderPair, into_value::TryIntoHeaderValue, map::HeaderMap, shared::{ - parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, - LanguageTag, Quality, QualityItem, + parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag, + Quality, QualityItem, }, utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode}, }; -// re-export list is explicit so that any updates to `http` do not conflict with this set -pub use self::common::{ - CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY, CROSS_ORIGIN_RESOURCE_POLICY, - PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST, X_FORWARDED_PROTO, -}; - /// An interface for types that already represent a valid header. pub trait Header: TryIntoHeaderValue { /// Returns the name of the header field. diff --git a/actix-http/src/header/shared/content_encoding.rs b/actix-http/src/header/shared/content_encoding.rs index bd25de70..c3b4bc4c 100644 --- a/actix-http/src/header/shared/content_encoding.rs +++ b/actix-http/src/header/shared/content_encoding.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, str::FromStr}; +use std::str::FromStr; use derive_more::{Display, Error}; use http::header::InvalidHeaderValue; diff --git a/actix-http/src/header/shared/http_date.rs b/actix-http/src/header/shared/http_date.rs index 21ed49f0..bdfbc705 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/shared/mod.rs b/actix-http/src/header/shared/mod.rs index 257e54d7..889c73c4 100644 --- a/actix-http/src/header/shared/mod.rs +++ b/actix-http/src/header/shared/mod.rs @@ -1,5 +1,7 @@ //! Originally taken from `hyper::header::shared`. +pub use language_tags::LanguageTag; + mod charset; mod content_encoding; mod extended; @@ -7,10 +9,11 @@ mod http_date; mod quality; mod quality_item; -pub use self::charset::Charset; -pub use self::content_encoding::ContentEncoding; -pub use self::extended::{parse_extended_value, ExtendedValue}; -pub use self::http_date::HttpDate; -pub use self::quality::{q, Quality}; -pub use self::quality_item::QualityItem; -pub use language_tags::LanguageTag; +pub use self::{ + charset::Charset, + content_encoding::ContentEncoding, + extended::{parse_extended_value, ExtendedValue}, + http_date::HttpDate, + quality::{q, Quality}, + quality_item::QualityItem, +}; diff --git a/actix-http/src/header/shared/quality.rs b/actix-http/src/header/shared/quality.rs index c80dd0a8..c2276cf1 100644 --- a/actix-http/src/header/shared/quality.rs +++ b/actix-http/src/header/shared/quality.rs @@ -1,7 +1,4 @@ -use std::{ - convert::{TryFrom, TryInto}, - fmt, -}; +use std::fmt; use derive_more::{Display, Error}; diff --git a/actix-http/src/header/shared/quality_item.rs b/actix-http/src/header/shared/quality_item.rs index 0b35b540..a41369c2 100644 --- a/actix-http/src/header/shared/quality_item.rs +++ b/actix-http/src/header/shared/quality_item.rs @@ -1,8 +1,7 @@ -use std::{cmp, convert::TryFrom as _, fmt, str}; - -use crate::error::ParseError; +use std::{cmp, fmt, str}; use super::Quality; +use crate::error::ParseError; /// Represents an item with a quality value as defined /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). diff --git a/actix-http/src/header/utils.rs b/actix-http/src/header/utils.rs index f4f34d34..caaab3b1 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/http_message.rs b/actix-http/src/http_message.rs index 198254e0..3ba9ef75 100644 --- a/actix-http/src/http_message.rs +++ b/actix-http/src/http_message.rs @@ -61,9 +61,7 @@ pub trait HttpMessage: Sized { fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> { if let Some(mime_type) = self.mime_type()? { if let Some(charset) = mime_type.get_param("charset") { - if let Some(enc) = - Encoding::for_label_no_replacement(charset.as_str().as_bytes()) - { + if let Some(enc) = Encoding::for_label_no_replacement(charset.as_str().as_bytes()) { Ok(enc) } else { Err(ContentTypeError::UnknownEncoding) @@ -146,7 +144,7 @@ mod tests { .finish(); assert_eq!(req.content_type(), "text/plain"); let req = TestRequest::default() - .insert_header(("content-type", "application/json; charset=utf=8")) + .insert_header(("content-type", "application/json; charset=utf-8")) .finish(); assert_eq!(req.content_type(), "application/json"); let req = TestRequest::default().finish(); diff --git a/actix-http/src/lib.rs b/actix-http/src/lib.rs index 05f80eba..f9697c4d 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,15 +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_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] -pub use ::http::{uri, uri::Uri}; -pub use ::http::{Method, StatusCode, Version}; +pub use http::{uri, uri::Uri, Method, StatusCode, Version}; pub mod body; mod builder; @@ -41,7 +43,6 @@ pub mod error; mod extensions; pub mod h1; #[cfg(feature = "http2")] -#[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub mod h2; pub mod header; mod helpers; @@ -56,26 +57,32 @@ mod responses; mod service; pub mod test; #[cfg(feature = "ws")] -#[cfg_attr(docsrs, doc(cfg(feature = "ws")))] pub mod ws; -pub use self::builder::HttpServiceBuilder; -pub use self::config::ServiceConfig; -pub use self::error::Error; -pub use self::extensions::Extensions; -pub use self::header::ContentEncoding; -pub use self::http_message::HttpMessage; -pub use self::keep_alive::KeepAlive; -pub use self::message::ConnectionType; -pub use self::message::Message; #[allow(deprecated)] -pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream}; -pub use self::requests::{Request, RequestHead, RequestHeadType}; -pub use self::responses::{Response, ResponseBuilder, ResponseHead}; -pub use self::service::HttpService; -#[cfg(any(feature = "openssl", feature = "rustls"))] -#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))] +pub use self::payload::PayloadStream; +#[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, + config::ServiceConfig, + error::Error, + extensions::Extensions, + header::ContentEncoding, + http_message::HttpMessage, + keep_alive::KeepAlive, + message::{ConnectionType, Message}, + payload::{BoxedPayloadStream, Payload}, + requests::{Request, RequestHead, RequestHeadType}, + responses::{Response, ResponseBuilder, ResponseHead}, + service::HttpService, +}; /// A major HTTP protocol version. #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] diff --git a/actix-http/src/message.rs b/actix-http/src/message.rs index 7469d74e..47b128fd 100644 --- a/actix-http/src/message.rs +++ b/actix-http/src/message.rs @@ -16,6 +16,7 @@ pub enum ConnectionType { } bitflags! { + #[derive(Debug, Clone, Copy)] pub(crate) struct Flags: u8 { const CLOSE = 0b0000_0001; const KEEP_ALIVE = 0b0000_0010; diff --git a/actix-http/src/notify_on_drop.rs b/actix-http/src/notify_on_drop.rs index 98544bb5..95904b28 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 4558801f..9ceb2a20 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/mod.rs b/actix-http/src/requests/mod.rs index fc35da65..4a27818a 100644 --- a/actix-http/src/requests/mod.rs +++ b/actix-http/src/requests/mod.rs @@ -3,5 +3,7 @@ mod head; mod request; -pub use self::head::{RequestHead, RequestHeadType}; -pub use self::request::Request; +pub use self::{ + head::{RequestHead, RequestHeadType}, + request::Request, +}; diff --git a/actix-http/src/requests/request.rs b/actix-http/src/requests/request.rs index ac358e8d..6a267a7a 100644 --- a/actix-http/src/requests/request.rs +++ b/actix-http/src/requests/request.rs @@ -10,8 +10,7 @@ use std::{ use http::{header, Method, Uri, Version}; use crate::{ - header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, - RequestHead, + header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, RequestHead, }; /// An HTTP request. @@ -174,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 @@ -234,7 +233,6 @@ impl

fmt::Debug for Request

{ #[cfg(test)] mod tests { use super::*; - use std::convert::TryFrom; #[test] fn test_basics() { diff --git a/actix-http/src/responses/builder.rs b/actix-http/src/responses/builder.rs index 063af92d..91c69ba5 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/responses/mod.rs b/actix-http/src/responses/mod.rs index 899232b9..d9962823 100644 --- a/actix-http/src/responses/mod.rs +++ b/actix-http/src/responses/mod.rs @@ -5,7 +5,5 @@ mod head; #[allow(clippy::module_inception)] mod response; -pub use self::builder::ResponseBuilder; pub(crate) use self::head::BoxedResponseHead; -pub use self::head::ResponseHead; -pub use self::response::Response; +pub use self::{builder::ResponseBuilder, head::ResponseHead, response::Response}; diff --git a/actix-http/src/service.rs b/actix-http/src/service.rs index 62128f3e..a58be93c 100644 --- a/actix-http/src/service.rs +++ b/actix-http/src/service.rs @@ -30,9 +30,9 @@ use crate::{ /// /// # Automatic HTTP Version Selection /// There are two ways to select the HTTP version of an incoming connection: -/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both -/// versions are supported automatically when using either of the `.rustls()` or `.openssl()` -/// finalizing methods. +/// - One is to rely on the ALPN information that is provided when using TLS (HTTPS); both versions +/// are supported automatically when using either of the `.rustls()` or `.openssl()` finalizing +/// methods. /// - The other is to read the first few bytes of the TCP stream. This is the only viable approach /// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use /// the `.tcp_auto_h2c()` finalizing method to enable this behavior. @@ -200,13 +200,8 @@ where /// The resulting service only supports HTTP/1.x. pub fn tcp( self, - ) -> impl ServiceFactory< - TcpStream, - Config = (), - Response = (), - Error = DispatchError, - InitError = (), - > { + ) -> impl ServiceFactory + { fn_service(|io: TcpStream| async { let peer_addr = io.peer_addr().ok(); Ok((io, Protocol::Http1, peer_addr)) @@ -217,16 +212,10 @@ where /// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2 /// on plaintext connections. #[cfg(feature = "http2")] - #[cfg_attr(docsrs, doc(cfg(feature = "http2")))] pub fn tcp_auto_h2c( self, - ) -> impl ServiceFactory< - TcpStream, - Config = (), - Response = (), - Error = DispatchError, - InitError = (), - > { + ) -> impl ServiceFactory + { fn_service(move |io: TcpStream| async move { // subset of HTTP/2 preface defined by RFC 9113 §3.4 // this subset was chosen to maximize likelihood that peeking only once will allow us to @@ -252,14 +241,25 @@ where } /// Configuration options used when accepting TLS connection. -#[cfg(any(feature = "openssl", feature = "rustls"))] -#[cfg_attr(docsrs, doc(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 { @@ -309,7 +309,6 @@ mod openssl { U::InitError: fmt::Debug, { /// Create OpenSSL based service. - #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] pub fn openssl( self, acceptor: SslAcceptor, @@ -324,7 +323,6 @@ mod openssl { } /// Create OpenSSL based service with custom TLS acceptor configuration. - #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] pub fn openssl_with_config( self, acceptor: SslAcceptor, @@ -366,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, }; @@ -403,8 +401,7 @@ mod rustls { U::Error: fmt::Display + Into>, U::InitError: fmt::Debug, { - /// Create Rustls based service. - #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))] + /// Create Rustls v0.20 based service. pub fn rustls( self, config: ServerConfig, @@ -418,8 +415,7 @@ mod rustls { self.rustls_with_config(config, TlsAcceptorConfig::default()) } - /// Create Rustls based service with custom TLS acceptor configuration. - #[cfg_attr(docsrs, doc(cfg(feature = "rustls")))] + /// Create Rustls v0.20 based service with custom TLS acceptor configuration. pub fn rustls_with_config( self, mut config: ServerConfig, @@ -464,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 @@ -569,10 +853,7 @@ where } } - pub(super) fn _poll_ready( - &self, - cx: &mut Context<'_>, - ) -> Poll>> { + pub(super) fn _poll_ready(&self, cx: &mut Context<'_>) -> Poll>> { ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; @@ -631,10 +912,7 @@ where }) } - fn call( - &self, - (io, proto, peer_addr): (T, Protocol, Option), - ) -> Self::Future { + fn call(&self, (io, proto, peer_addr): (T, Protocol, Option)) -> Self::Future { let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); match proto { diff --git a/actix-http/src/ws/codec.rs b/actix-http/src/ws/codec.rs index 6a149f9a..ad487e40 100644 --- a/actix-http/src/ws/codec.rs +++ b/actix-http/src/ws/codec.rs @@ -74,6 +74,7 @@ pub struct Codec { } bitflags! { + #[derive(Debug, Clone, Copy)] struct Flags: u8 { const SERVER = 0b0000_0001; const CONTINUATION = 0b0000_0010; @@ -295,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/dispatcher.rs b/actix-http/src/ws/dispatcher.rs index 396f1e86..1354d5ae 100644 --- a/actix-http/src/ws/dispatcher.rs +++ b/actix-http/src/ws/dispatcher.rs @@ -70,15 +70,14 @@ mod inner { task::{Context, Poll}, }; + use actix_codec::Framed; use actix_service::{IntoService, Service}; use futures_core::stream::Stream; use local_channel::mpsc; use pin_project_lite::pin_project; - use tracing::debug; - - use actix_codec::Framed; use tokio::io::{AsyncRead, AsyncWrite}; use tokio_util::codec::{Decoder, Encoder}; + use tracing::debug; use crate::{body::BoxBody, Response}; @@ -413,9 +412,7 @@ mod inner { } State::Error(_) => { // flush write buffer - if !this.framed.is_write_buf_empty() - && this.framed.flush(cx).is_pending() - { + if !this.framed.is_write_buf_empty() && this.framed.flush(cx).is_pending() { return Poll::Pending; } Poll::Ready(Err(this.state.take_error())) diff --git a/actix-http/src/ws/frame.rs b/actix-http/src/ws/frame.rs index c7e0427e..35b3f8e6 100644 --- a/actix-http/src/ws/frame.rs +++ b/actix-http/src/ws/frame.rs @@ -1,4 +1,4 @@ -use std::convert::TryFrom; +use std::cmp::min; use bytes::{Buf, BufMut, BytesMut}; use tracing::debug; @@ -96,6 +96,10 @@ impl Parser { // not enough data if src.len() < idx + length { + let min_length = min(length, max_size); + if src.capacity() < idx + min_length { + src.reserve(idx + min_length - src.capacity()); + } return Ok(None); } @@ -174,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); }; @@ -217,9 +221,10 @@ impl Parser { #[cfg(test)] mod tests { - use super::*; use bytes::Bytes; + use super::*; + struct F { finished: bool, opcode: OpCode, diff --git a/actix-http/src/ws/mask.rs b/actix-http/src/ws/mask.rs index be72e563..115a8cf9 100644 --- a/actix-http/src/ws/mask.rs +++ b/actix-http/src/ws/mask.rs @@ -50,7 +50,7 @@ mod tests { #[test] fn test_apply_mask() { let mask = [0x6d, 0xb6, 0xb2, 0x80]; - let unmasked = vec![ + let unmasked = [ 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9, 0x12, 0x03, ]; diff --git a/actix-http/src/ws/mod.rs b/actix-http/src/ws/mod.rs index 75d4ca62..3ed53b70 100644 --- a/actix-http/src/ws/mod.rs +++ b/actix-http/src/ws/mod.rs @@ -8,8 +8,7 @@ use std::io; use derive_more::{Display, Error, From}; use http::{header, Method, StatusCode}; -use crate::body::BoxBody; -use crate::{header::HeaderValue, RequestHead, Response, ResponseBuilder}; +use crate::{body::BoxBody, header::HeaderValue, RequestHead, Response, ResponseBuilder}; mod codec; mod dispatcher; @@ -17,48 +16,50 @@ mod frame; mod mask; mod proto; -pub use self::codec::{Codec, Frame, Item, Message}; -pub use self::dispatcher::Dispatcher; -pub use self::frame::Parser; -pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; +pub use self::{ + codec::{Codec, Frame, Item, Message}, + dispatcher::Dispatcher, + frame::Parser, + proto::{hash_key, CloseCode, CloseReason, OpCode}, +}; /// WebSocket protocol errors. #[derive(Debug, Display, Error, From)] pub enum ProtocolError { /// Received an unmasked frame from client. - #[display(fmt = "Received an unmasked frame from client.")] + #[display(fmt = "received an unmasked frame from client")] UnmaskedFrame, /// Received a masked frame from server. - #[display(fmt = "Received a masked frame from server.")] + #[display(fmt = "received a masked frame from server")] MaskedFrame, /// Encountered invalid opcode. - #[display(fmt = "Invalid opcode: {}.", _0)] + #[display(fmt = "invalid opcode ({})", _0)] InvalidOpcode(#[error(not(source))] u8), /// Invalid control frame length - #[display(fmt = "Invalid control frame length: {}.", _0)] + #[display(fmt = "invalid control frame length ({})", _0)] InvalidLength(#[error(not(source))] usize), /// Bad opcode. - #[display(fmt = "Bad opcode.")] + #[display(fmt = "bad opcode")] BadOpCode, /// A payload reached size limit. - #[display(fmt = "A payload reached size limit.")] + #[display(fmt = "payload reached size limit")] Overflow, - /// Continuation is not started. - #[display(fmt = "Continuation is not started.")] + /// Continuation has not started. + #[display(fmt = "continuation has not started")] ContinuationNotStarted, /// Received new continuation but it is already started. - #[display(fmt = "Received new continuation but it is already started.")] + #[display(fmt = "received new continuation but it has already started")] ContinuationStarted, /// Unknown continuation fragment. - #[display(fmt = "Unknown continuation fragment: {}.", _0)] + #[display(fmt = "unknown continuation fragment: {}", _0)] ContinuationFragment(#[error(not(source))] OpCode), /// I/O error. @@ -70,27 +71,27 @@ pub enum ProtocolError { #[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] pub enum HandshakeError { /// Only get method is allowed. - #[display(fmt = "Method not allowed.")] + #[display(fmt = "method not allowed")] GetMethodRequired, /// Upgrade header if not set to WebSocket. - #[display(fmt = "WebSocket upgrade is expected.")] + #[display(fmt = "WebSocket upgrade is expected")] NoWebsocketUpgrade, /// Connection header is not set to upgrade. - #[display(fmt = "Connection upgrade is expected.")] + #[display(fmt = "connection upgrade is expected")] NoConnectionUpgrade, /// WebSocket version header is not set. - #[display(fmt = "WebSocket version header is required.")] + #[display(fmt = "WebSocket version header is required")] NoVersionHeader, /// Unsupported WebSocket version. - #[display(fmt = "Unsupported WebSocket version.")] + #[display(fmt = "unsupported WebSocket version")] UnsupportedVersion, /// WebSocket key is not set or wrong. - #[display(fmt = "Unknown websocket key.")] + #[display(fmt = "unknown WebSocket key")] BadWebsocketKey, } @@ -219,10 +220,8 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder { #[cfg(test)] mod tests { - use crate::{header, Method}; - use super::*; - use crate::test::TestRequest; + 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 7222168b..27815eaf 100644 --- a/actix-http/src/ws/proto.rs +++ b/actix-http/src/ws/proto.rs @@ -1,8 +1,6 @@ -use std::{ - convert::{From, Into}, - fmt, -}; +use std::fmt; +use base64::prelude::*; use tracing::error; /// Operation codes defined in [RFC 6455 §11.8]. @@ -244,7 +242,7 @@ pub fn hash_key(key: &[u8]) -> [u8; 28] { }; let mut hash_b64 = [0; 28]; - let n = base64::encode_config_slice(hash, base64::STANDARD, &mut hash_b64); + let n = BASE64_STANDARD.encode_slice(hash, &mut hash_b64).unwrap(); assert_eq!(n, 28); hash_b64 diff --git a/actix-http/tests/test_openssl.rs b/actix-http/tests/test_openssl.rs index 7464bee4..4dd22b58 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(); @@ -321,8 +322,7 @@ async fn h2_body_length() { let mut srv = test_server(move || { HttpService::build() .h2(|_| async { - let body = - once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) }); + let body = once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) }); Ok::<_, Infallible>( Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), diff --git a/actix-http/tests/test_rustls.rs b/actix-http/tests/test_rustls.rs index 0b8197a6..3ca0d94c 100644 --- a/actix-http/tests/test_rustls.rs +++ b/actix-http/tests/test_rustls.rs @@ -1,10 +1,9 @@ -#![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, TryFrom}, + convert::Infallible, io::{self, BufReader, Write}, net::{SocketAddr, TcpStream as StdTcpStream}, sync::Arc, @@ -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,17 +84,14 @@ 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(); config.alpn_protocols.push(client_alpn_protocol.to_vec()); - let mut sess = rustls::ClientConnection::new( - Arc::new(config), - ServerName::try_from("localhost").unwrap(), - ) - .unwrap(); + let mut sess = + rustls::ClientConnection::new(Arc::new(config), ServerName::try_from("localhost").unwrap()) + .unwrap(); let mut sock = StdTcpStream::connect(addr).unwrap(); let mut stream = rustls::Stream::new(&mut sess, &mut sock); @@ -112,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; @@ -126,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; @@ -144,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; @@ -162,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)), ) @@ -183,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; @@ -209,7 +206,7 @@ async fn h2_content_length() { ]; ok::<_, Infallible>(Response::new(statuses[indx])) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -281,7 +278,7 @@ async fn h2_headers() { } ok::<_, Infallible>(config.body(data.clone())) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -320,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; @@ -337,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; @@ -363,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; @@ -388,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; @@ -414,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; @@ -438,7 +435,7 @@ async fn h2_body_chunked_explicit() { .body(BodyStream::new(body)), ) }) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -467,7 +464,7 @@ async fn h2_response_http_error_handling() { ) })) })) - .rustls(tls_config()) + .rustls_0_23(tls_config()) }) .await; @@ -497,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; @@ -514,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; @@ -537,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; @@ -559,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; @@ -585,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 2efb336a..4ba64a53 100644 --- a/actix-http/tests/test_server.rs +++ b/actix-http/tests/test_server.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use std::{ convert::Infallible, io::{Read, Write}, @@ -139,7 +137,7 @@ async fn expect_continue_h1() { #[actix_rt::test] async fn chunked_payload() { - let chunk_sizes = vec![32768, 32, 32768]; + let chunk_sizes = [32768, 32, 32768]; let total_size: usize = chunk_sizes.iter().sum(); let mut srv = test_server(|| { @@ -149,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| { @@ -166,8 +164,7 @@ async fn chunked_payload() { for chunk_size in chunk_sizes.iter() { let mut bytes = Vec::new(); - let random_bytes: Vec = - (0..*chunk_size).map(|_| rand::random::()).collect(); + let random_bytes: Vec = (0..*chunk_size).map(|_| rand::random::()).collect(); bytes.extend(format!("{:X}\r\n", chunk_size).as_bytes()); bytes.extend(&random_bytes[..]); @@ -352,8 +349,7 @@ async fn http10_keepalive() { .await; let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); - let _ = - stream.write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"); + let _ = stream.write_all(b"GET /test/tests/test HTTP/1.0\r\nconnection: keep-alive\r\n\r\n"); let mut data = vec![0; 1024]; let _ = stream.read(&mut data); assert_eq!(&data[..17], b"HTTP/1.0 200 OK\r\n"); @@ -404,7 +400,7 @@ async fn content_length() { let mut srv = test_server(|| { HttpService::build() .h1(|req: Request| { - let indx: usize = req.uri().path()[1..].parse().unwrap(); + let idx: usize = req.uri().path()[1..].parse().unwrap(); let statuses = [ StatusCode::NO_CONTENT, StatusCode::CONTINUE, @@ -413,7 +409,7 @@ async fn content_length() { StatusCode::OK, StatusCode::NOT_FOUND, ]; - ok::<_, Infallible>(Response::new(statuses[indx])) + ok::<_, Infallible>(Response::new(statuses[idx])) }) .tcp() }) @@ -795,8 +791,9 @@ async fn not_modified_spec_h1() { .map_into_boxed_body(), // with no content-length - "/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234") - .map_into_boxed_body(), + "/body" => { + Response::with_body(StatusCode::NOT_MODIFIED, "1234").map_into_boxed_body() + } // with manual content-length header and specific None body "/cl-none" => { diff --git a/actix-http/tests/test_ws.rs b/actix-http/tests/test_ws.rs index a9c1acd3..9a78074c 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, @@ -39,13 +37,13 @@ impl WsService { #[derive(Debug, Display, Error, From)] enum WsServiceError { - #[display(fmt = "http error")] + #[display(fmt = "HTTP error")] Http(actix_http::Error), - #[display(fmt = "ws handshake error")] + #[display(fmt = "WS handshake error")] Ws(actix_http::ws::HandshakeError), - #[display(fmt = "io error")] + #[display(fmt = "I/O error")] Io(std::io::Error), #[display(fmt = "dispatcher error")] diff --git a/actix-multipart-derive/CHANGES.md b/actix-multipart-derive/CHANGES.md new file mode 100644 index 00000000..1b44ba4b --- /dev/null +++ b/actix-multipart-derive/CHANGES.md @@ -0,0 +1,14 @@ +# Changes + +## 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 + +- Add `MultipartForm` derive macro. diff --git a/actix-multipart-derive/Cargo.toml b/actix-multipart-derive/Cargo.toml new file mode 100644 index 00000000..e978864a --- /dev/null +++ b/actix-multipart-derive/Cargo.toml @@ -0,0 +1,31 @@ +[package] +name = "actix-multipart-derive" +version = "0.6.1" +authors = ["Jacob Halsey "] +description = "Multipart form derive macro for Actix Web" +keywords = ["http", "web", "framework", "async", "futures"] +homepage.workspace = true +repository.workspace = true +license.workspace = true +edition.workspace = true +rust-version.workspace = true + +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true + +[lib] +proc-macro = true + +[dependencies] +darling = "0.20" +parse-size = "1" +proc-macro2 = "1" +quote = "1" +syn = "2" + +[dev-dependencies] +actix-multipart = "0.6" +actix-web = "4" +rustversion = "1" +trybuild = "1" diff --git a/actix-multipart-derive/LICENSE-APACHE b/actix-multipart-derive/LICENSE-APACHE new file mode 120000 index 00000000..965b606f --- /dev/null +++ b/actix-multipart-derive/LICENSE-APACHE @@ -0,0 +1 @@ +../LICENSE-APACHE \ No newline at end of file diff --git a/actix-multipart-derive/LICENSE-MIT b/actix-multipart-derive/LICENSE-MIT new file mode 120000 index 00000000..76219eb7 --- /dev/null +++ b/actix-multipart-derive/LICENSE-MIT @@ -0,0 +1 @@ +../LICENSE-MIT \ No newline at end of file diff --git a/actix-multipart-derive/README.md b/actix-multipart-derive/README.md new file mode 100644 index 00000000..ec0afffd --- /dev/null +++ b/actix-multipart-derive/README.md @@ -0,0 +1,16 @@ +# `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.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.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) + + diff --git a/actix-multipart-derive/src/lib.rs b/actix-multipart-derive/src/lib.rs new file mode 100644 index 00000000..9552ad2d --- /dev/null +++ b/actix-multipart-derive/src/lib.rs @@ -0,0 +1,315 @@ +//! Multipart form derive macro for Actix Web. +//! +//! See [`macro@MultipartForm`] for usage examples. + +#![deny(rust_2018_idioms, nonstandard_style)] +#![warn(future_incompatible)] +#![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))] + +use std::collections::HashSet; + +use darling::{FromDeriveInput, FromField, FromMeta}; +use parse_size::parse_size; +use proc_macro::TokenStream; +use proc_macro2::Ident; +use quote::quote; +use syn::{parse_macro_input, Type}; + +#[derive(FromMeta)] +enum DuplicateField { + Ignore, + Deny, + Replace, +} + +impl Default for DuplicateField { + fn default() -> Self { + Self::Ignore + } +} + +#[derive(FromDeriveInput, Default)] +#[darling(attributes(multipart), default)] +struct MultipartFormAttrs { + deny_unknown_fields: bool, + duplicate_field: DuplicateField, +} + +#[derive(FromField, Default)] +#[darling(attributes(multipart), default)] +struct FieldAttrs { + rename: Option, + limit: Option, +} + +struct ParsedField<'t> { + serialization_name: String, + rust_name: &'t Ident, + limit: Option, + ty: &'t Type, +} + +/// Implements `MultipartCollect` for a struct so that it can be used with the `MultipartForm` +/// extractor. +/// +/// # Basic Use +/// +/// Each field type should implement the `FieldReader` trait: +/// +/// ``` +/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +/// +/// #[derive(MultipartForm)] +/// struct ImageUpload { +/// description: Text, +/// timestamp: Text, +/// image: TempFile, +/// } +/// ``` +/// +/// # Optional and List Fields +/// +/// You can also use `Vec` and `Option` provided that `T: FieldReader`. +/// +/// A [`Vec`] field corresponds to an upload with multiple parts under the [same field +/// name](https://www.rfc-editor.org/rfc/rfc7578#section-4.3). +/// +/// ``` +/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +/// +/// #[derive(MultipartForm)] +/// struct Form { +/// category: Option>, +/// files: Vec, +/// } +/// ``` +/// +/// # Field Renaming +/// +/// You can use the `#[multipart(rename = "foo")]` attribute to receive a field by a different name. +/// +/// ``` +/// use actix_multipart::form::{tempfile::TempFile, MultipartForm}; +/// +/// #[derive(MultipartForm)] +/// struct Form { +/// #[multipart(rename = "files[]")] +/// files: Vec, +/// } +/// ``` +/// +/// # Field Limits +/// +/// You can use the `#[multipart(limit = "")]` attribute to set field level limits. The limit +/// string is parsed using [parse_size]. +/// +/// Note: the form is also subject to the global limits configured using `MultipartFormConfig`. +/// +/// ``` +/// use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; +/// +/// #[derive(MultipartForm)] +/// struct Form { +/// #[multipart(limit = "2 KiB")] +/// description: Text, +/// +/// #[multipart(limit = "512 MiB")] +/// files: Vec, +/// } +/// ``` +/// +/// # Unknown Fields +/// +/// By default fields with an unknown name are ignored. They can be rejected using the +/// `#[multipart(deny_unknown_fields)]` attribute: +/// +/// ``` +/// # use actix_multipart::form::MultipartForm; +/// #[derive(MultipartForm)] +/// #[multipart(deny_unknown_fields)] +/// struct Form { } +/// ``` +/// +/// # Duplicate Fields +/// +/// The behaviour for when multiple fields with the same name are received can be changed using the +/// `#[multipart(duplicate_field = "")]` attribute: +/// +/// - "ignore": (default) Extra fields are ignored. I.e., the first one is persisted. +/// - "deny": A `MultipartError::UnsupportedField` error response is returned. +/// - "replace": Each field is processed, but only the last one is persisted. +/// +/// Note that `Vec` fields will ignore this option. +/// +/// ``` +/// # use actix_multipart::form::MultipartForm; +/// #[derive(MultipartForm)] +/// #[multipart(duplicate_field = "deny")] +/// struct Form { } +/// ``` +/// +/// [parse_size]: https://docs.rs/parse-size/1/parse_size +#[proc_macro_derive(MultipartForm, attributes(multipart))] +pub fn impl_multipart_form(input: proc_macro::TokenStream) -> proc_macro::TokenStream { + let input: syn::DeriveInput = parse_macro_input!(input); + + let name = &input.ident; + + let data_struct = match &input.data { + syn::Data::Struct(data_struct) => data_struct, + _ => { + return compile_err(syn::Error::new( + input.ident.span(), + "`MultipartForm` can only be derived for structs", + )) + } + }; + + let fields = match &data_struct.fields { + syn::Fields::Named(fields_named) => fields_named, + _ => { + return compile_err(syn::Error::new( + input.ident.span(), + "`MultipartForm` can only be derived for a struct with named fields", + )) + } + }; + + let attrs = match MultipartFormAttrs::from_derive_input(&input) { + Ok(attrs) => attrs, + Err(err) => return err.write_errors().into(), + }; + + // Parse the field attributes + let parsed = match fields + .named + .iter() + .map(|field| { + let rust_name = field.ident.as_ref().unwrap(); + let attrs = FieldAttrs::from_field(field).map_err(|err| err.write_errors())?; + let serialization_name = attrs.rename.unwrap_or_else(|| rust_name.to_string()); + + let limit = match attrs.limit.map(|limit| match parse_size(&limit) { + Ok(size) => Ok(usize::try_from(size).unwrap()), + Err(err) => Err(syn::Error::new( + field.ident.as_ref().unwrap().span(), + format!("Could not parse size limit `{}`: {}", limit, err), + )), + }) { + Some(Err(err)) => return Err(compile_err(err)), + limit => limit.map(Result::unwrap), + }; + + Ok(ParsedField { + serialization_name, + rust_name, + limit, + ty: &field.ty, + }) + }) + .collect::, TokenStream>>() + { + Ok(attrs) => attrs, + Err(err) => return err, + }; + + // Check that field names are unique + let mut set = HashSet::new(); + for field in &parsed { + if !set.insert(field.serialization_name.clone()) { + return compile_err(syn::Error::new( + field.rust_name.span(), + format!("Multiple fields named: `{}`", field.serialization_name), + )); + } + } + + // Return value when a field name is not supported by the form + let unknown_field_result = if attrs.deny_unknown_fields { + quote!(::std::result::Result::Err( + ::actix_multipart::MultipartError::UnsupportedField(field.name().to_string()) + )) + } else { + quote!(::std::result::Result::Ok(())) + }; + + // Value for duplicate action + let duplicate_field = match attrs.duplicate_field { + DuplicateField::Ignore => quote!(::actix_multipart::form::DuplicateField::Ignore), + DuplicateField::Deny => quote!(::actix_multipart::form::DuplicateField::Deny), + DuplicateField::Replace => quote!(::actix_multipart::form::DuplicateField::Replace), + }; + + // limit() implementation + let mut limit_impl = quote!(); + for field in &parsed { + let name = &field.serialization_name; + if let Some(value) = field.limit { + limit_impl.extend(quote!( + #name => ::std::option::Option::Some(#value), + )); + } + } + + // handle_field() implementation + let mut handle_field_impl = quote!(); + for field in &parsed { + let name = &field.serialization_name; + let ty = &field.ty; + + handle_field_impl.extend(quote!( + #name => ::std::boxed::Box::pin( + <#ty as ::actix_multipart::form::FieldGroupReader>::handle_field(req, field, limits, state, #duplicate_field) + ), + )); + } + + // from_state() implementation + let mut from_state_impl = quote!(); + for field in &parsed { + let name = &field.serialization_name; + let rust_name = &field.rust_name; + let ty = &field.ty; + from_state_impl.extend(quote!( + #rust_name: <#ty as ::actix_multipart::form::FieldGroupReader>::from_state(#name, &mut state)?, + )); + } + + let gen = quote! { + impl ::actix_multipart::form::MultipartCollect for #name { + fn limit(field_name: &str) -> ::std::option::Option { + match field_name { + #limit_impl + _ => None, + } + } + + fn handle_field<'t>( + req: &'t ::actix_web::HttpRequest, + field: ::actix_multipart::Field, + limits: &'t mut ::actix_multipart::form::Limits, + state: &'t mut ::actix_multipart::form::State, + ) -> ::std::pin::Pin<::std::boxed::Box> + 't>> { + match field.name() { + #handle_field_impl + _ => return ::std::boxed::Box::pin(::std::future::ready(#unknown_field_result)), + } + } + + fn from_state(mut state: ::actix_multipart::form::State) -> ::std::result::Result { + Ok(Self { + #from_state_impl + }) + } + + } + }; + gen.into() +} + +/// Transform a syn error into a token stream for returning. +fn compile_err(err: syn::Error) -> TokenStream { + TokenStream::from(err.to_compile_error()) +} diff --git a/actix-multipart-derive/tests/trybuild.rs b/actix-multipart-derive/tests/trybuild.rs new file mode 100644 index 00000000..6b25d78d --- /dev/null +++ b/actix-multipart-derive/tests/trybuild.rs @@ -0,0 +1,16 @@ +#[rustversion::stable(1.72)] // MSRV +#[test] +fn compile_macros() { + let t = trybuild::TestCases::new(); + + t.pass("tests/trybuild/all-required.rs"); + t.pass("tests/trybuild/optional-and-list.rs"); + t.pass("tests/trybuild/rename.rs"); + t.pass("tests/trybuild/deny-unknown.rs"); + + t.pass("tests/trybuild/deny-duplicates.rs"); + t.compile_fail("tests/trybuild/deny-parse-fail.rs"); + + t.pass("tests/trybuild/size-limits.rs"); + t.compile_fail("tests/trybuild/size-limit-parse-fail.rs"); +} diff --git a/actix-multipart-derive/tests/trybuild/all-required.rs b/actix-multipart-derive/tests/trybuild/all-required.rs new file mode 100644 index 00000000..1b4a824d --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/all-required.rs @@ -0,0 +1,19 @@ +use actix_web::{web, App, Responder}; + +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; + +#[derive(Debug, MultipartForm)] +struct ImageUpload { + description: Text, + timestamp: Text, + image: TempFile, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/deny-duplicates.rs b/actix-multipart-derive/tests/trybuild/deny-duplicates.rs new file mode 100644 index 00000000..9fcc1506 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-duplicates.rs @@ -0,0 +1,16 @@ +use actix_web::{web, App, Responder}; + +use actix_multipart::form::MultipartForm; + +#[derive(MultipartForm)] +#[multipart(duplicate_field = "deny")] +struct Form {} + +async fn handler(_form: MultipartForm

) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/deny-parse-fail.rs b/actix-multipart-derive/tests/trybuild/deny-parse-fail.rs new file mode 100644 index 00000000..5ea566fb --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-parse-fail.rs @@ -0,0 +1,7 @@ +use actix_multipart::form::MultipartForm; + +#[derive(MultipartForm)] +#[multipart(duplicate_field = "no")] +struct Form {} + +fn main() {} diff --git a/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr b/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr new file mode 100644 index 00000000..d25e4352 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-parse-fail.stderr @@ -0,0 +1,5 @@ +error: Unknown literal value `no` + --> tests/trybuild/deny-parse-fail.rs:4:31 + | +4 | #[multipart(duplicate_field = "no")] + | ^^^^ diff --git a/actix-multipart-derive/tests/trybuild/deny-unknown.rs b/actix-multipart-derive/tests/trybuild/deny-unknown.rs new file mode 100644 index 00000000..e0346062 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/deny-unknown.rs @@ -0,0 +1,16 @@ +use actix_web::{web, App, Responder}; + +use actix_multipart::form::MultipartForm; + +#[derive(MultipartForm)] +#[multipart(deny_unknown_fields)] +struct Form {} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/optional-and-list.rs b/actix-multipart-derive/tests/trybuild/optional-and-list.rs new file mode 100644 index 00000000..deef3de5 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/optional-and-list.rs @@ -0,0 +1,18 @@ +use actix_web::{web, App, Responder}; + +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; + +#[derive(MultipartForm)] +struct Form { + category: Option>, + files: Vec, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/rename.rs b/actix-multipart-derive/tests/trybuild/rename.rs new file mode 100644 index 00000000..1f66bbb4 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/rename.rs @@ -0,0 +1,18 @@ +use actix_web::{web, App, Responder}; + +use actix_multipart::form::{tempfile::TempFile, MultipartForm}; + +#[derive(MultipartForm)] +struct Form { + #[multipart(rename = "files[]")] + files: Vec, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.rs b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.rs new file mode 100644 index 00000000..c3d49531 --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.rs @@ -0,0 +1,21 @@ +use actix_multipart::form::{text::Text, MultipartForm}; + +#[derive(MultipartForm)] +struct Form { + #[multipart(limit = "2 bytes")] + description: Text, +} + +#[derive(MultipartForm)] +struct Form2 { + #[multipart(limit = "2 megabytes")] + description: Text, +} + +#[derive(MultipartForm)] +struct Form3 { + #[multipart(limit = "four meters")] + description: Text, +} + +fn main() {} diff --git a/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr new file mode 100644 index 00000000..fc02a78c --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/size-limit-parse-fail.stderr @@ -0,0 +1,17 @@ +error: Could not parse size limit `2 bytes`: invalid digit found in string + --> tests/trybuild/size-limit-parse-fail.rs:6:5 + | +6 | description: Text, + | ^^^^^^^^^^^ + +error: Could not parse size limit `2 megabytes`: invalid digit found in string + --> tests/trybuild/size-limit-parse-fail.rs:12:5 + | +12 | description: Text, + | ^^^^^^^^^^^ + +error: Could not parse size limit `four meters`: invalid digit found in string + --> tests/trybuild/size-limit-parse-fail.rs:18:5 + | +18 | description: Text, + | ^^^^^^^^^^^ diff --git a/actix-multipart-derive/tests/trybuild/size-limits.rs b/actix-multipart-derive/tests/trybuild/size-limits.rs new file mode 100644 index 00000000..92c3d0db --- /dev/null +++ b/actix-multipart-derive/tests/trybuild/size-limits.rs @@ -0,0 +1,21 @@ +use actix_web::{web, App, Responder}; + +use actix_multipart::form::{tempfile::TempFile, text::Text, MultipartForm}; + +#[derive(MultipartForm)] +struct Form { + #[multipart(limit = "2 KiB")] + description: Text, + + #[multipart(limit = "512 MiB")] + files: Vec, +} + +async fn handler(_form: MultipartForm) -> impl Responder { + "Hello World!" +} + +#[actix_web::main] +async fn main() { + App::new().default_service(web::to(handler)); +} diff --git a/actix-multipart/CHANGES.md b/actix-multipart/CHANGES.md index 655487e5..a91edf9c 100644 --- a/actix-multipart/CHANGES.md +++ b/actix-multipart/CHANGES.md @@ -1,39 +1,57 @@ # Changes -## Unreleased - 2022-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 + +- Added `MultipartForm` typed data extractor. [#2883] + +[#2883]: https://github.com/actix/actix-web/pull/2883 + +## 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. -- `Field::content_type()` now returns `Option<&mime::Mime>` [#2880] -[#2880]: https://github.com/actix/actix-web/pull/2880 +[#2885]: https://github.com/actix/actix-web/pull/2885 +## 0.4.0 -## 0.4.0 - 2022-02-25 - No significant changes since `0.4.0-beta.13`. +## 0.4.0-beta.13 -## 0.4.0-beta.13 - 2022-01-31 - No significant changes since `0.4.0-beta.12`. +## 0.4.0-beta.12 -## 0.4.0-beta.12 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. +## 0.4.0-beta.11 -## 0.4.0-beta.11 - 2021-12-27 - No significant changes since `0.4.0-beta.10`. +## 0.4.0-beta.10 -## 0.4.0-beta.10 - 2021-12-11 - No significant changes since `0.4.0-beta.9`. +## 0.4.0-beta.9 -## 0.4.0-beta.9 - 2021-12-01 - Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463] [#2463]: https://github.com/actix/actix-web/pull/2463 +## 0.4.0-beta.8 -## 0.4.0-beta.8 - 2021-11-22 - Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451] - Added `MultipartError::NoContentDisposition` variant. [#2451] - Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451] @@ -43,97 +61,97 @@ [#2451]: https://github.com/actix/actix-web/pull/2451 +## 0.4.0-beta.7 -## 0.4.0-beta.7 - 2021-10-20 - Minimum supported Rust version (MSRV) is now 1.52. +## 0.4.0-beta.6 -## 0.4.0-beta.6 - 2021-09-09 - Minimum supported Rust version (MSRV) is now 1.51. +## 0.4.0-beta.5 -## 0.4.0-beta.5 - 2021-06-17 - No notable changes. +## 0.4.0-beta.4 -## 0.4.0-beta.4 - 2021-04-02 - No notable changes. +## 0.4.0-beta.3 -## 0.4.0-beta.3 - 2021-03-09 - No notable changes. +## 0.4.0-beta.2 -## 0.4.0-beta.2 - 2021-02-10 - No notable changes. +## 0.4.0-beta.1 -## 0.4.0-beta.1 - 2021-01-07 - Fix multipart consuming payload before header checks. [#1513] - Update `bytes` to `1.0`. [#1813] [#1813]: https://github.com/actix/actix-web/pull/1813 [#1513]: https://github.com/actix/actix-web/pull/1513 +## 0.3.0 -## 0.3.0 - 2020-09-11 - No significant changes from `0.3.0-beta.2`. +## 0.3.0-beta.2 -## 0.3.0-beta.2 - 2020-09-10 - Update `actix-*` dependencies to latest versions. +## 0.3.0-beta.1 -## 0.3.0-beta.1 - 2020-07-15 - Update `actix-web` to 3.0.0-beta.1 +## 0.3.0-alpha.1 -## 0.3.0-alpha.1 - 2020-05-25 - Update `actix-web` to 3.0.0-alpha.3 - Bump minimum supported Rust version to 1.40 - Minimize `futures` dependencies - 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 4651d455..5e9b78d8 100644 --- a/actix-multipart/Cargo.toml +++ b/actix-multipart/Cargo.toml @@ -1,34 +1,70 @@ [package] name = "actix-multipart" -version = "0.4.0" -authors = ["Nikolay Kim "] +version = "0.6.2" +authors = [ + "Nikolay Kim ", + "Jacob Halsey ", +] 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 = "2018" +edition = "2021" -[lib] -name = "actix_multipart" -path = "src/lib.rs" +[package.metadata.docs.rs] +rustdoc-args = ["--cfg", "docsrs"] +all-features = true + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_http::*", + "actix_multipart_derive::*", + "actix_utils::*", + "actix_web::*", + "bytes::*", + "futures_core::*", + "mime::*", + "serde_json::*", + "serde_plain::*", + "serde::*", + "tempfile::*", +] + +[features] +default = ["tempfile", "derive"] +derive = ["actix-multipart-derive"] +tempfile = ["dep:tempfile", "tokio/fs"] [dependencies] +actix-multipart-derive = { version = "=0.6.1", optional = true } actix-utils = "3" actix-web = { version = "4", default-features = false } bytes = "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"] } httparse = "1.3" local-waker = "0.1" log = "0.4" -mime = "0.3" memchr = "2.5" +mime = "0.3" +rand = "0.8" +serde = "1" +serde_json = "1" +serde_plain = "1" +tempfile = { version = "3.4", optional = true } +tokio = { version = "1.24.2", features = ["sync", "io-util"] } [dev-dependencies] -actix-rt = "2.2" 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"] } -tokio = { version = "1.18.4", features = ["sync"] } +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 21999716..d61347f3 100644 --- a/actix-multipart/README.md +++ b/actix-multipart/README.md @@ -1,17 +1,68 @@ -# 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.4.0)](https://docs.rs/actix-multipart/0.4.0) -![Version](https://img.shields.io/badge/rustc-1.59+-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.4.0/status.svg)](https://deps.rs/crate/actix-multipart/0.4.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.54 + + +Multipart form support for Actix Web. + +## Examples + +```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 +} +``` + + + +[More available in the examples repo →](https://github.com/actix/examples/tree/master/forms/multipart) + +Curl request : + +```bash +curl -v --request POST \ + --url http://localhost:8080/videos \ + -F 'json={"name": "Cargo.lock"};type=application/json' \ + -F file=@./Cargo.lock +``` diff --git a/actix-multipart/src/error.rs b/actix-multipart/src/error.rs index 7d0da35e..77b5a559 100644 --- a/actix-multipart/src/error.rs +++ b/actix-multipart/src/error.rs @@ -1,12 +1,15 @@ //! Error and Result module -use actix_web::error::{ParseError, PayloadError}; -use actix_web::http::StatusCode; -use actix_web::ResponseError; + +use actix_web::{ + error::{ParseError, PayloadError}, + http::StatusCode, + ResponseError, +}; use derive_more::{Display, Error, From}; -/// A set of errors that can occur during parsing multipart streams -#[non_exhaustive] +/// A set of errors that can occur during parsing multipart streams. #[derive(Debug, Display, From, Error)] +#[non_exhaustive] pub enum MultipartError { /// Content-Disposition header is not found or is not equal to "form-data". /// @@ -46,12 +49,41 @@ pub enum MultipartError { /// Not consumed #[display(fmt = "Multipart stream is not consumed")] NotConsumed, + + /// An error from a field handler in a form + #[display( + fmt = "An error occurred processing field `{}`: {}", + field_name, + source + )] + Field { + field_name: String, + source: actix_web::Error, + }, + + /// Duplicate field + #[display(fmt = "Duplicate field found for: `{}`", _0)] + #[from(ignore)] + DuplicateField(#[error(not(source))] String), + + /// Missing field + #[display(fmt = "Field with name `{}` is required", _0)] + #[from(ignore)] + MissingField(#[error(not(source))] String), + + /// Unknown field + #[display(fmt = "Unsupported field `{}`", _0)] + #[from(ignore)] + UnsupportedField(#[error(not(source))] String), } /// Return `BadRequest` for `MultipartError` impl ResponseError for MultipartError { fn status_code(&self) -> StatusCode { - StatusCode::BAD_REQUEST + match &self { + MultipartError::Field { source, .. } => source.as_response_error().status_code(), + _ => StatusCode::BAD_REQUEST, + } } } diff --git a/actix-multipart/src/extractor.rs b/actix-multipart/src/extractor.rs index d45c4869..56ed69ae 100644 --- a/actix-multipart/src/extractor.rs +++ b/actix-multipart/src/extractor.rs @@ -9,8 +9,7 @@ use crate::server::Multipart; /// /// Content-type: multipart/form-data; /// -/// ## Server example -/// +/// # Examples /// ``` /// use actix_web::{web, HttpResponse, Error}; /// use actix_multipart::Multipart; diff --git a/actix-multipart/src/form/bytes.rs b/actix-multipart/src/form/bytes.rs new file mode 100644 index 00000000..3c5e2eb1 --- /dev/null +++ b/actix-multipart/src/form/bytes.rs @@ -0,0 +1,49 @@ +//! Reads a field into memory. + +use actix_web::HttpRequest; +use bytes::BytesMut; +use futures_core::future::LocalBoxFuture; +use futures_util::TryStreamExt as _; +use mime::Mime; + +use crate::{ + form::{FieldReader, Limits}, + Field, MultipartError, +}; + +/// Read the field into memory. +#[derive(Debug)] +pub struct Bytes { + /// The data. + pub data: bytes::Bytes, + + /// The value of the `Content-Type` header. + pub content_type: Option, + + /// The `filename` value in the `Content-Disposition` header. + pub file_name: Option, +} + +impl<'t> FieldReader<'t> for Bytes { + type Future = LocalBoxFuture<'t, Result>; + + fn read_field(_: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future { + Box::pin(async move { + let mut buf = BytesMut::with_capacity(131_072); + + while let Some(chunk) = field.try_next().await? { + limits.try_consume_limits(chunk.len(), true)?; + buf.extend(chunk); + } + + Ok(Bytes { + data: buf.freeze(), + content_type: field.content_type().map(ToOwned::to_owned), + file_name: field + .content_disposition() + .get_filename() + .map(str::to_owned), + }) + }) + } +} diff --git a/actix-multipart/src/form/json.rs b/actix-multipart/src/form/json.rs new file mode 100644 index 00000000..bb4e03bf --- /dev/null +++ b/actix-multipart/src/form/json.rs @@ -0,0 +1,211 @@ +//! Deserializes a field as JSON. + +use std::sync::Arc; + +use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError}; +use derive_more::{Deref, DerefMut, Display, Error}; +use futures_core::future::LocalBoxFuture; +use serde::de::DeserializeOwned; + +use super::FieldErrorHandler; +use crate::{ + form::{bytes::Bytes, FieldReader, Limits}, + Field, MultipartError, +}; + +/// Deserialize from JSON. +#[derive(Debug, Deref, DerefMut)] +pub struct Json(pub T); + +impl Json { + pub fn into_inner(self) -> T { + self.0 + } +} + +impl<'t, T> FieldReader<'t> for Json +where + T: DeserializeOwned + 'static, +{ + type Future = LocalBoxFuture<'t, Result>; + + fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { + Box::pin(async move { + let config = JsonConfig::from_req(req); + let field_name = field.name().to_owned(); + + if config.validate_content_type { + let valid = if let Some(mime) = field.content_type() { + mime.subtype() == mime::JSON || mime.suffix() == Some(mime::JSON) + } else { + false + }; + + if !valid { + return Err(MultipartError::Field { + field_name, + source: config.map_error(req, JsonFieldError::ContentType), + }); + } + } + + let bytes = Bytes::read_field(req, field, limits).await?; + + Ok(Json(serde_json::from_slice(bytes.data.as_ref()).map_err( + |err| MultipartError::Field { + field_name, + source: config.map_error(req, JsonFieldError::Deserialize(err)), + }, + )?)) + }) + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +pub enum JsonFieldError { + /// Deserialize error. + #[display(fmt = "Json deserialize error: {}", _0)] + Deserialize(serde_json::Error), + + /// Content type error. + #[display(fmt = "Content type error")] + ContentType, +} + +impl ResponseError for JsonFieldError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +/// Configuration for the [`Json`] field reader. +#[derive(Clone)] +pub struct JsonConfig { + err_handler: FieldErrorHandler, + validate_content_type: bool, +} + +const DEFAULT_CONFIG: JsonConfig = JsonConfig { + err_handler: None, + validate_content_type: true, +}; + +impl JsonConfig { + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(JsonFieldError, &HttpRequest) -> Error + Send + Sync + 'static, + { + self.err_handler = Some(Arc::new(f)); + self + } + + /// Extract payload config from app data. Check both `T` and `Data`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } + + fn map_error(&self, req: &HttpRequest, err: JsonFieldError) -> Error { + if let Some(err_handler) = self.err_handler.as_ref() { + (*err_handler)(err, req) + } else { + err.into() + } + } + + /// Sets whether or not the field must have a valid `Content-Type` header to be parsed. + pub fn validate_content_type(mut self, validate_content_type: bool) -> Self { + self.validate_content_type = validate_content_type; + self + } +} + +impl Default for JsonConfig { + fn default() -> Self { + DEFAULT_CONFIG + } +} + +#[cfg(test)] +mod tests { + use std::collections::HashMap; + + use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; + use bytes::Bytes; + + use crate::form::{ + json::{Json, JsonConfig}, + MultipartForm, + }; + + #[derive(MultipartForm)] + struct JsonForm { + json: Json>, + } + + async fn test_json_route(form: MultipartForm) -> impl Responder { + let mut expected = HashMap::new(); + expected.insert("key1".to_owned(), "value1".to_owned()); + expected.insert("key2".to_owned(), "value2".to_owned()); + assert_eq!(&*form.json, &expected); + 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(|| { + App::new() + .route("/", web::post().to(test_json_route)) + .app_data(JsonConfig::default().validate_content_type(false)) + }); + + 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] + async fn test_content_type_validation() { + let srv = actix_test::start(|| { + App::new() + .route("/", web::post().to(test_json_route)) + .app_data(JsonConfig::default().validate_content_type(true)) + }); + + // Deny because wrong content type + 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 (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 new file mode 100644 index 00000000..68cdefec --- /dev/null +++ b/actix-multipart/src/form/mod.rs @@ -0,0 +1,799 @@ +//! Process and extract typed data from a multipart stream. + +use std::{ + any::Any, + collections::HashMap, + future::{ready, Future}, + sync::Arc, +}; + +use actix_web::{dev, error::PayloadError, web, Error, FromRequest, HttpRequest}; +use derive_more::{Deref, DerefMut}; +use futures_core::future::LocalBoxFuture; +use futures_util::{TryFutureExt as _, TryStreamExt as _}; + +use crate::{Field, Multipart, MultipartError}; + +pub mod bytes; +pub mod json; +#[cfg(feature = "tempfile")] +pub mod tempfile; +pub mod text; + +#[cfg(feature = "derive")] +pub use actix_multipart_derive::MultipartForm; + +type FieldErrorHandler = Option Error + Send + Sync>>; + +/// Trait that data types to be used in a multipart form struct should implement. +/// +/// It represents an asynchronous handler that processes a multipart field to produce `Self`. +pub trait FieldReader<'t>: Sized + Any { + /// Future that resolves to a `Self`. + type Future: Future>; + + /// The form will call this function to handle the field. + /// + /// # Panics + /// + /// When reading the `field` payload using its `Stream` implementation, polling (manually or via + /// `next()`/`try_next()`) may panic after the payload is exhausted. If this is a problem for + /// your implementation of this method, you should [`fuse()`] the `Field` first. + /// + /// [`fuse()`]: futures_util::stream::StreamExt::fuse() + fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future; +} + +/// Used to accumulate the state of the loaded fields. +#[doc(hidden)] +#[derive(Default, Deref, DerefMut)] +pub struct State(pub HashMap>); + +/// Trait that the field collection types implement, i.e. `Vec`, `Option`, or `T` itself. +#[doc(hidden)] +pub trait FieldGroupReader<'t>: Sized + Any { + type Future: Future>; + + /// The form will call this function for each matching field. + fn handle_field( + req: &'t HttpRequest, + field: Field, + limits: &'t mut Limits, + state: &'t mut State, + duplicate_field: DuplicateField, + ) -> Self::Future; + + /// Construct `Self` from the group of processed fields. + fn from_state(name: &str, state: &'t mut State) -> Result; +} + +impl<'t, T> FieldGroupReader<'t> for Option +where + T: FieldReader<'t>, +{ + type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; + + fn handle_field( + req: &'t HttpRequest, + field: Field, + limits: &'t mut Limits, + state: &'t mut State, + duplicate_field: DuplicateField, + ) -> Self::Future { + if state.contains_key(field.name()) { + match duplicate_field { + DuplicateField::Ignore => return Box::pin(ready(Ok(()))), + + DuplicateField::Deny => { + return Box::pin(ready(Err(MultipartError::DuplicateField( + field.name().to_owned(), + )))) + } + + DuplicateField::Replace => {} + } + } + + Box::pin(async move { + let field_name = field.name().to_owned(); + let t = T::read_field(req, field, limits).await?; + state.insert(field_name, Box::new(t)); + Ok(()) + }) + } + + fn from_state(name: &str, state: &'t mut State) -> Result { + Ok(state.remove(name).map(|m| *m.downcast::().unwrap())) + } +} + +impl<'t, T> FieldGroupReader<'t> for Vec +where + T: FieldReader<'t>, +{ + type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; + + fn handle_field( + req: &'t HttpRequest, + field: Field, + limits: &'t mut Limits, + state: &'t mut State, + _duplicate_field: DuplicateField, + ) -> Self::Future { + Box::pin(async move { + // Note: Vec GroupReader always allows duplicates + + let field_name = field.name().to_owned(); + + let vec = state + .entry(field_name) + .or_insert_with(|| Box::>::default()) + .downcast_mut::>() + .unwrap(); + + let item = T::read_field(req, field, limits).await?; + vec.push(item); + + Ok(()) + }) + } + + fn from_state(name: &str, state: &'t mut State) -> Result { + Ok(state + .remove(name) + .map(|m| *m.downcast::>().unwrap()) + .unwrap_or_default()) + } +} + +impl<'t, T> FieldGroupReader<'t> for T +where + T: FieldReader<'t>, +{ + type Future = LocalBoxFuture<'t, Result<(), MultipartError>>; + + fn handle_field( + req: &'t HttpRequest, + field: Field, + limits: &'t mut Limits, + state: &'t mut State, + duplicate_field: DuplicateField, + ) -> Self::Future { + if state.contains_key(field.name()) { + match duplicate_field { + DuplicateField::Ignore => return Box::pin(ready(Ok(()))), + + DuplicateField::Deny => { + return Box::pin(ready(Err(MultipartError::DuplicateField( + field.name().to_owned(), + )))) + } + + DuplicateField::Replace => {} + } + } + + Box::pin(async move { + let field_name = field.name().to_owned(); + let t = T::read_field(req, field, limits).await?; + state.insert(field_name, Box::new(t)); + Ok(()) + }) + } + + fn from_state(name: &str, state: &'t mut State) -> Result { + state + .remove(name) + .map(|m| *m.downcast::().unwrap()) + .ok_or_else(|| MultipartError::MissingField(name.to_owned())) + } +} + +/// Trait that allows a type to be used in the [`struct@MultipartForm`] extractor. +/// +/// You should use the [`macro@MultipartForm`] macro to derive this for your struct. +pub trait MultipartCollect: Sized { + /// An optional limit in bytes to be applied a given field name. Note this limit will be shared + /// across all fields sharing the same name. + fn limit(field_name: &str) -> Option; + + /// The extractor will call this function for each incoming field, the state can be updated + /// with the processed field data. + fn handle_field<'t>( + req: &'t HttpRequest, + field: Field, + limits: &'t mut Limits, + state: &'t mut State, + ) -> LocalBoxFuture<'t, Result<(), MultipartError>>; + + /// Once all the fields have been processed and stored in the state, this is called + /// to convert into the struct representation. + fn from_state(state: State) -> Result; +} + +#[doc(hidden)] +pub enum DuplicateField { + /// Additional fields are not processed. + Ignore, + + /// An error will be raised. + Deny, + + /// All fields will be processed, the last one will replace all previous. + Replace, +} + +/// Used to keep track of the remaining limits for the form and current field. +pub struct Limits { + pub total_limit_remaining: usize, + pub memory_limit_remaining: usize, + pub field_limit_remaining: Option, +} + +impl Limits { + pub fn new(total_limit: usize, memory_limit: usize) -> Self { + Self { + total_limit_remaining: total_limit, + memory_limit_remaining: memory_limit, + field_limit_remaining: None, + } + } + + /// This function should be called within a [`FieldReader`] when reading each chunk of a field + /// to ensure that the form limits are not exceeded. + /// + /// # Arguments + /// + /// * `bytes` - The number of bytes being read from this chunk + /// * `in_memory` - Whether to consume from the memory limits + pub fn try_consume_limits( + &mut self, + bytes: usize, + in_memory: bool, + ) -> Result<(), MultipartError> { + self.total_limit_remaining = self + .total_limit_remaining + .checked_sub(bytes) + .ok_or(MultipartError::Payload(PayloadError::Overflow))?; + + if in_memory { + self.memory_limit_remaining = self + .memory_limit_remaining + .checked_sub(bytes) + .ok_or(MultipartError::Payload(PayloadError::Overflow))?; + } + + if let Some(field_limit) = self.field_limit_remaining { + self.field_limit_remaining = Some( + field_limit + .checked_sub(bytes) + .ok_or(MultipartError::Payload(PayloadError::Overflow))?, + ); + } + + Ok(()) + } +} + +/// Typed `multipart/form-data` extractor. +/// +/// To extract typed data from a multipart stream, the inner type `T` must implement the +/// [`MultipartCollect`] trait. You should use the [`macro@MultipartForm`] macro to derive this +/// for your struct. +/// +/// Add a [`MultipartFormConfig`] to your app data to configure extraction. +#[derive(Deref, DerefMut)] +pub struct MultipartForm(pub T); + +impl MultipartForm { + /// Unwrap into inner `T` value. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl FromRequest for MultipartForm +where + T: MultipartCollect, +{ + type Error = Error; + type Future = LocalBoxFuture<'static, Result>; + + #[inline] + fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { + let mut payload = Multipart::new(req.headers(), payload.take()); + + let config = MultipartFormConfig::from_req(req); + let mut limits = Limits::new(config.total_limit, config.memory_limit); + + let req = req.clone(); + let req2 = req.clone(); + let err_handler = config.err_handler.clone(); + + Box::pin( + async move { + let mut state = State::default(); + // We need to ensure field limits are shared for all instances of this field name + let mut field_limits = HashMap::>::new(); + + while let Some(field) = payload.try_next().await? { + // Retrieve the limit for this field + let entry = field_limits + .entry(field.name().to_owned()) + .or_insert_with(|| T::limit(field.name())); + + limits.field_limit_remaining.clone_from(entry); + + T::handle_field(&req, field, &mut limits, &mut state).await?; + + // Update the stored limit + *entry = limits.field_limit_remaining; + } + let inner = T::from_state(state)?; + Ok(MultipartForm(inner)) + } + .map_err(move |err| { + if let Some(handler) = err_handler { + (*handler)(err, &req2) + } else { + err.into() + } + }), + ) + } +} + +type MultipartFormErrorHandler = + Option Error + Send + Sync>>; + +/// [`struct@MultipartForm`] extractor configuration. +/// +/// Add to your app data to have it picked up by [`struct@MultipartForm`] extractors. +#[derive(Clone)] +pub struct MultipartFormConfig { + total_limit: usize, + memory_limit: usize, + err_handler: MultipartFormErrorHandler, +} + +impl MultipartFormConfig { + /// Sets maximum accepted payload size for the entire form. By default this limit is 50MiB. + pub fn total_limit(mut self, total_limit: usize) -> Self { + self.total_limit = total_limit; + self + } + + /// Sets maximum accepted data that will be read into memory. By default this limit is 2MiB. + pub fn memory_limit(mut self, memory_limit: usize) -> Self { + self.memory_limit = memory_limit; + self + } + + /// Sets custom error handler. + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(MultipartError, &HttpRequest) -> Error + Send + Sync + 'static, + { + self.err_handler = Some(Arc::new(f)); + self + } + + /// Extracts payload config from app data. Check both `T` and `Data`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } +} + +const DEFAULT_CONFIG: MultipartFormConfig = MultipartFormConfig { + total_limit: 52_428_800, // 50 MiB + memory_limit: 2_097_152, // 2 MiB + err_handler: None, +}; + +impl Default for MultipartFormConfig { + fn default() -> Self { + DEFAULT_CONFIG + } +} + +#[cfg(test)] +mod tests { + use actix_http::encoding::Decoder; + use actix_multipart_rfc7578::client::multipart; + use actix_test::TestServer; + use actix_web::{ + dev::Payload, http::StatusCode, web, App, HttpRequest, HttpResponse, Resource, Responder, + }; + use awc::{Client, ClientResponse}; + use futures_core::future::LocalBoxFuture; + use futures_util::TryStreamExt as _; + + use super::MultipartForm; + use crate::{ + form::{ + bytes::Bytes, tempfile::TempFile, text::Text, FieldReader, Limits, MultipartFormConfig, + }, + Field, MultipartError, + }; + + pub async fn send_form( + srv: &TestServer, + form: multipart::Form<'static>, + uri: &'static str, + ) -> ClientResponse> { + Client::default() + .post(srv.url(uri)) + .content_type(form.content_type()) + .send_body(multipart::Body::from(form)) + .await + .unwrap() + } + + /// Test `Option` fields. + #[derive(MultipartForm)] + struct TestOptions { + field1: Option>, + field2: Option>, + } + + async fn test_options_route(form: MultipartForm) -> impl Responder { + assert!(form.field1.is_some()); + assert!(form.field2.is_none()); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_options() { + let srv = actix_test::start(|| App::new().route("/", web::post().to(test_options_route))); + + let mut form = multipart::Form::default(); + form.add_text("field1", "value"); + + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + } + + /// Test `Vec` fields. + #[derive(MultipartForm)] + struct TestVec { + list1: Vec>, + list2: Vec>, + } + + async fn test_vec_route(form: MultipartForm) -> impl Responder { + let form = form.into_inner(); + let strings = form + .list1 + .into_iter() + .map(|s| s.into_inner()) + .collect::>(); + assert_eq!(strings, vec!["value1", "value2", "value3"]); + assert_eq!(form.list2.len(), 0); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_vec() { + let srv = actix_test::start(|| App::new().route("/", web::post().to(test_vec_route))); + + let mut form = multipart::Form::default(); + form.add_text("list1", "value1"); + form.add_text("list1", "value2"); + form.add_text("list1", "value3"); + + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + } + + /// Test the `rename` field attribute. + #[derive(MultipartForm)] + struct TestFieldRenaming { + #[multipart(rename = "renamed")] + field1: Text, + #[multipart(rename = "field1")] + field2: Text, + field3: Text, + } + + async fn test_field_renaming_route(form: MultipartForm) -> impl Responder { + assert_eq!(&*form.field1, "renamed"); + assert_eq!(&*form.field2, "field1"); + assert_eq!(&*form.field3, "field3"); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_field_renaming() { + let srv = + actix_test::start(|| App::new().route("/", web::post().to(test_field_renaming_route))); + + let mut form = multipart::Form::default(); + form.add_text("renamed", "renamed"); + form.add_text("field1", "field1"); + form.add_text("field3", "field3"); + + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + } + + /// Test the `deny_unknown_fields` struct attribute. + #[derive(MultipartForm)] + #[multipart(deny_unknown_fields)] + struct TestDenyUnknown {} + + #[derive(MultipartForm)] + struct TestAllowUnknown {} + + async fn test_deny_unknown_route(_: MultipartForm) -> impl Responder { + HttpResponse::Ok().finish() + } + + async fn test_allow_unknown_route(_: MultipartForm) -> impl Responder { + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_deny_unknown() { + let srv = actix_test::start(|| { + App::new() + .route("/deny", web::post().to(test_deny_unknown_route)) + .route("/allow", web::post().to(test_allow_unknown_route)) + }); + + let mut form = multipart::Form::default(); + form.add_text("unknown", "value"); + let response = send_form(&srv, form, "/deny").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let mut form = multipart::Form::default(); + form.add_text("unknown", "value"); + let response = send_form(&srv, form, "/allow").await; + assert_eq!(response.status(), StatusCode::OK); + } + + /// Test the `duplicate_field` struct attribute. + #[derive(MultipartForm)] + #[multipart(duplicate_field = "deny")] + struct TestDuplicateDeny { + _field: Text, + } + + #[derive(MultipartForm)] + #[multipart(duplicate_field = "replace")] + struct TestDuplicateReplace { + field: Text, + } + + #[derive(MultipartForm)] + #[multipart(duplicate_field = "ignore")] + struct TestDuplicateIgnore { + field: Text, + } + + async fn test_duplicate_deny_route(_: MultipartForm) -> impl Responder { + HttpResponse::Ok().finish() + } + + async fn test_duplicate_replace_route( + form: MultipartForm, + ) -> impl Responder { + assert_eq!(&*form.field, "second_value"); + HttpResponse::Ok().finish() + } + + async fn test_duplicate_ignore_route( + form: MultipartForm, + ) -> impl Responder { + assert_eq!(&*form.field, "first_value"); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_duplicate_field() { + let srv = actix_test::start(|| { + App::new() + .route("/deny", web::post().to(test_duplicate_deny_route)) + .route("/replace", web::post().to(test_duplicate_replace_route)) + .route("/ignore", web::post().to(test_duplicate_ignore_route)) + }); + + let mut form = multipart::Form::default(); + form.add_text("_field", "first_value"); + form.add_text("_field", "second_value"); + let response = send_form(&srv, form, "/deny").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + let mut form = multipart::Form::default(); + form.add_text("field", "first_value"); + form.add_text("field", "second_value"); + let response = send_form(&srv, form, "/replace").await; + assert_eq!(response.status(), StatusCode::OK); + + let mut form = multipart::Form::default(); + form.add_text("field", "first_value"); + form.add_text("field", "second_value"); + let response = send_form(&srv, form, "/ignore").await; + assert_eq!(response.status(), StatusCode::OK); + } + + /// Test the Limits. + #[derive(MultipartForm)] + struct TestMemoryUploadLimits { + field: Bytes, + } + + #[derive(MultipartForm)] + struct TestFileUploadLimits { + field: TempFile, + } + + async fn test_upload_limits_memory( + form: MultipartForm, + ) -> impl Responder { + assert!(!form.field.data.is_empty()); + HttpResponse::Ok().finish() + } + + async fn test_upload_limits_file(form: MultipartForm) -> impl Responder { + assert!(form.field.size > 0); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_memory_limits() { + let srv = actix_test::start(|| { + App::new() + .route("/text", web::post().to(test_upload_limits_memory)) + .route("/file", web::post().to(test_upload_limits_file)) + .app_data( + MultipartFormConfig::default() + .memory_limit(20) + .total_limit(usize::MAX), + ) + }); + + // Exceeds the 20 byte memory limit + let mut form = multipart::Form::default(); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/text").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Memory limit should not apply when the data is being streamed to disk + let mut form = multipart::Form::default(); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/file").await; + assert_eq!(response.status(), StatusCode::OK); + } + + #[actix_rt::test] + async fn test_total_limit() { + let srv = actix_test::start(|| { + App::new() + .route("/text", web::post().to(test_upload_limits_memory)) + .route("/file", web::post().to(test_upload_limits_file)) + .app_data( + MultipartFormConfig::default() + .memory_limit(usize::MAX) + .total_limit(20), + ) + }); + + // Within the 20 byte limit + let mut form = multipart::Form::default(); + form.add_text("field", "7 bytes"); + let response = send_form(&srv, form, "/text").await; + assert_eq!(response.status(), StatusCode::OK); + + // Exceeds the 20 byte overall limit + let mut form = multipart::Form::default(); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/text").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Exceeds the 20 byte overall limit + let mut form = multipart::Form::default(); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/file").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[derive(MultipartForm)] + struct TestFieldLevelLimits { + #[multipart(limit = "30B")] + field: Vec, + } + + async fn test_field_level_limits_route( + form: MultipartForm, + ) -> impl Responder { + assert!(!form.field.is_empty()); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_field_level_limits() { + let srv = actix_test::start(|| { + App::new() + .route("/", web::post().to(test_field_level_limits_route)) + .app_data( + MultipartFormConfig::default() + .memory_limit(usize::MAX) + .total_limit(usize::MAX), + ) + }); + + // Within the 30 byte limit + let mut form = multipart::Form::default(); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + + // Exceeds the the 30 byte limit + let mut form = multipart::Form::default(); + form.add_text("field", "this string is more than 30 bytes long"); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Total of values (14 bytes) is within 30 byte limit for "field" + let mut form = multipart::Form::default(); + form.add_text("field", "7 bytes"); + form.add_text("field", "7 bytes"); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + + // Total of values exceeds 30 byte limit for "field" + let mut form = multipart::Form::default(); + form.add_text("field", "this string is 28 bytes long"); + form.add_text("field", "this string is 28 bytes long"); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + } + + #[should_panic(expected = "called `Result::unwrap()` on an `Err` value: Connect(Disconnected)")] + #[actix_web::test] + async fn field_try_next_panic() { + #[derive(Debug)] + struct NullSink; + + impl<'t> FieldReader<'t> for NullSink { + type Future = LocalBoxFuture<'t, Result>; + + fn read_field( + _: &'t HttpRequest, + mut field: Field, + _limits: &'t mut Limits, + ) -> Self::Future { + Box::pin(async move { + // exhaust field stream + while let Some(_chunk) = field.try_next().await? {} + + // poll again, crash + let _post = field.try_next().await; + + Ok(Self) + }) + } + } + + #[allow(dead_code)] + #[derive(MultipartForm)] + struct NullSinkForm { + foo: NullSink, + } + + async fn null_sink(_form: MultipartForm) -> impl Responder { + "unreachable" + } + + let srv = actix_test::start(|| App::new().service(Resource::new("/").post(null_sink))); + + let mut form = multipart::Form::default(); + form.add_text("foo", "data is not important to this test"); + + // panics with Err(Connect(Disconnected)) due to form NullSink panic + let _res = send_form(&srv, form, "/").await; + } +} diff --git a/actix-multipart/src/form/tempfile.rs b/actix-multipart/src/form/tempfile.rs new file mode 100644 index 00000000..9371a026 --- /dev/null +++ b/actix-multipart/src/form/tempfile.rs @@ -0,0 +1,199 @@ +//! Writes a field to a temporary file on disk. + +use std::{ + io, + path::{Path, PathBuf}, + sync::Arc, +}; + +use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError}; +use derive_more::{Display, Error}; +use futures_core::future::LocalBoxFuture; +use futures_util::TryStreamExt as _; +use mime::Mime; +use tempfile::NamedTempFile; +use tokio::io::AsyncWriteExt; + +use super::FieldErrorHandler; +use crate::{ + form::{FieldReader, Limits}, + Field, MultipartError, +}; + +/// Write the field to a temporary file on disk. +#[derive(Debug)] +pub struct TempFile { + /// The temporary file on disk. + pub file: NamedTempFile, + + /// The value of the `content-type` header. + pub content_type: Option, + + /// The `filename` value in the `content-disposition` header. + pub file_name: Option, + + /// The size in bytes of the file. + pub size: usize, +} + +impl<'t> FieldReader<'t> for TempFile { + type Future = LocalBoxFuture<'t, Result>; + + fn read_field(req: &'t HttpRequest, mut field: Field, limits: &'t mut Limits) -> Self::Future { + Box::pin(async move { + let config = TempFileConfig::from_req(req); + let field_name = field.name().to_owned(); + let mut size = 0; + + let file = config + .create_tempfile() + .map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?; + + let mut file_async = + tokio::fs::File::from_std(file.reopen().map_err(|err| { + config.map_error(req, &field_name, TempFileError::FileIo(err)) + })?); + + while let Some(chunk) = field.try_next().await? { + limits.try_consume_limits(chunk.len(), false)?; + size += chunk.len(); + file_async.write_all(chunk.as_ref()).await.map_err(|err| { + config.map_error(req, &field_name, TempFileError::FileIo(err)) + })?; + } + + file_async + .flush() + .await + .map_err(|err| config.map_error(req, &field_name, TempFileError::FileIo(err)))?; + + Ok(TempFile { + file, + content_type: field.content_type().map(ToOwned::to_owned), + file_name: field + .content_disposition() + .get_filename() + .map(str::to_owned), + size, + }) + }) + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +pub enum TempFileError { + /// File I/O Error + #[display(fmt = "File I/O error: {}", _0)] + FileIo(std::io::Error), +} + +impl ResponseError for TempFileError { + fn status_code(&self) -> StatusCode { + StatusCode::INTERNAL_SERVER_ERROR + } +} + +/// Configuration for the [`TempFile`] field reader. +#[derive(Clone)] +pub struct TempFileConfig { + err_handler: FieldErrorHandler, + directory: Option, +} + +impl TempFileConfig { + fn create_tempfile(&self) -> io::Result { + if let Some(ref dir) = self.directory { + NamedTempFile::new_in(dir) + } else { + NamedTempFile::new() + } + } +} + +impl TempFileConfig { + /// Sets custom error handler. + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(TempFileError, &HttpRequest) -> Error + Send + Sync + 'static, + { + self.err_handler = Some(Arc::new(f)); + self + } + + /// Extracts payload config from app data. Check both `T` and `Data`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } + + fn map_error(&self, req: &HttpRequest, field_name: &str, err: TempFileError) -> MultipartError { + let source = if let Some(ref err_handler) = self.err_handler { + (err_handler)(err, req) + } else { + err.into() + }; + + MultipartError::Field { + field_name: field_name.to_owned(), + source, + } + } + + /// Sets the directory that temp files will be created in. + /// + /// The default temporary file location is platform dependent. + pub fn directory(mut self, dir: impl AsRef) -> Self { + self.directory = Some(dir.as_ref().to_owned()); + self + } +} + +const DEFAULT_CONFIG: TempFileConfig = TempFileConfig { + err_handler: None, + directory: None, +}; + +impl Default for TempFileConfig { + fn default() -> Self { + DEFAULT_CONFIG + } +} + +#[cfg(test)] +mod tests { + use std::io::{Cursor, Read}; + + use actix_multipart_rfc7578::client::multipart; + use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; + + use crate::form::{tempfile::TempFile, tests::send_form, MultipartForm}; + + #[derive(MultipartForm)] + struct FileForm { + file: TempFile, + } + + async fn test_file_route(form: MultipartForm) -> impl Responder { + let mut form = form.into_inner(); + let mut contents = String::new(); + form.file.file.read_to_string(&mut contents).unwrap(); + assert_eq!(contents, "Hello, world!"); + assert_eq!(form.file.file_name.unwrap(), "testfile.txt"); + assert_eq!(form.file.content_type.unwrap(), mime::TEXT_PLAIN); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_file_upload() { + let srv = actix_test::start(|| App::new().route("/", web::post().to(test_file_route))); + + let mut form = multipart::Form::default(); + let bytes = Cursor::new("Hello, world!"); + form.add_reader_file_with_mime("file", bytes, "testfile.txt", mime::TEXT_PLAIN); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + } +} diff --git a/actix-multipart/src/form/text.rs b/actix-multipart/src/form/text.rs new file mode 100644 index 00000000..83e21152 --- /dev/null +++ b/actix-multipart/src/form/text.rs @@ -0,0 +1,196 @@ +//! Deserializes a field from plain text. + +use std::{str, sync::Arc}; + +use actix_web::{http::StatusCode, web, Error, HttpRequest, ResponseError}; +use derive_more::{Deref, DerefMut, Display, Error}; +use futures_core::future::LocalBoxFuture; +use serde::de::DeserializeOwned; + +use super::FieldErrorHandler; +use crate::{ + form::{bytes::Bytes, FieldReader, Limits}, + Field, MultipartError, +}; + +/// Deserialize from plain text. +/// +/// Internally this uses [`serde_plain`] for deserialization, which supports primitive types +/// including strings, numbers, and simple enums. +#[derive(Debug, Deref, DerefMut)] +pub struct Text(pub T); + +impl Text { + /// Unwraps into inner value. + pub fn into_inner(self) -> T { + self.0 + } +} + +impl<'t, T> FieldReader<'t> for Text +where + T: DeserializeOwned + 'static, +{ + type Future = LocalBoxFuture<'t, Result>; + + fn read_field(req: &'t HttpRequest, field: Field, limits: &'t mut Limits) -> Self::Future { + Box::pin(async move { + let config = TextConfig::from_req(req); + let field_name = field.name().to_owned(); + + if config.validate_content_type { + let valid = if let Some(mime) = field.content_type() { + mime.subtype() == mime::PLAIN || mime.suffix() == Some(mime::PLAIN) + } else { + // https://datatracker.ietf.org/doc/html/rfc7578#section-4.4 + // content type defaults to text/plain, so None should be considered valid + true + }; + + if !valid { + return Err(MultipartError::Field { + field_name, + source: config.map_error(req, TextError::ContentType), + }); + } + } + + let bytes = Bytes::read_field(req, field, limits).await?; + + let text = str::from_utf8(&bytes.data).map_err(|err| MultipartError::Field { + field_name: field_name.clone(), + source: config.map_error(req, TextError::Utf8Error(err)), + })?; + + Ok(Text(serde_plain::from_str(text).map_err(|err| { + MultipartError::Field { + field_name, + source: config.map_error(req, TextError::Deserialize(err)), + } + })?)) + }) + } +} + +#[derive(Debug, Display, Error)] +#[non_exhaustive] +pub enum TextError { + /// UTF-8 decoding error. + #[display(fmt = "UTF-8 decoding error: {}", _0)] + Utf8Error(str::Utf8Error), + + /// Deserialize error. + #[display(fmt = "Plain text deserialize error: {}", _0)] + Deserialize(serde_plain::Error), + + /// Content type error. + #[display(fmt = "Content type error")] + ContentType, +} + +impl ResponseError for TextError { + fn status_code(&self) -> StatusCode { + StatusCode::BAD_REQUEST + } +} + +/// Configuration for the [`Text`] field reader. +#[derive(Clone)] +pub struct TextConfig { + err_handler: FieldErrorHandler, + validate_content_type: bool, +} + +impl TextConfig { + /// Sets custom error handler. + pub fn error_handler(mut self, f: F) -> Self + where + F: Fn(TextError, &HttpRequest) -> Error + Send + Sync + 'static, + { + self.err_handler = Some(Arc::new(f)); + self + } + + /// Extracts payload config from app data. Check both `T` and `Data`, in that order, and fall + /// back to the default payload config. + fn from_req(req: &HttpRequest) -> &Self { + req.app_data::() + .or_else(|| req.app_data::>().map(|d| d.as_ref())) + .unwrap_or(&DEFAULT_CONFIG) + } + + fn map_error(&self, req: &HttpRequest, err: TextError) -> Error { + if let Some(ref err_handler) = self.err_handler { + (err_handler)(err, req) + } else { + err.into() + } + } + + /// Sets whether or not the field must have a valid `Content-Type` header to be parsed. + /// + /// Note that an empty `Content-Type` is also accepted, as the multipart specification defines + /// `text/plain` as the default for text fields. + pub fn validate_content_type(mut self, validate_content_type: bool) -> Self { + self.validate_content_type = validate_content_type; + self + } +} + +const DEFAULT_CONFIG: TextConfig = TextConfig { + err_handler: None, + validate_content_type: true, +}; + +impl Default for TextConfig { + fn default() -> Self { + DEFAULT_CONFIG + } +} + +#[cfg(test)] +mod tests { + use std::io::Cursor; + + use actix_multipart_rfc7578::client::multipart; + use actix_web::{http::StatusCode, web, App, HttpResponse, Responder}; + + use crate::form::{ + tests::send_form, + text::{Text, TextConfig}, + MultipartForm, + }; + + #[derive(MultipartForm)] + struct TextForm { + number: Text, + } + + async fn test_text_route(form: MultipartForm) -> impl Responder { + assert_eq!(*form.number, 1025); + HttpResponse::Ok().finish() + } + + #[actix_rt::test] + async fn test_content_type_validation() { + let srv = actix_test::start(|| { + App::new() + .route("/", web::post().to(test_text_route)) + .app_data(TextConfig::default().validate_content_type(true)) + }); + + // Deny because wrong content type + let bytes = Cursor::new("1025"); + let mut form = multipart::Form::default(); + form.add_reader_file_with_mime("number", bytes, "", mime::APPLICATION_OCTET_STREAM); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::BAD_REQUEST); + + // Allow because correct content type + let bytes = Cursor::new("1025"); + let mut form = multipart::Form::default(); + form.add_reader_file_with_mime("number", bytes, "", mime::TEXT_PLAIN); + let response = send_form(&srv, form, "/").await; + assert_eq!(response.status(), StatusCode::OK); + } +} diff --git a/actix-multipart/src/lib.rs b/actix-multipart/src/lib.rs index 37d03db4..51b06db3 100644 --- a/actix-multipart/src/lib.rs +++ b/actix-multipart/src/lib.rs @@ -1,12 +1,63 @@ //! 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))] + +// This allows us to use the actix_multipart_derive within this crate's tests +#[cfg(test)] +extern crate self as actix_multipart; mod error; mod extractor; +pub mod form; mod server; +pub mod test; -pub use self::error::MultipartError; -pub use self::server::{Field, Multipart}; +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 9e0becd5..0256aa7b 100644 --- a/actix-multipart/src/server.rs +++ b/actix-multipart/src/server.rs @@ -2,9 +2,7 @@ use std::{ cell::{Cell, RefCell, RefMut}, - cmp, - convert::TryFrom, - fmt, + cmp, fmt, marker::PhantomData, pin::Pin, rc::Rc, @@ -163,8 +161,8 @@ impl InnerMultipart { for h in hdrs { let name = HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?; - let value = HeaderValue::try_from(h.value) - .map_err(|_| ParseError::Header)?; + let value = + HeaderValue::try_from(h.value).map_err(|_| ParseError::Header)?; headers.append(name, value); } @@ -224,8 +222,7 @@ impl InnerMultipart { if chunk.len() < boundary.len() { continue; } - if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() - { + if &chunk[..2] == b"--" && &chunk[2..chunk.len() - 2] == boundary.as_bytes() { break; } else { if chunk.len() < boundary.len() + 2 { @@ -255,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) @@ -270,7 +267,7 @@ impl InnerMultipart { match field.borrow_mut().poll(safety) { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(_))) => continue, - Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Poll::Ready(None) => true, } } @@ -289,8 +286,7 @@ impl InnerMultipart { match self.state { // read until first boundary InnerState::FirstBoundary => { - match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)? - { + match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)? { Some(eof) => { if eof { self.state = InnerState::Eof; @@ -469,7 +465,12 @@ impl Stream for Field { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { let this = self.get_mut(); let mut inner = this.inner.borrow_mut(); - if let Some(mut buffer) = inner.payload.as_ref().unwrap().get_mut(&this.safety) { + if let Some(mut buffer) = inner + .payload + .as_ref() + .expect("Field should not be polled after completion") + .get_mut(&this.safety) + { // check safety and poll read payload to buffer. buffer.poll_stream(cx)?; } else if !this.safety.is_clean() { @@ -500,6 +501,7 @@ impl fmt::Debug for Field { } struct InnerField { + /// Payload is initialized as Some and is `take`n when the field stream finishes. payload: Option, boundary: String, eof: bool, @@ -647,7 +649,12 @@ impl InnerField { return Poll::Ready(None); } - let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) { + let result = if let Some(mut payload) = self + .payload + .as_ref() + .expect("Field should not be polled after completion") + .get_mut(s) + { if !self.eof { let res = if let Some(ref mut len) = self.length { InnerField::read_len(&mut payload, len) @@ -658,7 +665,7 @@ impl InnerField { match res { Poll::Pending => return Poll::Pending, Poll::Ready(Some(Ok(bytes))) => return Poll::Ready(Some(Ok(bytes))), - Poll::Ready(Some(Err(e))) => return Poll::Ready(Some(Err(e))), + Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))), Poll::Ready(None) => self.eof = true, } } @@ -667,21 +674,21 @@ impl InnerField { Ok(None) => Poll::Pending, Ok(Some(line)) => { if line.as_ref() != b"\r\n" { - log::warn!( - "multipart field did not read all the data or it is malformed" - ); + log::warn!("multipart field did not read all the data or it is malformed"); } Poll::Ready(None) } - Err(e) => Poll::Ready(Some(Err(e))), + Err(err) => Poll::Ready(Some(Err(err))), } } else { Poll::Pending }; if let Poll::Ready(None) = result { - self.payload.take(); + // drop payload buffer and make future un-poll-able + let _ = self.payload.take(); } + result } } @@ -746,7 +753,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(), @@ -794,7 +801,7 @@ impl PayloadBuffer { loop { match Pin::new(&mut self.stream).poll_next(cx) { Poll::Ready(Some(Ok(data))) => self.buf.extend_from_slice(&data), - Poll::Ready(Some(Err(e))) => return Err(e), + Poll::Ready(Some(Err(err))) => return Err(err), Poll::Ready(None) => { self.eof = true; return Ok(()); @@ -860,19 +867,24 @@ impl PayloadBuffer { #[cfg(test)] mod tests { - use super::*; - - use actix_http::h1::Payload; - use actix_web::http::header::{DispositionParam, DispositionType}; - use actix_web::rt; - use actix_web::test::TestRequest; - use actix_web::FromRequest; - use bytes::Bytes; - use futures_util::{future::lazy, StreamExt as _}; use std::time::Duration; + + use actix_http::h1; + use actix_web::{ + http::header::{DispositionParam, DispositionType}, + rt, + test::TestRequest, + FromRequest, + }; + 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(); @@ -968,6 +980,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\ @@ -993,7 +1025,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(); @@ -1020,7 +1052,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(); @@ -1083,7 +1115,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); @@ -1119,7 +1151,7 @@ mod tests { #[actix_rt::test] async fn test_basic() { - let (_, payload) = Payload::create(false); + let (_, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(payload.buf.len(), 0); @@ -1129,7 +1161,7 @@ mod tests { #[actix_rt::test] async fn test_eof() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_max(4).unwrap()); @@ -1145,7 +1177,7 @@ mod tests { #[actix_rt::test] async fn test_err() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_max(1).unwrap()); sender.set_error(PayloadError::Incomplete(None)); @@ -1154,7 +1186,7 @@ mod tests { #[actix_rt::test] async fn test_readmax() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); sender.feed_data(Bytes::from("line1")); @@ -1171,7 +1203,7 @@ mod tests { #[actix_rt::test] async fn test_readexactly() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_exact(2)); @@ -1189,7 +1221,7 @@ mod tests { #[actix_rt::test] async fn test_readuntil() { - let (mut sender, payload) = Payload::create(false); + let (mut sender, payload) = h1::Payload::create(false); let mut payload = PayloadBuffer::new(payload); assert_eq!(None, payload.read_until(b"ne").unwrap()); @@ -1230,7 +1262,7 @@ mod tests { #[actix_rt::test] async fn test_multipart_payload_consumption() { // with sample payload and HttpRequest with no headers - let (_, inner_payload) = Payload::create(false); + let (_, inner_payload) = h1::Payload::create(false); let mut payload = actix_web::dev::Payload::from(inner_payload); let req = TestRequest::default().to_http_request(); @@ -1322,7 +1354,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 00000000..77d91828 --- /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 51e7cbc1..6305b45c 100644 --- a/actix-router/CHANGES.md +++ b/actix-router/CHANGES.md @@ -1,17 +1,27 @@ # Changes -## Unreleased - 2022-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 -## 0.5.1 - 2022-09-19 - 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 -## 0.5.0 - 2022-02-22 ### Added + - Add `Path::as_str`. [#2590] - Add `ResourceDef::set_name`. [#373][net#373] - Add `RouterBuilder::push`. [#2612] @@ -23,6 +33,7 @@ - Support multi-pattern prefixes and joins. [#2356] ### Changed + - Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] - Deprecate `Path::path`. [#2590] - Disallow prefix routes with tail segments. [#379][net#379] @@ -47,6 +58,7 @@ - Return type of `ResourceDef::pattern` is now `Option<&str>`. [#373][net#373] ### Fixed + - Fix `ResourceDef`'s `PartialEq` implementation. [#373][net#373] - Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] - Improve malformed path error message. [#384][net#384] @@ -55,6 +67,7 @@ - Static patterns in multi-patterns are no longer interpreted as regex. [#366][net#366] ### Removed + - `ResourceDef::name_mut`. [#373][net#373] - Unused `ResourceInfo`. [#2612] @@ -77,11 +90,11 @@ [net#380]: https://github.com/actix/actix-net/pull/380 [net#384]: https://github.com/actix/actix-net/pull/384 -
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] - Change signature of `ResourceDef::capture_match_info_fn` to remove `user_data` parameter. [#2612] @@ -92,33 +105,33 @@ [#2612]: https://github.com/actix/actix-web/pull/2612 [#2613]: https://github.com/actix/actix-web/pull/2613 +## 0.5.0-rc.2 -## 0.5.0-rc.2 - 2022-01-21 - Add `Path::as_str`. [#2590] - Deprecate `Path::path`. [#2590] [#2590]: https://github.com/actix/actix-web/pull/2590 +## 0.5.0-rc.1 -## 0.5.0-rc.1 - 2022-01-14 - `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 -## 0.5.0-beta.4 - 2022-01-04 - `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 -## 0.5.0-beta.3 - 2021-12-17 - Minimum supported Rust version (MSRV) is now 1.52. +## 0.5.0-beta.2 -## 0.5.0-beta.2 - 2021-09-09 - Introduce `ResourceDef::join`. [#380][net#380] - Disallow prefix routes with tail segments. [#379][net#379] - Enforce path separators on dynamic prefixes. [#378][net#378] @@ -137,8 +150,8 @@ [#2355]: https://github.com/actix/actix-web/pull/2355 [#2356]: https://github.com/actix/actix-web/pull/2356 +## 0.5.0-beta.1 -## 0.5.0-beta.1 - 2021-07-20 - 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] - Fix segment interpolation leaving `Path` in unintended state after matching. [#368][net#368] @@ -167,8 +180,8 @@
+## 0.4.0 -## 0.4.0 - 2021-06-06 - 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] - Fixed a safety bug where `Path` could return a malformed string after percent decoding. [#359][net#359] @@ -179,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 -## 0.3.0 - 2019-12-31 - Version was yanked previously. See https://crates.io/crates/actix-router/0.3.0 +## 0.2.7 -## 0.2.7 - 2021-02-06 - Add `Router::recognize_checked` [#247][net#247] [net#247]: https://github.com/actix/actix-net/pull/247 +## 0.2.6 -## 0.2.6 - 2021-01-09 - 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 -## 0.2.5 - 2020-09-20 - Fix `from_hex()` method +## 0.2.4 -## 0.2.4 - 2019-12-31 - Add `ResourceDef::resource_path_named()` path generation method +## 0.2.3 -## 0.2.3 - 2019-12-25 - Add impl `IntoPattern` for `&String` +## 0.2.2 -## 0.2.2 - 2019-12-25 - Use `IntoPattern` for `RouterBuilder::path()` +## 0.2.1 -## 0.2.1 - 2019-12-25 - Add `IntoPattern` trait - Add multi-pattern resources +## 0.2.0 -## 0.2.0 - 2019-12-07 - Update http to 0.2 - Update regex to 1.3 - Use bytestring instead of string +## 0.1.5 -## 0.1.5 - 2019-05-15 - Remove debug prints +## 0.1.4 -## 0.1.4 - 2019-05-15 - Fix checked resource match +## 0.1.3 -## 0.1.3 - 2019-04-22 -- Added support for `remainder match` (i.e "/path/{tail}*") +- Added support for `remainder match` (i.e "/path/{tail}\*") +## 0.1.2 -## 0.1.2 - 2019-04-07 - Export `Quoter` type - Allow to reset `Path` instance +## 0.1.1 -## 0.1.1 - 2019-04-03 - Get dynamic segment by name instead of iterator. +## 0.1.0 -## 0.1.0 - 2019-03-09 - Initial release diff --git a/actix-router/Cargo.toml b/actix-router/Cargo.toml index f3a5f15e..7e7e3beb 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,33 +8,40 @@ 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 = "2018" +edition = "2021" -[lib] -name = "actix_router" -path = "src/lib.rs" +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "http::*", + "serde::*", +] [features] -default = ["http"] +default = ["http", "unicode"] +http = ["dep:http"] +unicode = ["dep:regex"] [dependencies] bytestring = ">=0.1.5, <2" -http = { version = "0.2.5", optional = true } -regex = "1.5" +cfg-if = "1" +http = { version = "0.2.7", optional = true } +regex = { version = "1.5", optional = true } +regex-lite = "0.1" serde = "1" tracing = { version = "0.1.30", default-features = false, features = ["log"] } [dev-dependencies] -criterion = { version = "0.4", features = ["html_reports"] } -http = "0.2.5" +criterion = { version = "0.5", features = ["html_reports"] } +http = "0.2.7" serde = { version = "1", features = ["derive"] } 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 00000000..12d1b014 --- /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 9ca06da3..2428a767 100644 --- a/actix-router/benches/quoter.rs +++ b/actix-router/benches/quoter.rs @@ -1,16 +1,15 @@ -#![allow(clippy::uninlined_format_args)] +use std::{borrow::Cow, fmt::Write as _}; use criterion::{black_box, criterion_group, criterion_main, Criterion}; -use std::borrow::Cow; - 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 458e0893..ce2dcf8f 100644 --- a/actix-router/src/de.rs +++ b/actix-router/src/de.rs @@ -1,10 +1,14 @@ use std::borrow::Cow; -use serde::de::{self, Deserializer, Error as DeError, Visitor}; -use serde::forward_to_deserialize_any; +use serde::{ + de::{self, Deserializer, Error as DeError, Visitor}, + forward_to_deserialize_any, +}; -use crate::path::{Path, PathIter}; -use crate::{Quoter, ResourcePath}; +use crate::{ + path::{Path, PathIter}, + Quoter, ResourcePath, +}; thread_local! { static FULL_QUOTER: Quoter = Quoter::new(b"", b""); @@ -486,11 +490,7 @@ impl<'de> de::VariantAccess<'de> for UnitVariant { Err(de::value::Error::custom("not supported")) } - fn struct_variant( - self, - _: &'static [&'static str], - _: V, - ) -> Result + fn struct_variant(self, _: &'static [&'static str], _: V) -> Result where V: Visitor<'de>, { @@ -500,12 +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; - use crate::router::Router; - use crate::ResourceDef; + use crate::{router::Router, ResourceDef}; #[derive(Deserialize)] struct MyStruct { @@ -572,13 +570,11 @@ mod tests { assert_eq!(s.key, "name"); assert_eq!(s.value, 32); - let s: (String, u8) = - de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + let s: (String, u8) = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); assert_eq!(s.0, "name"); assert_eq!(s.1, 32); - let res: Vec = - de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); + let res: Vec = de::Deserialize::deserialize(PathDeserializer::new(&path)).unwrap(); assert_eq!(res[0], "name".to_owned()); assert_eq!(res[1], "32".to_owned()); } diff --git a/actix-router/src/lib.rs b/actix-router/src/lib.rs index 418dd432..c4d0d2c8 100644 --- a/actix-router/src/lib.rs +++ b/actix-router/src/lib.rs @@ -2,14 +2,15 @@ #![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))] mod de; mod path; mod pattern; mod quoter; +mod regex_set; mod resource; mod resource_path; mod router; @@ -17,13 +18,14 @@ mod router; #[cfg(feature = "http")] mod url; -pub use self::de::PathDeserializer; -pub use self::path::Path; -pub use self::pattern::{IntoPatterns, Patterns}; -pub use self::quoter::Quoter; -pub use self::resource::ResourceDef; -pub use self::resource_path::{Resource, ResourcePath}; -pub use self::router::{ResourceId, Router, RouterBuilder}; - #[cfg(feature = "http")] pub use self::url::Url; +pub use self::{ + de::PathDeserializer, + path::Path, + pattern::{IntoPatterns, Patterns}, + quoter::Quoter, + resource::ResourceDef, + resource_path::{Resource, ResourcePath}, + router::{ResourceId, Router, RouterBuilder}, +}; diff --git a/actix-router/src/path.rs b/actix-router/src/path.rs index 34dabcfb..9031ab76 100644 --- a/actix-router/src/path.rs +++ b/actix-router/src/path.rs @@ -1,7 +1,9 @@ -use std::borrow::Cow; -use std::ops::{DerefMut, Index}; +use std::{ + borrow::Cow, + ops::{DerefMut, Index}, +}; -use serde::de; +use serde::{de, Deserialize}; use crate::{de::PathDeserializer, Resource, ResourcePath}; @@ -22,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)>, } @@ -81,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(); } @@ -101,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), @@ -147,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. @@ -166,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 00000000..48f38df2 --- /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 f198115a..3a102945 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. @@ -252,7 +255,7 @@ impl ResourceDef { /// Multi-pattern resources can be constructed by providing a slice (or vec) of patterns. /// /// # Panics - /// Panics if path pattern is malformed. + /// Panics if any path patterns are malformed. /// /// # Examples /// ``` @@ -501,7 +504,12 @@ impl ResourceDef { let patterns = self .pattern_iter() .flat_map(move |this| other.pattern_iter().map(move |other| (this, other))) - .map(|(this, other)| [this, other].join("")) + .map(|(this, other)| { + let mut pattern = String::with_capacity(this.len() + other.len()); + pattern.push_str(this); + pattern.push_str(other); + pattern + }) .collect::>(); match patterns.len() { @@ -598,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()) } @@ -701,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, }; @@ -838,6 +846,7 @@ impl ResourceDef { fn construct(paths: T, is_prefix: bool) -> Self { let patterns = paths.patterns(); + let (pat_type, segments) = match &patterns { Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false), @@ -864,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(); ( @@ -1389,8 +1398,6 @@ mod tests { #[cfg(feature = "http")] #[test] fn parse_urlencoded_param() { - use std::convert::TryFrom; - let re = ResourceDef::new("/user/{id}/test"); let mut path = Path::new("/user/2345/test"); @@ -1530,7 +1537,12 @@ mod tests { assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter())); let mut s = String::new(); - assert!(resource.resource_path_from_iter(&mut s, &mut vec!["item", "item2"].iter())); + + assert!(resource.resource_path_from_iter( + &mut s, + #[allow(clippy::useless_vec)] + &mut vec!["item", "item2"].iter() + )); assert_eq!(s, "/user/item/item2/"); } @@ -1743,9 +1755,7 @@ mod tests { ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}"); // panics - ResourceDef::new( - "/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}", - ); + ResourceDef::new("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}"); } #[test] diff --git a/actix-router/src/router.rs b/actix-router/src/router.rs index 064c5e90..1dd4449d 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)) @@ -117,11 +118,7 @@ where U: Default, { /// Registers resource for specified path. - pub fn path( - &mut self, - path: impl IntoPatterns, - val: T, - ) -> (&mut ResourceDef, &mut T, &mut U) { + pub fn path(&mut self, path: impl IntoPatterns, val: T) -> (&mut ResourceDef, &mut T, &mut U) { self.push(ResourceDef::new(path), val, U::default()) } @@ -142,8 +139,10 @@ where #[cfg(test)] mod tests { - use crate::path::Path; - use crate::router::{ResourceId, Router}; + use crate::{ + path::Path, + router::{ResourceId, Router}, + }; #[allow(clippy::cognitive_complexity)] #[test] @@ -188,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 8ac03386..b3d9e112 100644 --- a/actix-router/src/url.rs +++ b/actix-router/src/url.rs @@ -1,6 +1,4 @@ -use crate::ResourcePath; - -use crate::Quoter; +use crate::{Quoter, ResourcePath}; thread_local! { static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+"); @@ -64,8 +62,9 @@ impl ResourcePath for Url { #[cfg(test)] mod tests { + use std::fmt::Write as _; + use http::Uri; - use std::convert::TryFrom; use super::*; use crate::{Path, ResourceDef}; @@ -81,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 c8fe5420..ec2dd677 100644 --- a/actix-test/CHANGES.md +++ b/actix-test/CHANGES.md @@ -1,72 +1,98 @@ # Changes -## Unreleased - 2022-xx-xx +## Unreleased + +## 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 + +- Add `TestServerConfig::port()` setter method. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. +## 0.1.0 -## 0.1.0 - 2022-07-24 - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. +## 0.1.0-beta.13 -## 0.1.0-beta.13 - 2022-02-16 - No significant changes since `0.1.0-beta.12`. +## 0.1.0-beta.12 -## 0.1.0-beta.12 - 2022-01-31 - Rename `TestServerConfig::{client_timeout => client_request_timeout}`. [#2611] [#2611]: https://github.com/actix/actix-web/pull/2611 +## 0.1.0-beta.11 -## 0.1.0-beta.11 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. +## 0.1.0-beta.10 -## 0.1.0-beta.10 - 2021-12-27 - No significant changes since `0.1.0-beta.9`. +## 0.1.0-beta.9 -## 0.1.0-beta.9 - 2021-12-17 - 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 -## 0.1.0-beta.8 - 2021-12-11 - No significant changes since `0.1.0-beta.7`. +## 0.1.0-beta.7 -## 0.1.0-beta.7 - 2021-11-22 - Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] [#2408]: https://github.com/actix/actix-web/pull/2408 +## 0.1.0-beta.6 -## 0.1.0-beta.6 - 2021-11-15 - No significant changes from `0.1.0-beta.5`. +## 0.1.0-beta.5 -## 0.1.0-beta.5 - 2021-10-20 - Updated rustls to v0.20. [#2414] - Minimum supported Rust version (MSRV) is now 1.52. [#2414]: https://github.com/actix/actix-web/pull/2414 +## 0.1.0-beta.4 -## 0.1.0-beta.4 - 2021-09-09 - Minimum supported Rust version (MSRV) is now 1.51. +## 0.1.0-beta.3 -## 0.1.0-beta.3 - 2021-06-20 - No significant changes from `0.1.0-beta.2`. +## 0.1.0-beta.2 -## 0.1.0-beta.2 - 2021-04-17 - No significant changes from `0.1.0-beta.1`. +## 0.1.0-beta.1 -## 0.1.0-beta.1 - 2021-04-02 - Move integration testing structs from `actix-web`. [#2112] [#2112]: https://github.com/actix/actix-web/pull/2112 diff --git a/actix-test/Cargo.toml b/actix-test/Cargo.toml index d489d4a0..e810ae80 100644 --- a/actix-test/Cargo.toml +++ b/actix-test/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-test" -version = "0.1.0" +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", @@ -16,26 +16,50 @@ categories = [ "web-programming::websocket", ] license = "MIT OR Apache-2.0" -edition = "2018" +edition = "2021" + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_codec::*", + "actix_http_test::*", + "actix_http::*", + "actix_service::*", + "actix_web::*", + "awc::*", + "bytes::*", + "futures_core::*", + "http::*", + "openssl::*", + "rustls::*", + "tokio::*", +] [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 = [] } @@ -43,6 +67,9 @@ log = "0.4" serde = { version = "1", features = ["derive"] } serde_json = "1" serde_urlencoded = "0.7" -tls-openssl = { package = "openssl", version = "0.10.9", optional = true } -tls-rustls = { package = "rustls", version = "0.20.0", optional = true } -tokio = { version = "1.18.4", features = ["sync"] } +tls-openssl = { package = "openssl", version = "0.10.55", 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/README.md b/actix-test/README.md new file mode 100644 index 00000000..1a9b6f22 --- /dev/null +++ b/actix-test/README.md @@ -0,0 +1,45 @@ +# `actix-test` + + + +[![crates.io](https://img.shields.io/crates/v/actix-test?label=latest)](https://crates.io/crates/actix-test) +[![Documentation](https://docs.rs/actix-test/badge.svg?version=0.1.5)](https://docs.rs/actix-test/0.1.5) +![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) +![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-test.svg) +
+[![dependency status](https://deps.rs/crate/actix-test/0.1.5/status.svg)](https://deps.rs/crate/actix-test/0.1.5) +[![Download](https://img.shields.io/crates/d/actix-test.svg)](https://crates.io/crates/actix-test) +[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) + + + + + +Integration testing tools for Actix Web applications. + +The main integration testing tool is [`TestServer`]. It spawns a real HTTP server on an unused port and provides methods that use a real HTTP client. Therefore, it is much closer to real-world cases than using `init_service`, which skips HTTP encoding and decoding. + +## Examples + +```rust +use actix_web::{get, web, test, App, HttpResponse, Error, Responder}; + +#[get("/")] +async fn my_handler() -> Result { + Ok(HttpResponse::Ok()) +} + +#[actix_rt::test] +async fn test_example() { + let srv = actix_test::start(|| + App::new().service(my_handler) + ); + + let req = srv.get("/"); + let res = req.send().await.unwrap(); + + assert!(res.status().is_success()); +} +``` + + diff --git a/actix-test/src/lib.rs b/actix-test/src/lib.rs index 1aff2dc8..9be99978 100644 --- a/actix-test/src/lib.rs +++ b/actix-test/src/lib.rs @@ -5,6 +5,7 @@ //! real-world cases than using `init_service`, which skips HTTP encoding and decoding. //! //! # Examples +//! //! ``` //! use actix_web::{get, web, test, App, HttpResponse, Error, Responder}; //! @@ -28,11 +29,12 @@ #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] +#![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))] #[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}; @@ -42,8 +44,8 @@ use actix_http::{header::HeaderMap, ws, HttpService, Method, Request, Response}; pub use actix_http_test::unused_addr; use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _}; pub use actix_web::test::{ - call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service, - read_body, read_body_json, status_service, TestRequest, + call_and_read_body, call_and_read_body_json, call_service, init_service, ok_service, read_body, + read_body_json, status_service, TestRequest, }; use actix_web::{ body::MessageBody, @@ -51,7 +53,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; @@ -138,29 +140,37 @@ 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:0").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(); let timeout = cfg.client_request_timeout; - let builder = Server::build().workers(1).disable_signals().system_exit(); + let builder = Server::build() + .workers(cfg.workers) + .disable_signals() + .system_exit(); let srv = match srv_cfg.stream { StreamType::Tcp => match srv_cfg.tp { HttpVer::Http1 => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -172,11 +182,8 @@ where .tcp() }), HttpVer::Http2 => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -188,11 +195,8 @@ where .tcp() }), HttpVer::Both => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -207,11 +211,8 @@ where #[cfg(feature = "openssl")] StreamType::Openssl(acceptor) => match cfg.tp { HttpVer::Http1 => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -223,11 +224,8 @@ where .openssl(acceptor.clone()) }), HttpVer::Http2 => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -239,11 +237,8 @@ where .openssl(acceptor.clone()) }), HttpVer::Both => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -255,14 +250,11 @@ 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, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -274,11 +266,8 @@ where .rustls(config.clone()) }), HttpVer::Http2 => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -290,11 +279,8 @@ where .rustls(config.clone()) }), HttpVer::Both => builder.listen("test", tcp, move || { - let app_cfg = AppConfig::__priv_test_new( - false, - local_addr.to_string(), - local_addr, - ); + let app_cfg = + AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr); let fac = factory() .into_factory() @@ -306,6 +292,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"); @@ -337,7 +449,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)) @@ -351,7 +463,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 { @@ -371,13 +489,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. @@ -390,6 +515,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 { @@ -399,46 +528,120 @@ 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. + pub fn port(mut self, port: u16) -> Self { + self.port = port; + self + } + + /// Sets number of workers for the test server. + /// + /// By default, the server uses 1 worker + pub fn workers(mut self, workers: usize) -> Self { + 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 @@ -465,9 +668,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 33d4712f..3e854c0b 100644 --- a/actix-web-actors/CHANGES.md +++ b/actix-web-actors/CHANGES.md @@ -1,70 +1,80 @@ # Changes -## Unreleased - 2022-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 + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. +## 4.1.0 -## 4.1.0 - 2022-03-02 - Add support for `actix` version `0.13`. [#2675] [#2675]: https://github.com/actix/actix-web/pull/2675 +## 4.0.0 -## 4.0.0 - 2022-02-25 - No significant changes since `4.0.0-beta.12`. +## 4.0.0-beta.12 -## 4.0.0-beta.12 - 2022-02-16 - No significant changes since `4.0.0-beta.11`. +## 4.0.0-beta.11 -## 4.0.0-beta.11 - 2022-01-31 - No significant changes since `4.0.0-beta.10`. +## 4.0.0-beta.10 -## 4.0.0-beta.10 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. +## 4.0.0-beta.9 -## 4.0.0-beta.9 - 2021-12-27 - No significant changes since `4.0.0-beta.8`. +## 4.0.0-beta.8 -## 4.0.0-beta.8 - 2021-12-11 - Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920] - Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920] - Minimum supported Rust version (MSRV) is now 1.52. [#1920]: https://github.com/actix/actix-web/pull/1920 +## 4.0.0-beta.7 -## 4.0.0-beta.7 - 2021-09-09 - Minimum supported Rust version (MSRV) is now 1.51. +## 4.0.0-beta.6 -## 4.0.0-beta.6 - 2021-06-26 - Update `actix` to `0.12`. [#2277] [#2277]: https://github.com/actix/actix-web/pull/2277 +## 4.0.0-beta.5 -## 4.0.0-beta.5 - 2021-06-17 - No notable changes. +## 4.0.0-beta.4 -## 4.0.0-beta.4 - 2021-04-02 - No notable changes. +## 4.0.0-beta.3 -## 4.0.0-beta.3 - 2021-03-09 - No notable changes. +## 4.0.0-beta.2 -## 4.0.0-beta.2 - 2021-02-10 - No notable changes. +## 4.0.0-beta.1 -## 4.0.0-beta.1 - 2021-01-07 - Update `pin-project` to `1.0`. - Update `bytes` to `1.0`. [#1813] - `WebsocketContext::text` now takes an `Into`. [#1864] @@ -72,64 +82,63 @@ [#1813]: https://github.com/actix/actix-web/pull/1813 [#1864]: https://github.com/actix/actix-web/pull/1864 +## 3.0.0 -## 3.0.0 - 2020-09-11 - No significant changes from `3.0.0-beta.2`. +## 3.0.0-beta.2 -## 3.0.0-beta.2 - 2020-09-10 - Update `actix-*` dependencies to latest versions. +## 3.0.0-beta.1 -## [3.0.0-beta.1] - 2020-xx-xx - Update `actix-web` & `actix-http` dependencies to beta.1 - Bump minimum supported Rust version to 1.40 +## 3.0.0-alpha.1 -## [3.0.0-alpha.1] - 2020-05-08 - 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 `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 2158f59e..3c74a4f4 100644 --- a/actix-web-actors/Cargo.toml +++ b/actix-web-actors/Cargo.toml @@ -1,17 +1,23 @@ [package] name = "actix-web-actors" -version = "4.1.0" +version = "4.3.0" authors = ["Nikolay Kim "] description = "Actix actors support for Actix Web" keywords = ["actix", "http", "web", "framework", "async"] homepage = "https://actix.rs" repository = "https://github.com/actix/actix-web" license = "MIT OR Apache-2.0" -edition = "2018" +edition = "2021" -[lib] -name = "actix_web_actors" -path = "src/lib.rs" +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix::*", + "actix_http::*", + "actix_web::*", + "bytes::*", + "bytestring::*", + "futures_core::*", +] [dependencies] actix = { version = ">=0.12, <0.14", default-features = false } @@ -23,7 +29,7 @@ bytes = "1" bytestring = "1" futures-core = { version = "0.3.17", default-features = false } pin-project-lite = "0.2" -tokio = { version = "1.18.4", features = ["sync"] } +tokio = { version = "1.24.2", features = ["sync"] } tokio-util = { version = "0.7", features = ["codec"] } [dev-dependencies] @@ -32,7 +38,6 @@ actix-test = "0.1" awc = { version = "3", default-features = false } actix-web = { version = "4", features = ["macros"] } +env_logger = "0.11" +futures-util = { version = "0.3.17", default-features = false, features = ["std"] } mime = "0.3" - -env_logger = "0.9" -futures-util = { version = "0.3.17", default-features = false } diff --git a/actix-web-actors/README.md b/actix-web-actors/README.md index a0578994..feb3d1b3 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.1.0)](https://docs.rs/actix-web-actors/4.1.0) -![Version](https://img.shields.io/badge/rustc-1.59+-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.1.0/status.svg)](https://deps.rs/crate/actix-web-actors/4.1.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.54 + diff --git a/actix-web-actors/src/context.rs b/actix-web-actors/src/context.rs index f7b11c78..23e33645 100644 --- a/actix-web-actors/src/context.rs +++ b/actix-web-actors/src/context.rs @@ -1,11 +1,13 @@ -use std::collections::VecDeque; -use std::future::Future; -use std::pin::Pin; -use std::task::{Context, Poll}; +use std::{ + collections::VecDeque, + future::Future, + pin::Pin, + task::{Context, Poll}, +}; -use actix::dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope}; -use actix::fut::ActorFuture; use actix::{ + dev::{AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, ToEnvelope}, + fut::ActorFuture, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message, SpawnHandle, }; use actix_web::error::Error; @@ -246,11 +248,11 @@ where mod tests { use std::time::Duration; - use actix::Actor; - use actix_web::http::StatusCode; - use actix_web::test::{call_service, init_service, read_body, TestRequest}; - use actix_web::{web, App, HttpResponse}; - use bytes::Bytes; + use actix_web::{ + http::StatusCode, + test::{call_service, init_service, read_body, TestRequest}, + web, App, HttpResponse, + }; use super::*; diff --git a/actix-web-actors/src/lib.rs b/actix-web-actors/src/lib.rs index 7a34048d..d89b0ee3 100644 --- a/actix-web-actors/src/lib.rs +++ b/actix-web-actors/src/lib.rs @@ -57,7 +57,9 @@ #![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))] mod context; pub mod ws; diff --git a/actix-web-actors/src/ws.rs b/actix-web-actors/src/ws.rs index e1110edd..7f7607fa 100644 --- a/actix-web-actors/src/ws.rs +++ b/actix-web-actors/src/ws.rs @@ -58,7 +58,6 @@ use std::{ collections::VecDeque, - convert::TryFrom, future::Future, io, mem, pin::Pin, @@ -67,17 +66,14 @@ use std::{ use actix::{ dev::{ - AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, - ToEnvelope, + AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope, }, fut::ActorFuture, Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage, SpawnHandle, }; use actix_http::ws::{hash_key, Codec}; -pub use actix_http::ws::{ - CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError, -}; +pub use actix_http::ws::{CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError}; use actix_web::{ error::{Error, PayloadError}, http::{ @@ -427,16 +423,16 @@ pub fn handshake_with_protocols( }; // check requested protocols - let protocol = - req.headers() - .get(&header::SEC_WEBSOCKET_PROTOCOL) - .and_then(|req_protocols| { - let req_protocols = req_protocols.to_str().ok()?; - req_protocols - .split(',') - .map(|req_p| req_p.trim()) - .find(|req_p| protocols.iter().any(|p| p == req_p)) - }); + let protocol = req + .headers() + .get(&header::SEC_WEBSOCKET_PROTOCOL) + .and_then(|req_protocols| { + let req_protocols = req_protocols.to_str().ok()?; + req_protocols + .split(',') + .map(|req_p| req_p.trim()) + .find(|req_p| protocols.iter().any(|p| p == req_p)) + }); let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) .upgrade("websocket") @@ -714,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 { @@ -779,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}"), ))))); } } @@ -821,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 cb37bfdb..d143723f 100644 --- a/actix-web-codegen/CHANGES.md +++ b/actix-web-codegen/CHANGES.md @@ -1,44 +1,66 @@ # Changes -## Unreleased - 2022-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 + +- Add support for custom methods with the `#[route]` macro. [#2969] + +[#2969]: https://github.com/actix/actix-web/pull/2969 + +## 4.1.0 -## 4.1.0 - 2022-09-11 - 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 -## 4.0.1 - 2022-06-11 - 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 -## 4.0.0 - 2022-02-24 - Version aligned with `actix-web` and will remain in sync going forward. - No significant changes since `0.5.0`. +## 0.5.0 -## 0.5.0 - 2022-02-24 - No significant changes since `0.5.0-rc.2`. +## 0.5.0-rc.2 -## 0.5.0-rc.2 - 2022-02-01 - No significant changes since `0.5.0-rc.1`. +## 0.5.0-rc.1 -## 0.5.0-rc.1 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. +## 0.5.0-beta.6 -## 0.5.0-beta.6 - 2021-12-11 - No significant changes since `0.5.0-beta.5`. +## 0.5.0-beta.5 -## 0.5.0-beta.5 - 2021-10-20 - Improve error recovery potential when macro input is invalid. [#2410] - Add `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] - Minimum supported Rust version (MSRV) is now 1.52. @@ -46,90 +68,90 @@ [#2410]: https://github.com/actix/actix-web/pull/2410 [#2409]: https://github.com/actix/actix-web/pull/2409 +## 0.5.0-beta.4 -## 0.5.0-beta.4 - 2021-09-09 - In routing macros, paths are now validated at compile time. [#2350] - Minimum supported Rust version (MSRV) is now 1.51. [#2350]: https://github.com/actix/actix-web/pull/2350 +## 0.5.0-beta.3 -## 0.5.0-beta.3 - 2021-06-17 - No notable changes. +## 0.5.0-beta.2 -## 0.5.0-beta.2 - 2021-03-09 - Preserve doc comments when using route macros. [#2022] - Add `name` attribute to `route` macro. [#1934] [#2022]: https://github.com/actix/actix-web/pull/2022 [#1934]: https://github.com/actix/actix-web/pull/1934 +## 0.5.0-beta.1 -## 0.5.0-beta.1 - 2021-02-10 - Use new call signature for `System::new`. +## 0.4.0 -## 0.4.0 - 2020-09-20 - Added compile success and failure testing. [#1677] - Add `route` macro for supporting multiple HTTP methods guards. [#1674] [#1677]: https://github.com/actix/actix-web/pull/1677 [#1674]: https://github.com/actix/actix-web/pull/1674 +## 0.3.0 -## 0.3.0 - 2020-09-11 - No significant changes from `0.3.0-beta.1`. +## 0.3.0-beta.1 -## 0.3.0-beta.1 - 2020-07-14 - Add main entry-point macro that uses re-exported runtime. [#1559] [#1559]: https://github.com/actix/actix-web/pull/1559 +## 0.2.2 -## 0.2.2 - 2020-05-23 - Add resource middleware on actix-web-codegen [#1467] [#1467]: https://github.com/actix/actix-web/pull/1467 +## 0.2.1 -## 0.2.1 - 2020-02-25 - Add `#[allow(missing_docs)]` attribute to generated structs [#1368] - Allow the handler function to be named as `config` [#1290] [#1368]: https://github.com/actix/actix-web/issues/1368 [#1290]: https://github.com/actix/actix-web/issues/1290 +## 0.2.0 -## 0.2.0 - 2019-12-13 - Generate code for actix-web 2.0 +## 0.1.3 -## 0.1.3 - 2019-10-14 - Bump up `syn` & `quote` to 1.0 - Provide better error message +## 0.1.2 -## 0.1.2 - 2019-06-04 - Add macros for head, options, trace, connect and patch http methods +## 0.1.1 -## 0.1.1 - 2019-06-01 - Add syn "extra-traits" feature +## 0.1.0 -## 0.1.0 - 2019-05-18 - Release +## 0.1.0-beta.1 -## 0.1.0-beta.1 - 2019-04-20 - Gen code for actix-web 1.0.0-beta.1 +## 0.1.0-alpha.6 -## 0.1.0-alpha.6 - 2019-04-14 - Gen code for actix-web 1.0.0-alpha.6 +## 0.1.0-alpha.1 -## 0.1.0-alpha.1 - 2019-03-28 - Initial impl diff --git a/actix-web-codegen/Cargo.toml b/actix-web-codegen/Cargo.toml index da557744..7500807d 100644 --- a/actix-web-codegen/Cargo.toml +++ b/actix-web-codegen/Cargo.toml @@ -1,27 +1,32 @@ [package] name = "actix-web-codegen" -version = "4.1.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 = "2018" +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 = "1", features = ["full", "extra-traits"] } +syn = { version = "2", features = ["full", "extra-traits"] } [dev-dependencies] -actix-macros = "0.2.3" +actix-macros = "0.2.4" actix-rt = "2.2" actix-test = "0.1" actix-utils = "3" diff --git a/actix-web-codegen/README.md b/actix-web-codegen/README.md index 821236e4..e61bf5c7 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.1.0)](https://docs.rs/actix-web-codegen/4.1.0) -![Version](https://img.shields.io/badge/rustc-1.59+-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.1.0/status.svg)](https://deps.rs/crate/actix-web-codegen/4.1.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.54 + ## Compile Testing diff --git a/actix-web-codegen/src/lib.rs b/actix-web-codegen/src/lib.rs index 4b6dc43c..c518007a 100644 --- a/actix-web-codegen/src/lib.rs +++ b/actix-web-codegen/src/lib.rs @@ -75,11 +75,15 @@ #![recursion_limit = "512"] #![deny(rust_2018_idioms, nonstandard_style)] #![warn(future_incompatible)] +#![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))] use proc_macro::TokenStream; use quote::quote; mod route; +mod scope; /// Creates resource handler, allowing multiple HTTP method guards. /// @@ -105,7 +109,7 @@ mod route; /// ``` /// # use actix_web::HttpResponse; /// # use actix_web_codegen::route; -/// #[route("/test", method = "GET", method = "HEAD")] +/// #[route("/test", method = "GET", method = "HEAD", method = "CUSTOM")] /// async fn example() -> HttpResponse { /// HttpResponse::Ok().finish() /// } @@ -150,37 +154,37 @@ pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream { macro_rules! method_macro { ($variant:ident, $method:ident) => { -#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] -/// -/// # Syntax -/// ```plain -#[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)] -/// ``` -/// -/// # Attributes -/// - `"path"`: Raw literal string with path for which to register handler. -/// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the function -/// name of handler is used. -/// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`. -/// - `wrap = "Middleware"`: Registers a resource middleware. -/// -/// # Notes -/// Function name can be specified as any expression that is going to be accessible to the -/// generate code, e.g `my_guard` or `my_module::my_guard`. -/// -/// # Examples -/// ``` -/// # use actix_web::HttpResponse; -#[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")] -#[doc = concat!("#[", stringify!($method), r#"("/")]"#)] -/// async fn example() -> HttpResponse { -/// HttpResponse::Ok().finish() -/// } -/// ``` -#[proc_macro_attribute] -pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream { - route::with_method(Some(route::MethodType::$variant), args, input) -} + #[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")] + /// + /// # Syntax + /// ```plain + #[doc = concat!("#[", stringify!($method), r#"("path"[, attributes])]"#)] + /// ``` + /// + /// # Attributes + /// - `"path"`: Raw literal string with path for which to register handler. + /// - `name = "resource_name"`: Specifies resource name for the handler. If not set, the + /// function name of handler is used. + /// - `guard = "function_name"`: Registers function as guard using `actix_web::guard::fn_guard`. + /// - `wrap = "Middleware"`: Registers a resource middleware. + /// + /// # Notes + /// Function name can be specified as any expression that is going to be accessible to the + /// generate code, e.g `my_guard` or `my_module::my_guard`. + /// + /// # Examples + /// ``` + /// # use actix_web::HttpResponse; + #[doc = concat!("# use actix_web_codegen::", stringify!($method), ";")] + #[doc = concat!("#[", stringify!($method), r#"("/")]"#)] + /// async fn example() -> HttpResponse { + /// HttpResponse::Ok().finish() + /// } + /// ``` + #[proc_macro_attribute] + pub fn $method(args: TokenStream, input: TokenStream) -> TokenStream { + route::with_method(Some(route::MethodType::$variant), args, input) + } }; } @@ -194,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 @@ -218,7 +259,7 @@ pub fn main(_: TokenStream, item: TokenStream) -> TokenStream { output } -/// Marks async test functions to use the actix system entry-point. +/// Marks async test functions to use the Actix Web system entry-point. /// /// # Examples /// ``` @@ -237,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 e5493702..e24903e3 100644 --- a/actix-web-codegen/src/route.rs +++ b/actix-web-codegen/src/route.rs @@ -1,16 +1,65 @@ -use std::{collections::HashSet, convert::TryFrom}; +use std::collections::HashSet; use actix_router::ResourceDef; use proc_macro::TokenStream; use proc_macro2::{Span, TokenStream as TokenStream2}; use quote::{quote, ToTokens, TokenStreamExt}; -use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path}; +use syn::{punctuated::Punctuated, Ident, LitStr, Path, Token}; -macro_rules! method_type { +use crate::input_and_compile_error; + +#[derive(Debug)] +pub struct RouteArgs { + pub(crate) path: syn::LitStr, + pub(crate) options: Punctuated, +} + +impl syn::parse::Parse for RouteArgs { + fn parse(input: syn::parse::ParseStream<'_>) -> syn::Result { + // path to match: "/foo" + let path = input.parse::().map_err(|mut err| { + err.combine(syn::Error::new( + err.span(), + r#"invalid service definition, expected #[("")]"#, + )); + + err + })?; + + // verify that path pattern is valid + let _ = ResourceDef::new(path.value()); + + // if there's no comma, assume that no options are provided + if !input.peek(Token![,]) { + return Ok(Self { + path, + options: Punctuated::new(), + }); + } + + // advance past comma separator + input.parse::()?; + + // if next char is a literal, assume that it is a string and show multi-path error + if input.cursor().literal().is_some() { + return Err(syn::Error::new( + Span::call_site(), + r#"Multiple paths specified! There should be only one."#, + )); + } + + // zero or more options: name = "foo" + let options = input.parse_terminated(syn::MetaNameValue::parse, Token![,])?; + + Ok(Self { path, options }) + } +} + +macro_rules! standard_method_type { ( $($variant:ident, $upper:ident, $lower:ident,)+ ) => { - #[derive(Debug, PartialEq, Eq, Hash)] + #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub enum MethodType { $( $variant, @@ -27,11 +76,11 @@ macro_rules! method_type { fn parse(method: &str) -> Result { match method { $(stringify!($upper) => Ok(Self::$variant),)+ - _ => Err(format!("Unexpected HTTP method: `{}`", method)), + _ => Err(format!("HTTP method must be uppercase: `{}`", method)), } } - 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(()), @@ -41,7 +90,7 @@ macro_rules! method_type { }; } -method_type! { +standard_method_type! { Get, GET, get, Post, POST, post, Put, PUT, put, @@ -53,13 +102,6 @@ method_type! { Patch, PATCH, patch, } -impl ToTokens for MethodType { - fn to_tokens(&self, stream: &mut TokenStream2) { - let ident = Ident::new(self.as_str(), Span::call_site()); - stream.append(ident); - } -} - impl TryFrom<&syn::LitStr> for MethodType { type Error = syn::Error; @@ -69,118 +111,210 @@ impl TryFrom<&syn::LitStr> for MethodType { } } +impl ToTokens for MethodType { + fn to_tokens(&self, stream: &mut TokenStream2) { + let ident = Ident::new(self.as_str(), Span::call_site()); + stream.append(ident); + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +enum MethodTypeExt { + Standard(MethodType), + Custom(LitStr), +} + +impl MethodTypeExt { + /// Returns a single method guard token stream. + fn to_tokens_single_guard(&self) -> TokenStream2 { + match self { + MethodTypeExt::Standard(method) => { + quote! { + .guard(::actix_web::guard::#method()) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .guard(::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + )) + } + } + } + } + + /// Returns a multi-method guard chain token stream. + fn to_tokens_multi_guard(&self, or_chain: Vec) -> TokenStream2 { + debug_assert!( + !or_chain.is_empty(), + "empty or_chain passed to multi-guard constructor" + ); + + match self { + MethodTypeExt::Standard(method) => { + quote! { + .guard( + ::actix_web::guard::Any(::actix_web::guard::#method()) + #(#or_chain)* + ) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .guard( + ::actix_web::guard::Any( + ::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + ) + ) + #(#or_chain)* + ) + } + } + } + } + + /// Returns a token stream containing the `.or` chain to be passed in to + /// [`MethodTypeExt::to_tokens_multi_guard()`]. + fn to_tokens_multi_guard_or_chain(&self) -> TokenStream2 { + match self { + MethodTypeExt::Standard(method_type) => { + quote! { + .or(::actix_web::guard::#method_type()) + } + } + MethodTypeExt::Custom(lit) => { + quote! { + .or( + ::actix_web::guard::Method( + ::actix_web::http::Method::from_bytes(#lit.as_bytes()).unwrap() + ) + ) + } + } + } + } +} + +impl ToTokens for MethodTypeExt { + fn to_tokens(&self, stream: &mut TokenStream2) { + match self { + MethodTypeExt::Custom(lit_str) => { + let ident = Ident::new(lit_str.value().as_str(), Span::call_site()); + stream.append(ident); + } + MethodTypeExt::Standard(method) => method.to_tokens(stream), + } + } +} + +impl TryFrom<&syn::LitStr> for MethodTypeExt { + type Error = syn::Error; + + fn try_from(value: &syn::LitStr) -> Result { + match MethodType::try_from(value) { + Ok(method) => Ok(MethodTypeExt::Standard(method)), + Err(_) if value.value().chars().all(|c| c.is_ascii_uppercase()) => { + Ok(MethodTypeExt::Custom(value.clone())) + } + Err(err) => Err(err), + } + } +} + struct Args { path: syn::LitStr, resource_name: Option, guards: Vec, - wrappers: Vec, - methods: HashSet, + wrappers: Vec, + methods: HashSet, } impl Args { - fn new(args: AttributeArgs, method: Option) -> syn::Result { - let mut path = None; + fn new(args: RouteArgs, method: Option) -> syn::Result { let mut resource_name = None; let mut guards = Vec::new(); let mut wrappers = Vec::new(); let mut methods = HashSet::new(); - if args.is_empty() { - return Err(syn::Error::new( - Span::call_site(), - format!( - r#"invalid service definition, expected #[{}("")]"#, - method - .map_or("route", |it| it.as_str()) - .to_ascii_lowercase() - ), - )); - } - let is_route_macro = method.is_none(); if let Some(method) = method { - methods.insert(method); + methods.insert(MethodTypeExt::Standard(method)); } - for arg in args { - match arg { - NestedMeta::Lit(syn::Lit::Str(lit)) => match path { - None => { - let _ = ResourceDef::new(lit.value()); - path = Some(lit); - } - _ => { + for nv in args.options { + if nv.path.is_ident("name") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + resource_name = Some(lit); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute name expects literal string", + )); + } + } else if nv.path.is_ident("guard") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + guards.push(lit.parse::()?); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute guard expects literal string", + )); + } + } else if nv.path.is_ident("wrap") { + if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value + { + wrappers.push(lit.parse()?); + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute wrap expects type", + )); + } + } 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", + )); + } else if let syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Str(lit), + .. + }) = nv.value.clone() + { + if !methods.insert(MethodTypeExt::try_from(&lit)?) { return Err(syn::Error::new_spanned( - lit, - "Multiple paths specified! Should be only one!", - )); - } - }, - NestedMeta::Meta(syn::Meta::NameValue(nv)) => { - if nv.path.is_ident("name") { - if let syn::Lit::Str(lit) = nv.lit { - resource_name = Some(lit); - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute name expects literal string!", - )); - } - } else if nv.path.is_ident("guard") { - if let syn::Lit::Str(lit) = nv.lit { - guards.push(lit.parse::()?); - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute guard expects literal string!", - )); - } - } else if nv.path.is_ident("wrap") { - if let syn::Lit::Str(lit) = nv.lit { - wrappers.push(lit.parse()?); - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute wrap expects type", - )); - } - } 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", - )); - } else if let syn::Lit::Str(ref lit) = nv.lit { - let method = MethodType::try_from(lit)?; - if !methods.insert(method) { - return Err(syn::Error::new_spanned( - &nv.lit, - format!( - "HTTP method defined more than once: `{}`", - lit.value() - ), - )); - } - } else { - return Err(syn::Error::new_spanned( - nv.lit, - "Attribute method expects literal string!", - )); - } - } else { - return Err(syn::Error::new_spanned( - nv.path, - "Unknown attribute key is specified. Allowed: guard, method and wrap", + nv.value, + format!("HTTP method defined more than once: `{}`", lit.value()), )); } + } else { + return Err(syn::Error::new_spanned( + nv.value, + "Attribute method expects literal string", + )); } - arg => { - return Err(syn::Error::new_spanned(arg, "Unknown attribute.")); - } + } else { + return Err(syn::Error::new_spanned( + nv.path, + "Unknown attribute key is specified; allowed: guard, method and wrap", + )); } } + Ok(Args { - path: path.unwrap(), + path: args.path, resource_name, guards, wrappers, @@ -206,11 +340,7 @@ pub struct Route { } impl Route { - pub fn new( - args: AttributeArgs, - ast: syn::ItemFn, - method: Option, - ) -> syn::Result { + pub fn new(args: RouteArgs, ast: syn::ItemFn, method: Option) -> syn::Result { let name = ast.sig.ident.clone(); // Try and pull out the doc comments so that we can reapply them to the generated struct. @@ -218,7 +348,7 @@ impl Route { let doc_attributes = ast .attrs .iter() - .filter(|attr| attr.path.is_ident("doc")) + .filter(|attr| attr.path().is_ident("doc")) .cloned() .collect(); @@ -254,7 +384,7 @@ impl Route { let doc_attributes = ast .attrs .iter() - .filter(|attr| attr.path.is_ident("doc")) + .filter(|attr| attr.path().is_ident("doc")) .cloned() .collect(); @@ -283,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| { @@ -299,22 +436,19 @@ impl ToTokens for Route { .map_or_else(|| name.to_string(), LitStr::value); let method_guards = { - let mut others = methods.iter(); + debug_assert!(!methods.is_empty(), "Args::methods should not be empty"); - // unwrapping since length is checked to be at least one + let mut others = methods.iter(); let first = others.next().unwrap(); if methods.len() > 1 { - quote! { - .guard( - ::actix_web::guard::Any(::actix_web::guard::#first()) - #(.or(::actix_web::guard::#others()))* - ) - } + let other_method_guards = others + .map(|method_ext| method_ext.to_tokens_multi_guard_or_chain()) + .collect(); + + first.to_tokens_multi_guard(other_method_guards) } else { - quote! { - .guard(::actix_web::guard::#first()) - } + first.to_tokens_single_guard() } }; @@ -325,7 +459,6 @@ impl ToTokens for Route { #(.guard(::actix_web::guard::fn_guard(#guards)))* #(.wrap(#wrappers))* .to(#name); - ::actix_web::dev::HttpServiceFactory::register(__resource, __config); } }) @@ -334,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) { @@ -353,7 +486,11 @@ pub(crate) fn with_method( args: TokenStream, input: TokenStream, ) -> TokenStream { - let args = parse_macro_input!(args as syn::AttributeArgs); + let args = match syn::parse(args) { + Ok(args) => args, + // on parse error, make IDEs happy; see fn docs + Err(err) => return input_and_compile_error(input, err), + }; let ast = match syn::parse::(input.clone()) { Ok(ast) => ast, @@ -378,7 +515,7 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream { let (methods, others) = ast .attrs .into_iter() - .map(|attr| match MethodType::from_path(&attr.path) { + .map(|attr| match MethodType::from_path(attr.path()) { Ok(method) => Ok((method, attr)), Err(_) => Err(attr), }) @@ -386,31 +523,27 @@ pub(crate) fn with_methods(input: TokenStream) -> TokenStream { ast.attrs = others.into_iter().map(Result::unwrap_err).collect(); - let methods = - match methods - .into_iter() - .map(Result::unwrap) - .map(|(method, attr)| { - attr.parse_meta().and_then(|args| { - if let Meta::List(args) = args { - Args::new(args.nested.into_iter().collect(), Some(method)) - } else { - Err(syn::Error::new_spanned(attr, "Invalid input for macro")) - } - }) - }) - .collect::, _>>() - { - Ok(methods) if methods.is_empty() => return input_and_compile_error( + let methods = match methods + .into_iter() + .map(Result::unwrap) + .map(|(method, attr)| { + attr.parse_args() + .and_then(|args| Args::new(args, Some(method))) + }) + .collect::, _>>() + { + Ok(methods) if methods.is_empty() => { + return input_and_compile_error( input, syn::Error::new( Span::call_site(), "The #[routes] macro requires at least one `#[(..)]` attribute.", ), - ), - Ok(methods) => methods, - Err(err) => return input_and_compile_error(input, err), - }; + ) + } + Ok(methods) => methods, + Err(err) => return input_and_compile_error(input, err), + }; match Route::multiple(methods, ast) { Ok(route) => route.into_token_stream().into(), @@ -418,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 00000000..067d95a6 --- /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 95% rename from actix-web-codegen/tests/test_macro.rs rename to actix-web-codegen/tests/routes.rs index 10e90696..fb50d4ae 100644 --- a/actix-web-codegen/tests/test_macro.rs +++ b/actix-web-codegen/tests/routes.rs @@ -86,7 +86,18 @@ async fn get_param_test(_: web::Path) -> impl Responder { HttpResponse::Ok() } -#[route("/multi", method = "GET", method = "POST", method = "HEAD")] +#[route("/hello", method = "HELLO")] +async fn custom_route_test() -> impl Responder { + HttpResponse::Ok() +} + +#[route( + "/multi", + method = "GET", + method = "POST", + method = "HEAD", + method = "HELLO" +)] async fn route_test() -> impl Responder { HttpResponse::Ok() } @@ -201,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 00000000..4ee6db16 --- /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 26aec7d2..91073cf3 100644 --- a/actix-web-codegen/tests/trybuild.rs +++ b/actix-web-codegen/tests/trybuild.rs @@ -1,4 +1,4 @@ -#[rustversion::stable(1.59)] // MSRV +#[rustversion::stable(1.72)] // MSRV #[test] fn compile_macros() { let t = trybuild::TestCases::new(); @@ -9,13 +9,20 @@ fn compile_macros() { t.pass("tests/trybuild/route-ok.rs"); t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); - t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-malformed-path-fail.rs"); + t.pass("tests/trybuild/route-custom-method.rs"); + t.compile_fail("tests/trybuild/route-custom-lowercase.rs"); + t.pass("tests/trybuild/routes-ok.rs"); 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-unexpected-method-fail.rs b/actix-web-codegen/tests/trybuild/route-custom-lowercase.rs similarity index 65% rename from actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs rename to actix-web-codegen/tests/trybuild/route-custom-lowercase.rs index 1a50e01b..61abb5bc 100644 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.rs +++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.rs @@ -1,6 +1,8 @@ use actix_web_codegen::*; +use actix_web::http::Method; +use std::str::FromStr; -#[route("/", method="UNEXPECTED")] +#[route("/", method = "hello")] async fn index() -> String { "Hello World!".to_owned() } @@ -11,7 +13,7 @@ async fn main() { let srv = actix_test::start(|| App::new().service(index)); - let request = srv.get("/"); + let request = srv.request(Method::from_str("hello").unwrap(), srv.url("/")); let response = request.send().await.unwrap(); assert!(response.status().is_success()); } diff --git a/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr new file mode 100644 index 00000000..c2a51d00 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-custom-lowercase.stderr @@ -0,0 +1,32 @@ +error: HTTP method must be uppercase: `hello` + --> tests/trybuild/route-custom-lowercase.rs:5:23 + | +5 | #[route("/", method = "hello")] + | ^^^^^^^ + +error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied + --> tests/trybuild/route-custom-lowercase.rs:14:55 + | +14 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | 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) + 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-custom-method.rs b/actix-web-codegen/tests/trybuild/route-custom-method.rs new file mode 100644 index 00000000..525a60b8 --- /dev/null +++ b/actix-web-codegen/tests/trybuild/route-custom-method.rs @@ -0,0 +1,37 @@ +use std::str::FromStr; + +use actix_web::http::Method; +use actix_web_codegen::route; + +#[route("/single", method = "CUSTOM")] +async fn index() -> String { + "Hello Single!".to_owned() +} + +#[route("/multi", method = "GET", method = "CUSTOM")] +async fn custom() -> String { + "Hello Multi!".to_owned() +} + +#[actix_web::main] +async fn main() { + use actix_web::App; + + let srv = actix_test::start(|| App::new().service(index).service(custom)); + + let request = srv.request(Method::GET, srv.url("/")); + let response = request.send().await.unwrap(); + assert!(response.status().is_client_error()); + + let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/single")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(Method::GET, srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); + + let request = srv.request(Method::from_str("CUSTOM").unwrap(), srv.url("/multi")); + let response = request.send().await.unwrap(); + assert!(response.status().is_success()); +} 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 7eac84f3..ae18f347 100644 --- a/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-duplicate-method-fail.stderr @@ -5,15 +5,28 @@ error: HTTP method defined more than once: `GET` | ^^^^^ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> tests/trybuild/route-duplicate-method-fail.rs:12:55 - | -12 | let srv = actix_test::start(|| App::new().service(index)); - | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` - | | - | required by a bound introduced by this call - | + --> tests/trybuild/route-duplicate-method-fail.rs:12:55 + | +12 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | 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) + and $N others note: required by a bound in `App::::service` - --> $WORKSPACE/actix-web/src/app.rs - | - | F: HttpServiceFactory + 'static, - | ^^^^^^^^^^^^^^^^^^ required by this 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 93c51010..c1100c78 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 bc8497c1..37d8354c 100644 --- a/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/route-missing-method-fail.stderr @@ -7,15 +7,28 @@ error: The #[route(..)] macro requires at least one `method` attribute = note: this error originates in the attribute macro `route` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> tests/trybuild/route-missing-method-fail.rs:12:55 - | -12 | let srv = actix_test::start(|| App::new().service(index)); - | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` - | | - | required by a bound introduced by this call - | + --> tests/trybuild/route-missing-method-fail.rs:12:55 + | +12 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | 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) + and $N others note: required by a bound in `App::::service` - --> $WORKSPACE/actix-web/src/app.rs - | - | F: HttpServiceFactory + 'static, - | ^^^^^^^^^^^^^^^^^^ required by this 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-unexpected-method-fail.stderr b/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr deleted file mode 100644 index 3df5d9f5..00000000 --- a/actix-web-codegen/tests/trybuild/route-unexpected-method-fail.stderr +++ /dev/null @@ -1,19 +0,0 @@ -error: Unexpected HTTP method: `UNEXPECTED` - --> tests/trybuild/route-unexpected-method-fail.rs:3:21 - | -3 | #[route("/", method="UNEXPECTED")] - | ^^^^^^^^^^^^ - -error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> tests/trybuild/route-unexpected-method-fail.rs:12:55 - | -12 | let srv = actix_test::start(|| App::new().service(index)); - | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` - | | - | required by a bound introduced by this call - | -note: required by a bound in `App::::service` - --> $WORKSPACE/actix-web/src/app.rs - | - | 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 785d6f32..40b19fc7 100644 --- a/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr +++ b/actix-web-codegen/tests/trybuild/routes-missing-args-fail.stderr @@ -1,4 +1,4 @@ -error: invalid service definition, expected #[get("")] +error: unexpected end of input, expected string literal --> tests/trybuild/routes-missing-args-fail.rs:4:1 | 4 | #[get] @@ -6,22 +6,43 @@ error: invalid service definition, expected #[get("")] | = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) -error: Invalid input for macro +error: invalid service definition, expected #[("")] --> tests/trybuild/routes-missing-args-fail.rs:4:1 | 4 | #[get] | ^^^^^^ + | + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) + +error: expected attribute arguments in parentheses: #[get(...)] + --> tests/trybuild/routes-missing-args-fail.rs:4:3 + | +4 | #[get] + | ^^^ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> tests/trybuild/routes-missing-args-fail.rs:13:55 - | -13 | let srv = actix_test::start(|| App::new().service(index)); - | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` - | | - | required by a bound introduced by this call - | + --> tests/trybuild/routes-missing-args-fail.rs:13:55 + | +13 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | 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) + and $N others note: required by a bound in `App::::service` - --> $WORKSPACE/actix-web/src/app.rs - | - | F: HttpServiceFactory + 'static, - | ^^^^^^^^^^^^^^^^^^ required by this 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 38a6d2f9..ff7f00b3 100644 --- a/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr +++ b/actix-web-codegen/tests/trybuild/routes-missing-method-fail.stderr @@ -7,15 +7,28 @@ error: The #[routes] macro requires at least one `#[(..)]` attribute. = note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info) error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied - --> tests/trybuild/routes-missing-method-fail.rs:12:55 - | -12 | let srv = actix_test::start(|| App::new().service(index)); - | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}` - | | - | required by a bound introduced by this call - | + --> tests/trybuild/routes-missing-method-fail.rs:12:55 + | +12 | let srv = actix_test::start(|| App::new().service(index)); + | ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for fn item `fn() -> impl std::future::Future {index}` + | | + | 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) + and $N others note: required by a bound in `App::::service` - --> $WORKSPACE/actix-web/src/app.rs - | - | F: HttpServiceFactory + 'static, - | ^^^^^^^^^^^^^^^^^^ required by this 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 00000000..ec021d5e --- /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 00000000..0ab33596 --- /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 00000000..39bcb9d1 --- /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 00000000..d59842e3 --- /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 00000000..e5d47898 --- /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 00000000..4491f42d --- /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 00000000..84632b59 --- /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 00000000..66933432 --- /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 cffc81ff..ab81599e 100644 --- a/actix-web-codegen/tests/trybuild/simple-fail.stderr +++ b/actix-web-codegen/tests/trybuild/simple-fail.stderr @@ -1,28 +1,44 @@ -error: Unknown attribute. - --> $DIR/simple-fail.rs:3:15 +error: expected `=` + --> $DIR/simple-fail.rs:3:1 | 3 | #[get("/one", other)] - | ^^^^^ + | ^^^^^^^^^^^^^^^^^^^^^ + | + = note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info) -error: expected identifier or literal +error: expected string literal --> $DIR/simple-fail.rs:8:8 | 8 | #[post(/two)] | ^ -error: Unknown attribute. +error: invalid service definition, expected #[("")] + --> $DIR/simple-fail.rs:8:8 + | +8 | #[post(/two)] + | ^ + +error: expected string literal --> $DIR/simple-fail.rs:15:9 | 15 | #[patch(PATCH_PATH)] | ^^^^^^^^^^ -error: Multiple paths specified! Should be only one! - --> $DIR/simple-fail.rs:20:19 +error: invalid service definition, expected #[("")] + --> $DIR/simple-fail.rs:15:9 + | +15 | #[patch(PATCH_PATH)] + | ^^^^^^^^^^ + +error: Multiple paths specified! There should be only one. + --> $DIR/simple-fail.rs:20:1 | 20 | #[delete("/four", "/five")] - | ^^^^^^^ + | ^^^^^^^^^^^^^^^^^^^^^^^^^^^ + | + = 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 dc38da66..8cc626f3 100644 --- a/actix-web/CHANGES.md +++ b/actix-web/CHANGES.md @@ -1,16 +1,114 @@ # Changelog -## Unreleased - 2022-xx-xx +## Unreleased + ### Added -- Add `ContentDisposition::attachment` constructor. [#2867] + +- Add `web::Html` responder. +- Add `HttpRequest::full_url()` method to get the complete URL of the request. +- Add level setup for `middleware::Compress`. + +### 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. +- 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 + +- Handler functions can now receive up to 16 extractor parameters. +- The `Compress` middleware no longer compresses image or video content. +- 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 + +### Added + +- Add support for custom methods with the `#[route]` macro. [#2969] + +[#2969]: https://github.com/actix/actix-web/pull/2969 + +## 4.3.0 + +### Added + +- Add `ContentDisposition::attachment()` constructor. [#2867] - Add `ErrorHandlers::default_handler()` (as well as `default_handler_{server, client}()`) to make registering handlers for groups of response statuses easier. [#2784] - Add `Logger::custom_response_replace()`. [#2631] - Add rudimentary redirection service at `web::redirect()` / `web::Redirect`. [#1961] -- Add `guard::Acceptable` for matching against `Accept` header mime types. [#2265] -- Add fallible versions of test helpers: `try_call_service`, `try_call_and_read_body_json`, `try_read_body`, and `try_read_body_json`. [#2961] -- Add level setup for `middleware::Compress`. [#2948] +- Add `guard::Acceptable` for matching against `Accept` header MIME types. [#2265] +- Add fallible versions of `test` helpers: `try_call_service()`, `try_call_and_read_body_json()`, `try_read_body()`, and `try_read_body_json()`. [#2961] ### Fixed + - Add `Allow` header to `Resource`'s default responses when no routes are matched. [#2949] [#1961]: https://github.com/actix/actix-web/pull/1961 @@ -20,40 +118,46 @@ [#2867]: https://github.com/actix/actix-web/pull/2867 [#2949]: https://github.com/actix/actix-web/pull/2949 [#2961]: https://github.com/actix/actix-web/pull/2961 -[#2948]: https://github.com/actix/actix-web/pull/2948 -## 4.2.1 - 2022-09-12 +## 4.2.1 + ### Fixed + - Bump minimum version of `actix-http` dependency to fix compatibility issue. [#2871] [#2871]: https://github.com/actix/actix-web/pull/2871 +## 4.2.0 -## 4.2.0 - 2022-09-11 ### Added + - Add `#[routes]` macro to support multiple paths for one handler. [#2718] - Add `ServiceRequest::{parts, request}()` getter methods. [#2786] - Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752] ### Changed + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. [#2718]: https://github.com/actix/actix-web/pull/2718 [#2752]: https://github.com/actix/actix-web/pull/2752 [#2786]: https://github.com/actix/actix-web/pull/2786 +## 4.1.0 -## 4.1.0 - 2022-06-11 ### Added + - Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647] - Add `Route::wrap()` to allow individual routes to use middleware. [#2725] - Add `ServiceConfig::default_service()`. [#2338] [#2743] - Implement `ResponseError` for `std::convert::Infallible` ### Changed + - Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency. ### Fixed + - Clear connection-level data on `HttpRequest` drop. [#2742] [#2338]: https://github.com/actix/actix-web/pull/2338 @@ -62,14 +166,16 @@ [#2742]: https://github.com/actix/actix-web/pull/2742 [#2743]: https://github.com/actix/actix-web/pull/2743 +## 4.0.1 -## 4.0.1 - 2022-02-25 ### Fixed + - Use stable version in readme example. +## 4.0.0 -## 4.0.0 - 2022-02-25 ### Dependencies + - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `actix-web-codegen` to `4.0.0`. - Updated `cookie` to `0.16`. [#2555] @@ -79,6 +185,7 @@ - Updated `tokio` to `1`. ### Added + - Crate Features: - `cookies`; enabled by default. [#2619] - `compress-brotli`; enabled by default. [#2618] @@ -125,6 +232,7 @@ - Connection data set through the `HttpServer::on_connect` callback is now accessible only from the new `HttpRequest::conn_data()` and `ServiceRequest::conn_data()` methods. [#2491] ### Changed + - Functions: - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] - `guard::Not` is now generic over the type of guard it wraps. [#2552] @@ -190,6 +298,7 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] - Scope and Resource middleware can access data items set on their own layer. [#2288] - Multiple calls to `App::data()` with the same type now keeps the latest call's data. [#1906] @@ -203,11 +312,13 @@ - Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html ### Removed + - Crate Features: - `compress` feature. [#2065] - Functions: @@ -336,50 +447,58 @@ [#2663]: https://github.com/actix/actix-web/pull/2663 [871ca5e4]: https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7 -
4.0.0 Pre-Releases -## 4.0.0-rc.3 - 2022-02-08 +## 4.0.0-rc.3 + ### Changed + - `middleware::Condition` gained a broader compatibility; `Compat` is needed in fewer cases. [#2635] ### Added + - Implement `Responder` for `Vec`. [#2625] - Re-export `KeepAlive` in `http` mod. [#2625] [#2625]: https://github.com/actix/actix-web/pull/2625 [#2635]: https://github.com/actix/actix-web/pull/2635 +## 4.0.0-rc.2 -## 4.0.0-rc.2 - 2022-02-02 ### Added + - On-by-default `macros` feature flag to enable routing and runtime macros. [#2619] ### Removed + - `rt::{Arbiter, ArbiterHandle}` re-exports. [#2619] [#2619]: https://github.com/actix/actix-web/pull/2619 +## 4.0.0-rc.1 -## 4.0.0-rc.1 - 2022-01-31 ### Changed + - Rename `HttpServer::{client_timeout => client_request_timeout}`. [#2611] - Rename `HttpServer::{client_shutdown => client_disconnect_timeout}`. [#2611] ### Removed + - `impl Future for HttpResponse`. [#2601] [#2601]: https://github.com/actix/actix-web/pull/2601 [#2611]: https://github.com/actix/actix-web/pull/2611 +## 4.0.0-beta.21 -## 4.0.0-beta.21 - 2022-01-21 ### Added + - `HttpResponse::add_removal_cookie`. [#2586] - `Logger::log_target`. [#2594] ### Removed + - `HttpRequest::req_data[_mut]()`; request-local data is still available through `.extensions()`. [#2585] - `HttpRequestBuilder::del_cookie`. [#2591] @@ -388,13 +507,15 @@ [#2591]: https://github.com/actix/actix-web/pull/2591 [#2594]: https://github.com/actix/actix-web/pull/2594 +## 4.0.0-beta.20 -## 4.0.0-beta.20 - 2022-01-14 ### Added + - `GuardContext::header` [#2569] - `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988] ### Changed + - `HttpResponse` can now be used as a `Responder` with any body type. [#2567] - `Result` extractor wrapper can now convert error types. [#2581] - Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581] @@ -408,52 +529,61 @@ [#2582]: https://github.com/actix/actix-web/pull/2582 [#2584]: https://github.com/actix/actix-web/pull/2584 +## 4.0.0-beta.19 -## 4.0.0-beta.19 - 2022-01-04 ### Added + - `impl Hash` for `http::header::Encoding`. [#2501] - `AcceptEncoding::negotiate()`. [#2501] ### Changed + - `AcceptEncoding::preference` now returns `Option>`. [#2501] - Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501] - `http::header::Encoding` now only represents `Content-Encoding` types. [#2501] ### Fixed + - Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501] ### Removed + - `Compress::new`; restricting compression algorithm is done through feature flags. [#2501] - `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565] [#2501]: https://github.com/actix/actix-web/pull/2501 [#2565]: https://github.com/actix/actix-web/pull/2565 +## 4.0.0-beta.18 -## 4.0.0-beta.18 - 2021-12-29 ### Changed + - Update `cookie` dependency (re-exported) to `0.16`. [#2555] - Minimum supported Rust version (MSRV) is now 1.54. ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [#2555]: https://github.com/actix/actix-web/pull/2555 -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html +## 4.0.0-beta.17 -## 4.0.0-beta.17 - 2021-12-29 ### Added + - `guard::GuardContext` for use with the `Guard` trait. [#2552] - `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552] ### Changed + - `Guard` trait now receives a `&GuardContext`. [#2552] - `guard::fn_guard` functions now receives a `&GuardContext`. [#2552] - Some guards now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552] - The `Not` guard is now generic over the type of guard it wraps. [#2552] ### Fixed + - Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554] - `ConnectionInfo::peer_addr` will not return the port number. [#2554] - `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554] @@ -461,22 +591,25 @@ [#2552]: https://github.com/actix/actix-web/pull/2552 [#2554]: https://github.com/actix/actix-web/pull/2554 +## 4.0.0-beta.16 -## 4.0.0-beta.16 - 2021-12-27 ### Changed + - No longer require `Scope` service body type to be boxed. [#2523] - No longer require `Resource` service body type to be boxed. [#2526] [#2523]: https://github.com/actix/actix-web/pull/2523 [#2526]: https://github.com/actix/actix-web/pull/2526 +## 4.0.0-beta.15 -## 4.0.0-beta.15 - 2021-12-17 ### Added + - Method on `Responder` trait (`customize`) for customizing responders and `CustomizeResponder` struct. [#2510] - Implement `Debug` for `DefaultHeaders`. [#2510] ### Changed + - Align `DefaultHeader` method terminology, deprecating previous methods. [#2510] - Response service types in `ErrorHandlers` middleware now use `ServiceResponse>` to allow changing the body type. [#2515] - Both variants in `ErrorHandlerResponse` now use `ServiceResponse>`. [#2515] @@ -486,6 +619,7 @@ - Relax body type and error bounds on test utilities. [#2518] ### Removed + - Top-level `EitherExtractError` export. [#2510] - Conversion implementations for `either` crate. [#2516] - `test::load_stream` and `test::load_body`; replace usage with `body::to_bytes`. [#2518] @@ -495,9 +629,10 @@ [#2516]: https://github.com/actix/actix-web/pull/2516 [#2518]: https://github.com/actix/actix-web/pull/2518 +## 4.0.0-beta.14 -## 4.0.0-beta.14 - 2021-12-11 ### Added + - Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480] - `AcceptEncoding` typed header. [#2482] - `Range` typed header. [#2485] @@ -508,6 +643,7 @@ - `ServiceResponse::into_parts`. [#2499] ### Changed + - Rename `Accept::{mime_precedence => ranked}`. [#2480] - Rename `Accept::{mime_preference => preference}`. [#2480] - Un-deprecate `App::data_factory`. [#2484] @@ -517,11 +653,13 @@ - Request-local data container is no longer part of a `RequestHead`. Instead it is a distinct part of a `Request`. [#2487] ### Fixed + - Accept wildcard `*` items in `AcceptLanguage`. [#2480] - Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468] - Typed headers containing lists that require one or more items now enforce this minimum. [#2482] ### Removed + - `ConnectionInfo::get`. [#2487] [#2430]: https://github.com/actix/actix-web/pull/2430 @@ -536,46 +674,54 @@ [#2493]: https://github.com/actix/actix-web/pull/2493 [#2499]: https://github.com/actix/actix-web/pull/2499 +## 4.0.0-beta.13 -## 4.0.0-beta.13 - 2021-11-30 ### Changed + - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 +## 4.0.0-beta.12 -## 4.0.0-beta.12 - 2021-11-22 ### Changed + - Compress middleware's response type is now `AnyBody>`. [#2448] ### Fixed + - Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448] ### Removed + - `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446] [#2446]: https://github.com/actix/actix-web/pull/2446 [#2448]: https://github.com/actix/actix-web/pull/2448 +## 4.0.0-beta.11 -## 4.0.0-beta.11 - 2021-11-15 ### Added + - Re-export `dev::ServerHandle` from `actix-server`. [#2442] ### Changed + - `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423] - Update `actix-server` to `2.0.0-beta.9`. [#2442] [#2423]: https://github.com/actix/actix-web/pull/2423 [#2442]: https://github.com/actix/actix-web/pull/2442 +## 4.0.0-beta.10 -## 4.0.0-beta.10 - 2021-10-20 ### Added + - Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362] - `#[actix_web::test]` macro for setting up tests with a runtime. [#2409] ### Changed + - Associated type `FromRequest::Config` was removed. [#2233] - Inner field made private on `web::Payload`. [#2384] - `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403] @@ -583,6 +729,7 @@ - Minimum supported Rust version (MSRV) is now 1.52. ### Removed + - Useless `ServiceResponse::checked_expr` method. [#2401] [#2233]: https://github.com/actix/actix-web/pull/2233 @@ -593,18 +740,21 @@ [#2409]: https://github.com/actix/actix-web/pull/2409 [#2414]: https://github.com/actix/actix-web/pull/2414 +## 4.0.0-beta.9 -## 4.0.0-beta.9 - 2021-09-09 ### Added + - Re-export actix-service `ServiceFactory` in `dev` module. [#2325] ### Changed + - Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344] - Move `BaseHttpResponse` to `dev::Response`. [#2379] - Enable `TestRequest::param` to accept more than just static strings. [#2172] - Minimum supported Rust version (MSRV) is now 1.51. ### Fixed + - Fix quality parse error in Accept-Encoding header. [#2344] - Re-export correct type at `web::HttpResponse`. [#2379] @@ -613,20 +763,23 @@ [#2344]: https://github.com/actix/actix-web/pull/2344 [#2379]: https://github.com/actix/actix-web/pull/2379 +## 4.0.0-beta.8 -## 4.0.0-beta.8 - 2021-06-26 ### Added + - Add `ServiceRequest::parts_mut`. [#2177] - Add extractors for `Uri` and `Method`. [#2263] - Add extractors for `ConnectionInfo` and `PeerAddr`. [#2263] - Add `Route::service` for using hand-written services as handlers. [#2262] ### Changed + - Change compression algorithm features flags. [#2250] - Deprecate `App::data` and `App::data_factory`. [#2271] - Smarter extraction of `ConnectionInfo` parts. [#2282] ### Fixed + - Scope and Resource middleware can access data items set on their own layer. [#2288] [#2177]: https://github.com/actix/actix-web/pull/2177 @@ -637,12 +790,14 @@ [#2282]: https://github.com/actix/actix-web/pull/2282 [#2288]: https://github.com/actix/actix-web/pull/2288 +## 4.0.0-beta.7 -## 4.0.0-beta.7 - 2021-06-17 ### Added + - `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200] ### Changed + - Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162] - `ServiceResponse::error_response` now uses body type of `Body`. [#2201] - `ServiceResponse::checked_expr` now returns a `Result`. [#2201] @@ -655,6 +810,7 @@ - `middleware::normalize` now will not try to normalize URIs with no valid path [#2246] ### Removed + - `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201] [#2162]: https://github.com/actix/actix-web/pull/2162 @@ -663,36 +819,39 @@ [#2253]: https://github.com/actix/actix-web/pull/2253 [#2246]: https://github.com/actix/actix-web/pull/2246 +## 4.0.0-beta.6 -## 4.0.0-beta.6 - 2021-04-17 ### Added + - `HttpResponse` and `HttpResponseBuilder` types. [#2065] ### Changed + - Most error types are now marked `#[non_exhaustive]`. [#2148] - Methods on `ContentDisposition` that took `T: AsRef` now take `impl AsRef`. [#2065]: https://github.com/actix/actix-web/pull/2065 [#2148]: https://github.com/actix/actix-web/pull/2148 +## 4.0.0-beta.5 -## 4.0.0-beta.5 - 2021-04-02 ### Added + - `Header` extractor for extracting common HTTP headers in handlers. [#2094] - Added `TestServer::client_headers` method. [#2097] ### Changed -- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed - instead of skipping. (Only the first error is kept when multiple error occur) [#2093] + +- `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed instead of skipping. (Only the first error is kept when multiple error occur) [#2093] ### Fixed + - Double ampersand in Logger format is escaped correctly. [#2067] ### Removed -- The `client` mod was removed. Clients should now use `awc` directly. - [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) -- Integration testing was moved to new `actix-test` crate. Namely these items from the `test` - module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] + +- The `client` mod was removed. Clients should now use `awc` directly. [871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7) +- Integration testing was moved to new `actix-test` crate. Namely these items from the `test` module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112] [#2067]: https://github.com/actix/actix-web/pull/2067 [#2093]: https://github.com/actix/actix-web/pull/2093 @@ -700,50 +859,48 @@ [#2097]: https://github.com/actix/actix-web/pull/2097 [#2112]: https://github.com/actix/actix-web/pull/2112 +## 4.0.0-beta.4 -## 4.0.0-beta.4 - 2021-03-09 ### Changed + - Feature `cookies` is now optional and enabled by default. [#1981] -- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default - behaviour of the `web::Json` extractor. [#2010] +- `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default behaviour of the `web::Json` extractor. [#2010] [#1981]: https://github.com/actix/actix-web/pull/1981 [#2010]: https://github.com/actix/actix-web/pull/2010 +## 4.0.0-beta.3 -## 4.0.0-beta.3 - 2021-02-10 - Update `actix-web-codegen` to `0.5.0-beta.1`. +## 4.0.0-beta.2 -## 4.0.0-beta.2 - 2021-02-10 ### Added -- The method `Either, web::Form>::into_inner()` which returns the inner type for - whichever variant was created. Also works for `Either, web::Json>`. [#1894] + +- The method `Either, web::Form>::into_inner()` which returns the inner type for whichever variant was created. Also works for `Either, web::Json>`. [#1894] - Add `services!` macro for helping register multiple services to `App`. [#1933] - Enable registering a vec of services of the same type to `App` [#1933] ### Changed -- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. - Making it simpler and more performant. [#1891] + +- Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly. Making it simpler and more performant. [#1891] - `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893] - `ServiceRequest::from_request` can no longer fail. [#1893] - Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894] -- `test::{call_service, read_response, read_response_json, send_request}` take `&Service` - in argument [#1905] -- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure - argument. [#1905] +- `test::{call_service, read_response, read_response_json, send_request}` take `&Service` in argument [#1905] +- `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure argument. [#1905] - `web::block` no longer requires the output is a Result. [#1957] ### Fixed + - Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906] ### Removed + - Public field of `web::Path` has been made private. [#1894] - Public field of `web::Query` has been made private. [#1894] - `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869] -- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the - layered data model by calling `ServiceRequest::add_data_container` when handling - requests instead. [#1906] +- `AppService::set_service_data`; for custom HTTP service factories adding application data, use the layered data model by calling `ServiceRequest::add_data_container` when handling requests instead. [#1906] [#1891]: https://github.com/actix/actix-web/pull/1891 [#1893]: https://github.com/actix/actix-web/pull/1893 @@ -754,30 +911,30 @@ [#1933]: https://github.com/actix/actix-web/pull/1933 [#1957]: https://github.com/actix/actix-web/pull/1957 +## 4.0.0-beta.1 -## 4.0.0-beta.1 - 2021-01-07 ### Added -- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and - `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] + +- `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and `Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865] ### Changed + - Update `actix-*` dependencies to tokio `1.0` based versions. [#1813] - Bumped `rand` to `0.8`. - Update `rust-tls` to `0.19`. [#1813] - Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852] -- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration - guide for implications. [#1875] +- The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration guide for implications. [#1875] - Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875] - MSRV is now 1.46.0. ### Fixed + - Added the underlying parse error to `test::read_body_json`'s panic message. [#1812] ### Removed -- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now - exposed directly by the `middleware` module. -- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported - from `actix_web::error` module. [#1878] + +- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now exposed directly by the `middleware` module. +- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported from `actix_web::error` module. [#1878] [#1812]: https://github.com/actix/actix-web/pull/1812 [#1813]: https://github.com/actix/actix-web/pull/1813 @@ -788,15 +945,18 @@
-## 3.3.3 - 2021-12-18 +## 3.3.3 + ### Changed + - Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529] [#2529]: https://github.com/actix/actix-web/pull/2529 +## 3.3.2 -## 3.3.2 - 2020-12-01 ### Fixed + - Removed an occasional `unwrap` on `None` panic in `NormalizePathNormalization`. [#1762] - Fix `match_pattern()` returning `None` for scope with empty path resource. [#1798] - Increase minimum `socket2` version. [#1803] @@ -805,24 +965,27 @@ [#1798]: https://github.com/actix/actix-web/pull/1798 [#1803]: https://github.com/actix/actix-web/pull/1803 +## 3.3.1 -## 3.3.1 - 2020-11-29 - Ensure `actix-http` dependency uses same `serde_urlencoded`. +## 3.3.0 -## 3.3.0 - 2020-11-25 ### Added + - Add `Either` extractor helper. [#1788] ### Changed + - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 [#1788]: https://github.com/actix/actix-web/pull/1788 +## 3.2.0 -## 3.2.0 - 2020-10-30 ### Added + - Implement `exclude_regex` for Logger middleware. [#1723] - Add request-local data extractor `web::ReqData`. [#1748] - Add ability to register closure for request middleware logging. [#1749] @@ -830,9 +993,10 @@ - Expose `on_connect` for access to the connection stream before request is handled. [#1754] ### Changed -- Updated actix-web-codegen dependency for access to new `#[route(...)]` multi-method macro. + +- Updated `actix-web-codegen` dependency for access to new `#[route(...)]` multi-method macro. - Print non-configured `Data` type when attempting extraction. [#1743] -- Re-export bytes::Buf{Mut} in web module. [#1750] +- Re-export `bytes::Buf{Mut}` in web module. [#1750] - Upgrade `pin-project` to `1.0`. [#1723]: https://github.com/actix/actix-web/pull/1723 @@ -840,77 +1004,80 @@ [#1748]: https://github.com/actix/actix-web/pull/1748 [#1750]: https://github.com/actix/actix-web/pull/1750 [#1754]: https://github.com/actix/actix-web/pull/1754 +[#1757]: https://github.com/actix/actix-web/pull/1757 [#1749]: https://github.com/actix/actix-web/pull/1749 +## 3.1.0 -## 3.1.0 - 2020-09-29 ### Changed -- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` - to retain any trailing slashes. [#1695] -- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` - via `web::Data::from` [#1710] + +- Add `TrailingSlash::MergeOnly` behaviour to `NormalizePath`, which allows `NormalizePath` to retain any trailing slashes. [#1695] +- Remove bound `std::marker::Sized` from `web::Data` to support storing `Arc` via `web::Data::from` [#1710] ### Fixed + - `ResourceMap` debug printing is no longer infinitely recursive. [#1708] [#1695]: https://github.com/actix/actix-web/pull/1695 [#1708]: https://github.com/actix/actix-web/pull/1708 [#1710]: https://github.com/actix/actix-web/pull/1710 +## 3.0.2 -## 3.0.2 - 2020-09-15 ### Fixed + - `NormalizePath` when used with `TrailingSlash::Trim` no longer trims the root path "/". [#1678] [#1678]: https://github.com/actix/actix-web/pull/1678 +## 3.0.1 -## 3.0.1 - 2020-09-13 ### Changed + - `middleware::normalize::TrailingSlash` enum is now accessible. [#1673] [#1673]: https://github.com/actix/actix-web/pull/1673 +## 3.0.0 -## 3.0.0 - 2020-09-11 - No significant changes from `3.0.0-beta.4`. +## 3.0.0-beta.4 -## 3.0.0-beta.4 - 2020-09-09 ### Added -- `middleware::NormalizePath` now has configurable behavior for either always having a trailing - slash, or as the new addition, always trimming trailing slashes. [#1639] + +- `middleware::NormalizePath` now has configurable behavior for either always having a trailing slash, or as the new addition, always trimming trailing slashes. [#1639] ### Changed + - Update actix-codec and actix-utils dependencies. [#1634] -- `FormConfig` and `JsonConfig` configurations are now also considered when set - using `App::data`. [#1641] +- `FormConfig` and `JsonConfig` configurations are now also considered when set using `App::data`. [#1641] - `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. [#1655] -- `HttpServer::maxconnrate` is renamed to the more expressive - `HttpServer::max_connection_rate`. [#1655] +- `HttpServer::maxconnrate` is renamed to the more expressive `HttpServer::max_connection_rate`. [#1655] [#1639]: https://github.com/actix/actix-web/pull/1639 [#1641]: https://github.com/actix/actix-web/pull/1641 [#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 -## 3.0.0-beta.2 - 2020-08-17 ### Changed -- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set - using `App::data`. [#1610] -- `web::Path` now has a public representation: `web::Path(pub T)` that enables - destructuring. [#1594] -- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to - access `HttpRequest` which already allows this. [#1618] + +- `PayloadConfig` is now also considered in `Bytes` and `String` extractors when set using `App::data`. [#1610] +- `web::Path` now has a public representation: `web::Path(pub T)` that enables destructuring. [#1594] +- `ServiceRequest::app_data` allows retrieval of non-Data data without splitting into parts to access `HttpRequest` which already allows this. [#1618] - Re-export all error types from `awc`. [#1621] - MSRV is now 1.42.0. ### Fixed + - Memory leak of app data in pooled requests. [#1609] [#1594]: https://github.com/actix/actix-web/pull/1594 @@ -919,28 +1086,32 @@ [#1618]: https://github.com/actix/actix-web/pull/1618 [#1621]: https://github.com/actix/actix-web/pull/1621 +## 3.0.0-beta.1 -## 3.0.0-beta.1 - 2020-07-13 ### Added + - Re-export `actix_rt::main` as `actix_web::main`. -- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched - resource pattern. +- `HttpRequest::match_pattern` and `ServiceRequest::match_pattern` for extracting the matched resource pattern. - `HttpRequest::match_name` and `ServiceRequest::match_name` for extracting matched resource name. ### Changed + - Fix actix_http::h1::dispatcher so it returns when HW_BUFFER_SIZE is reached. Should reduce peak memory consumption during large uploads. [#1550] - Migrate cookie handling to `cookie` crate. Actix-web no longer requires `ring` dependency. - MSRV is now 1.41.1 ### Fixed + - `NormalizePath` improved consistency when path needs slashes added _and_ removed. +## 3.0.0-alpha.3 -## 3.0.0-alpha.3 - 2020-05-21 ### Added + - Add option to create `Data` from `Arc` [#1509] ### Changed + - Resources and Scopes can now access non-overridden data types set on App (or containing scopes) when setting their own data. [#1486] - Fix audit issue logging by default peer address [#1485] - Bump minimum supported Rust version to 1.40 @@ -949,7 +1120,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 @@ -963,8 +1134,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 @@ -981,7 +1151,7 @@ [#1308]: https://github.com/actix/actix-web/pull/1308 -## [2.0.0] - 2019-12-25 +## 2.0.0 ### Changed @@ -991,7 +1161,7 @@ - Allow to specify multi-patterns for resources -## [2.0.0-rc] - 2019-12-20 +## 2.0.0-rc ### Changed @@ -1009,33 +1179,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 @@ -1043,8 +1211,7 @@ - Remove implementation of `Responder` for `()`. (#1167) - -## [1.0.9] - 2019-11-14 +## 1.0.9 ### Added @@ -1054,20 +1221,17 @@ - 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 -- Add `Scope::register_data` and `Resource::register_data` methods, parallel to - `App::register_data`. +- Add `Scope::register_data` and `Resource::register_data` methods, parallel to `App::register_data`. - Add `middleware::Condition` that conditionally enables another middleware - Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload` -- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, - which is useful for example with systemd. +- Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path, which is useful for example with systemd. ### Changed @@ -1075,15 +1239,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 @@ -1093,8 +1255,7 @@ - Add `into_inner` to `Data` -- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set - the header in test requests. +- Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set the header in test requests. ### Changed @@ -1106,36 +1267,31 @@ - Update url to 2.1 - -## [1.0.5] - 2019-07-18 +## 1.0.5 ### Added - Unix domain sockets (HttpServer::bind_uds) #92 -- Actix now logs errors resulting in "internal server error" responses always, with the `error` - logging level +- Actix now logs errors resulting in "internal server error" responses always, with the `error` logging level ### Fixed - Restored logging of errors through the `Logger` middleware - -## [1.0.4] - 2019-07-17 +## 1.0.4 ### Added - Add `Responder` impl for `(T, StatusCode) where T: Responder` -- Allow to access app's resource map via - `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. +- Allow to access app's resource map via `ServiceRequest::resource_map()` and `HttpRequest::resource_map()` methods. ### Changed - Upgrade `rand` dependency version to 0.7 - -## [1.0.3] - 2019-06-28 +## 1.0.3 ### Added @@ -1143,10 +1299,9 @@ ### Changed -- Use `encoding_rs` crate instead of unmaintained `encoding` crate +- Use `encoding_rs` crate instead of unmaintained `encoding` crate - -## [1.0.2] - 2019-06-17 +## 1.0.2 ### Changed @@ -1154,8 +1309,7 @@ - Move identity middleware to `actix-identity` crate. - -## [1.0.1] - 2019-06-17 +## 1.0.1 ### Added @@ -1179,8 +1333,7 @@ - HttpRequest::url_for is broken with nested scopes #915 - -## [1.0.0] - 2019-06-05 +## 1.0.0 ### Added @@ -1188,8 +1341,7 @@ - Add `ServiceRequest::set_payload()` method. -- Add `test::TestRequest::set_json()` convenience method to automatically - serialize data and set header in test requests. +- Add `test::TestRequest::set_json()` convenience method to automatically serialize data and set header in test requests. - Add macros for head, options, trace, connect and patch http methods @@ -1203,8 +1355,7 @@ - Clear http requests pool on app service drop #860 - -## [1.0.0-rc] - 2019-05-18 +## 1.0.0-rc ### Added @@ -1219,8 +1370,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 @@ -1231,8 +1381,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 @@ -1240,11 +1389,9 @@ ### Changed -- Extractor configuration could be registered with `App::data()` - or with `Resource::data()` #775 +- Extractor configuration could be registered with `App::data()` or with `Resource::data()` #775 -- Route data is unified with app data, `Route::data()` moved to resource - level to `Resource::data()` +- Route data is unified with app data, `Route::data()` moved to resource level to `Resource::data()` - CORS handling without headers #702 @@ -1258,8 +1405,7 @@ - `App::data_factory()` is deleted. - -## [1.0.0-beta.2] - 2019-04-24 +## 1.0.0-beta.2 ### Added @@ -1267,7 +1413,7 @@ - Add helper functions for reading response body `test::read_body()` -- Add support for `remainder match` (i.e "/path/{tail}*") +- Add support for `remainder match` (i.e "/path/{tail}\*") - Extend `Responder` trait, allow to override status code and headers. @@ -1281,13 +1427,11 @@ - Fix async web::Data factory handling - -## [1.0.0-beta.1] - 2019-04-20 +## 1.0.0-beta.1 ### Added -- Add helper functions for reading test response body, - `test::read_response()` and test::read_response_json()` +- Add helper functions for reading test response body, `test::read_response()` and test::read_response_json()` - Add `.peer_addr()` #744 @@ -1307,8 +1451,7 @@ - Fixed `TestRequest::app_data()` - -## [1.0.0-alpha.6] - 2019-04-14 +## 1.0.0-alpha.6 ### Changed @@ -1316,13 +1459,11 @@ - Remove generic type for request payload, always use default. -- Removed `Decompress` middleware. Bytes, String, Json, Form extractors - automatically decompress payload. +- Removed `Decompress` middleware. Bytes, String, Json, Form extractors automatically decompress payload. - Make extractor config type explicit. Add `FromRequest::Config` associated type. - -## [1.0.0-alpha.5] - 2019-04-12 +## 1.0.0-alpha.5 ### Added @@ -1332,8 +1473,7 @@ - Removed native-tls support - -## [1.0.0-alpha.4] - 2019-04-08 +## 1.0.0-alpha.4 ### Added @@ -1355,8 +1495,7 @@ - Fix body propagation in Response::from_error. #760 - -## [1.0.0-alpha.3] - 2019-04-02 +## 1.0.0-alpha.3 ### Changed @@ -1370,8 +1509,7 @@ - Removed unused `actix_web::web::md()` - -## [1.0.0-alpha.2] - 2019-03-29 +## 1.0.0-alpha.2 ### Added @@ -1383,7 +1521,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 d1ae89c5..3827d440 100644 --- a/actix-web/Cargo.toml +++ b/actix-web/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "actix-web" -version = "4.2.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,64 @@ categories = [ "web-programming::websocket" ] homepage = "https://actix.rs" -repository = "https://github.com/actix/actix-web.git" -license = "MIT OR Apache-2.0" -edition = "2018" +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" +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_http::*", + "actix_router::*", + "actix_rt::*", + "actix_server::*", + "actix_service::*", + "actix_utils::*", + "actix_web_codegen::*", + "bytes::*", + "cookie::*", + "cookie", + "futures_core::*", + "http::*", + "language_tags::*", + "mime::*", + "openssl::*", + "rustls::*", + "serde_json::*", + "serde_urlencoded::*", + "serde::*", + "serde::*", + "tokio::*", + "url::*", +] [features] -default = ["macros", "compress-brotli", "compress-gzip", "compress-zstd", "cookies"] +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,19 +81,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"] -# TLS via OpenSSL -openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] +# HTTP/2 support (including h2c). +http2 = ["actix-http/http2"] -# TLS via Rustls -rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] +# TLS via OpenSSL +openssl = ["http2", "actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] + +# 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. @@ -59,6 +116,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 } @@ -66,13 +131,13 @@ 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.2.2", features = ["http2", "ws"] } -actix-router = "0.5" -actix-web-codegen = { version = "4.1", 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.7" +ahash = "0.8" bytes = "1" bytestring = "1" cfg-if = "1" @@ -81,42 +146,43 @@ derive_more = "0.99.8" encoding_rs = "0.8" futures-core = { version = "0.3.17", default-features = false } futures-util = { version = "0.3.17", default-features = false } -http = "0.2.8" itoa = "1" language-tags = "0.3" 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" smallvec = "1.6.1" -socket2 = "0.4.0" +socket2 = "0.5" time = { version = "0.3", default-features = false, features = ["formatting"] } 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" -const-str = "0.4" -criterion = { version = "0.4", features = ["html_reports"] } -env_logger = "0.9" +brotli = "6" +const-str = "0.5" +core_affinity = "0.8" +criterion = { version = "0.5", features = ["html_reports"] } +env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false, features = ["std"] } rand = "0.8" -rcgen = "0.9" -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.9" } -tls-rustls = { package = "rustls", version = "0.20.0" } -tokio = { version = "1.18.4", features = ["rt-multi-thread", "macros"] } -zstd = "0.12" +tls-openssl = { package = "openssl", version = "0.10.55" } +tls-rustls = { package = "rustls", version = "0.23" } +tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } +zstd = "0.13" [[test]] name = "test_server" diff --git a/actix-web/MIGRATION-0.x.md b/actix-web/MIGRATION-0.x.md index 1b60c36d..6af51464 100644 --- a/actix-web/MIGRATION-0.x.md +++ b/actix-web/MIGRATION-0.x.md @@ -1,7 +1,6 @@ # 0.7.15 -- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in - your routes, you should use `%20`. +- The `' '` character is not percent decoded anymore before matching routes. If you need to use it in your routes, you should use `%20`. instead of @@ -29,13 +28,11 @@ fn main() { # 0.7.4 -- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple - even for handler with one parameter. +- `Route::with_config()`/`Route::with_async_config()` always passes configuration objects as tuple even for handler with one parameter. # 0.7 -- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload - use `HttpMessage::payload()` method. +- `HttpRequest` does not implement `Stream` anymore. If you need to read request payload use `HttpMessage::payload()` method. instead of @@ -60,8 +57,7 @@ fn index(req: HttpRequest) -> impl Responder { } ``` -- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) - trait uses `&HttpRequest` instead of `&mut HttpRequest`. +- [Middleware](https://actix.rs/actix-web/actix_web/middleware/trait.Middleware.html) trait uses `&HttpRequest` instead of `&mut HttpRequest`. - Removed `Route::with2()` and `Route::with3()` use tuple of extractors instead. @@ -81,14 +77,11 @@ fn index((query, json): (Query<..>, Json impl Responder {} - `Handler::handle()` accepts reference to `HttpRequest<_>` instead of value -- Removed deprecated `HttpServer::threads()`, use - [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. +- Removed deprecated `HttpServer::threads()`, use [HttpServer::workers()](https://actix.rs/actix-web/actix_web/server/struct.HttpServer.html#method.workers) instead. -- Renamed `client::ClientConnectorError::Connector` to - `client::ClientConnectorError::Resolver` +- Renamed `client::ClientConnectorError::Connector` to `client::ClientConnectorError::Resolver` -- `Route::with()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_config()` +- `Route::with()` does not return `ExtractorConfig`, to configure extractor use `Route::with_config()` instead of @@ -116,23 +109,19 @@ fn main() { } ``` -- `Route::with_async()` does not return `ExtractorConfig`, to configure - extractor use `Route::with_async_config()` +- `Route::with_async()` does not return `ExtractorConfig`, to configure extractor use `Route::with_async_config()` # 0.6 - `Path` extractor return `ErrorNotFound` on failure instead of `ErrorBadRequest` -- `ws::Message::Close` now includes optional close reason. - `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. +- `ws::Message::Close` now includes optional close reason. `ws::CloseCode::Status` and `ws::CloseCode::Empty` have been removed. - `HttpServer::threads()` renamed to `HttpServer::workers()`. -- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. - Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. +- `HttpServer::start_ssl()` and `HttpServer::start_tls()` deprecated. Use `HttpServer::bind_ssl()` and `HttpServer::bind_tls()` instead. -- `HttpRequest::extensions()` returns read only reference to the request's Extension - `HttpRequest::extensions_mut()` returns mutable reference. +- `HttpRequest::extensions()` returns read only reference to the request's Extension `HttpRequest::extensions_mut()` returns mutable reference. - Instead of @@ -146,8 +135,7 @@ fn main() { - `FromRequest::Result` has to implement `Into>` -- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) - is generic over `S` +- [`Responder::respond_to()`](https://actix.rs/actix-web/actix_web/trait.Responder.html#tymethod.respond_to) is generic over `S` - Use `Query` extractor instead of HttpRequest::query()`. @@ -163,23 +151,19 @@ or let q = Query::>::extract(req); ``` -- Websocket operations are implemented as `WsWriter` trait. - you need to use `use actix_web::ws::WsWriter` +- Websocket operations are implemented as `WsWriter` trait. you need to use `use actix_web::ws::WsWriter` # 0.5 -- `HttpResponseBuilder::body()`, `.finish()`, `.json()` - methods return `HttpResponse` instead of `Result` +- `HttpResponseBuilder::body()`, `.finish()`, `.json()` methods return `HttpResponse` instead of `Result` -- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` - moved to `actix_web::http` module +- `actix_web::Method`, `actix_web::StatusCode`, `actix_web::Version` moved to `actix_web::http` module - `actix_web::header` moved to `actix_web::http::header` - `NormalizePath` moved to `actix_web::http` module -- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, - shortcut for `actix_web::server::HttpServer::new()` +- `HttpServer` moved to `actix_web::server`, added new `actix_web::server::new()` function, shortcut for `actix_web::server::HttpServer::new()` - `DefaultHeaders` middleware does not use separate builder, all builder methods moved to type itself @@ -187,11 +171,9 @@ let q = Query::>::extract(req); - `CookieSessionBackendBuilder` removed, all methods moved to `CookieSessionBackend` type -- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` - functions should be used instead +- `actix_web::httpcodes` module is deprecated, `HttpResponse::Ok()`, `HttpResponse::Found()` and other `HttpResponse::XXX()` functions should be used instead -- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` - instead of `Result<_, http::Error>` +- `ClientRequestBuilder::body()` returns `Result<_, actix_web::Error>` instead of `Result<_, http::Error>` - `Application` renamed to a `App` diff --git a/actix-web/MIGRATION-1.0.md b/actix-web/MIGRATION-1.0.md index 94c6321a..d72bb63b 100644 --- a/actix-web/MIGRATION-1.0.md +++ b/actix-web/MIGRATION-1.0.md @@ -88,8 +88,7 @@ ) ``` -- Resource registration. 1.0 version uses generalized resource - registration via `.service()` method. +- Resource registration. 1.0 version uses generalized resource registration via `.service()` method. instead of @@ -97,9 +96,7 @@ App.new().resource("/welcome", |r| r.f(welcome)) ``` - use App's or Scope's `.service()` method. `.service()` method accepts - object that implements `HttpServiceFactory` trait. By default - actix-web provides `Resource` and `Scope` services. + use App's or Scope's `.service()` method. `.service()` method accepts object that implements `HttpServiceFactory` trait. By default actix-web provides `Resource` and `Scope` services. ```rust App.new().service( @@ -164,9 +161,7 @@ } ``` -- `.f()`, `.a()` and `.h()` handler registration methods have been removed. - Use `.to()` for handlers and `.to_async()` for async handlers. Handler function - must use extractors. +- `.f()`, `.a()` and `.h()` handler registration methods have been removed. Use `.to()` for handlers and `.to_async()` for async handlers. Handler function must use extractors. instead of @@ -210,9 +205,7 @@ } ``` -- `State` is now `Data`. You register Data during the App initialization process - and then access it from handlers either using a Data extractor or using - HttpRequest's api. +- `State` is now `Data`. You register Data during the App initialization process and then access it from handlers either using a Data extractor or using HttpRequest's api. instead of @@ -277,8 +270,7 @@ .route("/index.html", web::get().to(index)); ``` -- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` - method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. +- `HttpRequest::body()`, `HttpRequest::urlencoded()`, `HttpRequest::json()`, `HttpRequest::multipart()` method have been removed. Use `Bytes`, `String`, `Form`, `Json`, `Multipart` extractors instead. instead of @@ -317,8 +309,7 @@ use `use actix_multipart::Multipart` -- Response compression is not enabled by default. - To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. +- Response compression is not enabled by default. To enable, use `Compress` middleware, `App::new().wrap(Compress::default())`. - Session middleware moved to actix-session crate diff --git a/actix-web/MIGRATION-2.0.md b/actix-web/MIGRATION-2.0.md index 0455062d..41e6104b 100644 --- a/actix-web/MIGRATION-2.0.md +++ b/actix-web/MIGRATION-2.0.md @@ -1,21 +1,16 @@ # Migrating to 2.0.0 -- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to - `.await` on `run` method result, in that case it awaits server exit. +- `HttpServer::start()` renamed to `HttpServer::run()`. It also possible to `.await` on `run` method result, in that case it awaits server exit. -- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. - Stored data is available via `HttpRequest::app_data()` method at runtime. +- `App::register_data()` renamed to `App::app_data()` and accepts any type `T: 'static`. Stored data is available via `HttpRequest::app_data()` method at runtime. - Extractor configuration must be registered with `App::app_data()` instead of `App::data()` -- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` - replace `fn` with `async fn` to convert sync handler to async +- Sync handlers has been removed. `.to_async()` method has been renamed to `.to()` replace `fn` with `async fn` to convert sync handler to async -- `actix_http_test::TestServer` moved to `actix_web::test` module. To start - test server use `test::start()` or `test_start_with_config()` methods +- `actix_http_test::TestServer` moved to `actix_web::test` module. To start test server use `test::start()` or `test_start_with_config()` methods -- `ResponseError` trait has been reafctored. `ResponseError::error_response()` renders - http response. +- `ResponseError` trait has been refactored. `ResponseError::error_response()` renders http response. - Feature `rust-tls` renamed to `rustls` diff --git a/actix-web/MIGRATION-3.0.md b/actix-web/MIGRATION-3.0.md index 54bcd58b..89255e43 100644 --- a/actix-web/MIGRATION-3.0.md +++ b/actix-web/MIGRATION-3.0.md @@ -1,31 +1,23 @@ # Migrating to 3.0.0 -- The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to - simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. +- The return type for `ServiceRequest::app_data::()` was changed from returning a `Data` to simply a `T`. To access a `Data` use `ServiceRequest::app_data::>()`. - Cookie handling has been offloaded to the `cookie` crate: - `USERINFO_ENCODE_SET` is no longer exposed. Percent-encoding is still supported; check docs. - Some types now require lifetime parameters. -- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects - any `actix-web` method previously expecting a time v0.1 input. +- The time crate was updated to `v0.2`, a major breaking change to the time crate, which affects any `actix-web` method previously expecting a time v0.1 input. -- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now - result in `SameSite=None` being sent with the response Set-Cookie header. - To create a cookie without a SameSite attribute, remove any calls setting same_site. +- Setting a cookie's SameSite property, explicitly, to `SameSite::None` will now result in `SameSite=None` being sent with the response Set-Cookie header. To create a cookie without a SameSite attribute, remove any calls setting same_site. -- actix-http support for Actors messages was moved to actix-http crate and is enabled - with feature `actors` +- actix-http support for Actors messages was moved to actix-http crate and is enabled with feature `actors` -- content_length function is removed from actix-http. - You can set Content-Length by normally setting the response body or calling no_chunking function. +- content_length function is removed from actix-http. You can set Content-Length by normally setting the response body or calling no_chunking function. -- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a - `u64` instead of a `usize`. +- `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a `u64` instead of a `usize`. -- Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use - destructuring or `.into_inner()`. For example: +- Code that was using `path.` to access a `web::Path<(A, B, C)>`s elements now needs to use destructuring or `.into_inner()`. For example: ```rust // Previously: @@ -44,9 +36,7 @@ } ``` -- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. - It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, - or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. +- `middleware::NormalizePath` can now also be configured to trim trailing slashes instead of always keeping one. It will need `middleware::normalize::TrailingSlash` when being constructed with `NormalizePath::new(...)`, or for an easier migration you can replace `wrap(middleware::NormalizePath)` with `wrap(middleware::NormalizePath::new(TrailingSlash::MergeOnly))`. - `HttpServer::maxconn` is renamed to the more expressive `HttpServer::max_connections`. diff --git a/actix-web/MIGRATION-4.0.md b/actix-web/MIGRATION-4.0.md index fbeae068..08c89635 100644 --- a/actix-web/MIGRATION-4.0.md +++ b/actix-web/MIGRATION-4.0.md @@ -31,7 +31,6 @@ Headings marked with :warning: are **breaking behavioral changes**. They will pr - [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously) - [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain) - [`web::block`](#webblock) -- ## MSRV @@ -373,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 65076e0b..3f9d3e0d 100644 --- a/actix-web/README.md +++ b/actix-web/README.md @@ -5,17 +5,21 @@

+ + [![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.2.1)](https://docs.rs/actix-web/4.2.1) -![MSRV](https://img.shields.io/badge/rustc-1.59+-ab6000.svg) +[![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.2.1/status.svg)](https://deps.rs/crate/actix-web/4.2.1) +[![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) + +

@@ -33,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.59+ +- Runs on stable Rust 1.72+ ## Documentation @@ -94,7 +98,7 @@ You may consider checking out [this directory](https://github.com/actix/examples ## Benchmarks -One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite). +One of the fastest web frameworks available according to the [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r21&test=composite). ## License @@ -105,4 +109,4 @@ This project is licensed under either of the following licenses, at your option: ## Code of Conduct -Contribution to the actix-web repo is organized under the terms of the Contributor Covenant. The Actix team promises to intervene to uphold that code of conduct. +Contribution to the `actix/actix-web` repo is organized under the terms of the Contributor Covenant. The Actix team promises to intervene to uphold that code of conduct. diff --git a/actix-web/benches/responder.rs b/actix-web/benches/responder.rs index 20aae335..c675eadf 100644 --- a/actix-web/benches/responder.rs +++ b/actix-web/benches/responder.rs @@ -87,7 +87,7 @@ fn future_responder(c: &mut Criterion) { let start = Instant::now(); - let _res = rt.block_on(async { futs.await }); + let _res = rt.block_on(futs); start.elapsed() }) @@ -99,8 +99,7 @@ fn responder(c: &mut Criterion) { let req = TestRequest::default().to_http_request(); c.bench_function("responder", move |b| { b.iter_custom(|_| { - let responders = - (0..100_000).map(|_| StringResponder(String::from("Hello World!!"))); + let responders = (0..100_000).map(|_| StringResponder(String::from("Hello World!!"))); let start = Instant::now(); let _res = rt.block_on(async { diff --git a/actix-web/benches/server.rs b/actix-web/benches/server.rs index 2c9f71dc..0d45c940 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/benches/service.rs b/actix-web/benches/service.rs index 87e51f17..9b29df8e 100644 --- a/actix-web/benches/service.rs +++ b/actix-web/benches/service.rs @@ -1,11 +1,12 @@ -use actix_service::Service; -use actix_web::dev::{ServiceRequest, ServiceResponse}; -use actix_web::{web, App, Error, HttpResponse}; -use criterion::{criterion_main, Criterion}; -use std::cell::RefCell; -use std::rc::Rc; +use std::{cell::RefCell, rc::Rc}; -use actix_web::test::{init_service, ok_service, TestRequest}; +use actix_service::Service; +use actix_web::{ + dev::{ServiceRequest, ServiceResponse}, + test::{init_service, ok_service, TestRequest}, + web, App, Error, HttpResponse, +}; +use criterion::{criterion_main, Criterion}; /// Criterion Benchmark for async Service /// Should be used from within criterion group: diff --git a/actix-web/examples/basic.rs b/actix-web/examples/basic.rs index 60715f47..b8bc0982 100644 --- a/actix-web/examples/basic.rs +++ b/actix-web/examples/basic.rs @@ -1,5 +1,3 @@ -#![allow(clippy::uninlined_format_args)] - use actix_web::{get, middleware, web, App, HttpRequest, HttpResponse, HttpServer}; #[get("/resource1/{name}/index.html")] @@ -22,6 +20,8 @@ async fn no_params() -> &'static str { 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() .wrap(middleware::DefaultHeaders::new().add(("X-Version", "0.2"))) diff --git a/actix-web/examples/macroless.rs b/actix-web/examples/macroless.rs index d3589da2..78ffd45c 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/on-connect.rs b/actix-web/examples/on-connect.rs index 57017fcd..dc9273b4 100644 --- a/actix-web/examples/on-connect.rs +++ b/actix-web/examples/on-connect.rs @@ -4,13 +4,10 @@ //! For an example of extracting a client TLS certificate, see: //! -#![allow(clippy::uninlined_format_args)] - use std::{any::Any, io, net::SocketAddr}; use actix_web::{ - dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer, - Responder, + dev::Extensions, rt::net::TcpStream, web, App, HttpRequest, HttpResponse, HttpServer, Responder, }; #[allow(dead_code)] @@ -24,12 +21,9 @@ struct ConnectionInfo { async fn route_whoami(req: HttpRequest) -> impl Responder { match req.conn_data::() { Some(info) => HttpResponse::Ok().body(format!( - "Here is some info about your connection:\n\n{:#?}", - info + "Here is some info about your connection:\n\n{info:#?}", )), - None => { - HttpResponse::InternalServerError().body("Missing expected request extension data") - } + None => HttpResponse::InternalServerError().body("Missing expected request extension data"), } } @@ -54,8 +48,8 @@ async fn main() -> io::Result<()> { HttpServer::new(|| App::new().default_service(web::to(route_whoami))) .on_connect(get_conn_info) - .bind(bind)? - .workers(1) + .bind_auto_h2c(bind)? + .workers(2) .run() .await } diff --git a/actix-web/examples/uds.rs b/actix-web/examples/uds.rs index ba4b25a2..e854bb3b 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}; @@ -41,7 +39,7 @@ async fn main() -> std::io::Result<()> { ) .service(web::resource("/test1.html").to(|| async { "Test\r\n" })) }) - .bind_uds("/Users/fafhrd91/uds-test")? + .bind_uds("/Users/me/uds-test")? .workers(1) .run() .await diff --git a/actix-web/examples/worker-cpu-pin.rs b/actix-web/examples/worker-cpu-pin.rs new file mode 100644 index 00000000..58e06082 --- /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 353b82b1..3d86d1f9 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) => { @@ -264,12 +266,8 @@ where pub fn default_service(mut self, svc: F) -> Self where F: IntoServiceFactory, - U: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - > + 'static, + U: ServiceFactory + + 'static, U::InitError: fmt::Debug, { let svc = svc @@ -323,16 +321,7 @@ where /// Middleware can be applied similarly to individual `Scope`s and `Resource`s. /// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`]. /// - /// # Middleware Order - /// Notice that the keyword for registering middleware is `wrap`. As you register middleware - /// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first - /// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in - /// the builder chain). Consequently, the *first* middleware registered in the builder chain is - /// the *last* to start executing during request processing. - /// - /// Ordering is less obvious when wrapped services also have middleware applied. In this case, - /// middlewares are run in reverse order for `App` _and then_ in reverse order for the - /// wrapped service. + /// For more info on middleware take a look at the [`middleware` module][crate::middleware]. /// /// # Examples /// ``` @@ -482,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 0b5ba2ab..65a6ed87 100644 --- a/actix-web/src/app_service.rs +++ b/actix-web/src/app_service.rs @@ -21,7 +21,7 @@ use crate::{ Error, HttpResponse, }; -/// Service factory to convert `Request` to a `ServiceRequest`. +/// Service factory to convert [`Request`] to a [`ServiceRequest`]. /// /// It also executes data factories. pub struct AppInit @@ -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 @@ -155,7 +151,7 @@ where app_state: Rc, } -/// A collection of [`AppInitService`] state that shared across `HttpRequest`s. +/// A collection of state for [`AppInitService`] that is shared across [`HttpRequest`]s. pub(crate) struct AppInitServiceState { rmap: Rc, config: AppConfig, @@ -163,6 +159,7 @@ pub(crate) struct AppInitServiceState { } impl AppInitServiceState { + /// Constructs state collection from resource map and app config. pub(crate) fn new(rmap: Rc, config: AppConfig) -> Rc { Rc::new(AppInitServiceState { rmap, @@ -171,16 +168,19 @@ impl AppInitServiceState { }) } + /// Returns a reference to the application's resource map. #[inline] pub(crate) fn rmap(&self) -> &ResourceMap { &self.rmap } + /// Returns a reference to the application's configuration. #[inline] pub(crate) fn config(&self) -> &AppConfig { &self.config } + /// Returns a reference to the application's request pool. #[inline] pub(crate) fn pool(&self) -> &HttpRequestPool { &self.pool @@ -263,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)) } })); @@ -344,13 +345,17 @@ impl ServiceFactory for AppEntry { #[cfg(test)] mod tests { - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::Arc; + use std::sync::{ + atomic::{AtomicBool, Ordering}, + Arc, + }; use actix_service::Service; - use crate::test::{init_service, TestRequest}; - use crate::{web, App, HttpResponse}; + use crate::{ + test::{init_service, TestRequest}, + web, App, HttpResponse, + }; struct DropData(Arc); diff --git a/actix-web/src/config.rs b/actix-web/src/config.rs index 68bea34c..5e8b056f 100644 --- a/actix-web/src/config.rs +++ b/actix-web/src/config.rs @@ -141,14 +141,14 @@ impl AppConfig { self.secure } - /// Returns the socket address of the local half of this TCP connection + /// Returns the socket address of the local half of this TCP connection. pub fn local_addr(&self) -> SocketAddr { self.addr } #[cfg(test)] pub(crate) fn set_host(&mut self, host: &str) { - self.host = host.to_owned(); + host.clone_into(&mut self.host); } } @@ -232,12 +232,8 @@ impl ServiceConfig { pub fn default_service(&mut self, f: F) -> &mut Self where F: IntoServiceFactory, - U: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - > + 'static, + U: ServiceFactory + + 'static, U::InitError: std::fmt::Debug, { let svc = f @@ -308,9 +304,11 @@ mod tests { use bytes::Bytes; use super::*; - use crate::http::{Method, StatusCode}; - use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest}; - use crate::{web, App, HttpRequest, HttpResponse}; + use crate::{ + http::{Method, StatusCode}, + test::{assert_body_eq, call_service, init_service, read_body, TestRequest}, + web, App, HttpRequest, HttpResponse, + }; // allow deprecated `ServiceConfig::data` #[allow(deprecated)] diff --git a/actix-web/src/data.rs b/actix-web/src/data.rs index 89104a1a..acbb8e23 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; @@ -186,12 +203,14 @@ mod tests { #[allow(deprecated)] #[actix_rt::test] async fn test_data_extractor() { - let srv = init_service(App::new().data("TEST".to_string()).service( - web::resource("/").to(|data: web::Data| { - assert_eq!(data.to_lowercase(), "test"); - HttpResponse::Ok() - }), - )) + let srv = init_service( + App::new() + .data("TEST".to_string()) + .service(web::resource("/").to(|data: web::Data| { + assert_eq!(data.to_lowercase(), "test"); + HttpResponse::Ok() + })), + ) .await; let req = TestRequest::default().to_request(); @@ -286,16 +305,17 @@ mod tests { #[allow(deprecated)] #[actix_rt::test] async fn test_override_data() { - let srv = - init_service(App::new().data(1usize).service( - web::resource("/").data(10usize).route(web::get().to( + let srv = init_service( + App::new() + .data(1usize) + .service(web::resource("/").data(10usize).route(web::get().to( |data: web::Data| { assert_eq!(**data, 10); HttpResponse::Ok() }, - )), - )) - .await; + ))), + ) + .await; let req = TestRequest::default().to_request(); let resp = srv.call(req).await.unwrap(); diff --git a/actix-web/src/dev.rs b/actix-web/src/dev.rs index 5c7adfda..2a0791a1 100644 --- a/actix-web/src/dev.rs +++ b/actix-web/src/dev.rs @@ -7,26 +7,25 @@ //! - [`ConnectionInfo`]: Connection information //! - [`PeerAddr`]: Connection information +#[cfg(feature = "__compress")] +pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead}; +use actix_router::Patterns; pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_server::{Server, ServerHandle}; pub use actix_service::{ always_ready, fn_factory, fn_service, forward_ready, Service, ServiceFactory, Transform, }; -#[cfg(feature = "__compress")] -pub use actix_http::encoding::Decoder as Decompress; - -pub use crate::config::{AppConfig, AppService}; #[doc(hidden)] pub use crate::handler::Handler; -pub use crate::info::{ConnectionInfo, PeerAddr}; -pub use crate::rmap::ResourceMap; -pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}; - -pub use crate::types::{JsonBody, Readlines, UrlEncoded}; - -use actix_router::Patterns; +pub use crate::{ + config::{AppConfig, AppService}, + info::{ConnectionInfo, PeerAddr}, + rmap::ResourceMap, + service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService}, + types::{JsonBody, Readlines, UrlEncoded}, +}; pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns { match &mut patterns { diff --git a/actix-web/src/error/error.rs b/actix-web/src/error/error.rs index 3a5a128f..670a58a0 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/macros.rs b/actix-web/src/error/macros.rs index 78b1ed9f..8634557c 100644 --- a/actix-web/src/error/macros.rs +++ b/actix-web/src/error/macros.rs @@ -42,8 +42,7 @@ macro_rules! downcast_dyn { /// Downcasts generic body to a specific type. #[allow(dead_code)] pub fn downcast_ref(&self) -> Option<&T> { - if self.__private_get_type_id__(PrivateHelper(())).0 - == std::any::TypeId::of::() + if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::() { // SAFETY: external crates cannot override the default // implementation of `__private_get_type_id__`, since @@ -59,8 +58,7 @@ macro_rules! downcast_dyn { /// Downcasts a generic body to a mutable specific type. #[allow(dead_code)] pub fn downcast_mut(&mut self) -> Option<&mut T> { - if self.__private_get_type_id__(PrivateHelper(())).0 - == std::any::TypeId::of::() + if self.__private_get_type_id__(PrivateHelper(())).0 == std::any::TypeId::of::() { // SAFETY: external crates cannot override the default // implementation of `__private_get_type_id__`, since @@ -76,7 +74,8 @@ macro_rules! downcast_dyn { }; } -pub(crate) use {downcast_dyn, downcast_get_type_id}; +pub(crate) use downcast_dyn; +pub(crate) use downcast_get_type_id; #[cfg(test)] mod tests { diff --git a/actix-web/src/error/mod.rs b/actix-web/src/error/mod.rs index 604c539f..25535332 100644 --- a/actix-web/src/error/mod.rs +++ b/actix-web/src/error/mod.rs @@ -5,14 +5,10 @@ // expanded manually. // // See -pub use actix_http::error::{ - ContentTypeError, DispatchError, HttpError, ParseError, PayloadError, -}; - +pub use actix_http::error::{ContentTypeError, DispatchError, HttpError, ParseError, PayloadError}; use derive_more::{Display, Error, From}; use serde_json::error::Error as JsonError; -use serde_urlencoded::de::Error as FormDeError; -use serde_urlencoded::ser::Error as FormError; +use serde_urlencoded::{de::Error as FormDeError, ser::Error as FormError}; use url::ParseError as UrlParseError; use crate::http::StatusCode; @@ -23,10 +19,8 @@ mod internal; mod macros; mod response_error; -pub use self::error::Error; -pub use self::internal::*; -pub use self::response_error::ResponseError; -pub(crate) use macros::{downcast_dyn, downcast_get_type_id}; +pub(crate) use self::macros::{downcast_dyn, downcast_get_type_id}; +pub use self::{error::Error, internal::*, response_error::ResponseError}; /// A convenience [`Result`](std::result::Result) for Actix Web operations. /// @@ -106,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, } @@ -238,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/error/response_error.rs b/actix-web/src/error/response_error.rs index 7d2c0615..f5d8cf46 100644 --- a/actix-web/src/error/response_error.rs +++ b/actix-web/src/error/response_error.rs @@ -152,7 +152,7 @@ mod tests { let resp_err: &dyn ResponseError = &err; let err = resp_err.downcast_ref::().unwrap(); - assert_eq!(err.to_string(), "Payload reached size limit."); + assert_eq!(err.to_string(), "payload reached size limit"); let not_err = resp_err.downcast_ref::(); assert!(not_err.is_none()); diff --git a/actix-web/src/extract.rs b/actix-web/src/extract.rs index d4f5cc91..249b5611 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 { .. } => {}, @@ -416,6 +416,10 @@ mod tuple_from_req { tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J } tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K } tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L } + tuple_from_req! { TupleFromRequest13; A, B, C, D, E, F, G, H, I, J, K, L, M } + tuple_from_req! { TupleFromRequest14; A, B, C, D, E, F, G, H, I, J, K, L, M, N } + tuple_from_req! { TupleFromRequest15; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O } + tuple_from_req! { TupleFromRequest16; A, B, C, D, E, F, G, H, I, J, K, L, M, N, O, P } } #[cfg(test)] @@ -425,8 +429,10 @@ mod tests { use serde::Deserialize; use super::*; - use crate::test::TestRequest; - use crate::types::{Form, FormConfig}; + use crate::{ + test::TestRequest, + types::{Form, FormConfig}, + }; #[derive(Deserialize, Debug, PartialEq)] struct Info { diff --git a/actix-web/src/guard/acceptable.rs b/actix-web/src/guard/acceptable.rs index a31494a1..8fa7165c 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 new file mode 100644 index 00000000..a971a3e3 --- /dev/null +++ b/actix-web/src/guard/host.rs @@ -0,0 +1,209 @@ +use actix_http::{header, uri::Uri, RequestHead}; + +use super::{Guard, GuardContext}; + +/// Creates a guard that matches requests targeting a specific host. +/// +/// # Matching Host +/// This guard will: +/// - match against the `Host` header, if present; +/// - fall-back to matching against the request target's host, if present; +/// - return false if host cannot be determined; +/// +/// # Matching Scheme +/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using +/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent +/// the guard from matching successfully. +/// +/// # Examples +/// The `Host` guard can be used to set up a form of [virtual hosting] within a single app. +/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard +/// definitions they become safe to use in this way. Without these host guards, only routes under +/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` +/// and `localhost` as the `Host` guards. +/// ``` +/// use actix_web::{web, http::Method, guard, App, HttpResponse}; +/// +/// App::new() +/// .service( +/// web::scope("") +/// .guard(guard::Host("www.rust-lang.org")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("marketing site") +/// })), +/// ) +/// .service( +/// web::scope("") +/// .guard(guard::Host("play.rust-lang.org")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("playground frontend") +/// })), +/// ); +/// ``` +/// +/// The example below additionally guards on the host URI's scheme. This could allow routing to +/// different handlers for `http:` vs `https:` visitors; to redirect, for example. +/// ``` +/// use actix_web::{web, guard::Host, HttpResponse}; +/// +/// web::scope("/admin") +/// .guard(Host("admin.rust-lang.org").scheme("https")) +/// .default_service(web::to(|| async { +/// HttpResponse::Ok().body("admin connection is secure") +/// })); +/// ``` +/// +/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting +#[allow(non_snake_case)] +pub fn Host(host: impl AsRef) -> HostGuard { + HostGuard { + host: host.as_ref().to_string(), + scheme: None, + } +} + +fn get_host_uri(req: &RequestHead) -> Option { + req.headers + .get(header::HOST) + .and_then(|host_value| host_value.to_str().ok()) + .or_else(|| req.uri.host()) + .and_then(|host| host.parse().ok()) +} + +#[doc(hidden)] +pub struct HostGuard { + host: String, + scheme: Option, +} + +impl HostGuard { + /// Set request scheme to match + pub fn scheme>(mut self, scheme: H) -> HostGuard { + self.scheme = Some(scheme.as_ref().to_string()); + self + } +} + +impl Guard for HostGuard { + fn check(&self, ctx: &GuardContext<'_>) -> bool { + // parse host URI from header or request target + let req_host_uri = match get_host_uri(ctx.head()) { + Some(uri) => uri, + + // no match if host cannot be determined + None => return false, + }; + + match req_host_uri.host() { + // fall through to scheme checks + Some(uri_host) if self.host == uri_host => {} + + // Either: + // - request's host does not match guard's host; + // - It was possible that the parsed URI from request target did not contain a host. + _ => return false, + } + + if let Some(ref scheme) = self.scheme { + if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { + return scheme == req_host_uri_scheme; + } + + // TODO: is this the correct behavior? + // falls through if scheme cannot be determined + } + + // all conditions passed + true + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::TestRequest; + + #[test] + fn host_from_header() { + let req = TestRequest::default() + .insert_header(( + header::HOST, + header::HeaderValue::from_static("www.rust-lang.org"), + )) + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_without_header() { + let req = TestRequest::default() + .uri("www.rust-lang.org") + .to_srv_request(); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } + + #[test] + fn host_scheme() { + let req = TestRequest::default() + .insert_header(( + header::HOST, + header::HeaderValue::from_static("https://www.rust-lang.org"), + )) + .to_srv_request(); + + let host = Host("www.rust-lang.org").scheme("https"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org"); + assert!(host.check(&req.guard_ctx())); + + let host = Host("www.rust-lang.org").scheme("http"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("blog.rust-lang.org").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("crates.io").scheme("https"); + assert!(!host.check(&req.guard_ctx())); + + let host = Host("localhost"); + assert!(!host.check(&req.guard_ctx())); + } +} diff --git a/actix-web/src/guard/mod.rs b/actix-web/src/guard/mod.rs index e086f864..41609953 100644 --- a/actix-web/src/guard/mod.rs +++ b/actix-web/src/guard/mod.rs @@ -18,6 +18,7 @@ //! There are shortcuts for routes with method guards in the [`web`](crate::web) module: //! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by //! the following calls are equivalent: +//! //! - `web::get()` (recommended form) //! - `web::route().guard(guard::Get())` //! @@ -28,9 +29,11 @@ //! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting. //! //! # Examples +//! //! In the following code, the `/guarded` resource has one defined route whose handler will only be -//! called if the request method is `POST` and there is a request header with name and value equal -//! to `x-guarded` and `secret`, respectively. +//! called if the request method is GET or POST and there is a `x-guarded` request header with value +//! equal to `secret`. +//! //! ``` //! use actix_web::{web, http::Method, guard, HttpResponse}; //! @@ -48,16 +51,20 @@ use std::{ cell::{Ref, RefMut}, - convert::TryFrom, rc::Rc, }; -use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead}; +use actix_http::{header, Extensions, Method as HttpMethod, RequestHead}; use crate::{http::header::Header, service::ServiceRequest, HttpMessage as _}; mod acceptable; -pub use self::acceptable::Acceptable; +mod host; + +pub use self::{ + acceptable::Acceptable, + host::{Host, HostGuard}, +}; /// Provides access to request parts that are useful during routing. #[derive(Debug)] @@ -103,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. @@ -371,127 +384,9 @@ impl Guard for HeaderGuard { } } -/// Creates a guard that matches requests targetting a specific host. -/// -/// # Matching Host -/// This guard will: -/// - match against the `Host` header, if present; -/// - fall-back to matching against the request target's host, if present; -/// - return false if host cannot be determined; -/// -/// # Matching Scheme -/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using -/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent -/// the guard from matching successfully. -/// -/// # Examples -/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards. -/// -/// The example below additionally guards on the host URI's scheme. This could allow routing to -/// different handlers for `http:` vs `https:` visitors; to redirect, for example. -/// ``` -/// use actix_web::{web, guard::Host, HttpResponse}; -/// -/// web::scope("/admin") -/// .guard(Host("admin.rust-lang.org").scheme("https")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("admin connection is secure") -/// })); -/// ``` -/// -/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app. -/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard -/// definitions they become safe to use in this way. Without these host guards, only routes under -/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1` -/// and `localhost` as the `Host` guards. -/// ``` -/// use actix_web::{web, http::Method, guard, App, HttpResponse}; -/// -/// App::new() -/// .service( -/// web::scope("") -/// .guard(guard::Host("www.rust-lang.org")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("marketing site") -/// })), -/// ) -/// .service( -/// web::scope("") -/// .guard(guard::Host("play.rust-lang.org")) -/// .default_service(web::to(|| async { -/// HttpResponse::Ok().body("playground frontend") -/// })), -/// ); -/// ``` -/// -/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting -#[allow(non_snake_case)] -pub fn Host(host: impl AsRef) -> HostGuard { - HostGuard { - host: host.as_ref().to_string(), - scheme: None, - } -} - -fn get_host_uri(req: &RequestHead) -> Option { - req.headers - .get(header::HOST) - .and_then(|host_value| host_value.to_str().ok()) - .or_else(|| req.uri.host()) - .and_then(|host| host.parse().ok()) -} - -#[doc(hidden)] -pub struct HostGuard { - host: String, - scheme: Option, -} - -impl HostGuard { - /// Set request scheme to match - pub fn scheme>(mut self, scheme: H) -> HostGuard { - self.scheme = Some(scheme.as_ref().to_string()); - self - } -} - -impl Guard for HostGuard { - fn check(&self, ctx: &GuardContext<'_>) -> bool { - // parse host URI from header or request target - let req_host_uri = match get_host_uri(ctx.head()) { - Some(uri) => uri, - - // no match if host cannot be determined - None => return false, - }; - - match req_host_uri.host() { - // fall through to scheme checks - Some(uri_host) if self.host == uri_host => {} - - // Either: - // - request's host does not match guard's host; - // - It was possible that the parsed URI from request target did not contain a host. - _ => return false, - } - - if let Some(ref scheme) = self.scheme { - if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() { - return scheme == req_host_uri_scheme; - } - - // TODO: is this the correct behavior? - // falls through if scheme cannot be determined - } - - // all conditions passed - true - } -} - #[cfg(test)] mod tests { - use actix_http::{header, Method}; + use actix_http::Method; use super::*; use crate::test::TestRequest; @@ -515,90 +410,6 @@ mod tests { assert!(!hdr.check(&req.guard_ctx())); } - #[test] - fn host_from_header() { - let req = TestRequest::default() - .insert_header(( - header::HOST, - header::HeaderValue::from_static("www.rust-lang.org"), - )) - .to_srv_request(); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - - #[test] - fn host_without_header() { - let req = TestRequest::default() - .uri("www.rust-lang.org") - .to_srv_request(); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - - #[test] - fn host_scheme() { - let req = TestRequest::default() - .insert_header(( - header::HOST, - header::HeaderValue::from_static("https://www.rust-lang.org"), - )) - .to_srv_request(); - - let host = Host("www.rust-lang.org").scheme("https"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org"); - assert!(host.check(&req.guard_ctx())); - - let host = Host("www.rust-lang.org").scheme("http"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("blog.rust-lang.org").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("crates.io").scheme("https"); - assert!(!host.check(&req.guard_ctx())); - - let host = Host("localhost"); - assert!(!host.check(&req.guard_ctx())); - } - #[test] fn method_guards() { let get_req = TestRequest::get().to_srv_request(); @@ -707,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 522a48b8..6e4e2250 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 { /// ... @@ -151,6 +158,10 @@ factory_tuple! { A B C D E F G H I } factory_tuple! { A B C D E F G H I J } factory_tuple! { A B C D E F G H I J K } factory_tuple! { A B C D E F G H I J K L } +factory_tuple! { A B C D E F G H I J K L M } +factory_tuple! { A B C D E F G H I J K L M N } +factory_tuple! { A B C D E F G H I J K L M N O } +factory_tuple! { A B C D E F G H I J K L M N O P } #[cfg(test)] mod tests { @@ -163,10 +174,11 @@ 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: (), + _13: (), _14: (), _15: (), _16: (), ) {} assert_impl_handler(handler_min); diff --git a/actix-web/src/http/header/accept.rs b/actix-web/src/http/header/accept.rs index 1be136b1..99c95175 100644 --- a/actix-web/src/http/header/accept.rs +++ b/actix-web/src/http/header/accept.rs @@ -78,7 +78,7 @@ common_header! { // Tests from the RFC crate::http::header::common_header_test!( test1, - vec![b"audio/*; q=0.2, audio/basic"], + [b"audio/*; q=0.2, audio/basic"], Some(Accept(vec![ QualityItem::new("audio/*".parse().unwrap(), q(0.2)), QualityItem::max("audio/basic".parse().unwrap()), @@ -86,7 +86,7 @@ common_header! { crate::http::header::common_header_test!( test2, - vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], + [b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"], Some(Accept(vec![ QualityItem::new(mime::TEXT_PLAIN, q(0.5)), QualityItem::max(mime::TEXT_HTML), @@ -99,13 +99,13 @@ common_header! { // Custom tests crate::http::header::common_header_test!( test3, - vec![b"text/plain; charset=utf-8"], + [b"text/plain; charset=utf-8"], Some(Accept(vec![ QualityItem::max(mime::TEXT_PLAIN_UTF_8), ]))); crate::http::header::common_header_test!( test4, - vec![b"text/plain; charset=utf-8; q=0.5"], + [b"text/plain; charset=utf-8; q=0.5"], Some(Accept(vec![ QualityItem::new(mime::TEXT_PLAIN_UTF_8, q(0.5)), ]))); diff --git a/actix-web/src/http/header/accept_charset.rs b/actix-web/src/http/header/accept_charset.rs index c7f7e1a6..43a7861f 100644 --- a/actix-web/src/http/header/accept_charset.rs +++ b/actix-web/src/http/header/accept_charset.rs @@ -57,6 +57,6 @@ common_header! { test_parse_and_format { // Test case from RFC - common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]); + common_header_test!(test1, [b"iso-8859-5, unicode-1-1;q=0.8"]); } } diff --git a/actix-web/src/http/header/accept_encoding.rs b/actix-web/src/http/header/accept_encoding.rs index 8c35179b..19d64992 100644 --- a/actix-web/src/http/header/accept_encoding.rs +++ b/actix-web/src/http/header/accept_encoding.rs @@ -50,31 +50,31 @@ common_header! { (AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem>)* test_parse_and_format { - common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![]))); - common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![]))); + common_header_test!(no_headers, [b""; 0], Some(AcceptEncoding(vec![]))); + common_header_test!(empty_header, [b""; 1], Some(AcceptEncoding(vec![]))); common_header_test!( order_of_appearance, - vec![b"br, gzip"], + [b"br, gzip"], Some(AcceptEncoding(vec![ QualityItem::max(Preference::Specific(Encoding::brotli())), QualityItem::max(Preference::Specific(Encoding::gzip())), ])) ); - common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![ + common_header_test!(any, [b"*"], Some(AcceptEncoding(vec![ QualityItem::max(Preference::Any), ]))); // Note: Removed quality 1 from gzip - common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]); + common_header_test!(implicit_quality, [b"gzip, identity; q=0.5, *;q=0"]); // Note: Removed quality 1 from gzip - common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]); + common_header_test!(implicit_quality_out_of_order, [b"compress;q=0.5, gzip"]); common_header_test!( only_gzip_no_identity, - vec![b"gzip, *; q=0"], + [b"gzip, *; q=0"], Some(AcceptEncoding(vec![ QualityItem::max(Preference::Specific(Encoding::gzip())), QualityItem::zero(Preference::Any), @@ -94,10 +94,7 @@ impl AcceptEncoding { /// includes the server's supported encodings in the body plus a [`Vary`] header. /// /// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary - pub fn negotiate<'a>( - &self, - supported: impl Iterator, - ) -> Option { + pub fn negotiate<'a>(&self, supported: impl Iterator) -> Option { // 1. If no Accept-Encoding field is in the request, any content-coding is considered // acceptable by the user agent. @@ -152,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 @@ -170,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 @@ -177,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; } } @@ -206,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() @@ -213,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. @@ -375,18 +402,16 @@ mod tests { Some(Encoding::deflate()) ); assert_eq!( - test.negotiate( - [Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter() - ), + test.negotiate([Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()), Some(Encoding::gzip()) ); 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()) ); } @@ -403,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] @@ -425,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/accept_language.rs b/actix-web/src/http/header/accept_language.rs index 9943e121..b1d588f8 100644 --- a/actix-web/src/http/header/accept_language.rs +++ b/actix-web/src/http/header/accept_language.rs @@ -58,19 +58,19 @@ common_header! { (AcceptLanguage, header::ACCEPT_LANGUAGE) => (QualityItem>)* test_parse_and_format { - common_header_test!(no_headers, vec![b""; 0], Some(AcceptLanguage(vec![]))); + common_header_test!(no_headers, [b""; 0], Some(AcceptLanguage(vec![]))); - common_header_test!(empty_header, vec![b""; 1], Some(AcceptLanguage(vec![]))); + common_header_test!(empty_header, [b""; 1], Some(AcceptLanguage(vec![]))); common_header_test!( example_from_rfc, - vec![b"da, en-gb;q=0.8, en;q=0.7"] + [b"da, en-gb;q=0.8, en;q=0.7"] ); common_header_test!( not_ordered_by_weight, - vec![b"en-US, en; q=0.5, fr"], + [b"en-US, en; q=0.5, fr"], Some(AcceptLanguage(vec![ QualityItem::max("en-US".parse().unwrap()), QualityItem::new("en".parse().unwrap(), q(0.5)), @@ -80,7 +80,7 @@ common_header! { common_header_test!( has_wildcard, - vec![b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], + [b"fr-CH, fr; q=0.9, en; q=0.8, de; q=0.7, *; q=0.5"], Some(AcceptLanguage(vec![ QualityItem::max("fr-CH".parse().unwrap()), QualityItem::new("fr".parse().unwrap(), q(0.9)), @@ -137,7 +137,7 @@ impl AcceptLanguage { b.quality.cmp(&a.quality) }); - types.into_iter().map(|qitem| qitem.item).collect() + types.into_iter().map(|q_item| q_item.item).collect() } } diff --git a/actix-web/src/http/header/allow.rs b/actix-web/src/http/header/allow.rs index d0ef9648..b1c35c3d 100644 --- a/actix-web/src/http/header/allow.rs +++ b/actix-web/src/http/header/allow.rs @@ -48,15 +48,18 @@ crate::http::header::common_header! { (Allow, header::ALLOW) => (Method)* test_parse_and_format { - // From the RFC + // from the RFC + crate::http::header::common_header_test!( test1, - vec![b"GET, HEAD, PUT"], + [b"GET, HEAD, PUT"], Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT]))); - // Own tests + + // other tests + crate::http::header::common_header_test!( test2, - vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], + [b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"], Some(HeaderField(vec![ Method::OPTIONS, Method::GET, @@ -67,9 +70,10 @@ crate::http::header::common_header! { Method::TRACE, Method::CONNECT, Method::PATCH]))); + crate::http::header::common_header_test!( test3, - vec![b""], + [b""], Some(HeaderField(Vec::::new()))); } } diff --git a/actix-web/src/http/header/cache_control.rs b/actix-web/src/http/header/cache_control.rs index 37629313..77e22d1c 100644 --- a/actix-web/src/http/header/cache_control.rs +++ b/actix-web/src/http/header/cache_control.rs @@ -47,13 +47,13 @@ common_header! { (CacheControl, header::CACHE_CONTROL) => (CacheDirective)+ test_parse_and_format { - common_header_test!(no_headers, vec![b""; 0], None); - common_header_test!(empty_header, vec![b""; 1], None); - common_header_test!(bad_syntax, vec![b"foo="], None); + common_header_test!(no_headers, [b""; 0], None); + common_header_test!(empty_header, [b""; 1], None); + common_header_test!(bad_syntax, [b"foo="], None); common_header_test!( multiple_headers, - vec![&b"no-cache"[..], &b"private"[..]], + [&b"no-cache"[..], &b"private"[..]], Some(CacheControl(vec![ CacheDirective::NoCache, CacheDirective::Private, @@ -62,7 +62,7 @@ common_header! { common_header_test!( argument, - vec![b"max-age=100, private"], + [b"max-age=100, private"], Some(CacheControl(vec![ CacheDirective::MaxAge(100), CacheDirective::Private, @@ -71,7 +71,7 @@ common_header! { common_header_test!( extension, - vec![b"foo, bar=baz"], + [b"foo, bar=baz"], Some(CacheControl(vec![ CacheDirective::Extension("foo".to_owned(), None), CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned())), diff --git a/actix-web/src/http/header/content_disposition.rs b/actix-web/src/http/header/content_disposition.rs index f743302a..9725cd19 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; @@ -592,9 +595,8 @@ mod tests { fn test_from_raw_basic() { assert!(ContentDisposition::from_raw(&HeaderValue::from_static("")).is_err()); - let a = HeaderValue::from_static( - "form-data; dummy=3; name=upload; filename=\"sample.png\"", - ); + let a = + HeaderValue::from_static("form-data; dummy=3; name=upload; filename=\"sample.png\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, @@ -648,8 +650,8 @@ mod tests { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', - b'a', b't', b'e', b's', + 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a', + b't', b'e', b's', ], })], }; @@ -665,8 +667,8 @@ mod tests { charset: Charset::Ext(String::from("UTF-8")), language_tag: None, value: vec![ - 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', - b'a', b't', b'e', b's', + 0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20, 0xe2, 0x82, 0xac, 0x20, b'r', b'a', + b't', b'e', b's', ], })], }; @@ -742,8 +744,8 @@ mod tests { }; assert_eq!(a, b); - let a = ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")) - .unwrap(); + let a = + ContentDisposition::from_raw(&HeaderValue::from_static("unknown-disp-param")).unwrap(); let b = ContentDisposition { disposition: DispositionType::Ext(String::from("unknown-disp-param")), parameters: vec![], @@ -782,8 +784,7 @@ mod tests { Mainstream browsers like Firefox (gecko) and Chrome use UTF-8 directly as above. (And now, only UTF-8 is handled by this implementation.) */ - let a = - HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap(); + let a = HeaderValue::from_str("form-data; name=upload; filename=\"文件.webp\"").unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, @@ -803,9 +804,7 @@ mod tests { disposition: DispositionType::FormData, parameters: vec![ DispositionParam::Name(String::from("upload")), - DispositionParam::Filename(String::from( - "余固知謇謇之為患兮,忍而不能舍也.pptx", - )), + DispositionParam::Filename(String::from("余固知謇謇之為患兮,忍而不能舍也.pptx")), ], }; assert_eq!(a, b); @@ -870,8 +869,7 @@ mod tests { }; assert_eq!(a, b); - let a = - HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\""); + let a = HeaderValue::from_static("form-data; name=photo; filename=\"%74%65%73%74.png\""); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let b = ContentDisposition { disposition: DispositionType::FormData, diff --git a/actix-web/src/http/header/content_language.rs b/actix-web/src/http/header/content_language.rs index ff317e1d..5b0797ef 100644 --- a/actix-web/src/http/header/content_language.rs +++ b/actix-web/src/http/header/content_language.rs @@ -48,7 +48,7 @@ common_header! { (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem)+ test_parse_and_format { - crate::http::header::common_header_test!(test1, vec![b"da"]); - crate::http::header::common_header_test!(test2, vec![b"mi, en"]); + crate::http::header::common_header_test!(test1, [b"da"]); + crate::http::header::common_header_test!(test2, [b"mi, en"]); } } diff --git a/actix-web/src/http/header/content_length.rs b/actix-web/src/http/header/content_length.rs new file mode 100644 index 00000000..557c7c9f --- /dev/null +++ b/actix-web/src/http/header/content_length.rs @@ -0,0 +1,238 @@ +use std::{convert::Infallible, str}; + +use derive_more::{Deref, DerefMut}; + +use crate::{ + error::ParseError, + http::header::{ + from_one_raw_str, Header, HeaderName, HeaderValue, TryIntoHeaderValue, CONTENT_LENGTH, + }, + HttpMessage, +}; + +/// `Content-Length` header, defined in [RFC 9110 §8.6]. +/// +/// The Content-Length +/// +/// # ABNF +/// +/// ```plain +/// Content-Length = 1*DIGIT +/// ``` +/// +/// # Example Values +/// +/// - `0` +/// - `3495` +/// +/// # Examples +/// +/// ``` +/// use actix_web::{http::header::ContentLength, HttpResponse}; +/// +/// let res_empty = HttpResponse::Ok() +/// .insert_header(ContentLength(0)); +/// +/// let res_fake_cl = HttpResponse::Ok() +/// .insert_header(ContentLength(3_495)); +/// ``` +/// +/// [RFC 9110 §8.6]: https://www.rfc-editor.org/rfc/rfc9110#name-content-length +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord, Deref, DerefMut)] +pub struct ContentLength(pub usize); + +impl ContentLength { + /// Returns Content-Length value. + pub fn into_inner(&self) -> usize { + self.0 + } +} + +impl str::FromStr for ContentLength { + type Err = ::Err; + + #[inline] + fn from_str(val: &str) -> Result { + let val = val.trim(); + + // decoder prevents this case + debug_assert!(!val.starts_with('+')); + + val.parse().map(Self) + } +} + +impl TryIntoHeaderValue for ContentLength { + type Error = Infallible; + + fn try_into_value(self) -> Result { + Ok(HeaderValue::from(self.0)) + } +} + +impl Header for ContentLength { + fn name() -> HeaderName { + CONTENT_LENGTH + } + + fn parse(msg: &M) -> Result { + let val = from_one_raw_str(msg.headers().get(Self::name()))?; + + // decoder prevents multiple CL headers + debug_assert_eq!(msg.headers().get_all(Self::name()).count(), 1); + + Ok(val) + } +} + +impl From for usize { + fn from(ContentLength(len): ContentLength) -> Self { + len + } +} + +impl From for ContentLength { + fn from(len: usize) -> Self { + ContentLength(len) + } +} + +impl PartialEq for ContentLength { + fn eq(&self, other: &usize) -> bool { + self.0 == *other + } +} + +impl PartialEq for usize { + fn eq(&self, other: &ContentLength) -> bool { + *self == other.0 + } +} + +impl PartialOrd for ContentLength { + fn partial_cmp(&self, other: &usize) -> Option { + self.0.partial_cmp(other) + } +} + +impl PartialOrd for usize { + fn partial_cmp(&self, other: &ContentLength) -> Option { + self.partial_cmp(&other.0) + } +} + +#[cfg(test)] +mod tests { + use std::fmt; + + use super::*; + use crate::{test::TestRequest, HttpRequest}; + + fn req_from_raw_headers, V: AsRef<[u8]>>( + header_lines: I, + ) -> HttpRequest { + header_lines + .into_iter() + .fold(TestRequest::default(), |req, item| { + req.append_header((H::name(), item.as_ref().to_vec())) + }) + .to_http_request() + } + + #[track_caller] + pub(crate) fn assert_parse_fail< + H: Header + fmt::Debug, + I: IntoIterator, + V: AsRef<[u8]>, + >( + headers: I, + ) { + let req = req_from_raw_headers::(headers); + H::parse(&req).unwrap_err(); + } + + #[track_caller] + pub(crate) fn assert_parse_eq< + H: Header + fmt::Debug + PartialEq, + I: IntoIterator, + V: AsRef<[u8]>, + >( + headers: I, + expect: H, + ) { + let req = req_from_raw_headers::(headers); + assert_eq!(H::parse(&req).unwrap(), expect); + } + + #[test] + fn missing_header() { + assert_parse_fail::([""; 0]); + assert_parse_fail::([""]); + } + + #[test] + fn bad_header() { + assert_parse_fail::(["-123"]); + assert_parse_fail::(["123_456"]); + assert_parse_fail::(["123.456"]); + + // too large for u64 (2^64, 2^64 + 1) + assert_parse_fail::(["18446744073709551616"]); + assert_parse_fail::(["18446744073709551617"]); + + // hex notation + assert_parse_fail::(["0x123"]); + + // multi-value + assert_parse_fail::(["0, 123"]); + } + + #[test] + #[should_panic] + fn bad_header_plus() { + // prevented by HTTP decoder anyway + assert_parse_fail::(["+123"]); + } + + #[test] + #[should_panic] + fn bad_multiple_value() { + // prevented by HTTP decoder anyway + assert_parse_fail::(["0", "123"]); + } + + #[test] + fn good_header() { + assert_parse_eq::(["0"], ContentLength(0)); + assert_parse_eq::(["1"], ContentLength(1)); + assert_parse_eq::(["123"], ContentLength(123)); + + // value that looks like octal notation is not interpreted as such + assert_parse_eq::(["0123"], ContentLength(123)); + + // whitespace variations + assert_parse_eq::([" 0"], ContentLength(0)); + assert_parse_eq::(["0 "], ContentLength(0)); + assert_parse_eq::([" 0 "], ContentLength(0)); + + // large value (2^64 - 1) + assert_parse_eq::( + ["18446744073709551615"], + ContentLength(18_446_744_073_709_551_615), + ); + } + + #[test] + fn equality() { + assert!(ContentLength(0) == ContentLength(0)); + assert!(ContentLength(0) == 0); + assert!(0 != ContentLength(123)); + } + + #[test] + fn ordering() { + assert!(ContentLength(0) < ContentLength(123)); + assert!(ContentLength(0) < 123); + assert!(0 < ContentLength(123)); + } +} diff --git a/actix-web/src/http/header/content_range.rs b/actix-web/src/http/header/content_range.rs index bcbe77e6..2604f9ba 100644 --- a/actix-web/src/http/header/content_range.rs +++ b/actix-web/src/http/header/content_range.rs @@ -13,61 +13,60 @@ crate::http::header::common_header! { test_parse_and_format { crate::http::header::common_header_test!(test_bytes, - vec![b"bytes 0-499/500"], + [b"bytes 0-499/500"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: Some(500) }))); crate::http::header::common_header_test!(test_bytes_unknown_len, - vec![b"bytes 0-499/*"], + [b"bytes 0-499/*"], Some(ContentRange(ContentRangeSpec::Bytes { range: Some((0, 499)), instance_length: None }))); crate::http::header::common_header_test!(test_bytes_unknown_range, - vec![b"bytes */500"], + [b"bytes */500"], Some(ContentRange(ContentRangeSpec::Bytes { range: None, instance_length: Some(500) }))); crate::http::header::common_header_test!(test_unregistered, - vec![b"seconds 1-2"], + [b"seconds 1-2"], Some(ContentRange(ContentRangeSpec::Unregistered { unit: "seconds".to_owned(), resp: "1-2".to_owned() }))); crate::http::header::common_header_test!(test_no_len, - vec![b"bytes 0-499"], + [b"bytes 0-499"], None::); crate::http::header::common_header_test!(test_only_unit, - vec![b"bytes"], + [b"bytes"], None::); crate::http::header::common_header_test!(test_end_less_than_start, - vec![b"bytes 499-0/500"], + [b"bytes 499-0/500"], None::); crate::http::header::common_header_test!(test_blank, - vec![b""], + [b""], None::); crate::http::header::common_header_test!(test_bytes_many_spaces, - vec![b"bytes 1-2/500 3"], + [b"bytes 1-2/500 3"], None::); crate::http::header::common_header_test!(test_bytes_many_slashes, - vec![b"bytes 1-2/500/600"], + [b"bytes 1-2/500/600"], None::); crate::http::header::common_header_test!(test_bytes_many_dashes, - vec![b"bytes 1-2-3/500"], + [b"bytes 1-2-3/500"], None::); - } } @@ -113,22 +112,13 @@ pub enum ContentRangeSpec { }, } -fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> { - let mut iter = s.splitn(2, separator); - match (iter.next(), iter.next()) { - (Some(a), Some(b)) => Some((a, b)), - _ => None, - } -} - impl FromStr for ContentRangeSpec { type Err = ParseError; fn from_str(s: &str) -> Result { - let res = match split_in_two(s, ' ') { + let res = match s.split_once(' ') { Some(("bytes", resp)) => { - let (range, instance_length) = - split_in_two(resp, '/').ok_or(ParseError::Header)?; + let (range, instance_length) = resp.split_once('/').ok_or(ParseError::Header)?; let instance_length = if instance_length == "*" { None @@ -140,7 +130,7 @@ impl FromStr for ContentRangeSpec { None } else { let (first_byte, last_byte) = - split_in_two(range, '-').ok_or(ParseError::Header)?; + range.split_once('-').ok_or(ParseError::Header)?; let first_byte = first_byte.parse().map_err(|_| ParseError::Header)?; let last_byte = last_byte.parse().map_err(|_| ParseError::Header)?; if last_byte < first_byte { diff --git a/actix-web/src/http/header/content_type.rs b/actix-web/src/http/header/content_type.rs index 1fc75d0e..c43ef8a2 100644 --- a/actix-web/src/http/header/content_type.rs +++ b/actix-web/src/http/header/content_type.rs @@ -1,110 +1,104 @@ -use super::CONTENT_TYPE; use mime::Mime; +use super::CONTENT_TYPE; + crate::http::header::common_header! { - /// `Content-Type` header, defined - /// in [RFC 7231 §3.1.1.5](https://datatracker.ietf.org/doc/html/rfc7231#section-3.1.1.5) + /// `Content-Type` header, defined in [RFC 9110 §8.3]. /// - /// The `Content-Type` header field indicates the media type of the - /// associated representation: either the representation enclosed in the - /// message payload or the selected representation, as determined by the - /// message semantics. The indicated media type defines both the data - /// format and how that data is intended to be processed by a recipient, - /// within the scope of the received message semantics, after any content - /// codings indicated by Content-Encoding are decoded. + /// The `Content-Type` header field indicates the media type of the associated representation: + /// either the representation enclosed in the message payload or the selected representation, + /// as determined by the message semantics. The indicated media type defines both the data + /// format and how that data is intended to be processed by a recipient, within the scope of the + /// received message semantics, after any content codings indicated by Content-Encoding are + /// decoded. /// - /// Although the `mime` crate allows the mime options to be any slice, this crate - /// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If - /// this is an issue, it's possible to implement `Header` on a custom struct. + /// Although the `mime` crate allows the mime options to be any slice, this crate forces the use + /// of Vec. This is to make sure the same header can't have more than 1 type. If this is an + /// issue, it's possible to implement `Header` on a custom struct. /// /// # ABNF + /// /// ```plain /// Content-Type = media-type /// ``` /// /// # Example Values - /// * `text/html; charset=utf-8` - /// * `application/json` + /// + /// - `text/html; charset=utf-8` + /// - `application/json` /// /// # Examples - /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; - /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// ContentType::json() - /// ); - /// ``` /// /// ``` - /// use actix_web::HttpResponse; - /// use actix_web::http::header::ContentType; + /// use actix_web::{http::header::ContentType, HttpResponse}; /// - /// let mut builder = HttpResponse::Ok(); - /// builder.insert_header( - /// ContentType(mime::TEXT_HTML) - /// ); + /// let res_json = HttpResponse::Ok() + /// .insert_header(ContentType::json()); + /// + /// let res_html = HttpResponse::Ok() + /// .insert_header(ContentType(mime::TEXT_HTML)); /// ``` + /// + /// [RFC 9110 §8.3]: https://datatracker.ietf.org/doc/html/rfc9110#section-8.3 (ContentType, CONTENT_TYPE) => [Mime] test_parse_and_format { crate::http::header::common_header_test!( - test1, - vec![b"text/html"], + test_text_html, + [b"text/html"], Some(HeaderField(mime::TEXT_HTML))); + crate::http::header::common_header_test!( + test_image_star, + [b"image/*"], + Some(HeaderField(mime::IMAGE_STAR))); + } } impl ContentType { - /// A constructor to easily create a `Content-Type: application/json` - /// header. + /// Constructs a `Content-Type: application/json` header. #[inline] pub fn json() -> ContentType { ContentType(mime::APPLICATION_JSON) } - /// A constructor to easily create a `Content-Type: text/plain; - /// charset=utf-8` header. + /// Constructs a `Content-Type: text/plain; charset=utf-8` header. #[inline] pub fn plaintext() -> ContentType { ContentType(mime::TEXT_PLAIN_UTF_8) } - /// A constructor to easily create a `Content-Type: text/html; charset=utf-8` - /// header. + /// Constructs a `Content-Type: text/html; charset=utf-8` header. #[inline] pub fn html() -> ContentType { ContentType(mime::TEXT_HTML_UTF_8) } - /// A constructor to easily create a `Content-Type: text/xml` header. + /// Constructs a `Content-Type: text/xml` header. #[inline] pub fn xml() -> ContentType { ContentType(mime::TEXT_XML) } - /// A constructor to easily create a `Content-Type: - /// application/www-form-url-encoded` header. + /// Constructs a `Content-Type: application/www-form-url-encoded` header. #[inline] pub fn form_url_encoded() -> ContentType { ContentType(mime::APPLICATION_WWW_FORM_URLENCODED) } - /// A constructor to easily create a `Content-Type: image/jpeg` header. + /// Constructs a `Content-Type: image/jpeg` header. #[inline] pub fn jpeg() -> ContentType { ContentType(mime::IMAGE_JPEG) } - /// A constructor to easily create a `Content-Type: image/png` header. + /// Constructs a `Content-Type: image/png` header. #[inline] pub fn png() -> ContentType { ContentType(mime::IMAGE_PNG) } - /// A constructor to easily create a `Content-Type: - /// application/octet-stream` header. + /// Constructs a `Content-Type: application/octet-stream` header. #[inline] pub fn octet_stream() -> ContentType { ContentType(mime::APPLICATION_OCTET_STREAM) diff --git a/actix-web/src/http/header/date.rs b/actix-web/src/http/header/date.rs index f6274021..ac30424f 100644 --- a/actix-web/src/http/header/date.rs +++ b/actix-web/src/http/header/date.rs @@ -1,6 +1,7 @@ -use super::{HttpDate, DATE}; use std::time::SystemTime; +use super::{HttpDate, DATE}; + crate::http::header::common_header! { /// `Date` header, defined /// in [RFC 7231 §7.1.1.2](https://datatracker.ietf.org/doc/html/rfc7231#section-7.1.1.2) @@ -31,7 +32,7 @@ crate::http::header::common_header! { (Date, DATE) => [HttpDate] test_parse_and_format { - crate::http::header::common_header_test!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]); + crate::http::header::common_header_test!(test1, [b"Tue, 15 Nov 1994 08:12:31 GMT"]); } } diff --git a/actix-web/src/http/header/entity.rs b/actix-web/src/http/header/entity.rs index 0eaa12b5..a5ef3c5b 100644 --- a/actix-web/src/http/header/entity.rs +++ b/actix-web/src/http/header/entity.rs @@ -152,9 +152,7 @@ impl FromStr for EntityTag { return Err(crate::error::ParseError::Header); } // The etag is weak if its first char is not a DQUOTE. - if slice.len() >= 2 - && slice.starts_with('"') - && check_slice_validity(&slice[1..length - 1]) + if slice.len() >= 2 && slice.starts_with('"') && check_slice_validity(&slice[1..length - 1]) { // No need to check if the last char is a DQUOTE, // we already did that above. diff --git a/actix-web/src/http/header/etag.rs b/actix-web/src/http/header/etag.rs index 78f5447b..b8220828 100644 --- a/actix-web/src/http/header/etag.rs +++ b/actix-web/src/http/header/etag.rs @@ -49,50 +49,50 @@ crate::http::header::common_header! { test_parse_and_format { // From the RFC crate::http::header::common_header_test!(test1, - vec![b"\"xyzzy\""], + [b"\"xyzzy\""], Some(ETag(EntityTag::new_strong("xyzzy".to_owned())))); crate::http::header::common_header_test!(test2, - vec![b"W/\"xyzzy\""], + [b"W/\"xyzzy\""], Some(ETag(EntityTag::new_weak("xyzzy".to_owned())))); crate::http::header::common_header_test!(test3, - vec![b"\"\""], + [b"\"\""], Some(ETag(EntityTag::new_strong("".to_owned())))); // Own tests crate::http::header::common_header_test!(test4, - vec![b"\"foobar\""], + [b"\"foobar\""], Some(ETag(EntityTag::new_strong("foobar".to_owned())))); crate::http::header::common_header_test!(test5, - vec![b"\"\""], + [b"\"\""], Some(ETag(EntityTag::new_strong("".to_owned())))); crate::http::header::common_header_test!(test6, - vec![b"W/\"weak-etag\""], + [b"W/\"weak-etag\""], Some(ETag(EntityTag::new_weak("weak-etag".to_owned())))); crate::http::header::common_header_test!(test7, - vec![b"W/\"\x65\x62\""], + [b"W/\"\x65\x62\""], Some(ETag(EntityTag::new_weak("\u{0065}\u{0062}".to_owned())))); crate::http::header::common_header_test!(test8, - vec![b"W/\"\""], + [b"W/\"\""], Some(ETag(EntityTag::new_weak("".to_owned())))); crate::http::header::common_header_test!(test9, - vec![b"no-dquotes"], + [b"no-dquotes"], None::); crate::http::header::common_header_test!(test10, - vec![b"w/\"the-first-w-is-case-sensitive\""], + [b"w/\"the-first-w-is-case-sensitive\""], None::); crate::http::header::common_header_test!(test11, - vec![b""], + [b""], None::); crate::http::header::common_header_test!(test12, - vec![b"\"unmatched-dquotes1"], + [b"\"unmatched-dquotes1"], None::); crate::http::header::common_header_test!(test13, - vec![b"unmatched-dquotes2\""], + [b"unmatched-dquotes2\""], None::); crate::http::header::common_header_test!(test14, - vec![b"matched-\"dquotes\""], + [b"matched-\"dquotes\""], None::); crate::http::header::common_header_test!(test15, - vec![b"\""], + [b"\""], None::); } } diff --git a/actix-web/src/http/header/expires.rs b/actix-web/src/http/header/expires.rs index 55fe5acc..b677ab52 100644 --- a/actix-web/src/http/header/expires.rs +++ b/actix-web/src/http/header/expires.rs @@ -36,6 +36,6 @@ crate::http::header::common_header! { test_parse_and_format { // Test case from RFC - crate::http::header::common_header_test!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]); + crate::http::header::common_header_test!(test1, [b"Thu, 01 Dec 1994 16:00:00 GMT"]); } } diff --git a/actix-web/src/http/header/if_match.rs b/actix-web/src/http/header/if_match.rs index e299d30f..e0b46a6c 100644 --- a/actix-web/src/http/header/if_match.rs +++ b/actix-web/src/http/header/if_match.rs @@ -52,17 +52,17 @@ common_header! { test_parse_and_format { crate::http::header::common_header_test!( test1, - vec![b"\"xyzzy\""], + [b"\"xyzzy\""], Some(HeaderField::Items( vec![EntityTag::new_strong("xyzzy".to_owned())]))); crate::http::header::common_header_test!( test2, - vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], + [b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""], Some(HeaderField::Items( vec![EntityTag::new_strong("xyzzy".to_owned()), EntityTag::new_strong("r2d2xxxx".to_owned()), EntityTag::new_strong("c3piozzzz".to_owned())]))); - crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any)); + crate::http::header::common_header_test!(test3, [b"*"], Some(IfMatch::Any)); } } diff --git a/actix-web/src/http/header/if_modified_since.rs b/actix-web/src/http/header/if_modified_since.rs index 89721094..8547ff49 100644 --- a/actix-web/src/http/header/if_modified_since.rs +++ b/actix-web/src/http/header/if_modified_since.rs @@ -35,6 +35,6 @@ crate::http::header::common_header! { test_parse_and_format { // Test case from RFC - crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/actix-web/src/http/header/if_none_match.rs b/actix-web/src/http/header/if_none_match.rs index 86d7da9b..1a424df9 100644 --- a/actix-web/src/http/header/if_none_match.rs +++ b/actix-web/src/http/header/if_none_match.rs @@ -52,11 +52,11 @@ crate::http::header::common_header! { (IfNoneMatch, IF_NONE_MATCH) => {Any / (EntityTag)+} test_parse_and_format { - crate::http::header::common_header_test!(test1, vec![b"\"xyzzy\""]); - crate::http::header::common_header_test!(test2, vec![b"W/\"xyzzy\""]); - crate::http::header::common_header_test!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); - crate::http::header::common_header_test!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); - crate::http::header::common_header_test!(test5, vec![b"*"]); + crate::http::header::common_header_test!(test1, [b"\"xyzzy\""]); + crate::http::header::common_header_test!(test2, [b"W/\"xyzzy\""]); + crate::http::header::common_header_test!(test3, [b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]); + crate::http::header::common_header_test!(test4, [b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]); + crate::http::header::common_header_test!(test5, [b"*"]); } } diff --git a/actix-web/src/http/header/if_range.rs b/actix-web/src/http/header/if_range.rs index eb3632a4..3e8727ab 100644 --- a/actix-web/src/http/header/if_range.rs +++ b/actix-web/src/http/header/if_range.rs @@ -4,9 +4,7 @@ use super::{ from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, InvalidHeaderValue, TryIntoHeaderValue, Writer, }; -use crate::error::ParseError; -use crate::http::header; -use crate::HttpMessage; +use crate::{error::ParseError, http::header, HttpMessage}; /// `If-Range` header, defined /// in [RFC 7233 §3.2](https://datatracker.ietf.org/doc/html/rfc7233#section-3.2) @@ -113,7 +111,7 @@ mod test_parse_and_format { use super::IfRange as HeaderField; use crate::http::header::*; - crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); - crate::http::header::common_header_test!(test2, vec![b"\"abc\""]); - crate::http::header::common_header_test!(test3, vec![b"this-is-invalid"], None::); + crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test2, [b"\"abc\""]); + crate::http::header::common_header_test!(test3, [b"this-is-invalid"], None::); } diff --git a/actix-web/src/http/header/if_unmodified_since.rs b/actix-web/src/http/header/if_unmodified_since.rs index 2ee3160b..afa4eb8e 100644 --- a/actix-web/src/http/header/if_unmodified_since.rs +++ b/actix-web/src/http/header/if_unmodified_since.rs @@ -35,6 +35,6 @@ crate::http::header::common_header! { test_parse_and_format { // Test case from RFC - crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/actix-web/src/http/header/last_modified.rs b/actix-web/src/http/header/last_modified.rs index 59e649be..724a38bb 100644 --- a/actix-web/src/http/header/last_modified.rs +++ b/actix-web/src/http/header/last_modified.rs @@ -34,6 +34,6 @@ crate::http::header::common_header! { test_parse_and_format { // Test case from RFC - crate::http::header::common_header_test!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); + crate::http::header::common_header_test!(test1, [b"Sat, 29 Oct 1994 19:43:31 GMT"]); } } diff --git a/actix-web/src/http/header/macros.rs b/actix-web/src/http/header/macros.rs index b40eca03..d9755e15 100644 --- a/actix-web/src/http/header/macros.rs +++ b/actix-web/src/http/header/macros.rs @@ -314,7 +314,7 @@ macro_rules! common_header { }; } -pub(crate) use {common_header, common_header_test_module}; - +pub(crate) use common_header; #[cfg(test)] pub(crate) use common_header_test; +pub(crate) use common_header_test_module; diff --git a/actix-web/src/http/header/mod.rs b/actix-web/src/http/header/mod.rs index 9807d5f5..51ac4fcf 100644 --- a/actix-web/src/http/header/mod.rs +++ b/actix-web/src/http/header/mod.rs @@ -6,8 +6,6 @@ use std::fmt; -use bytes::{Bytes, BytesMut}; - // re-export from actix-http // - header name / value types // - relevant traits for converting to header name / value @@ -16,6 +14,7 @@ use bytes::{Bytes, BytesMut}; // - the few typed headers from actix-http // - header parsing utils pub use actix_http::header::*; +use bytes::{Bytes, BytesMut}; mod accept; mod accept_charset; @@ -25,6 +24,7 @@ mod allow; mod cache_control; mod content_disposition; mod content_language; +mod content_length; mod content_range; mod content_type; mod date; @@ -43,32 +43,34 @@ mod preference; mod range; #[cfg(test)] -pub(crate) use macros::common_header_test; -pub(crate) use macros::{common_header, common_header_test_module}; - -pub use self::accept::Accept; -pub use self::accept_charset::AcceptCharset; -pub use self::accept_encoding::AcceptEncoding; -pub use self::accept_language::AcceptLanguage; -pub use self::allow::Allow; -pub use self::cache_control::{CacheControl, CacheDirective}; -pub use self::content_disposition::{ContentDisposition, DispositionParam, DispositionType}; -pub use self::content_language::ContentLanguage; -pub use self::content_range::{ContentRange, ContentRangeSpec}; -pub use self::content_type::ContentType; -pub use self::date::Date; -pub use self::encoding::Encoding; -pub use self::entity::EntityTag; -pub use self::etag::ETag; -pub use self::expires::Expires; -pub use self::if_match::IfMatch; -pub use self::if_modified_since::IfModifiedSince; -pub use self::if_none_match::IfNoneMatch; -pub use self::if_range::IfRange; -pub use self::if_unmodified_since::IfUnmodifiedSince; -pub use self::last_modified::LastModified; -pub use self::preference::Preference; -pub use self::range::{ByteRangeSpec, Range}; +pub(crate) use self::macros::common_header_test; +pub(crate) use self::macros::{common_header, common_header_test_module}; +pub use self::{ + accept::Accept, + accept_charset::AcceptCharset, + accept_encoding::AcceptEncoding, + accept_language::AcceptLanguage, + allow::Allow, + cache_control::{CacheControl, CacheDirective}, + content_disposition::{ContentDisposition, DispositionParam, DispositionType}, + content_language::ContentLanguage, + content_length::ContentLength, + content_range::{ContentRange, ContentRangeSpec}, + content_type::ContentType, + date::Date, + encoding::Encoding, + entity::EntityTag, + etag::ETag, + expires::Expires, + if_match::IfMatch, + if_modified_since::IfModifiedSince, + if_none_match::IfNoneMatch, + if_range::IfRange, + if_unmodified_since::IfUnmodifiedSince, + last_modified::LastModified, + preference::Preference, + range::{ByteRangeSpec, Range}, +}; /// Format writer ([`fmt::Write`]) for a [`BytesMut`]. #[derive(Debug, Default)] diff --git a/actix-web/src/info.rs b/actix-web/src/info.rs index 7c685406..1b2e554f 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()?; @@ -76,7 +90,6 @@ impl ConnectionInfo { for (name, val) in req .headers .get_all(&header::FORWARDED) - .into_iter() .filter_map(|hdr| hdr.to_str().ok()) // "for=1.2.3.4, for=5.6.7.8; scheme=https" .flat_map(|val| val.split(';')) @@ -101,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" => { @@ -369,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 6a94976c..20539138 100644 --- a/actix-web/src/lib.rs +++ b/actix-web/src/lib.rs @@ -64,15 +64,22 @@ //! - `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_cfg))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] + +pub use actix_http::{body, HttpMessage}; +#[cfg(feature = "cookies")] +#[doc(inline)] +pub use cookie; mod app; mod app_service; @@ -102,31 +109,25 @@ pub mod test; pub(crate) mod types; pub mod web; -pub use crate::app::App; #[doc(inline)] pub use crate::error::Result; -pub use crate::error::{Error, ResponseError}; -pub use crate::extract::FromRequest; -pub use crate::handler::Handler; -pub use crate::request::HttpRequest; -pub use crate::resource::Resource; -pub use crate::response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}; -pub use crate::route::Route; -pub use crate::scope::Scope; -pub use crate::server::HttpServer; -pub use crate::types::Either; - -pub use actix_http::{body, HttpMessage}; - -#[cfg(feature = "cookies")] -#[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] -#[doc(inline)] -pub use cookie; +pub use crate::{ + app::App, + error::{Error, ResponseError}, + extract::FromRequest, + handler::Handler, + request::HttpRequest, + resource::Resource, + response::{CustomizeResponder, HttpResponse, HttpResponseBuilder, Responder}, + route::Route, + scope::Scope, + server::HttpServer, + types::Either, +}; macro_rules! codegen_reexport { ($name:ident) => { #[cfg(feature = "macros")] - #[cfg_attr(docsrs, doc(cfg(feature = "macros")))] pub use actix_web_codegen::$name; }; } @@ -144,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/authors-guide.md b/actix-web/src/middleware/authors-guide.md index a8d1edea..64bad15c 100644 --- a/actix-web/src/middleware/authors-guide.md +++ b/actix-web/src/middleware/authors-guide.md @@ -13,4 +13,5 @@ ## When To (Not) Use Middleware ## Author's References + - `EitherBody` + when is middleware appropriate: https://discord.com/channels/771444961383153695/952016890723729428 diff --git a/actix-web/src/middleware/compat.rs b/actix-web/src/middleware/compat.rs index ee8b8a49..963dfdab 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 { @@ -146,14 +137,13 @@ mod tests { // easier to code when cookies feature is disabled #![allow(unused_imports)] - use super::*; - use actix_service::IntoService; + use super::*; 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, }; @@ -207,9 +197,9 @@ mod tests { #[actix_rt::test] async fn test_condition_scope_middleware() { let srv = |req: ServiceRequest| { - Box::pin(async move { - Ok(req.into_response(HttpResponse::InternalServerError().finish())) - }) + Box::pin( + async move { Ok(req.into_response(HttpResponse::InternalServerError().finish())) }, + ) }; let logger = Logger::default(); @@ -226,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 d5e408cb..ebd49611 100644 --- a/actix-web/src/middleware/compress.rs +++ b/actix-web/src/middleware/compress.rs @@ -12,13 +12,14 @@ use actix_http::{encoding::Encoder, header::ContentEncoding}; use actix_service::{Service, Transform}; use actix_utils::future::{ok, Either, Ready}; use futures_core::ready; +use mime::Mime; use once_cell::sync::Lazy; use pin_project_lite::pin_project; use crate::{ body::{EitherBody, MessageBody}, http::{ - header::{self, AcceptEncoding, Encoding, HeaderValue}, + header::{self, AcceptEncoding, ContentEncoding, Encoding, HeaderValue}, StatusCode, }, service::{ServiceRequest, ServiceResponse}, @@ -33,7 +34,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 @@ -257,20 +258,41 @@ where { type Output = Result>>, Error>; - fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { - let this = self.project(); + fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { + let this = self.as_mut().project(); match ready!(this.fut.poll(cx)) { Ok(resp) => { let enc = match this.encoding { Encoding::Known(enc) => *enc, Encoding::Unknown(enc) => { - unimplemented!("encoding {} should not be here", enc); + unimplemented!("encoding '{enc}' should not be here"); } }; let level = this.inner.level(&enc); Poll::Ready(Ok(resp.map_body(move |head, body| { + let content_type = head.headers.get(header::CONTENT_TYPE); + + fn default_compress_predicate(content_type: Option<&HeaderValue>) -> bool { + match content_type { + None => true, + Some(hdr) => { + match hdr.to_str().ok().and_then(|hdr| hdr.parse::().ok()) { + Some(mime) if mime.type_().as_str() == "image" => false, + Some(mime) if mime.type_().as_str() == "video" => false, + _ => true, + } + } + } + } + + let enc = if default_compress_predicate(content_type) { + enc + } else { + ContentEncoding::Identity + }; + EitherBody::left(Encoder::response_with_level(enc, head, body, level)) }))) } @@ -334,8 +356,18 @@ static SUPPORTED_ENCODINGS: &[Encoding] = &[ mod tests { use std::collections::HashSet; + use static_assertions::assert_impl_all; + use super::*; - use crate::{middleware::DefaultHeaders, test, web, App}; + use crate::{http::header::ContentType, middleware::DefaultHeaders, test, web, App}; + + const HTML_DATA_PART: &str = "

hello world

) -> Vec { use std::io::Read as _; @@ -345,23 +377,55 @@ mod tests { buf } + #[track_caller] + fn assert_successful_res_with_content_type(res: &ServiceResponse, ct: &str) { + assert!(res.status().is_success()); + assert!( + res.headers() + .get(header::CONTENT_TYPE) + .expect("content-type header should be present") + .to_str() + .expect("content-type header should be utf-8") + .contains(ct), + "response's content-type did not match {}", + ct + ); + } + + #[track_caller] + fn assert_successful_gzip_res_with_content_type(res: &ServiceResponse, ct: &str) { + assert_successful_res_with_content_type(res, ct); + assert_eq!( + res.headers() + .get(header::CONTENT_ENCODING) + .expect("response should be gzip compressed"), + "gzip", + ); + } + + #[track_caller] + fn assert_successful_identity_res_with_content_type(res: &ServiceResponse, ct: &str) { + assert_successful_res_with_content_type(res, ct); + assert!( + res.headers().get(header::CONTENT_ENCODING).is_none(), + "response should not be compressed", + ); + } + #[actix_rt::test] async fn prevents_double_compressing() { - const D: &str = "hello world "; - const DATA: &str = const_str::repeat!(D, 100); - let app = test::init_service({ App::new() .wrap(Compress::default()) .route( "/single", - web::get().to(move || HttpResponse::Ok().body(DATA)), + web::get().to(move || HttpResponse::Ok().body(TEXT_DATA)), ) .service( web::resource("/double") .wrap(Compress::default()) .wrap(DefaultHeaders::new().add(("x-double", "true"))) - .route(web::get().to(move || HttpResponse::Ok().body(DATA))), + .route(web::get().to(move || HttpResponse::Ok().body(TEXT_DATA))), ) }) .await; @@ -375,7 +439,7 @@ mod tests { assert_eq!(res.headers().get("x-double"), None); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); let bytes = test::read_body(res).await; - assert_eq!(gzip_decode(bytes), DATA.as_bytes()); + assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes()); let req = test::TestRequest::default() .uri("/double") @@ -386,7 +450,7 @@ mod tests { assert_eq!(res.headers().get("x-double").unwrap(), "true"); assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip"); let bytes = test::read_body(res).await; - assert_eq!(gzip_decode(bytes), DATA.as_bytes()); + assert_eq!(gzip_decode(bytes), TEXT_DATA.as_bytes()); } #[actix_rt::test] @@ -397,7 +461,7 @@ mod tests { .default_service(web::to(move || { HttpResponse::Ok() .insert_header((header::VARY, "x-test")) - .finish() + .body(TEXT_DATA) })) }) .await; @@ -435,4 +499,88 @@ mod tests { let bytes = test::read_body(res).await; assert_eq!(gzip_decode(bytes), DATA.as_bytes()); } + + fn configure_predicate_test(cfg: &mut web::ServiceConfig) { + cfg.route( + "/html", + web::get().to(|| { + HttpResponse::Ok() + .content_type(ContentType::html()) + .body(HTML_DATA) + }), + ) + .route( + "/image", + web::get().to(|| { + HttpResponse::Ok() + .content_type(ContentType::jpeg()) + .body(TEXT_DATA) + }), + ); + } + + #[actix_rt::test] + async fn prevents_compression_jpeg() { + let app = test::init_service( + App::new() + .wrap(Compress::default()) + .configure(configure_predicate_test), + ) + .await; + + let req = + test::TestRequest::with_uri("/html").insert_header((header::ACCEPT_ENCODING, "gzip")); + let res = test::call_service(&app, req.to_request()).await; + assert_successful_gzip_res_with_content_type(&res, "text/html"); + assert_ne!(test::read_body(res).await, HTML_DATA.as_bytes()); + + let req = + test::TestRequest::with_uri("/image").insert_header((header::ACCEPT_ENCODING, "gzip")); + let res = test::call_service(&app, req.to_request()).await; + 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 5e106c11..5ee4467d 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 003abd40..f21afe6e 100644 --- a/actix-web/src/middleware/default_headers.rs +++ b/actix-web/src/middleware/default_headers.rs @@ -1,7 +1,6 @@ //! For middleware documentation, see [`DefaultHeaders`]. use std::{ - convert::TryFrom, future::Future, marker::PhantomData, pin::Pin, @@ -191,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 4ddbc631..aa6d1c8a 100644 --- a/actix-web/src/middleware/err_handlers.rs +++ b/actix-web/src/middleware/err_handlers.rs @@ -50,16 +50,24 @@ type DefaultHandler = Option>>; /// will pass by unchanged by this middleware. /// /// # Examples -/// ``` -/// use actix_web::http::{header, StatusCode}; -/// use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; -/// use actix_web::{dev, web, App, HttpResponse, Result}; /// -/// fn add_error_header(mut res: dev::ServiceResponse) -> Result> { +/// Adding a header: +/// +/// ``` +/// use actix_web::{ +/// dev::ServiceResponse, +/// http::{header, StatusCode}, +/// middleware::{ErrorHandlerResponse, ErrorHandlers}, +/// web, App, HttpResponse, Result, +/// }; +/// +/// fn add_error_header(mut res: ServiceResponse) -> Result> { /// res.response_mut().headers_mut().insert( /// header::CONTENT_TYPE, /// header::HeaderValue::from_static("Error"), /// ); +/// +/// // body is unchanged, map to "left" slot /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// @@ -67,24 +75,63 @@ type DefaultHandler = Option>>; /// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_header)) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` -/// ## Registering default handler +/// +/// Modifying response body: +/// /// ``` -/// # use actix_web::http::{header, StatusCode}; -/// # use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; -/// # use actix_web::{dev, web, App, HttpResponse, Result}; -/// fn add_error_header(mut res: dev::ServiceResponse) -> Result> { +/// use actix_web::{ +/// dev::ServiceResponse, +/// http::{header, StatusCode}, +/// middleware::{ErrorHandlerResponse, ErrorHandlers}, +/// web, App, HttpResponse, Result, +/// }; +/// +/// fn add_error_body(res: ServiceResponse) -> Result> { +/// // split service response into request and response components +/// let (req, res) = res.into_parts(); +/// +/// // set body of response to modified body +/// let res = res.set_body("An error occurred."); +/// +/// // modified bodies need to be boxed and placed in the "right" slot +/// let res = ServiceResponse::new(req, res) +/// .map_into_boxed_body() +/// .map_into_right_body(); +/// +/// Ok(ErrorHandlerResponse::Response(res)) +/// } +/// +/// let app = App::new() +/// .wrap(ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, add_error_body)) +/// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); +/// ``` +/// +/// Registering default handler: +/// +/// ``` +/// # use actix_web::{ +/// # dev::ServiceResponse, +/// # http::{header, StatusCode}, +/// # middleware::{ErrorHandlerResponse, ErrorHandlers}, +/// # web, App, HttpResponse, Result, +/// # }; +/// fn add_error_header(mut res: ServiceResponse) -> Result> { /// res.response_mut().headers_mut().insert( /// header::CONTENT_TYPE, /// header::HeaderValue::from_static("Error"), /// ); +/// +/// // body is unchanged, map to "left" slot /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// -/// fn handle_bad_request(mut res: dev::ServiceResponse) -> Result> { +/// fn handle_bad_request(mut res: ServiceResponse) -> Result> { /// res.response_mut().headers_mut().insert( /// header::CONTENT_TYPE, /// header::HeaderValue::from_static("Bad Request Error"), /// ); +/// +/// // body is unchanged, map to "left" slot /// Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// } /// @@ -98,20 +145,24 @@ type DefaultHandler = Option>>; /// ) /// .service(web::resource("/").route(web::get().to(HttpResponse::InternalServerError))); /// ``` -/// Alternatively, you can set default handlers for only client or only server errors: /// -/// ```rust -/// # use actix_web::http::{header, StatusCode}; -/// # use actix_web::middleware::{ErrorHandlerResponse, ErrorHandlers}; -/// # use actix_web::{dev, web, App, HttpResponse, Result}; -/// # fn add_error_header(mut res: dev::ServiceResponse) -> Result> { +/// You can set default handlers for all client (4xx) or all server (5xx) errors: +/// +/// ``` +/// # use actix_web::{ +/// # dev::ServiceResponse, +/// # http::{header, StatusCode}, +/// # middleware::{ErrorHandlerResponse, ErrorHandlers}, +/// # web, App, HttpResponse, Result, +/// # }; +/// # fn add_error_header(mut res: ServiceResponse) -> Result> { /// # res.response_mut().headers_mut().insert( /// # header::CONTENT_TYPE, /// # header::HeaderValue::from_static("Error"), /// # ); /// # Ok(ErrorHandlerResponse::Response(res.map_into_left_body())) /// # } -/// # fn handle_bad_request(mut res: dev::ServiceResponse) -> Result> { +/// # fn handle_bad_request(mut res: ServiceResponse) -> Result> { /// # res.response_mut().headers_mut().insert( /// # header::CONTENT_TYPE, /// # header::HeaderValue::from_static("Bad Request Error"), @@ -219,8 +270,8 @@ impl ErrorHandlers { handlers .get(status) .map(|h| h.as_ref()) - .or_else(|| status.is_client_error().then(|| default_client).flatten()) - .or_else(|| status.is_server_error().then(|| default_server).flatten()) + .or_else(|| status.is_client_error().then_some(default_client).flatten()) + .or_else(|| status.is_server_error().then_some(default_server).flatten()) } } @@ -356,10 +407,7 @@ mod tests { use super::*; use crate::{ body, - http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + http::header::{HeaderValue, CONTENT_TYPE}, test::{self, TestRequest}, }; @@ -489,21 +537,17 @@ mod tests { let mw_server = make_mw(StatusCode::INTERNAL_SERVER_ERROR).await; let mw_client = make_mw(StatusCode::BAD_REQUEST).await; - let resp = - test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let resp = - test::call_service(&mw_server, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw_server, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); } #[actix_rt::test] async fn default_handlers_separate_client_server() { #[allow(clippy::unnecessary_wraps)] - fn error_handler_client( - mut res: ServiceResponse, - ) -> Result> { + fn error_handler_client(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); @@ -511,9 +555,7 @@ mod tests { } #[allow(clippy::unnecessary_wraps)] - fn error_handler_server( - mut res: ServiceResponse, - ) -> Result> { + fn error_handler_server(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0002")); @@ -531,21 +573,17 @@ mod tests { let mw_server = make_mw(StatusCode::INTERNAL_SERVER_ERROR).await; let mw_client = make_mw(StatusCode::BAD_REQUEST).await; - let resp = - test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let resp = - test::call_service(&mw_server, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw_server, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0002"); } #[actix_rt::test] async fn default_handlers_specialization() { #[allow(clippy::unnecessary_wraps)] - fn error_handler_client( - mut res: ServiceResponse, - ) -> Result> { + fn error_handler_client(mut res: ServiceResponse) -> Result> { res.response_mut() .headers_mut() .insert(CONTENT_TYPE, HeaderValue::from_static("0001")); @@ -573,12 +611,10 @@ mod tests { let mw_client = make_mw(StatusCode::BAD_REQUEST).await; let mw_specific = make_mw(StatusCode::UNPROCESSABLE_ENTITY).await; - let resp = - test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw_client, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001"); - let resp = - test::call_service(&mw_specific, TestRequest::default().to_srv_request()).await; + let resp = test::call_service(&mw_specific, TestRequest::default().to_srv_request()).await; assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0003"); } } 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 ae7da1d8..de374a57 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 5fec5a01..dc1b0239 100644 --- a/actix-web/src/middleware/logger.rs +++ b/actix-web/src/middleware/logger.rs @@ -3,7 +3,6 @@ use std::{ borrow::Cow, collections::HashSet, - convert::TryFrom, env, fmt::{self, Display as _}, future::Future, @@ -19,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::{ @@ -88,7 +90,7 @@ pub struct Logger(Rc); struct Inner { format: Format, exclude: HashSet, - exclude_regex: RegexSet, + exclude_regex: Vec, log_target: Cow<'static, str>, } @@ -98,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!()), })) } @@ -115,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 } @@ -241,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!()), })) } @@ -301,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 { @@ -357,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() { @@ -490,12 +493,8 @@ impl Format { unreachable!("regex and code mismatch") } } - "i" => { - FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap()) - } - "o" => { - FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap()) - } + "i" => FormatText::RequestHeader(HeaderName::try_from(key.as_str()).unwrap()), + "o" => FormatText::ResponseHeader(HeaderName::try_from(key.as_str()).unwrap()), "e" => FormatText::EnvironHeader(key.as_str().to_owned()), "xi" => FormatText::CustomRequest(key.as_str().to_owned(), None), "xo" => FormatText::CustomResponse(key.as_str().to_owned(), None), @@ -711,9 +710,7 @@ impl FormatText { } /// Converter to get a String from something that writes to a Formatter. -pub(crate) struct FormatDisplay<'a>( - &'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>, -); +pub(crate) struct FormatDisplay<'a>(&'a dyn Fn(&mut fmt::Formatter<'_>) -> Result<(), fmt::Error>); impl<'a> fmt::Display for FormatDisplay<'a> { fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> { @@ -723,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 0a61ad6c..1c27b111 100644 --- a/actix-web/src/middleware/mod.rs +++ b/actix-web/src/middleware/mod.rs @@ -1,34 +1,248 @@ //! A collection of common middleware. +//! +//! # What Is Middleware? +//! +//! Actix Web's middleware system allows us to add additional behavior to request/response +//! processing. Middleware can hook into incoming request and outgoing response processes, enabling +//! us to modify requests and responses as well as halt request processing to return a response +//! early. +//! +//! Typically, middleware is involved in the following actions: +//! +//! - Pre-process the request (e.g., [normalizing paths](NormalizePath)) +//! - Post-process a response (e.g., [logging][Logger]) +//! - Modify application state (through [`ServiceRequest`][crate::dev::ServiceRequest]) +//! - Access external services (e.g., [sessions](https://docs.rs/actix-session), etc.) +//! +//! Middleware is registered for each [`App`], [`Scope`](crate::Scope), or +//! [`Resource`](crate::Resource) and executed in opposite order as registration. In general, a +//! middleware is a pair of types that implements the [`Service`] trait and [`Transform`] trait, +//! respectively. The [`new_transform`] and [`call`] methods must return a [`Future`], though it +//! can often be [an immediately-ready one](actix_utils::future::Ready). +//! +//! # Ordering +//! +//! ``` +//! # use actix_web::{web, middleware, get, App, Responder}; +//! # +//! # // some basic types to make sure this compiles +//! # type ExtractorA = web::Json; +//! # type ExtractorB = ExtractorA; +//! #[get("/")] +//! async fn service(a: ExtractorA, b: ExtractorB) -> impl Responder { "Hello, World!" } +//! +//! # fn main() { +//! # // These aren't snake_case, because they are supposed to be unit structs. +//! # type MiddlewareA = middleware::Compress; +//! # type MiddlewareB = middleware::Compress; +//! # type MiddlewareC = middleware::Compress; +//! let app = App::new() +//! .wrap(MiddlewareA::default()) +//! .wrap(MiddlewareB::default()) +//! .wrap(MiddlewareC::default()) +//! .service(service); +//! # } +//! ``` +//! +//! ```plain +//! Request +//! ⭣ +//! ╭────────────────────┼────╮ +//! │ MiddlewareC │ │ +//! │ ╭──────────────────┼───╮│ +//! │ │ MiddlewareB │ ││ +//! │ │ ╭────────────────┼──╮││ +//! │ │ │ MiddlewareA │ │││ +//! │ │ │ ╭──────────────┼─╮│││ +//! │ │ │ │ ExtractorA │ ││││ +//! │ │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┼┈┤│││ +//! │ │ │ │ ExtractorB │ ││││ +//! │ │ │ ├┈┈┈┈┈┈┈┈┈┈┈┈┈┈┼┈┤│││ +//! │ │ │ │ service │ ││││ +//! │ │ │ ╰──────────────┼─╯│││ +//! │ │ ╰────────────────┼──╯││ +//! │ ╰──────────────────┼───╯│ +//! ╰────────────────────┼────╯ +//! ⭣ +//! Response +//! ``` +//! The request _first_ gets processed by the middleware specified _last_ - `MiddlewareC`. It passes +//! the request (modified a modified one) to the next middleware - `MiddlewareB` - _or_ directly +//! responds to the request (e.g. when the request was invalid or an error occurred). `MiddlewareB` +//! 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 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 +//! layer exposed to a Request is the outermost layer (i.e., the _last_ registered in the builder +//! chain, in the example above: `MiddlewareC`). Consequently, the _first_ middleware registered in +//! the builder chain is the _last_ to start executing during request processing (`MiddlewareA`). +//! Ordering is less obvious when wrapped services also have middleware applied. In this case, +//! middleware are run in reverse order for [`App`] _and then_ in reverse order for the wrapped +//! service. +//! +//! # Middleware Traits +//! +//! ## `Transform` +//! +//! The [`Transform`] trait is the builder for the actual [`Service`]s that handle the requests. All +//! the middleware you pass to the `wrap` methods implement this trait. During construction, each +//! thread assembles a chain of [`Service`]s by calling [`new_transform`] and passing the next +//! [`Service`] (`S`) in the chain. The created [`Service`] handles requests of type `Req`. +//! +//! In the example from the [ordering](#ordering) section, the chain would be: +//! +//! ```plain +//! MiddlewareCService { +//! next: MiddlewareBService { +//! next: MiddlewareAService { ... } +//! } +//! } +//! ``` +//! +//! ## `Service` +//! +//! A [`Service`] `S` represents an asynchronous operation that turns a request of type `Req` into a +//! response of type [`S::Response`](crate::dev::Service::Response) or an error of type +//! [`S::Error`](crate::dev::Service::Error). You can think of the service of being roughly: +//! +//! ```ignore +//! async fn(&self, req: Req) -> Result +//! ``` +//! +//! In most cases the [`Service`] implementation will, at some point, call the wrapped [`Service`] +//! in its [`call`] implementation. +//! +//! Note that the [`Service`]s created by [`new_transform`] don't need to be [`Send`] or [`Sync`]. +//! +//! # Example +//! +//! ``` +//! use std::{future::{ready, Ready, Future}, pin::Pin}; +//! +//! use actix_web::{ +//! dev::{forward_ready, Service, ServiceRequest, ServiceResponse, Transform}, +//! web, Error, +//! # App +//! }; +//! +//! pub struct SayHi; +//! +//! // `S` - type of the next service +//! // `B` - type of response's body +//! impl Transform for SayHi +//! where +//! S: Service, Error = Error>, +//! S::Future: 'static, +//! B: 'static, +//! { +//! type Response = ServiceResponse; +//! type Error = Error; +//! type InitError = (); +//! type Transform = SayHiMiddleware; +//! type Future = Ready>; +//! +//! fn new_transform(&self, service: S) -> Self::Future { +//! ready(Ok(SayHiMiddleware { service })) +//! } +//! } +//! +//! pub struct SayHiMiddleware { +//! /// The next service to call +//! service: S, +//! } +//! +//! // This future doesn't have the requirement of being `Send`. +//! // See: futures_util::future::LocalBoxFuture +//! type LocalBoxFuture = Pin + 'static>>; +//! +//! // `S`: type of the wrapped service +//! // `B`: type of the body - try to be generic over the body where possible +//! impl Service for SayHiMiddleware +//! where +//! S: Service, Error = Error>, +//! S::Future: 'static, +//! B: 'static, +//! { +//! type Response = ServiceResponse; +//! type Error = Error; +//! type Future = LocalBoxFuture>; +//! +//! // This service is ready when its next service is ready +//! forward_ready!(service); +//! +//! fn call(&self, req: ServiceRequest) -> Self::Future { +//! println!("Hi from start. You requested: {}", req.path()); +//! +//! // A more complex middleware, could return an error or an early response here. +//! +//! let fut = self.service.call(req); +//! +//! Box::pin(async move { +//! let res = fut.await?; +//! +//! println!("Hi from response"); +//! Ok(res) +//! }) +//! } +//! } +//! +//! # fn main() { +//! let app = App::new() +//! .wrap(SayHi) +//! .route("/", web::get().to(|| async { "Hello, middleware!" })); +//! # } +//! ``` +//! +//! # Simpler Middleware +//! +//! In many cases, you _can_ actually use an async function via a helper that will provide a more +//! natural flow for your behavior. +//! +//! The experimental `actix_web_lab` crate provides a [`from_fn`][lab_from_fn] utility which allows +//! an async fn to be wrapped and used in the same way as other middleware. See the +//! [`from_fn`][lab_from_fn] docs for more info and examples of it's use. +//! +//! While [`from_fn`][lab_from_fn] is experimental currently, it's likely this helper will graduate +//! to Actix Web in some form, so feedback is appreciated. +//! +//! [`Future`]: std::future::Future +//! [`App`]: crate::App +//! [`FromRequest`]: crate::FromRequest +//! [`Service`]: crate::dev::Service +//! [`Transform`]: crate::dev::Transform +//! [`call`]: crate::dev::Service::call() +//! [`new_transform`]: crate::dev::Transform::new_transform() +//! [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; -pub use self::compat::Compat; -pub use self::condition::Condition; -pub use self::default_headers::DefaultHeaders; -pub use self::err_handlers::{ErrorHandlerResponse, ErrorHandlers}; -pub use self::logger::Logger; -#[cfg(test)] -pub(crate) use self::noop::Noop; -pub use self::normalize::{NormalizePath, TrailingSlash}; - -#[cfg(feature = "__compress")] -mod compress; - #[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(test)] mod tests { - use crate::{http::StatusCode, App}; - use super::*; + use crate::{http::StatusCode, App}; #[test] fn common_combinations() { diff --git a/actix-web/src/middleware/normalize.rs b/actix-web/src/middleware/normalize.rs index 3ab90848..482107ec 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}, @@ -15,11 +18,12 @@ use crate::{ /// /// The default is `TrailingSlash::Trim`. #[non_exhaustive] -#[derive(Debug, Clone, Copy)] +#[derive(Debug, Clone, Copy, Default)] pub enum TrailingSlash { /// Trim trailing slashes from the end of the path. /// /// Using this will require all routes to omit trailing slashes for them to be accessible. + #[default] Trim, /// Only merge any present multiple trailing slashes. @@ -33,12 +37,6 @@ pub enum TrailingSlash { Always, } -impl Default for TrailingSlash { - fn default() -> Self { - TrailingSlash::Trim - } -} - /// Middleware for normalizing a request's path so that routes can be matched more flexibly. /// /// # Normalization Steps @@ -210,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 5611cc36..bd29a140 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, ); } @@ -181,9 +181,8 @@ impl Responder for Redirect { #[cfg(test)] mod tests { - use crate::{dev::Service, http::StatusCode, test, App}; - use super::*; + 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 6a32bf83..47b3e3d8 100644 --- a/actix-web/src/request.rs +++ b/actix-web/src/request.rs @@ -91,6 +91,35 @@ impl HttpRequest { &self.head().uri } + /// Returns request's original full URL. + /// + /// Reconstructed URL is best-effort, using [`connection_info`](HttpRequest::connection_info()) + /// to get forwarded scheme & host. + /// + /// ``` + /// use actix_web::test::TestRequest; + /// let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo") + /// .insert_header(("host", "example.com")) + /// .to_http_request(); + /// + /// assert_eq!( + /// req.full_url().as_str(), + /// "http://example.com/api?id=4&name=foo", + /// ); + /// ``` + pub fn full_url(&self) -> url::Url { + let info = self.connection_info(); + let scheme = info.scheme(); + let host = info.host(); + let path_and_query = self + .uri() + .path_and_query() + .map(|paq| paq.as_str()) + .unwrap_or("/"); + + url::Url::parse(&format!("{scheme}://{host}{path_and_query}")).unwrap() + } + /// Read the Request method. #[inline] pub fn method(&self) -> &Method { @@ -260,7 +289,7 @@ impl HttpRequest { Ref::map(self.extensions(), |data| data.get().unwrap()) } - /// App config + /// Returns a reference to the application's connection configuration. #[inline] pub fn app_config(&self) -> &AppConfig { self.app_state().config() @@ -311,7 +340,6 @@ impl HttpRequest { /// Load request cookies. #[cfg(feature = "cookies")] - #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] pub fn cookies(&self) -> Result>>, CookieParseError> { use actix_http::header::COOKIE; @@ -335,7 +363,6 @@ impl HttpRequest { /// Return request cookie. #[cfg(feature = "cookies")] - #[cfg_attr(docsrs, doc(cfg(feature = "cookies")))] pub fn cookie(&self, name: &str) -> Option> { if let Ok(cookies) = self.cookies() { for cookie in cookies.iter() { @@ -437,16 +464,28 @@ impl fmt::Debug for HttpRequest { self.inner.head.method, self.path() )?; + if !self.query_string().is_empty() { writeln!(f, " query: ?{:?}", self.query_string())?; } + if !self.match_info().is_empty() { writeln!(f, " params: {:?}", self.match_info())?; } + writeln!(f, " headers:")?; + for (key, val) in self.headers().iter() { - writeln!(f, " {:?}: {:?}", key, val)?; + match key { + // redact sensitive header values from debug output + &crate::http::header::AUTHORIZATION + | &crate::http::header::PROXY_AUTHORIZATION + | &crate::http::header::COOKIE => writeln!(f, " {:?}: {:?}", key, "*redacted*")?, + + _ => writeln!(f, " {:?}: {:?}", key, val)?, + } } + Ok(()) } } @@ -513,7 +552,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, @@ -655,13 +694,13 @@ mod tests { #[actix_rt::test] async fn test_drop_http_request_pool() { - let srv = init_service(App::new().service(web::resource("/").to( - |req: HttpRequest| { + let srv = init_service( + App::new().service(web::resource("/").to(|req: HttpRequest| { HttpResponse::Ok() .insert_header(("pool_cap", req.app_state().pool().cap)) .finish() - }, - ))) + })), + ) .await; let req = TestRequest::default().to_request(); @@ -809,10 +848,7 @@ mod tests { web::scope("/user/{id}") .service(web::resource("/profile").route(web::get().to( move |req: HttpRequest| { - assert_eq!( - req.match_pattern(), - Some("/user/{id}/profile".to_owned()) - ); + assert_eq!(req.match_pattern(), Some("/user/{id}/profile".to_owned())); HttpResponse::Ok().finish() }, @@ -913,4 +949,70 @@ mod tests { let body = read_body(bar_resp).await; assert_eq!(body, "http://localhost:8080/bar/nested"); } + + #[test] + fn authorization_header_hidden_in_debug() { + let authorization_header = "Basic bXkgdXNlcm5hbWU6bXkgcGFzc3dvcmQK"; + let req = TestRequest::get() + .insert_header((crate::http::header::AUTHORIZATION, authorization_header)) + .to_http_request(); + + assert!(!format!("{:?}", req).contains(authorization_header)); + } + + #[test] + fn proxy_authorization_header_hidden_in_debug() { + let proxy_authorization_header = "secret value"; + let req = TestRequest::get() + .insert_header(( + crate::http::header::PROXY_AUTHORIZATION, + proxy_authorization_header, + )) + .to_http_request(); + + assert!(!format!("{:?}", req).contains(proxy_authorization_header)); + } + + #[test] + fn cookie_header_hidden_in_debug() { + let cookie_header = "secret"; + let req = TestRequest::get() + .insert_header((crate::http::header::COOKIE, cookie_header)) + .to_http_request(); + + assert!(!format!("{:?}", req).contains(cookie_header)); + } + + #[test] + fn other_header_visible_in_debug() { + let location_header = "192.0.0.1"; + let req = TestRequest::get() + .insert_header((crate::http::header::LOCATION, location_header)) + .to_http_request(); + + assert!(format!("{:?}", req).contains(location_header)); + } + + #[test] + fn check_full_url() { + let req = TestRequest::with_uri("/api?id=4&name=foo").to_http_request(); + assert_eq!( + req.full_url().as_str(), + "http://localhost:8080/api?id=4&name=foo", + ); + + let req = TestRequest::with_uri("https://example.com/api?id=4&name=foo").to_http_request(); + assert_eq!( + req.full_url().as_str(), + "https://example.com/api?id=4&name=foo", + ); + + let req = TestRequest::with_uri("http://10.1.2.3:8443/api?id=4&name=foo") + .insert_header(("host", "example.com")) + .to_http_request(); + assert_eq!( + req.full_url().as_str(), + "http://example.com/api?id=4&name=foo", + ); + } } diff --git a/actix-web/src/request_data.rs b/actix-web/src/request_data.rs index 719e6551..bffbf74d 100644 --- a/actix-web/src/request_data.rs +++ b/actix-web/src/request_data.rs @@ -27,7 +27,6 @@ use crate::{ /// # Examples /// ```no_run /// # use actix_web::{web, HttpResponse, HttpRequest, Responder, HttpMessage as _}; -/// /// #[derive(Debug, Clone, PartialEq)] /// struct FlagFromMiddleware(String); /// diff --git a/actix-web/src/resource.rs b/actix-web/src/resource.rs index 99703675..00555b7b 100644 --- a/actix-web/src/resource.rs +++ b/actix-web/src/resource.rs @@ -21,7 +21,7 @@ use crate::{ BoxedHttpService, BoxedHttpServiceFactory, HttpServiceFactory, ServiceRequest, ServiceResponse, }, - Error, FromRequest, HttpResponse, Responder, + web, Error, FromRequest, HttpResponse, Responder, }; /// A collection of [`Route`]s that respond to the same path pattern. @@ -38,11 +38,13 @@ use crate::{ /// /// let app = App::new().service( /// web::resource("/") -/// .route(web::get().to(|| HttpResponse::Ok()))); +/// .get(|| HttpResponse::Ok()) +/// .post(|| async { "Hello World!" }) +/// ); /// ``` /// -/// If no matching route is found, [a 405 response is returned with an appropriate Allow header][RFC -/// 9110 §15.5.6]. This default behavior can be overridden using +/// If no matching route is found, an empty 405 response is returned which includes an +/// [appropriate Allow header][RFC 9110 §15.5.6]. This default behavior can be overridden using /// [`default_service()`](Self::default_service). /// /// [RFC 9110 §15.5.6]: https://www.rfc-editor.org/rfc/rfc9110.html#section-15.5.6 @@ -58,6 +60,7 @@ pub struct Resource { } impl Resource { + /// Constructs new resource that matches a `path` pattern. pub fn new(path: T) -> Resource { let fref = Rc::new(RefCell::new(None)); @@ -350,12 +353,8 @@ where pub fn default_service(mut self, f: F) -> Self where F: IntoServiceFactory, - U: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - > + 'static, + U: ServiceFactory + + 'static, U::InitError: fmt::Debug, { // create and configure default resource @@ -368,6 +367,45 @@ where } } +macro_rules! route_shortcut { + ($method_fn:ident, $method_upper:literal) => { + #[doc = concat!(" Adds a ", $method_upper, " route.")] + /// + /// Use [`route`](Self::route) if you need to add additional guards. + /// + /// # Examples + /// + /// ``` + /// # use actix_web::web; + /// web::resource("/") + #[doc = concat!(" .", stringify!($method_fn), "(|| async { \"Hello World!\" })")] + /// # ; + /// ``` + pub fn $method_fn(self, handler: F) -> Self + where + F: Handler, + Args: FromRequest + 'static, + F::Output: Responder + 'static, + { + self.route(web::$method_fn().to(handler)) + } + }; +} + +/// Concise routes for well-known HTTP methods. +impl Resource +where + T: ServiceFactory, +{ + route_shortcut!(get, "GET"); + route_shortcut!(post, "POST"); + route_shortcut!(put, "PUT"); + route_shortcut!(patch, "PATCH"); + route_shortcut!(delete, "DELETE"); + route_shortcut!(head, "HEAD"); + route_shortcut!(trace, "TRACE"); +} + impl HttpServiceFactory for Resource where T: ServiceFactory< @@ -502,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] @@ -583,10 +615,8 @@ mod tests { let fut = srv.call(req); async { fut.await.map(|mut res| { - res.headers_mut().insert( - header::CONTENT_TYPE, - HeaderValue::from_static("0001"), - ); + res.headers_mut() + .insert(header::CONTENT_TYPE, HeaderValue::from_static("0001")); res }) } @@ -618,12 +648,9 @@ mod tests { #[actix_rt::test] async fn test_pattern() { - let srv = init_service( - App::new().service( - web::resource(["/test", "/test2"]) - .to(|| async { Ok::<_, Error>(HttpResponse::Ok()) }), - ), - ) + let srv = init_service(App::new().service( + web::resource(["/test", "/test2"]).to(|| async { Ok::<_, Error>(HttpResponse::Ok()) }), + )) .await; let req = TestRequest::with_uri("/test").to_request(); let resp = call_service(&srv, req).await; @@ -744,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() }, @@ -762,17 +789,18 @@ mod tests { #[allow(deprecated)] #[actix_rt::test] async fn test_data_default_service() { - let srv = init_service( - App::new().data(1usize).service( - web::resource("/test") - .data(10usize) - .default_service(web::to(|data: web::Data| { - assert_eq!(**data, 10); - HttpResponse::Ok() - })), - ), - ) - .await; + let srv = + init_service( + App::new().data(1usize).service( + web::resource("/test") + .data(10usize) + .default_service(web::to(|data: web::Data| { + assert_eq!(**data, 10); + HttpResponse::Ok() + })), + ), + ) + .await; let req = TestRequest::get().uri("/test").to_request(); let resp = call_service(&srv, req).await; diff --git a/actix-web/src/response/builder.rs b/actix-web/src/response/builder.rs index 120d4c35..023842ee 100644 --- a/actix-web/src/response/builder.rs +++ b/actix-web/src/response/builder.rs @@ -1,6 +1,5 @@ use std::{ cell::{Ref, RefMut}, - convert::TryInto, future::Future, pin::Pin, task::{Context, Poll}, @@ -15,8 +14,10 @@ use crate::{ body::{BodyStream, BoxBody, MessageBody}, dev::Extensions, error::{Error, JsonPayloadError}, - http::header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue}, - http::{ConnectionType, StatusCode}, + http::{ + header::{self, HeaderName, TryIntoHeaderPair, TryIntoHeaderValue}, + ConnectionType, StatusCode, + }, BoxError, HttpRequest, HttpResponse, Responder, }; @@ -63,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()), }; } @@ -85,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()), }; } @@ -209,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 @@ -407,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, }; @@ -473,9 +471,8 @@ mod tests { #[actix_rt::test] async fn test_serde_json_in_body() { - let resp = HttpResponse::Ok().body( - serde_json::to_vec(&serde_json::json!({ "test-key": "test-value" })).unwrap(), - ); + let resp = HttpResponse::Ok() + .body(serde_json::to_vec(&serde_json::json!({ "test-key": "test-value" })).unwrap()); assert_eq!( body::to_bytes(resp.into_body()).await.unwrap().as_ref(), diff --git a/actix-web/src/response/customize_responder.rs b/actix-web/src/response/customize_responder.rs index f6f4b923..6a43ac5e 100644 --- a/actix-web/src/response/customize_responder.rs +++ b/actix-web/src/response/customize_responder.rs @@ -1,11 +1,13 @@ use actix_http::{ - body::EitherBody, error::HttpError, header::HeaderMap, header::TryIntoHeaderPair, + body::EitherBody, + error::HttpError, + header::{HeaderMap, TryIntoHeaderPair}, StatusCode, }; 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 { @@ -135,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 @@ -168,16 +193,13 @@ where #[cfg(test)] mod tests { - use bytes::Bytes; - use actix_http::body::to_bytes; + use bytes::Bytes; use super::*; use crate::{ - http::{ - header::{HeaderValue, CONTENT_TYPE}, - StatusCode, - }, + cookie::Cookie, + http::header::{HeaderValue, CONTENT_TYPE}, test::TestRequest, }; @@ -211,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/http_codes.rs b/actix-web/src/response/http_codes.rs index 98673534..db5f392c 100644 --- a/actix-web/src/response/http_codes.rs +++ b/actix-web/src/response/http_codes.rs @@ -25,12 +25,12 @@ impl HttpResponse { NonAuthoritativeInformation, StatusCode::NON_AUTHORITATIVE_INFORMATION ); - static_resp!(NoContent, StatusCode::NO_CONTENT); static_resp!(ResetContent, StatusCode::RESET_CONTENT); static_resp!(PartialContent, StatusCode::PARTIAL_CONTENT); static_resp!(MultiStatus, StatusCode::MULTI_STATUS); static_resp!(AlreadyReported, StatusCode::ALREADY_REPORTED); + static_resp!(ImUsed, StatusCode::IM_USED); static_resp!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); static_resp!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); @@ -42,10 +42,10 @@ impl HttpResponse { static_resp!(PermanentRedirect, StatusCode::PERMANENT_REDIRECT); static_resp!(BadRequest, StatusCode::BAD_REQUEST); - static_resp!(NotFound, StatusCode::NOT_FOUND); static_resp!(Unauthorized, StatusCode::UNAUTHORIZED); static_resp!(PaymentRequired, StatusCode::PAYMENT_REQUIRED); static_resp!(Forbidden, StatusCode::FORBIDDEN); + static_resp!(NotFound, StatusCode::NOT_FOUND); static_resp!(MethodNotAllowed, StatusCode::METHOD_NOT_ALLOWED); static_resp!(NotAcceptable, StatusCode::NOT_ACCEPTABLE); static_resp!( @@ -57,13 +57,18 @@ impl HttpResponse { static_resp!(Gone, StatusCode::GONE); static_resp!(LengthRequired, StatusCode::LENGTH_REQUIRED); static_resp!(PreconditionFailed, StatusCode::PRECONDITION_FAILED); - static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED); static_resp!(PayloadTooLarge, StatusCode::PAYLOAD_TOO_LARGE); static_resp!(UriTooLong, StatusCode::URI_TOO_LONG); static_resp!(UnsupportedMediaType, StatusCode::UNSUPPORTED_MEDIA_TYPE); static_resp!(RangeNotSatisfiable, StatusCode::RANGE_NOT_SATISFIABLE); static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED); + static_resp!(ImATeapot, StatusCode::IM_A_TEAPOT); + static_resp!(MisdirectedRequest, StatusCode::MISDIRECTED_REQUEST); static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY); + static_resp!(Locked, StatusCode::LOCKED); + static_resp!(FailedDependency, StatusCode::FAILED_DEPENDENCY); + static_resp!(UpgradeRequired, StatusCode::UPGRADE_REQUIRED); + static_resp!(PreconditionRequired, StatusCode::PRECONDITION_REQUIRED); static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS); static_resp!( RequestHeaderFieldsTooLarge, @@ -83,12 +88,16 @@ impl HttpResponse { static_resp!(VariantAlsoNegotiates, StatusCode::VARIANT_ALSO_NEGOTIATES); static_resp!(InsufficientStorage, StatusCode::INSUFFICIENT_STORAGE); static_resp!(LoopDetected, StatusCode::LOOP_DETECTED); + static_resp!(NotExtended, StatusCode::NOT_EXTENDED); + static_resp!( + NetworkAuthenticationRequired, + StatusCode::NETWORK_AUTHENTICATION_REQUIRED + ); } #[cfg(test)] mod tests { - use crate::http::StatusCode; - use crate::HttpResponse; + use crate::{http::StatusCode, HttpResponse}; #[test] fn test_build() { diff --git a/actix-web/src/response/mod.rs b/actix-web/src/response/mod.rs index 97714710..16bdc619 100644 --- a/actix-web/src/response/mod.rs +++ b/actix-web/src/response/mod.rs @@ -5,10 +5,7 @@ mod responder; #[allow(clippy::module_inception)] mod response; -pub use self::builder::HttpResponseBuilder; -pub use self::customize_responder::CustomizeResponder; -pub use self::responder::Responder; -pub use self::response::HttpResponse; - -#[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 da809198..90d8f6e5 100644 --- a/actix-web/src/response/responder.rs +++ b/actix-web/src/response/responder.rs @@ -21,7 +21,7 @@ use crate::{Error, HttpRequest, HttpResponse}; /// - `HttpResponse` and `HttpResponseBuilder` /// - `Option` where `R: Responder` /// - `Result` where `R: Responder` and [`E: ResponseError`](crate::ResponseError) -/// - `(R, StatusCode) where `R: Responder` +/// - `(R, StatusCode)` where `R: Responder` /// - `&'static str`, `String`, `&'_ String`, `Cow<'_, str>`, [`ByteString`](bytestring::ByteString) /// - `&'static [u8]`, `Vec`, `Bytes`, `BytesMut` /// - [`Json`](crate::web::Json) and [`Form`](crate::web::Form) where `T: Serialize` @@ -186,18 +186,13 @@ impl_into_string_responder!(Cow<'_, str>); #[cfg(test)] pub(crate) mod tests { - use actix_service::Service; - use bytes::{Bytes, BytesMut}; - use actix_http::body::to_bytes; + use actix_service::Service; 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 ead8badb..e16dc0cd 100644 --- a/actix-web/src/response/response.rs +++ b/actix-web/src/response/response.rs @@ -8,7 +8,6 @@ use actix_http::{ header::HeaderMap, Extensions, Response, ResponseHead, StatusCode, }; - #[cfg(feature = "cookies")] use { actix_http::{ @@ -400,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/rmap.rs b/actix-web/src/rmap.rs index 6e10717c..462f3b31 100644 --- a/actix-web/src/rmap.rs +++ b/actix-web/src/rmap.rs @@ -81,7 +81,7 @@ impl ResourceMap { "`pattern` and `nested` mismatch" ); // parents absorb references to the named resources of children - self.named.extend(new_node.named.clone().into_iter()); + self.named.extend(new_node.named.clone()); self.nodes.as_mut().unwrap().push(new_node); } else { let new_node = Rc::new(ResourceMap { @@ -136,7 +136,7 @@ impl ResourceMap { .root_rmap_fn(String::with_capacity(AVG_PATH_LEN), |mut acc, node| { node.pattern .resource_path_from_iter(&mut acc, &mut elements) - .then(|| acc) + .then_some(acc) }) .ok_or(UrlGenerationError::NotEnoughElements)?; @@ -149,7 +149,7 @@ impl ResourceMap { // external resource; third slash would be the root slash in the path let third_slash_index = path .char_indices() - .filter_map(|(i, c)| (c == '/').then(|| i)) + .filter_map(|(i, c)| (c == '/').then_some(i)) .nth(2) .unwrap_or(path.len()); diff --git a/actix-web/src/route.rs b/actix-web/src/route.rs index b37128f2..261e6b9a 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(); @@ -290,31 +291,32 @@ mod tests { #[actix_rt::test] async fn test_route() { - let srv = init_service( - App::new() - .service( - web::resource("/test") - .route(web::get().to(HttpResponse::Ok)) - .route(web::put().to(|| async { - Err::(error::ErrorBadRequest("err")) - })) - .route(web::post().to(|| async { - sleep(Duration::from_millis(100)).await; - Ok::<_, Infallible>(HttpResponse::Created()) - })) - .route(web::delete().to(|| async { - sleep(Duration::from_millis(100)).await; - Err::(error::ErrorBadRequest("err")) - })), - ) - .service(web::resource("/json").route(web::get().to(|| async { - sleep(Duration::from_millis(25)).await; - web::Json(MyObject { - name: "test".to_string(), - }) - }))), - ) - .await; + let srv = + init_service( + App::new() + .service( + web::resource("/test") + .route(web::get().to(HttpResponse::Ok)) + .route(web::put().to(|| async { + Err::(error::ErrorBadRequest("err")) + })) + .route(web::post().to(|| async { + sleep(Duration::from_millis(100)).await; + Ok::<_, Infallible>(HttpResponse::Created()) + })) + .route(web::delete().to(|| async { + sleep(Duration::from_millis(100)).await; + Err::(error::ErrorBadRequest("err")) + })), + ) + .service(web::resource("/json").route(web::get().to(|| async { + sleep(Duration::from_millis(25)).await; + web::Json(MyObject { + name: "test".to_string(), + }) + }))), + ) + .await; let req = TestRequest::with_uri("/test") .method(Method::GET) diff --git a/actix-web/src/rt.rs b/actix-web/src/rt.rs index 7973da73..e370e2c0 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}; //! @@ -66,8 +72,7 @@ // - Re-export but hide the runtime macros because they won't work directly but are required for // `#[actix_web::main]` and `#[actix_web::test]` to work. -pub use actix_rt::{net, pin, signal, spawn, task, time, Runtime, System, SystemRunner}; - #[cfg(feature = "macros")] #[doc(hidden)] pub use actix_macros::{main, test}; +pub use actix_rt::{net, pin, signal, spawn, task, time, Runtime, System, SystemRunner}; diff --git a/actix-web/src/scope.rs b/actix-web/src/scope.rs index 9af05674..adc9f75d 100644 --- a/actix-web/src/scope.rs +++ b/actix-web/src/scope.rs @@ -3,8 +3,8 @@ use std::{cell::RefCell, fmt, future::Future, mem, rc::Rc}; use actix_http::{body::MessageBody, Extensions}; use actix_router::{ResourceDef, Router}; use actix_service::{ - apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, - ServiceFactoryExt, Transform, + apply, apply_fn_factory, boxed, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt, + Transform, }; use futures_core::future::LocalBoxFuture; use futures_util::future::join_all; @@ -273,12 +273,8 @@ where pub fn default_service(mut self, f: F) -> Self where F: IntoServiceFactory, - U: ServiceFactory< - ServiceRequest, - Config = (), - Response = ServiceResponse, - Error = Error, - > + 'static, + U: ServiceFactory + + 'static, U::InitError: fmt::Debug, { // create and configure default resource @@ -474,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)) } })); @@ -551,7 +548,6 @@ impl ServiceFactory for ScopeEndpoint { #[cfg(test)] mod tests { - use actix_service::Service; use actix_utils::future::ok; use bytes::Bytes; @@ -563,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, }; @@ -604,11 +599,11 @@ mod tests { #[actix_rt::test] async fn test_scope() { - let srv = - init_service(App::new().service( - web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok)), - )) - .await; + let srv = init_service( + App::new() + .service(web::scope("/app").service(web::resource("/path1").to(HttpResponse::Ok))), + ) + .await; let req = TestRequest::with_uri("/app/path1").to_request(); let resp = srv.call(req).await.unwrap(); @@ -638,8 +633,7 @@ mod tests { #[actix_rt::test] async fn test_scope_root2() { let srv = init_service( - App::new() - .service(web::scope("/app/").service(web::resource("").to(HttpResponse::Ok))), + App::new().service(web::scope("/app/").service(web::resource("").to(HttpResponse::Ok))), ) .await; @@ -784,10 +778,11 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_no_slash() { - let srv = init_service(App::new().service(web::scope("/app").service( - web::scope("t1").service(web::resource("/path1").to(HttpResponse::Created)), - ))) - .await; + let srv = + init_service(App::new().service(web::scope("/app").service( + web::scope("t1").service(web::resource("/path1").to(HttpResponse::Created)), + ))) + .await; let req = TestRequest::with_uri("/app/t1/path1").to_request(); let resp = srv.call(req).await.unwrap(); @@ -845,12 +840,9 @@ mod tests { #[actix_rt::test] async fn test_nested_scope_with_variable_segment() { let srv = init_service(App::new().service(web::scope("/app").service( - web::scope("/{project_id}").service(web::resource("/path1").to( - |r: HttpRequest| { - HttpResponse::Created() - .body(format!("project: {}", &r.match_info()["project_id"])) - }, - )), + web::scope("/{project_id}").service(web::resource("/path1").to(|r: HttpRequest| { + HttpResponse::Created().body(format!("project: {}", &r.match_info()["project_id"])) + })), ))) .await; @@ -1065,15 +1057,16 @@ mod tests { #[allow(deprecated)] #[actix_rt::test] async fn test_override_data_default_service() { - let srv = init_service(App::new().data(1usize).service( - web::scope("app").data(10usize).default_service(web::to( - |data: web::Data| { - assert_eq!(**data, 10); - HttpResponse::Ok() - }, - )), - )) - .await; + let srv = + init_service(App::new().data(1usize).service( + web::scope("app").data(10usize).default_service(web::to( + |data: web::Data| { + assert_eq!(**data, 10); + HttpResponse::Ok() + }, + )), + )) + .await; let req = TestRequest::with_uri("/app/t").to_request(); let resp = call_service(&srv, req).await; @@ -1150,11 +1143,11 @@ mod tests { #[actix_rt::test] async fn test_url_for_nested() { let srv = init_service(App::new().service(web::scope("/a").service( - web::scope("/b").service(web::resource("/c/{stuff}").name("c").route( - web::get().to(|req: HttpRequest| { + web::scope("/b").service(web::resource("/c/{stuff}").name("c").route(web::get().to( + |req: HttpRequest| { HttpResponse::Ok().body(format!("{}", req.url_for("c", ["12345"]).unwrap())) - }), - )), + }, + ))), ))) .await; diff --git a/actix-web/src/server.rs b/actix-web/src/server.rs index 3a8897f1..33b1e189 100644 --- a/actix-web/src/server.rs +++ b/actix-web/src/server.rs @@ -7,19 +7,21 @@ use std::{ time::Duration, }; +#[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}; use actix_service::{ map_config, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, }; - #[cfg(feature = "openssl")] use actix_tls::accept::openssl::reexports::{AlpnError, SslAcceptor, SslAcceptorBuilder}; -#[cfg(feature = "rustls")] -use actix_tls::accept::rustls::reexports::ServerConfig as RustlsServerConfig; - -#[cfg(any(feature = "openssl", feature = "rustls"))] -use actix_http::TlsAcceptorConfig; use crate::{config::AppConfig, Error}; @@ -33,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, } @@ -41,10 +43,19 @@ struct Config { /// /// Create new HTTP server with application factory. /// -/// # HTTP/2 -/// Currently, HTTP/2 is only supported when using TLS (HTTPS). See `bind_rustls` or `bind_openssl`. +/// # Automatic HTTP Version Selection +/// +/// There are two ways to select the HTTP version of an incoming connection: +/// +/// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both +/// versions are supported automatically when using either of the `.bind_rustls()` or +/// `.bind_openssl()` methods. +/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach +/// for supporting H2C, which allows the HTTP/2 protocol to work over plaintext connections. Use +/// the `.bind_auto_h2c()` method to enable this behavior. /// /// # Examples +/// /// ```no_run /// use actix_web::{web, App, HttpResponse, HttpServer}; /// @@ -94,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, @@ -102,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, @@ -115,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 @@ -163,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 } @@ -215,9 +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"))] - #[cfg_attr(docsrs, doc(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() @@ -241,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. @@ -313,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<()> { @@ -339,7 +396,7 @@ where /// # ; Ok(()) } /// ``` pub fn bind(mut self, addrs: A) -> io::Result { - let sockets = self.bind2(addrs)?; + let sockets = bind_addrs(addrs, self.backlog)?; for lst in sockets { self = self.listen(lst)?; @@ -348,49 +405,93 @@ where Ok(self) } - fn bind2(&self, addrs: A) -> io::Result> { - let mut err = None; - let mut success = false; - let mut sockets = Vec::new(); + /// 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)?; - for addr in addrs.to_socket_addrs()? { - match create_tcp_listener(addr, self.backlog) { - Ok(lst) => { - success = true; - sockets.push(lst); - } - Err(e) => err = Some(e), - } + for lst in sockets { + self = self.listen_auto_h2c(lst)?; } - if success { - Ok(sockets) - } else if let Some(e) = err.take() { - Err(e) - } else { - Err(io::Error::new( - io::ErrorKind::Other, - "Can not bind to address.", - )) - } + Ok(self) } /// 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_attr(docsrs, doc(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 = self.bind2(addrs)?; + 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) } @@ -398,16 +499,15 @@ 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")] - #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] pub fn bind_openssl(mut self, addrs: A, builder: SslAcceptorBuilder) -> io::Result where A: net::ToSocketAddrs, { - let sockets = self.bind2(addrs)?; + let sockets = bind_addrs(addrs, self.backlog)?; let acceptor = openssl_acceptor(builder)?; for lst in sockets { @@ -436,19 +536,18 @@ where 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 cfg = cfg.lock().unwrap(); + let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); let mut svc = HttpService::build() - .keep_alive(c.keep_alive) - .client_request_timeout(c.client_request_timeout) - .client_disconnect_timeout(c.client_disconnect_timeout) + .keep_alive(cfg.keep_alive) + .client_request_timeout(cfg.client_request_timeout) + .client_disconnect_timeout(cfg.client_disconnect_timeout) .local_addr(addr); if let Some(handler) = on_connect_fn.clone() { - svc = svc.on_connect_ext(move |io: &_, ext: _| { - (handler)(io as &dyn Any, ext) - }) + svc = + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) }; let fac = factory() @@ -460,29 +559,89 @@ where })) .tcp() })?; + Ok(self) } - /// Binds to existing listener for accepting incoming TLS connection requests using Rustls. + /// Binds to existing listener for accepting incoming plaintext HTTP/1.x or HTTP/2 connections. + #[cfg(feature = "http2")] + pub fn listen_auto_h2c(mut self, lst: net::TcpListener) -> io::Result { + let cfg = self.config.clone(); + let factory = self.factory.clone(); + let addr = lst.local_addr().unwrap(); + + self.sockets.push(Socket { + addr, + scheme: "http", + }); + + let on_connect_fn = self.on_connect_fn.clone(); + + self.builder = + self.builder + .listen(format!("actix-web-service-{}", addr), lst, move || { + let cfg = cfg.lock().unwrap(); + let host = cfg.host.clone().unwrap_or_else(|| format!("{}", addr)); + + let mut svc = HttpService::build() + .keep_alive(cfg.keep_alive) + .client_request_timeout(cfg.client_request_timeout) + .client_disconnect_timeout(cfg.client_disconnect_timeout) + .local_addr(addr); + + if let Some(handler) = on_connect_fn.clone() { + svc = + svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)) + }; + + let fac = factory() + .into_factory() + .map_err(|err| err.into().error_response()); + + svc.finish(map_config(fac, move |_| { + AppConfig::new(false, host.clone(), addr) + })) + .tcp_auto_h2c() + })?; + + Ok(self) + } + + /// 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_attr(docsrs, doc(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(); @@ -529,13 +688,195 @@ 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. /// /// ALPN protocols "h2" and "http/1.1" are added to any configured ones. #[cfg(feature = "openssl")] - #[cfg_attr(docsrs, doc(cfg(feature = "openssl")))] pub fn listen_openssl( self, lst: net::TcpListener, @@ -681,8 +1022,7 @@ where .client_disconnect_timeout(c.client_disconnect_timeout); if let Some(handler) = on_connect_fn.clone() { - svc = svc - .on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)); + svc = svc.on_connect_ext(move |io: &_, ext: _| (handler)(io as &dyn Any, ext)); } let fac = factory() @@ -724,6 +1064,35 @@ where } } +/// Bind TCP listeners to socket addresses resolved from `addrs` with options. +fn bind_addrs(addrs: impl net::ToSocketAddrs, backlog: u32) -> io::Result> { + let mut err = None; + let mut success = false; + let mut sockets = Vec::new(); + + for addr in addrs.to_socket_addrs()? { + match create_tcp_listener(addr, backlog) { + Ok(lst) => { + success = true; + sockets.push(lst); + } + Err(error) => err = Some(error), + } + } + + if success { + Ok(sockets) + } else if let Some(err) = err.take() { + Err(err) + } else { + Err(io::Error::new( + io::ErrorKind::Other, + "Can not bind to address.", + )) + } +} + +/// Creates a TCP listener from socket address and options. fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result { use socket2::{Domain, Protocol, Socket, Type}; let domain = Domain::for_address(addr); @@ -736,7 +1105,7 @@ fn create_tcp_listener(addr: net::SocketAddr, backlog: u32) -> io::Result io::Result { builder.set_alpn_select_callback(|_, protocols| { diff --git a/actix-web/src/service.rs b/actix-web/src/service.rs index ea23f09f..a1672eba 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 @@ -238,11 +235,7 @@ impl ServiceRequest { self.req.connection_info() } - /// Returns reference to the Path parameters. - /// - /// Params is a container for URL parameters. A variable segment is specified in the form - /// `{identifier}`, where the identifier can be used later in a request handler to access the - /// matched value for that segment. + /// Counterpart to [`HttpRequest::match_info`]. #[inline] pub fn match_info(&self) -> &Path { self.req.match_info() @@ -267,12 +260,13 @@ impl ServiceRequest { } /// Returns a reference to the application's resource map. + /// Counterpart to [`HttpRequest::resource_map`]. #[inline] pub fn resource_map(&self) -> &ResourceMap { self.req.resource_map() } - /// Returns a reference to the application's configuration. + /// Counterpart to [`HttpRequest::app_config`]. #[inline] pub fn app_config(&self) -> &AppConfig { self.req.app_config() @@ -699,30 +693,36 @@ service_tuple! { A B C D E F G H I J K L } #[cfg(test)] mod tests { - use super::*; - use crate::test::{self, init_service, TestRequest}; - use crate::{guard, http, web, App, HttpResponse}; use actix_service::Service; use actix_utils::future::ok; + use super::*; + use crate::{ + guard, http, + test::{self, init_service, TestRequest}, + web, App, + }; + #[actix_rt::test] async fn test_service() { - let srv = init_service( - App::new().service(web::service("/test").name("test").finish( - |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), - )), - ) - .await; + let srv = + init_service( + App::new().service(web::service("/test").name("test").finish( + |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), + )), + ) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); - let srv = init_service( - App::new().service(web::service("/test").guard(guard::Get()).finish( - |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), - )), - ) - .await; + let srv = + init_service( + App::new().service(web::service("/test").guard(guard::Get()).finish( + |req: ServiceRequest| ok(req.into_response(HttpResponse::Ok().finish())), + )), + ) + .await; let req = TestRequest::with_uri("/test") .method(http::Method::PUT) .to_request(); @@ -734,18 +734,19 @@ mod tests { #[allow(deprecated)] #[actix_rt::test] async fn test_service_data() { - let srv = - init_service( - App::new() - .data(42u32) - .service(web::service("/test").name("test").finish( - |req: ServiceRequest| { + let srv = init_service( + App::new() + .data(42u32) + .service( + web::service("/test") + .name("test") + .finish(|req: ServiceRequest| { assert_eq!(req.app_data::>().unwrap().as_ref(), &42); ok(req.into_response(HttpResponse::Ok().finish())) - }, - )), - ) - .await; + }), + ), + ) + .await; let req = TestRequest::with_uri("/test").to_request(); let resp = srv.call(req).await.unwrap(); assert_eq!(resp.status(), http::StatusCode::OK); @@ -776,9 +777,7 @@ mod tests { async fn test_services_macro() { let scoped = services![ web::service("/scoped_test1").name("scoped_test1").finish( - |req: ServiceRequest| async { - Ok(req.into_response(HttpResponse::Ok().finish())) - } + |req: ServiceRequest| async { Ok(req.into_response(HttpResponse::Ok().finish())) } ), web::resource("/scoped_test2").to(|| async { "test2" }), ]; @@ -864,9 +863,7 @@ mod tests { svc.call(req) }) .route("/", web::get().to(|| async { "" })) - .service( - web::resource("/resource1/{name}/index.html").route(web::get().to(index)), - ), + .service(web::resource("/resource1/{name}/index.html").route(web::get().to(index))), ) .await; diff --git a/actix-web/src/test/mod.rs b/actix-web/src/test/mod.rs index 5d9367b8..5e647956 100644 --- a/actix-web/src/test/mod.rs +++ b/actix-web/src/test/mod.rs @@ -29,18 +29,20 @@ mod test_request; mod test_services; mod test_utils; -pub use self::test_request::TestRequest; #[allow(deprecated)] pub use self::test_services::{default_service, ok_service, simple_service, status_service}; -#[allow(deprecated)] -pub use self::test_utils::{ - call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, - read_body_json, read_response, read_response_json, try_call_and_read_body_json, - try_call_service, try_read_body, try_read_body_json, -}; - #[cfg(test)] pub(crate) use self::test_utils::try_init_service; +#[allow(deprecated)] +pub use self::test_utils::{read_response, read_response_json}; +pub use self::{ + test_request::TestRequest, + test_utils::{ + call_and_read_body, call_and_read_body_json, call_service, init_service, read_body, + read_body_json, try_call_and_read_body_json, try_call_service, try_read_body, + try_read_body_json, + }, +}; /// Reduces boilerplate code when testing expected response payloads. /// diff --git a/actix-web/src/test/test_request.rs b/actix-web/src/test/test_request.rs index e81561d1..f178d6f4 100644 --- a/actix-web/src/test/test_request.rs +++ b/actix-web/src/test/test_request.rs @@ -3,13 +3,17 @@ use std::{borrow::Cow, net::SocketAddr, rc::Rc}; use actix_http::{test::TestRequest as HttpTestRequest, Request}; use serde::Serialize; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::{ app_service::AppInitServiceState, config::AppConfig, data::Data, dev::{Extensions, Path, Payload, ResourceDef, Service, Url}, - http::header::ContentType, - http::{header::TryIntoHeaderPair, Method, Uri, Version}, + http::{ + header::{ContentType, TryIntoHeaderPair}, + Method, Uri, Version, + }, rmap::ResourceMap, service::{ServiceRequest, ServiceResponse}, test, @@ -17,9 +21,6 @@ use crate::{ HttpRequest, HttpResponse, }; -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, CookieJar}; - /// Test `Request` builder. /// /// For unit testing, actix provides a request builder type and a simple handler runner. TestRequest implements a builder-like pattern. @@ -85,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; /// @@ -170,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 { @@ -193,38 +195,43 @@ 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 { - let bytes = - serde_json::to_string(&data).expect("Failed to serialize test data to json"); + let bytes = serde_json::to_string(&data).expect("Failed to serialize test data to json"); self.req.set_payload(bytes); self.req.insert_header(ContentType::json()); self } - /// 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/test/test_utils.rs b/actix-web/src/test/test_utils.rs index 398b2960..4540d8a6 100644 --- a/actix-web/src/test/test_utils.rs +++ b/actix-web/src/test/test_utils.rs @@ -201,9 +201,7 @@ where } /// Fallible version of [`read_body`] that allows testing MessageBody reading errors. -pub async fn try_read_body( - res: ServiceResponse, -) -> Result::Error> +pub async fn try_read_body(res: ServiceResponse) -> Result::Error> where B: MessageBody, { @@ -268,7 +266,7 @@ where }) } -/// Fallible version of [`read_body_json`] that allows testing response deserialzation errors. +/// Fallible version of [`read_body_json`] that allows testing response deserialization errors. pub async fn try_read_body_json(res: ServiceResponse) -> Result> where B: MessageBody, @@ -359,13 +357,11 @@ where #[cfg(test)] mod tests { - use serde::{Deserialize, Serialize}; use super::*; use crate::{ - dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage, - HttpResponse, + dev::ServiceRequest, http::header, test::TestRequest, web, App, HttpMessage, HttpResponse, }; #[actix_rt::test] @@ -409,10 +405,11 @@ mod tests { #[actix_rt::test] async fn test_response_json() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; + let app = + init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); @@ -428,10 +425,11 @@ mod tests { #[actix_rt::test] async fn test_try_response_json_error() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; + let app = + init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); @@ -448,10 +446,11 @@ mod tests { #[actix_rt::test] async fn test_body_json() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; + let app = + init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; let payload = r#"{"id":"12345","name":"User name"}"#.as_bytes(); @@ -468,10 +467,11 @@ mod tests { #[actix_rt::test] async fn test_try_body_json_error() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; + let app = + init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; // Use a number for id to cause a deserialization error. let payload = r#"{"id":12345,"name":"User name"}"#.as_bytes(); @@ -489,10 +489,11 @@ mod tests { #[actix_rt::test] async fn test_request_response_form() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Form| HttpResponse::Ok().json(person)), - ))) - .await; + let app = + init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Form| HttpResponse::Ok().json(person)), + ))) + .await; let payload = Person { id: "12345".to_string(), @@ -532,10 +533,11 @@ mod tests { #[actix_rt::test] async fn test_request_response_json() { - let app = init_service(App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - ))) - .await; + let app = + init_service(App::new().service(web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ))) + .await; let payload = Person { id: "12345".to_string(), @@ -566,9 +568,11 @@ mod tests { InitError = (), >, > { - App::new().service(web::resource("/people").route( - web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), - )) + App::new().service( + web::resource("/people").route( + web::post().to(|person: web::Json| HttpResponse::Ok().json(person)), + ), + ) } async fn test_service( diff --git a/actix-web/src/types/either.rs b/actix-web/src/types/either.rs index 119dd0d6..7883e89f 100644 --- a/actix-web/src/types/either.rs +++ b/actix-web/src/types/either.rs @@ -238,8 +238,7 @@ where match res { Ok(bytes) => { let fallback = bytes.clone(); - let left = - L::from_request(this.req, &mut payload_from_bytes(bytes)); + let left = L::from_request(this.req, &mut payload_from_bytes(bytes)); EitherExtractState::Left { left, fallback } } Err(err) => break Err(EitherExtractError::Bytes(err)), @@ -266,10 +265,7 @@ where match res { Ok(data) => break Ok(Either::Right(data)), Err(err) => { - break Err(EitherExtractError::Extract( - left_err.take().unwrap(), - err, - )); + break Err(EitherExtractError::Extract(left_err.take().unwrap(), err)); } } } @@ -291,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 { @@ -304,7 +297,7 @@ mod tests { #[actix_rt::test] async fn test_either_extract_first_try() { let (req, mut pl) = TestRequest::default() - .set_form(&TestForm { + .set_form(TestForm { hello: "world".to_owned(), }) .to_http_parts(); @@ -320,7 +313,7 @@ mod tests { #[actix_rt::test] async fn test_either_extract_fallback() { let (req, mut pl) = TestRequest::default() - .set_json(&TestForm { + .set_json(TestForm { hello: "world".to_owned(), }) .to_http_parts(); @@ -339,31 +332,29 @@ mod tests { .set_payload(Bytes::from_static(b"!@$%^&*()")) .to_http_parts(); - let payload = Either::, Json>, Bytes>::from_request( - &req, &mut pl, - ) - .await - .unwrap() - .unwrap_right(); + let payload = + Either::, Json>, Bytes>::from_request(&req, &mut pl) + .await + .unwrap() + .unwrap_right(); assert_eq!(&payload.as_ref(), &b"!@$%^&*()"); } #[actix_rt::test] async fn test_either_extract_recursive_fallback_inner() { let (req, mut pl) = TestRequest::default() - .set_json(&TestForm { + .set_json(TestForm { hello: "world".to_owned(), }) .to_http_parts(); - let form = Either::, Json>, Bytes>::from_request( - &req, &mut pl, - ) - .await - .unwrap() - .unwrap_left() - .unwrap_right() - .into_inner(); + let form = + Either::, Json>, Bytes>::from_request(&req, &mut pl) + .await + .unwrap() + .unwrap_left() + .unwrap_right() + .into_inner(); assert_eq!(&form.hello, "world"); } } diff --git a/actix-web/src/types/form.rs b/actix-web/src/types/form.rs index d73f8ba7..d6381b99 100644 --- a/actix-web/src/types/form.rs +++ b/actix-web/src/types/form.rs @@ -20,9 +20,8 @@ use serde::{de::DeserializeOwned, Serialize}; #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ - body::EitherBody, error::UrlencodedError, extract::FromRequest, - http::header::CONTENT_LENGTH, web, Error, HttpMessage, HttpRequest, HttpResponse, - Responder, + body::EitherBody, error::UrlencodedError, extract::FromRequest, http::header::CONTENT_LENGTH, + web, Error, HttpMessage, HttpRequest, HttpResponse, Responder, }; /// URL encoded payload extractor and responder. @@ -417,13 +416,12 @@ mod tests { use serde::{Deserialize, Serialize}; use super::*; - use crate::test::TestRequest; use crate::{ http::{ - header::{HeaderValue, CONTENT_LENGTH, CONTENT_TYPE}, + header::{HeaderValue, CONTENT_TYPE}, StatusCode, }, - test::assert_body_eq, + test::{assert_body_eq, TestRequest}, }; #[derive(Deserialize, Serialize, Debug, PartialEq)] diff --git a/actix-web/src/types/header.rs b/actix-web/src/types/header.rs index 6ea77faf..977dc032 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)), } } } @@ -75,8 +75,10 @@ where #[cfg(test)] mod tests { use super::*; - use crate::http::{header, Method}; - use crate::test::TestRequest; + use crate::{ + http::{header, Method}, + test::TestRequest, + }; #[actix_rt::test] async fn test_header_extract() { diff --git a/actix-web/src/types/html.rs b/actix-web/src/types/html.rs new file mode 100644 index 00000000..c370ee07 --- /dev/null +++ b/actix-web/src/types/html.rs @@ -0,0 +1,66 @@ +//! Semantic HTML responder. See [`Html`]. + +use crate::{ + http::{ + header::{self, ContentType, TryIntoHeaderValue}, + StatusCode, + }, + HttpRequest, HttpResponse, Responder, +}; + +/// Semantic HTML responder. +/// +/// When used as a responder, creates a 200 OK response, sets the correct HTML content type, and +/// uses the string passed to [`Html::new()`] as the body. +/// +/// ``` +/// # use actix_web::web::Html; +/// Html::new("

Hello, World!

") +/// # ; +/// ``` +#[derive(Debug, Clone, PartialEq, Hash)] +pub struct Html(String); + +impl Html { + /// Constructs a new `Html` responder. + pub fn new(html: impl Into) -> Self { + Self(html.into()) + } +} + +impl Responder for Html { + type Body = String; + + fn respond_to(self, _req: &HttpRequest) -> HttpResponse { + let mut res = HttpResponse::with_body(StatusCode::OK, self.0); + res.headers_mut().insert( + header::CONTENT_TYPE, + ContentType::html().try_into_value().unwrap(), + ); + res + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::test::TestRequest; + + #[test] + fn responder() { + let req = TestRequest::default().to_http_request(); + + let res = Html::new("

Hello, World!

"); + let res = res.respond_to(&req); + + assert!(res.status().is_success()); + assert!(res + .headers() + .get(header::CONTENT_TYPE) + .unwrap() + .to_str() + .unwrap() + .starts_with("text/html")); + assert!(res.body().starts_with("

")); + } +} diff --git a/actix-web/src/types/json.rs b/actix-web/src/types/json.rs index 4eab5517..6b75c0cf 100644 --- a/actix-web/src/types/json.rs +++ b/actix-web/src/types/json.rs @@ -10,19 +10,18 @@ use std::{ task::{Context, Poll}, }; +use actix_http::Payload; use bytes::BytesMut; use futures_core::{ready, Stream as _}; use serde::{de::DeserializeOwned, Serialize}; -use actix_http::Payload; - #[cfg(feature = "__compress")] use crate::dev::Decompress; use crate::{ body::EitherBody, error::{Error, JsonPayloadError}, extract::FromRequest, - http::header::CONTENT_LENGTH, + http::header::{ContentLength, Header as _}, request::HttpRequest, web, HttpMessage, HttpResponse, Responder, }; @@ -158,8 +157,7 @@ impl FromRequest for Json { } } -type JsonErrorHandler = - Option Error + Send + Sync>>; +type JsonErrorHandler = Option Error + Send + Sync>>; pub struct JsonExtractFut { req: Option, @@ -330,25 +328,26 @@ 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 { return JsonBody::Error(Some(JsonPayloadError::ContentType)); } - let length = req - .headers() - .get(&CONTENT_LENGTH) - .and_then(|l| l.to_str().ok()) - .and_then(|s| s.parse::().ok()); + let length = ContentLength::parse(req).ok().map(|x| x.0); // Notice the content-length is not checked against limit of json config here. // As the internal usage always call JsonBody::limit after JsonBody::new. @@ -423,9 +422,7 @@ impl Future for JsonBody { let chunk = chunk?; let buf_len = buf.len() + chunk.len(); if buf_len > *limit { - return Poll::Ready(Err(JsonPayloadError::Overflow { - limit: *limit, - })); + return Poll::Ready(Err(JsonPayloadError::Overflow { limit: *limit })); } else { buf.extend_from_slice(&chunk); } @@ -508,8 +505,7 @@ mod tests { let msg = MyObject { name: "invalid request".to_string(), }; - let resp = - HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); + let resp = HttpResponse::BadRequest().body(serde_json::to_string(&msg).unwrap()); InternalError::from_response(err, resp).into() })) .to_http_parts(); @@ -734,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() @@ -747,7 +762,8 @@ mod tests { assert!(s.is_err()); let err_str = s.err().unwrap().to_string(); - assert!(err_str - .contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).")); + assert!( + err_str.contains("JSON payload (16 bytes) is larger than allowed (limit: 10 bytes).") + ); } } diff --git a/actix-web/src/types/mod.rs b/actix-web/src/types/mod.rs index bab7c3bc..cabe53d6 100644 --- a/actix-web/src/types/mod.rs +++ b/actix-web/src/types/mod.rs @@ -3,17 +3,21 @@ mod either; mod form; mod header; +mod html; mod json; mod path; mod payload; mod query; mod readlines; -pub use self::either::Either; -pub use self::form::{Form, FormConfig, UrlEncoded}; -pub use self::header::Header; -pub use self::json::{Json, JsonBody, JsonConfig}; -pub use self::path::{Path, PathConfig}; -pub use self::payload::{Payload, PayloadConfig}; -pub use self::query::{Query, QueryConfig}; -pub use self::readlines::Readlines; +pub use self::{ + either::Either, + form::{Form, FormConfig, UrlEncoded}, + header::Header, + html::Html, + json::{Json, JsonBody, JsonConfig}, + path::{Path, PathConfig}, + payload::{Payload, PayloadConfig}, + query::{Query, QueryConfig}, + readlines::Readlines, +}; diff --git a/actix-web/src/types/path.rs b/actix-web/src/types/path.rs index a90c912f..cc87bb80 100644 --- a/actix-web/src/types/path.rs +++ b/actix-web/src/types/path.rs @@ -156,8 +156,7 @@ mod tests { use serde::Deserialize; use super::*; - use crate::test::TestRequest; - use crate::{error, http, HttpResponse}; + use crate::{error, http, test::TestRequest, HttpResponse}; #[derive(Deserialize, Debug, Display)] #[display(fmt = "MyStruct({}, {})", key, value)] @@ -276,8 +275,7 @@ mod tests { async fn test_custom_err_handler() { let (req, mut pl) = TestRequest::with_uri("/name/user1/") .app_data(PathConfig::default().error_handler(|err, _| { - error::InternalError::from_response(err, HttpResponse::Conflict().finish()) - .into() + error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into() })) .to_http_parts(); diff --git a/actix-web/src/types/payload.rs b/actix-web/src/types/payload.rs index 4045cedb..e4db37d0 100644 --- a/actix-web/src/types/payload.rs +++ b/actix-web/src/types/payload.rs @@ -16,7 +16,7 @@ use futures_core::{ready, stream::Stream}; use mime::Mime; use crate::{ - dev, error::ErrorBadRequest, http::header, web, Error, FromRequest, HttpMessage, + body, dev, error::ErrorBadRequest, http::header, web, Error, FromRequest, HttpMessage, HttpRequest, }; @@ -51,6 +51,72 @@ impl Payload { pub fn into_inner(self) -> dev::Payload { self.0 } + + /// Buffers payload from request up to `limit` bytes. + /// + /// This method is preferred over [`Payload::to_bytes()`] since it will not lead to unexpected + /// memory exhaustion from massive payloads. Note that the other primitive extractors such as + /// [`Bytes`] and [`String`], as well as extractors built on top of them, already have this sort + /// of protection according to the configured (or default) [`PayloadConfig`]. + /// + /// # Errors + /// + /// - The outer error type, [`BodyLimitExceeded`](body::BodyLimitExceeded), is returned when the + /// payload is larger than `limit`. + /// - The inner error type is [the normal Actix Web error](crate::Error) and is only returned if + /// the payload stream yields an error for some reason. Such cases are usually caused by + /// unrecoverable connection issues. + /// + /// # Examples + /// + /// ``` + /// use actix_web::{error, web::Payload, Responder}; + /// + /// async fn limited_payload_handler(pl: Payload) -> actix_web::Result { + /// match pl.to_bytes_limited(5).await { + /// Ok(res) => res, + /// Err(err) => Err(error::ErrorPayloadTooLarge(err)), + /// } + /// } + /// ``` + pub async fn to_bytes_limited( + self, + limit: usize, + ) -> Result, body::BodyLimitExceeded> { + let stream = body::BodyStream::new(self.0); + + match body::to_bytes_limited(stream, limit).await { + Ok(Ok(body)) => Ok(Ok(body)), + Ok(Err(err)) => Ok(Err(err.into())), + Err(err) => Err(err), + } + } + + /// Buffers entire payload from request. + /// + /// Use of this method is discouraged unless you know for certain that requests will not be + /// large enough to exhaust memory. If this is not known, prefer [`Payload::to_bytes_limited()`] + /// or one of the higher level extractors like [`Bytes`] or [`String`] that implement size + /// limits according to the configured (or default) [`PayloadConfig`]. + /// + /// # Errors + /// + /// An error is only returned if the payload stream yields an error for some reason. Such cases + /// are usually caused by unrecoverable connection issues. + /// + /// # Examples + /// + /// ``` + /// use actix_web::{error, web::Payload, Responder}; + /// + /// async fn payload_handler(pl: Payload) -> actix_web::Result { + /// pl.to_bytes().await + /// } + /// ``` + pub async fn to_bytes(self) -> crate::Result { + let stream = body::BodyStream::new(self.0); + Ok(body::to_bytes(stream).await?) + } } impl Stream for Payload { @@ -65,7 +131,7 @@ impl Stream for Payload { /// See [here](#Examples) for example of usage as an extractor. impl FromRequest for Payload { type Error = Error; - type Future = Ready>; + type Future = Ready>; #[inline] fn from_request(_: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { @@ -374,12 +440,55 @@ impl Future for HttpMessageBody { #[cfg(test)] mod tests { - use bytes::Bytes; - use super::*; - use crate::http::{header, StatusCode}; - use crate::test::{call_service, init_service, TestRequest}; - use crate::{web, App, Responder}; + use crate::{ + http::StatusCode, + test::{call_service, init_service, read_body, TestRequest}, + App, Responder, + }; + + #[actix_rt::test] + async fn payload_to_bytes() { + async fn payload_handler(pl: Payload) -> crate::Result { + pl.to_bytes().await + } + + async fn limited_payload_handler(pl: Payload) -> crate::Result { + match pl.to_bytes_limited(5).await { + Ok(res) => res, + Err(_limited) => Err(ErrorBadRequest("too big")), + } + } + + let srv = init_service( + App::new() + .route("/all", web::to(payload_handler)) + .route("limited", web::to(limited_payload_handler)), + ) + .await; + + let req = TestRequest::with_uri("/all") + .set_payload("1234567890") + .to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + let body = read_body(res).await; + assert_eq!(body, "1234567890"); + + let req = TestRequest::with_uri("/limited") + .set_payload("1234567890") + .to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::BAD_REQUEST); + + let req = TestRequest::with_uri("/limited") + .set_payload("12345") + .to_request(); + let res = call_service(&srv, req).await; + assert_eq!(res.status(), StatusCode::OK); + let body = read_body(res).await; + assert_eq!(body, "12345"); + } #[actix_rt::test] async fn test_payload_config() { diff --git a/actix-web/src/web.rs b/actix-web/src/web.rs index 0533f7f8..20431375 100644 --- a/actix-web/src/web.rs +++ b/actix-web/src/web.rs @@ -21,17 +21,14 @@ use std::{borrow::Cow, future::Future}; use actix_router::IntoPatterns; pub use bytes::{Buf, BufMut, Bytes, BytesMut}; +pub use crate::{ + config::ServiceConfig, data::Data, redirect::Redirect, request_data::ReqData, types::*, +}; use crate::{ error::BlockingError, http::Method, service::WebService, FromRequest, Handler, Resource, Responder, Route, Scope, }; -pub use crate::config::ServiceConfig; -pub use crate::data::Data; -pub use crate::redirect::Redirect; -pub use crate::request_data::ReqData; -pub use crate::types::*; - /// Creates a new resource for a specific path. /// /// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c` @@ -200,10 +197,7 @@ pub fn service(path: T) -> WebService { /// // the client will resolve this redirect to /api/to-path /// .service(web::redirect("/api/from-path", "to-path")); /// ``` -pub fn redirect( - from: impl Into>, - to: impl Into>, -) -> Redirect { +pub fn redirect(from: impl Into>, to: impl Into>) -> Redirect { Redirect::new(from, to) } diff --git a/actix-web/tests/compression.rs b/actix-web/tests/compression.rs index b911b9d1..61ff1bff 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 861d76d9..039c0ffb 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 270223d6..960cf1e2 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::{ @@ -19,14 +19,13 @@ use actix_web::{ }; use bytes::Bytes; use futures_core::ready; -use rand::{distributions::Alphanumeric, Rng as _}; - #[cfg(feature = "openssl")] use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslMethod}, x509::X509, }; +use rand::{distributions::Alphanumeric, Rng as _}; mod utils; @@ -35,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(); @@ -94,9 +95,8 @@ impl futures_core::stream::Stream for TestBody { #[actix_rt::test] async fn test_body() { let srv = actix_test::start(|| { - App::new().service( - web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })), - ) + App::new() + .service(web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) }))) }); let mut res = srv.get("/").send().await.unwrap(); @@ -226,8 +226,7 @@ async fn test_body_chunked_implicit() { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::get().to(|| async { - HttpResponse::Ok() - .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) + HttpResponse::Ok().streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); @@ -256,8 +255,7 @@ async fn test_body_br_streaming() { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(|| async { - HttpResponse::Ok() - .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) + HttpResponse::Ok().streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); @@ -392,8 +390,7 @@ async fn test_body_zstd_streaming() { App::new() .wrap(Compress::default()) .service(web::resource("/").route(web::to(move || async { - HttpResponse::Ok() - .streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) + HttpResponse::Ok().streaming(TestBody::new(Bytes::from_static(STR.as_ref()), 24)) }))) }); @@ -686,15 +683,14 @@ async fn test_brotli_encoding_large_openssl() { use actix_web::http::header; let data = STR.repeat(10); - let srv = - actix_test::start_with(actix_test::config().openssl(openssl_config()), move || { - App::new().service(web::resource("/").route(web::to(|bytes: Bytes| async { - // echo decompressed request body back in response - HttpResponse::Ok() - .insert_header(header::ContentEncoding::Identity) - .body(bytes) - }))) - }); + let srv = actix_test::start_with(actix_test::config().openssl(openssl_config()), move || { + App::new().service(web::resource("/").route(web::to(|bytes: Bytes| async { + // echo decompressed request body back in response + HttpResponse::Ok() + .insert_header(header::ContentEncoding::Identity) + .body(bytes) + }))) + }); let mut res = srv .post("/") @@ -710,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() } @@ -749,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/actix-web/tests/utils.rs b/actix-web/tests/utils.rs index 2532640c..b9c70888 100644 --- a/actix-web/tests/utils.rs +++ b/actix-web/tests/utils.rs @@ -4,9 +4,10 @@ use std::io::{Read as _, Write as _}; pub mod gzip { - use super::*; use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); encoder.write_all(bytes.as_ref()).unwrap(); @@ -22,9 +23,10 @@ pub mod gzip { } pub mod deflate { - use super::*; use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); encoder.write_all(bytes.as_ref()).unwrap(); @@ -40,9 +42,10 @@ pub mod deflate { } pub mod brotli { - use super::*; use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = BrotliEncoder::new( Vec::new(), @@ -64,9 +67,10 @@ pub mod brotli { } pub mod zstd { - use super::*; use ::zstd::stream::{read::Decoder, write::Encoder}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = Encoder::new(Vec::new(), 3).unwrap(); encoder.write_all(bytes.as_ref()).unwrap(); diff --git a/awc/CHANGES.md b/awc/CHANGES.md index 7892d933..54c5e986 100644 --- a/awc/CHANGES.md +++ b/awc/CHANGES.md @@ -1,22 +1,59 @@ # Changes -## Unreleased - 2022-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 + ### Changed + +- `client::Connect` is now public to allow tunneling connection with `client::Connector`. + +## 3.1.0 + +### Changed + - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency. +## 3.0.1 -## 3.0.1 - 2022-08-25 ### Changed + - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency. ### Fixed + - Fixed handling of redirection requests that begin with `//`. [#2840] [#2840]: https://github.com/actix/actix-web/pull/2840 +## 3.0.0 -## 3.0.0 - 2022-03-07 ### Dependencies + - Updated `actix-*` to Tokio v1-based versions. [#1813] - Updated `bytes` to `1.0`. [#1813] - Updated `cookie` to `0.16`. [#2555] @@ -25,6 +62,7 @@ - Updated `tokio` to `1`. ### Added + - `trust-dns` crate feature to enable `trust-dns-resolver` as client DNS resolver; disabled by default. [#1969] - `cookies` crate feature; enabled by default. [#2619] - `compress-brotli` crate feature; enabled by default. [#2250] @@ -41,6 +79,7 @@ - `ClientBuilder::add_default_header()` (and deprecate `ClientBuilder::header()`). [#2510] ### Changed + - `client::Connector` type now only has one generic type for `actix_service::Service`. [#2063] - `client::error::ConnectError` Resolver variant contains `Box` type. [#1905] - `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905] @@ -58,6 +97,7 @@ - Minimum supported Rust version (MSRV) is now 1.54. ### Fixed + - Send headers along with redirected requests. [#2310] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] - Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] @@ -66,6 +106,7 @@ - `impl Stream` for `ClientResponse` no longer requires the body type be `Unpin`. [#2546] ### Removed + - `compress` crate feature. [#2250] - `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] - `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869] @@ -75,10 +116,10 @@ - `ClientBuilder::default` function [#2008] ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html - +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html [#1813]: https://github.com/actix/actix-web/pull/1813 [#1869]: https://github.com/actix/actix-web/pull/1869 [#1905]: https://github.com/actix/actix-web/pull/1905 @@ -108,46 +149,48 @@ [#2553]: https://github.com/actix/actix-web/pull/2553 [#2555]: https://github.com/actix/actix-web/pull/2555 -

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 -## 3.0.0-beta.20 - 2022-01-31 - No significant changes since `3.0.0-beta.19`. +## 3.0.0-beta.19 -## 3.0.0-beta.19 - 2022-01-21 - No significant changes since `3.0.0-beta.18`. +## 3.0.0-beta.18 -## 3.0.0-beta.18 - 2022-01-04 - Minimum supported Rust version (MSRV) is now 1.54. +## 3.0.0-beta.17 -## 3.0.0-beta.17 - 2021-12-29 ### Changed + - Update `cookie` dependency (re-exported) to `0.16`. [#2555] ### Security + - `cookie` upgrade addresses [`RUSTSEC-2020-0071`]. [#2555]: https://github.com/actix/actix-web/pull/2555 -[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html +[`rustsec-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html +## 3.0.0-beta.16 -## 3.0.0-beta.16 - 2021-12-29 - `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553] - `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553] - Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553] [#2553]: https://github.com/actix/actix-web/pull/2553 +## 3.0.0-beta.15 -## 3.0.0-beta.15 - 2021-12-27 - Rename `Connector::{ssl => openssl}`. [#2503] - Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503] - `ClientRequest::send_body` now takes an `impl MessageBody`. [#2546] @@ -159,89 +202,96 @@ [#2503]: https://github.com/actix/actix-web/pull/2503 [#2546]: https://github.com/actix/actix-web/pull/2546 +## 3.0.0-beta.14 -## 3.0.0-beta.14 - 2021-12-17 - Add `ClientBuilder::add_default_header` and deprecate `ClientBuilder::header`. [#2510] [#2510]: https://github.com/actix/actix-web/pull/2510 +## 3.0.0-beta.13 -## 3.0.0-beta.13 - 2021-12-11 - No significant changes since `3.0.0-beta.12`. +## 3.0.0-beta.12 -## 3.0.0-beta.12 - 2021-11-30 - Update `actix-tls` to `3.0.0-rc.1`. [#2474] [#2474]: https://github.com/actix/actix-web/pull/2474 +## 3.0.0-beta.11 -## 3.0.0-beta.11 - 2021-11-22 - No significant changes from `3.0.0-beta.10`. +## 3.0.0-beta.10 -## 3.0.0-beta.10 - 2021-11-15 - No significant changes from `3.0.0-beta.9`. +## 3.0.0-beta.9 -## 3.0.0-beta.9 - 2021-10-20 - Updated rustls to v0.20. [#2414] [#2414]: https://github.com/actix/actix-web/pull/2414 +## 3.0.0-beta.8 -## 3.0.0-beta.8 - 2021-09-09 ### Changed + - Send headers within the redirect requests. [#2310] [#2310]: https://github.com/actix/actix-web/pull/2310 +## 3.0.0-beta.7 -## 3.0.0-beta.7 - 2021-06-26 ### Changed + - Change compression algorithm features flags. [#2250] [#2250]: https://github.com/actix/actix-web/pull/2250 +## 3.0.0-beta.6 -## 3.0.0-beta.6 - 2021-06-17 - No significant changes since 3.0.0-beta.5. +## 3.0.0-beta.5 -## 3.0.0-beta.5 - 2021-04-17 ### Removed + - Deprecated methods on `ClientRequest`: `if_true`, `if_some`. [#2148] [#2148]: https://github.com/actix/actix-web/pull/2148 +## 3.0.0-beta.4 -## 3.0.0-beta.4 - 2021-04-02 ### Added + - Add `Client::headers` to get default mut reference of `HeaderMap` of client object. [#2114] ### Changed + - `ConnectorService` type is renamed to `BoxConnectorService`. [#2081] - Fix http/https encoding when enabling `compress` feature. [#2116] -- Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header - methods now take `TryIntoHeaderPair` tuples. [#2094] +- Rename `TestResponse::header` to `append_header`, `set` to `insert_header`. `TestResponse` header methods now take `TryIntoHeaderPair` tuples. [#2094] [#2081]: https://github.com/actix/actix-web/pull/2081 [#2094]: https://github.com/actix/actix-web/pull/2094 [#2114]: https://github.com/actix/actix-web/pull/2114 [#2116]: https://github.com/actix/actix-web/pull/2116 +## 3.0.0-beta.3 -## 3.0.0-beta.3 - 2021-03-08 ### Added + - `ClientResponse::timeout` for set the timeout of collecting response body. [#1931] - `ClientBuilder::local_address` for bind to a local ip address for this client. [#2024] ### Changed + - Feature `cookies` is now optional and enabled by default. [#1981] - `ClientBuilder::connector` method would take `actix_http::client::Connector` type. [#2008] - Basic auth password now takes blank passwords as an empty string instead of Option. [#2050] ### Removed + - `ClientBuilder::default` function [#2008] [#1931]: https://github.com/actix/actix-web/pull/1931 @@ -250,17 +300,20 @@ [#2024]: https://github.com/actix/actix-web/pull/2024 [#2050]: https://github.com/actix/actix-web/pull/2050 +## 3.0.0-beta.2 -## 3.0.0-beta.2 - 2021-02-10 ### Added + - `ClientRequest::insert_header` method which allows using typed headers. [#1869] - `ClientRequest::append_header` method which allows using typed headers. [#1869] - `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969] ### Changed + - Relax default timeout for `Connector` to 5 seconds(original 1 second). [#1905] ### Removed + - `ClientRequest::set`; use `ClientRequest::insert_header`. [#1869] - `ClientRequest::set_header`; use `ClientRequest::insert_header`. [#1869] - `ClientRequest::set_header_if_none`; use `ClientRequest::insert_header_if_none`. [#1869] @@ -270,9 +323,10 @@ [#1905]: https://github.com/actix/actix-web/pull/1905 [#1969]: https://github.com/actix/actix-web/pull/1969 +## 3.0.0-beta.1 -## 3.0.0-beta.1 - 2021-01-07 ### Changed + - Update `rand` to `0.8` - Update `bytes` to `1.0`. [#1813] - Update `rust-tls` to `0.19`. [#1813] @@ -281,57 +335,66 @@
-## 2.0.3 - 2020-11-29 +## 2.0.3 + ### Fixed + - Ensure `actix-http` dependency uses same `serde_urlencoded`. +## 2.0.2 -## 2.0.2 - 2020-11-25 ### Changed + - Upgrade `serde_urlencoded` to `0.7`. [#1773] [#1773]: https://github.com/actix/actix-web/pull/1773 +## 2.0.1 -## 2.0.1 - 2020-10-30 ### Changed + - Upgrade `base64` to `0.13`. [#1744] - Deprecate `ClientRequest::{if_some, if_true}`. [#1760] ### Fixed -- Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature - is enabled [#1737] + +- Use `Accept-Encoding: identity` instead of `Accept-Encoding: br` when no compression feature is enabled [#1737] [#1737]: https://github.com/actix/actix-web/pull/1737 [#1760]: https://github.com/actix/actix-web/pull/1760 [#1744]: https://github.com/actix/actix-web/pull/1744 +## 2.0.0 -## 2.0.0 - 2020-09-11 ### Changed + - `Client::build` was renamed to `Client::builder`. +## 2.0.0-beta.4 -## 2.0.0-beta.4 - 2020-09-09 ### Changed + - Update actix-codec & actix-tls dependencies. +## 2.0.0-beta.3 -## 2.0.0-beta.3 - 2020-08-17 ### Changed + - Update `rustls` to 0.18 +## 2.0.0-beta.2 -## 2.0.0-beta.2 - 2020-07-21 ### Changed + - Update `actix-http` dependency to 2.0.0-beta.2 +## 2.0.0-beta.1 -## [2.0.0-beta.1] - 2020-07-14 ### 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 @@ -341,46 +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 @@ -390,8 +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 @@ -399,15 +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 @@ -415,14 +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 @@ -432,8 +487,7 @@ - Upgrade actix-http dependency. - -## [0.1.1] - 2019-04-19 +## 0.1.1 ### Added @@ -443,20 +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 @@ -466,15 +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 @@ -482,7 +531,6 @@ - `ClientResponse::json()` - Loads and parse `application/json` encoded body - ### Changed - `ClientRequest::json()` accepts reference instead of object. @@ -491,8 +539,7 @@ - Renamed `ClientRequest::close_connection()` to `ClientRequest::force_close()` - -## [0.1.0-alpha.2] - 2019-03-29 +## 0.1.0-alpha.2 ### Added @@ -504,14 +551,12 @@ - Re-export `actix_http::client::Connector`. - ### Changed - Allow to override request's uri - 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 41be3ef8..4fc2057f 100644 --- a/awc/Cargo.toml +++ b/awc/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "awc" -version = "3.0.1" +version = "3.5.0" authors = ["Nikolay Kim "] description = "Async HTTP and WebSocket client library" keywords = ["actix", "http", "framework", "async", "web"] @@ -11,26 +11,67 @@ 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 = "2018" - -[lib] -name = "awc" -path = "src/lib.rs" +edition = "2021" [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", +] + +[package.metadata.cargo_check_external_types] +allowed_external_types = [ + "actix_codec::*", + "actix_http::*", + "actix_rt::*", + "actix_service::*", + "actix_tls::*", + "bytes::*", + "cookie::*", + "cookie", + "futures_core::*", + "h2::*", + "http::*", + "openssl::*", + "rustls::*", + "serde_json::*", + "serde_urlencoded::*", + "serde::*", + "tokio::*", +] [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 +80,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,20 +98,19 @@ dangerous-h2c = [] [dependencies] actix-codec = "0.5" actix-service = "2" -actix-http = { version = "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" -ahash = "0.7" -base64 = "0.13" +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.9" -http = "0.2.5" +h2 = "0.3.26" +http = "0.2.7" itoa = "1" log =" 0.4" mime = "0.3" @@ -80,35 +120,39 @@ rand = "0.8" serde = "1.0" serde_json = "1.0" serde_urlencoded = "0.7" -tokio = { version = "1.18.4", features = ["sync"] } +tokio = { version = "1.24.2", features = ["sync"] } cookie = { version = "0.16", features = ["percent-encode"], optional = true } -tls-openssl = { package = "openssl", version = "0.10.9", optional = true } -tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] } +tls-openssl = { package = "openssl", version = "0.10.55", optional = true } +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" -const-str = "0.4" -env_logger = "0.9" +brotli = "6" +const-str = "0.5" +env_logger = "0.11" flate2 = "1.0.13" futures-util = { version = "0.3.17", default-features = false } static_assertions = "1.1" -rcgen = "0.9" -rustls-pemfile = "1" -tokio = { version = "1.18.4", features = ["rt-multi-thread", "macros"] } -zstd = "0.12" +rcgen = "0.13" +rustls-pemfile = "2" +tokio = { version = "1.24.2", features = ["rt-multi-thread", "macros"] } +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 9f47e663..8e7b4281 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.0.1)](https://docs.rs/awc/3.0.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.0.1/status.svg)](https://deps.rs/crate/awc/3.0.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.54 +## 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 26edcfd6..16ad330b 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/any_body.rs b/awc/src/any_body.rs index d9c259d8..08f5cc25 100644 --- a/awc/src/any_body.rs +++ b/awc/src/any_body.rs @@ -4,11 +4,10 @@ use std::{ task::{Context, Poll}, }; +use actix_http::body::{BodySize, BoxBody, MessageBody}; use bytes::Bytes; use pin_project_lite::pin_project; -use actix_http::body::{BodySize, BoxBody, MessageBody}; - pin_project! { /// Represents various types of HTTP message body. #[derive(Clone)] diff --git a/awc/src/builder.rs b/awc/src/builder.rs index 34a5f850..5aae394f 100644 --- a/awc/src/builder.rs +++ b/awc/src/builder.rs @@ -1,4 +1,4 @@ -use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration}; +use std::{fmt, net::IpAddr, rc::Rc, time::Duration}; use actix_http::{ error::HttpError, @@ -7,6 +7,7 @@ use actix_http::{ }; use actix_rt::net::{ActixStream, TcpStream}; use actix_service::{boxed, Service}; +use base64::prelude::*; use crate::{ client::{ @@ -36,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< @@ -70,11 +77,8 @@ where /// Use custom connector service. pub fn connector(self, connector: Connector) -> ClientBuilder where - S1: Service< - ConnectInfo, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone + S1: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone + 'static, Io1: ActixStream + fmt::Debug + 'static, { @@ -210,7 +214,7 @@ where }; self.add_default_header(( header::AUTHORIZATION, - format!("Basic {}", base64::encode(auth)), + format!("Basic {}", BASE64_STANDARD.encode(auth)), )) } @@ -225,10 +229,7 @@ where /// Registers middleware, in the form of a middleware component (type), that runs during inbound /// and/or outbound processing in the request life-cycle (request -> response), /// modifying request/response as necessary, across all requests managed by the `Client`. - pub fn wrap( - self, - mw: M1, - ) -> ClientBuilder> + pub fn wrap(self, mw: M1) -> ClientBuilder> where M: Transform, M1: Transform, @@ -251,8 +252,7 @@ where pub fn finish(self) -> Client where M: Transform>, ConnectRequest> + 'static, - M::Transform: - Service, + M::Transform: Service, { let max_redirects = self.max_redirects; @@ -267,8 +267,7 @@ where fn _finish(self) -> Client where M: Transform>, ConnectRequest> + 'static, - M::Transform: - Service, + M::Transform: Service, { let mut connector = self.connector; diff --git a/awc/src/client/connection.rs b/awc/src/client/connection.rs index 9de4ece4..8164e2b5 100644 --- a/awc/src/client/connection.rs +++ b/awc/src/client/connection.rs @@ -7,19 +7,15 @@ use std::{ }; use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; +use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead}; use actix_rt::task::JoinHandle; use bytes::Bytes; use futures_core::future::LocalBoxFuture; use h2::client::SendRequest; -use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead}; - +use super::{error::SendRequestError, h1proto, h2proto, pool::Acquired}; use crate::BoxError; -use super::error::SendRequestError; -use super::pool::Acquired; -use super::{h1proto, h2proto}; - /// Trait alias for types impl [tokio::io::AsyncRead] and [tokio::io::AsyncWrite]. pub trait ConnectionIo: AsyncRead + AsyncWrite + Unpin + 'static {} @@ -83,10 +79,7 @@ impl AsyncWrite for H1Connection { self.io_pin_mut().poll_flush(cx) } - fn poll_shutdown( - self: Pin<&mut Self>, - cx: &mut Context<'_>, - ) -> Poll> { + fn poll_shutdown(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll> { self.io_pin_mut().poll_shutdown(cx) } @@ -387,8 +380,6 @@ mod test { use std::{ future::Future, net, - pin::Pin, - task::{Context, Poll}, time::{Duration, Instant}, }; @@ -401,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(); @@ -435,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 51d6e180..f3d44307 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,64 +103,119 @@ 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, - S1: Service< - ConnectInfo, - Response = TcpConnection, - Error = TcpConnectError, - > + Clone, + S1: Service, Response = TcpConnection, Error = TcpConnectError> + + Clone, { Connector { connector, @@ -161,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, @@ -189,21 +274,59 @@ where #[doc(hidden)] #[cfg(feature = "openssl")] #[deprecated(since = "3.0.0", note = "Renamed to `Connector::openssl`.")] - pub fn ssl( - mut self, - connector: actix_tls::connect::openssl::reexports::SslConnector, - ) -> Self { + pub fn ssl(mut self, connector: actix_tls::connect::openssl::reexports::SslConnector) -> Self { self.tls = OurTlsConnector::Openssl(connector); 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 } @@ -218,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 { @@ -231,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. @@ -312,9 +435,7 @@ where let tls = match self.tls { #[cfg(feature = "openssl")] - OurTlsConnector::OpensslBuilder(builder) => { - OurTlsConnector::Openssl(builder.build()) - } + OurTlsConnector::OpensslBuilder(builder) => OurTlsConnector::Openssl(builder.build()), tls => tls, }; @@ -332,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; @@ -382,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; @@ -413,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; @@ -467,9 +696,7 @@ pub struct TcpConnectorService { impl Service for TcpConnectorService where - S: Service, Error = ConnectError> - + Clone - + 'static, + S: Service, Error = ConnectError> + Clone + 'static, { type Response = (Io, Protocol); type Error = ConnectError; @@ -508,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, @@ -518,11 +756,19 @@ 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: Service, Error = ConnectError> - + Clone - + 'static, + Tcp: + Service, Error = ConnectError> + Clone + 'static, Tls: Service, Error = std::io::Error> + Clone + 'static, Tls::Response: IntoConnectionIo, IO: ConnectionIo, @@ -800,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, @@ -835,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. @@ -848,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/error.rs b/awc/src/client/error.rs index 9f290c5c..d351e106 100644 --- a/awc/src/client/error.rs +++ b/awc/src/client/error.rs @@ -1,11 +1,9 @@ use std::{fmt, io}; -use derive_more::{Display, From}; - use actix_http::error::{HttpError, ParseError}; - #[cfg(feature = "openssl")] use actix_tls::accept::openssl::reexports::Error as OpensslError; +use derive_more::{Display, From}; use crate::BoxError; diff --git a/awc/src/client/h1proto.rs b/awc/src/client/h1proto.rs index 8738c2f7..3f4c9f97 100644 --- a/awc/src/client/h1proto.rs +++ b/awc/src/client/h1proto.rs @@ -18,12 +18,11 @@ use futures_core::{ready, Stream}; use futures_util::SinkExt as _; use pin_project_lite::pin_project; -use crate::BoxError; - use super::{ connection::{ConnectionIo, H1Connection}, error::{ConnectError, SendRequestError}, }; +use crate::BoxError; pub(crate) async fn send_request( io: H1Connection, @@ -57,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 709896dd..c3f801f2 100644 --- a/awc/src/client/h2proto.rs +++ b/awc/src/client/h2proto.rs @@ -1,28 +1,29 @@ use std::future::Future; -use actix_utils::future::poll_fn; -use bytes::Bytes; -use h2::{ - client::{Builder, Connection, SendRequest}, - SendStream, -}; -use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; -use http::{request::Request, Method, Version}; -use log::trace; - use actix_http::{ body::{BodySize, MessageBody}, header::HeaderMap, Payload, RequestHeadType, ResponseHead, }; - -use crate::BoxError; +use actix_utils::future::poll_fn; +use bytes::Bytes; +use h2::{ + client::{Builder, Connection, SendRequest}, + SendStream, +}; +use http::{ + header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}, + request::Request, + Method, Version, +}; +use log::trace; use super::{ config::ConnectorConfig, connection::{ConnectionIo, H2Connection}, error::SendRequestError, }; +use crate::BoxError; pub(crate) async fn send_request( mut io: H2Connection, @@ -105,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) { @@ -119,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()); } }; @@ -168,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()); @@ -178,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/mod.rs b/awc/src/client/mod.rs index e898d2d0..c9fa3725 100644 --- a/awc/src/client/mod.rs +++ b/awc/src/client/mod.rs @@ -1,6 +1,6 @@ //! HTTP client. -use std::{convert::TryFrom, rc::Rc, time::Duration}; +use std::{rc::Rc, time::Duration}; use actix_http::{error::HttpError, header::HeaderMap, Method, RequestHead, Uri}; use actix_rt::net::TcpStream; @@ -19,9 +19,11 @@ mod h1proto; mod h2proto; mod pool; -pub use self::connection::{Connection, ConnectionIo}; -pub use self::connector::{Connector, ConnectorService}; -pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; +pub use self::{ + connection::{Connection, ConnectionIo}, + connector::{Connector, ConnectorService}, + error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}, +}; #[derive(Clone)] pub struct Connect { diff --git a/awc/src/client/pool.rs b/awc/src/client/pool.rs index 47c1fdd6..2938353f 100644 --- a/awc/src/client/pool.rs +++ b/awc/src/client/pool.rs @@ -2,7 +2,7 @@ use std::{ cell::RefCell, - collections::VecDeque, + collections::{HashMap, VecDeque}, future::Future, io, ops::Deref, @@ -17,18 +17,19 @@ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf}; use actix_http::Protocol; use actix_rt::time::{sleep, Sleep}; use actix_service::Service; -use ahash::AHashMap; use futures_core::future::LocalBoxFuture; use futures_util::FutureExt as _; use http::uri::Authority; use pin_project_lite::pin_project; use tokio::sync::{OwnedSemaphorePermit, Semaphore}; -use super::config::ConnectorConfig; -use super::connection::{ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner}; -use super::error::ConnectError; -use super::h2proto::handshake; -use super::Connect; +use super::{ + config::ConnectorConfig, + connection::{ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner}, + error::ConnectError, + h2proto::handshake, + Connect, +}; #[derive(Hash, Eq, PartialEq, Clone, Debug)] pub struct Key { @@ -62,7 +63,7 @@ where { fn new(config: ConnectorConfig) -> Self { let permits = Arc::new(Semaphore::new(config.limit)); - let available = RefCell::new(AHashMap::default()); + let available = RefCell::new(HashMap::default()); Self(Rc::new(ConnectionPoolInnerPriv { config, @@ -124,7 +125,7 @@ where Io: AsyncWrite + Unpin + 'static, { config: ConnectorConfig, - available: RefCell>>>, + available: RefCell>>>, permits: Arc, } @@ -202,7 +203,9 @@ where // check if the connection is still usable if let ConnectionInnerType::H1(ref mut io) = c.conn { let check = ConnectionCheckFuture { io }; - match check.now_or_never().expect("ConnectionCheckFuture must never yield with Poll::Pending.") { + match check.now_or_never().expect( + "ConnectionCheckFuture must never yield with Poll::Pending.", + ) { ConnectionState::Tainted => { inner.close(c.conn); continue; @@ -371,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/connect.rs b/awc/src/connect.rs index be1ea0fe..14ed9e95 100644 --- a/awc/src/connect.rs +++ b/awc/src/connect.rs @@ -13,9 +13,7 @@ use futures_core::{future::LocalBoxFuture, ready}; use crate::{ any_body::AnyBody, - client::{ - Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError, - }, + client::{Connect as ClientConnect, ConnectError, Connection, ConnectionIo, SendRequestError}, ClientResponse, }; @@ -62,9 +60,9 @@ impl ConnectResponse { pub fn into_client_response(self) -> ClientResponse { match self { ConnectResponse::Client(res) => res, - _ => panic!( - "ClientResponse only reachable with ConnectResponse::ClientResponse variant" - ), + _ => { + panic!("ClientResponse only reachable with ConnectResponse::ClientResponse variant") + } } } @@ -75,9 +73,9 @@ impl ConnectResponse { pub fn into_tunnel_response(self) -> (ResponseHead, Framed) { match self { ConnectResponse::Tunnel(head, framed) => (head, framed), - _ => panic!( - "TunnelResponse only reachable with ConnectResponse::TunnelResponse variant" - ), + _ => { + panic!("TunnelResponse only reachable with ConnectResponse::TunnelResponse variant") + } } } } diff --git a/awc/src/error.rs b/awc/src/error.rs index aa9dc4d9..0104e5fe 100644 --- a/awc/src/error.rs +++ b/awc/src/error.rs @@ -7,7 +7,6 @@ pub use actix_http::{ ws::{HandshakeError as WsHandshakeError, ProtocolError as WsProtocolError}, StatusCode, }; - use derive_more::{Display, From}; use serde_json::error::Error as JsonError; diff --git a/awc/src/frozen.rs b/awc/src/frozen.rs index 4023bd1c..8f324499 100644 --- a/awc/src/frozen.rs +++ b/awc/src/frozen.rs @@ -1,15 +1,14 @@ use std::{net, rc::Rc, time::Duration}; -use bytes::Bytes; -use futures_core::Stream; -use serde::Serialize; - use actix_http::{ body::MessageBody, error::HttpError, header::{HeaderMap, TryIntoHeaderPair}, Method, RequestHead, Uri, }; +use bytes::Bytes; +use futures_core::Stream; +use serde::Serialize; use crate::{ client::ClientConfig, diff --git a/awc/src/lib.rs b/awc/src/lib.rs index bb7f06c9..46048099 100644 --- a/awc/src/lib.rs +++ b/awc/src/lib.rs @@ -102,17 +102,17 @@ #![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")] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] pub use actix_http::body; - #[cfg(feature = "cookies")] pub use cookie; @@ -133,18 +133,18 @@ pub mod http { //! Various HTTP related types. // TODO: figure out how best to expose http::Error vs actix_http::Error - pub use actix_http::{ - header, uri, ConnectionType, Error, Method, StatusCode, Uri, Version, - }; + pub use actix_http::{header, uri, ConnectionType, Error, Method, StatusCode, Uri, Version}; } -pub use self::builder::ClientBuilder; -pub use self::client::{Client, Connector}; -pub use self::connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}; -pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder}; -pub use self::request::ClientRequest; #[allow(deprecated)] pub use self::responses::{ClientResponse, JsonBody, MessageBody, ResponseBody}; -pub use self::sender::SendClientRequest; +pub use self::{ + builder::ClientBuilder, + client::{Client, Connect, Connector}, + connect::{BoxConnectorService, BoxedSocket, ConnectRequest, ConnectResponse}, + frozen::{FrozenClientRequest, FrozenSendBuilder}, + request::ClientRequest, + sender::SendClientRequest, +}; pub(crate) type BoxError = Box; diff --git a/awc/src/middleware/mod.rs b/awc/src/middleware/mod.rs index 330e3b7f..8c63e9c7 100644 --- a/awc/src/middleware/mod.rs +++ b/awc/src/middleware/mod.rs @@ -1,11 +1,11 @@ mod redirect; -pub use self::redirect::Redirect; - use std::marker::PhantomData; use actix_service::Service; +pub use self::redirect::Redirect; + /// Trait for transform a type to another one. /// Both the input and output type should impl [actix_service::Service] trait. pub trait Transform { diff --git a/awc/src/middleware/redirect.rs b/awc/src/middleware/redirect.rs index 67ef5d76..0ea5f174 100644 --- a/awc/src/middleware/redirect.rs +++ b/awc/src/middleware/redirect.rs @@ -1,5 +1,4 @@ use std::{ - convert::TryFrom, future::Future, net::SocketAddr, pin::Pin, @@ -304,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() { @@ -450,8 +446,7 @@ mod tests { } async fn test(req: HttpRequest, body: Bytes) -> HttpResponse { - if (req.method() == Method::GET || req.method() == Method::HEAD) - && body.is_empty() + if (req.method() == Method::GET || req.method() == Method::HEAD) && body.is_empty() { HttpResponse::Ok().finish() } else { @@ -551,10 +546,7 @@ mod tests { let port = *req.app_data::().unwrap(); if req.headers().get(header::AUTHORIZATION).is_some() { HttpResponse::Found() - .append_header(( - "location", - format!("http://localhost:{}/", port).as_str(), - )) + .append_header(("location", format!("http://localhost:{}/", port).as_str())) .finish() } else { HttpResponse::InternalServerError().finish() diff --git a/awc/src/request.rs b/awc/src/request.rs index 331c80af..28ed8b5f 100644 --- a/awc/src/request.rs +++ b/awc/src/request.rs @@ -1,8 +1,4 @@ -use std::{convert::TryFrom, fmt, net, rc::Rc, time::Duration}; - -use bytes::Bytes; -use futures_core::Stream; -use serde::Serialize; +use std::{fmt, net, rc::Rc, time::Duration}; use actix_http::{ body::MessageBody, @@ -10,7 +6,13 @@ use actix_http::{ header::{self, HeaderMap, HeaderValue, TryIntoHeaderPair}, ConnectionType, Method, RequestHead, Uri, Version, }; +use base64::prelude::*; +use bytes::Bytes; +use futures_core::Stream; +use serde::Serialize; +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::{ client::ClientConfig, error::{FreezeRequestError, InvalidUrl}, @@ -19,9 +21,6 @@ use crate::{ BoxError, }; -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, CookieJar}; - /// An HTTP Client request builder /// /// This type can be used to construct an instance of `ClientRequest` through a @@ -84,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 } @@ -153,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 @@ -167,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 @@ -186,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 @@ -218,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 } @@ -238,7 +237,7 @@ impl ClientRequest { self.insert_header(( header::AUTHORIZATION, - format!("Basic {}", base64::encode(auth)), + format!("Basic {}", BASE64_STANDARD.encode(auth)), )) } @@ -290,10 +289,7 @@ impl ClientRequest { } /// Sets the query part of the request - pub fn query( - mut self, - query: &T, - ) -> Result { + pub fn query(mut self, query: &T) -> Result { let mut parts = self.head.uri.clone().into_parts(); if let Some(path_and_query) = parts.path_and_query { @@ -303,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()), } } @@ -315,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 { @@ -336,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( @@ -352,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( @@ -370,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( @@ -390,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( @@ -406,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 3912324b..e9c03d81 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/mod.rs b/awc/src/responses/mod.rs index 588ce014..95a07809 100644 --- a/awc/src/responses/mod.rs +++ b/awc/src/responses/mod.rs @@ -8,10 +8,9 @@ mod read_body; mod response; mod response_body; -pub use self::json_body::JsonBody; -pub use self::response::ClientResponse; #[allow(deprecated)] pub use self::response_body::{MessageBody, ResponseBody}; +pub use self::{json_body::JsonBody, response::ClientResponse}; /// Default body size limit: 2 MiB const DEFAULT_BODY_LIMIT: usize = 2 * 1024 * 1024; diff --git a/awc/src/responses/response.rs b/awc/src/responses/response.rs index c7c0a636..0eafcff0 100644 --- a/awc/src/responses/response.rs +++ b/awc/src/responses/response.rs @@ -7,8 +7,8 @@ use std::{ }; use actix_http::{ - error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, - Payload, ResponseHead, StatusCode, Version, + error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Payload, + ResponseHead, StatusCode, Version, }; use actix_rt::time::{sleep, Sleep}; use bytes::Bytes; @@ -16,11 +16,10 @@ use futures_core::Stream; use pin_project_lite::pin_project; use serde::de::DeserializeOwned; +use super::{JsonBody, ResponseBody, ResponseTimeout}; #[cfg(feature = "cookies")] use crate::cookie::{Cookie, ParseError as CookieParseError}; -use super::{JsonBody, ResponseBody, ResponseTimeout}; - pin_project! { /// Client Response pub struct ClientResponse { diff --git a/awc/src/responses/response_body.rs b/awc/src/responses/response_body.rs index 8d9d1274..0ff58341 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 cd30e571..8de1033a 100644 --- a/awc/src/sender.rs +++ b/awc/src/sender.rs @@ -13,15 +13,14 @@ use actix_http::{ header::{self, HeaderMap, HeaderName, TryIntoHeaderValue}, RequestHead, RequestHeadType, }; +#[cfg(feature = "__compress")] +use actix_http::{encoding::Decoder, header::ContentEncoding, Payload}; use actix_rt::time::{sleep, Sleep}; use bytes::Bytes; use derive_more::From; use futures_core::Stream; use serde::Serialize; -#[cfg(feature = "__compress")] -use actix_http::{encoding::Decoder, header::ContentEncoding, Payload}; - use crate::{ any_body::AnyBody, client::ClientConfig, @@ -106,8 +105,9 @@ impl Future for SendClientRequest { } let res = futures_core::ready!(send.as_mut().poll(cx)).map(|res| { - res.into_client_response()._timeout(delay.take()).map_body( - |head, payload| { + res.into_client_response() + ._timeout(delay.take()) + .map_body(|head, payload| { if *response_decompress { Payload::Stream { payload: Decoder::from_headers(payload, &head.headers), @@ -117,14 +117,13 @@ impl Future for SendClientRequest { payload: Decoder::new(payload, ContentEncoding::Identity), } } - }, - ) + }) }); 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"), }, } @@ -148,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"), }, } @@ -220,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) @@ -292,7 +291,7 @@ impl RequestSender { Ok(value) => { head.headers.insert(key, value); } - Err(e) => return Err(e.into()), + Err(err) => return Err(err.into()), } } } @@ -305,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 96ae1f0a..12658317 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 f905b8ef..c3340206 100644 --- a/awc/src/ws.rs +++ b/awc/src/ws.rs @@ -26,15 +26,17 @@ //! } //! ``` -use std::{convert::TryFrom, fmt, net::SocketAddr, str}; +use std::{fmt, net::SocketAddr, str}; use actix_codec::Framed; +pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; use actix_http::{ws, Payload, RequestHead}; use actix_rt::time::timeout; use actix_service::Service as _; +use base64::prelude::*; -pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; - +#[cfg(feature = "cookies")] +use crate::cookie::{Cookie, CookieJar}; use crate::{ client::ClientConfig, connect::{BoxedSocket, ConnectRequest}, @@ -46,9 +48,6 @@ use crate::{ ClientResponse, }; -#[cfg(feature = "cookies")] -use crate::cookie::{Cookie, CookieJar}; - /// WebSocket connection. pub struct WebsocketsRequest { pub(crate) head: RequestHead, @@ -65,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, @@ -83,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 { @@ -144,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 } @@ -178,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 } @@ -197,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 } @@ -218,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 } @@ -236,7 +235,10 @@ impl WebsocketsRequest { Some(password) => format!("{}:{}", username, password), None => format!("{}:", username), }; - self.header(AUTHORIZATION, format!("Basic {}", base64::encode(auth))) + self.header( + AUTHORIZATION, + format!("Basic {}", BASE64_STANDARD.encode(auth)), + ) } /// Set HTTP bearer authentication header @@ -321,7 +323,7 @@ impl WebsocketsRequest { // Generate a random key for the `Sec-WebSocket-Key` header which is a base64-encoded // (see RFC 4648 §4) value that, when decoded, is 16 bytes in length (RFC 6455 §1.3). let sec_key: [u8; 16] = rand::random(); - let key = base64::encode(sec_key); + let key = BASE64_STANDARD.encode(sec_key); self.head.headers.insert( header::SEC_WEBSOCKET_KEY, diff --git a/awc/tests/test_client.rs b/awc/tests/test_client.rs index 0949595c..76915630 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, @@ -12,18 +10,18 @@ use std::{ time::Duration, }; +use actix_http::{HttpService, StatusCode}; +use actix_http_test::test_server; +use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; use actix_utils::future::ok; +use actix_web::{dev::AppConfig, http::header, web, App, Error, HttpRequest, HttpResponse}; +use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; +use base64::prelude::*; use bytes::Bytes; use cookie::Cookie; use futures_util::stream; use rand::Rng; -use actix_http::{HttpService, StatusCode}; -use actix_http_test::test_server; -use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; -use actix_web::{dev::AppConfig, http::header, web, App, Error, HttpRequest, HttpResponse}; -use awc::error::{JsonPayloadError, PayloadError, SendRequestError}; - mod utils; const S: &str = "Hello World "; @@ -32,9 +30,8 @@ const STR: &str = const_str::repeat!(S, 100); #[actix_rt::test] async fn simple() { let srv = actix_test::start(|| { - App::new().service( - web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })), - ) + App::new() + .service(web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) }))) }); let request = srv.get("/").insert_header(("x-test", "111")).send(); @@ -60,9 +57,8 @@ async fn simple() { #[actix_rt::test] async fn json() { let srv = actix_test::start(|| { - App::new().service( - web::resource("/").route(web::to(|_: web::Json| HttpResponse::Ok())), - ) + App::new() + .service(web::resource("/").route(web::to(|_: web::Json| HttpResponse::Ok()))) }); let request = srv @@ -339,8 +335,7 @@ async fn connection_wait_queue() { .and_then( HttpService::new(map_config( App::new().service( - web::resource("/") - .route(web::to(|| async { HttpResponse::Ok().body(STR) })), + web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })), ), |_| AppConfig::default(), )) @@ -448,9 +443,7 @@ async fn no_decompress() { let srv = actix_test::start(|| { App::new() .wrap(actix_web::middleware::Compress::default()) - .service( - web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) })), - ) + .service(web::resource("/").route(web::to(|| async { HttpResponse::Ok().body(STR) }))) }); let mut res = awc::Client::new() @@ -783,7 +776,7 @@ async fn client_basic_auth() { .unwrap() .to_str() .unwrap() - == format!("Basic {}", base64::encode("username:password")) + == format!("Basic {}", BASE64_STANDARD.encode("username:password")) { HttpResponse::Ok() } else { @@ -832,12 +825,12 @@ async fn local_address() { let ip = IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)); let srv = actix_test::start(move || { - App::new().service(web::resource("/").route(web::to( - move |req: HttpRequest| async move { + App::new().service( + web::resource("/").route(web::to(move |req: HttpRequest| async move { assert_eq!(req.peer_addr().unwrap().ip(), ip); Ok::<_, Error>(HttpResponse::Ok()) - }, - ))) + })), + ) }); let client = awc::Client::builder().local_address(ip).finish(); diff --git a/awc/tests/test_connector.rs b/awc/tests/test_connector.rs index 0f0b8141..a8b7e98c 100644 --- a/awc/tests/test_connector.rs +++ b/awc/tests/test_connector.rs @@ -5,8 +5,7 @@ extern crate tls_openssl as openssl; use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{map_config, ServiceFactoryExt}; -use actix_web::http::Version; -use actix_web::{dev::AppConfig, web, App, HttpResponse}; +use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode}, @@ -14,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 652997de..7e832f67 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 40c9ab8f..95d4c15f 100644 --- a/awc/tests/test_ssl_client.rs +++ b/awc/tests/test_ssl_client.rs @@ -2,15 +2,16 @@ extern crate tls_openssl as openssl; -use std::sync::atomic::{AtomicUsize, Ordering}; -use std::sync::Arc; +use std::sync::{ + atomic::{AtomicUsize, Ordering}, + Arc, +}; use actix_http::HttpService; use actix_http_test::test_server; use actix_service::{fn_service, map_config, ServiceFactoryExt}; use actix_utils::future::ok; -use actix_web::http::Version; -use actix_web::{dev::AppConfig, web, App, HttpResponse}; +use actix_web::{dev::AppConfig, http::Version, web, App, HttpResponse}; use openssl::{ pkey::PKey, ssl::{SslAcceptor, SslConnector, SslMethod, SslVerifyMode}, @@ -18,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/awc/tests/utils.rs b/awc/tests/utils.rs index 2532640c..b9c70888 100644 --- a/awc/tests/utils.rs +++ b/awc/tests/utils.rs @@ -4,9 +4,10 @@ use std::io::{Read as _, Write as _}; pub mod gzip { - use super::*; use flate2::{read::GzDecoder, write::GzEncoder, Compression}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = GzEncoder::new(Vec::new(), Compression::fast()); encoder.write_all(bytes.as_ref()).unwrap(); @@ -22,9 +23,10 @@ pub mod gzip { } pub mod deflate { - use super::*; use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast()); encoder.write_all(bytes.as_ref()).unwrap(); @@ -40,9 +42,10 @@ pub mod deflate { } pub mod brotli { - use super::*; use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = BrotliEncoder::new( Vec::new(), @@ -64,9 +67,10 @@ pub mod brotli { } pub mod zstd { - use super::*; use ::zstd::stream::{read::Decoder, write::Encoder}; + use super::*; + pub fn encode(bytes: impl AsRef<[u8]>) -> Vec { let mut encoder = Encoder::new(Vec::new(), 3).unwrap(); encoder.write_all(bytes.as_ref()).unwrap(); diff --git a/clippy.toml b/clippy.toml deleted file mode 100644 index abe19b3a..00000000 --- a/clippy.toml +++ /dev/null @@ -1 +0,0 @@ -msrv = "1.59" diff --git a/justfile b/justfile new file mode 100644 index 00000000..646c6b44 --- /dev/null +++ b/justfile @@ -0,0 +1,120 @@ +_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 collect coverage info. +[private] +test-coverage toolchain="": + cargo {{ toolchain }} llvm-cov nextest --no-report {{ all_crate_features }} + cargo {{ toolchain }} llvm-cov --doc --no-report {{ all_crate_features }} + +# Test workspace and generate Codecov report. +test-coverage-codecov toolchain="": (test-coverage toolchain) + cargo {{ toolchain }} llvm-cov report --doctests --codecov --output-path=codecov.json + +# Test workspace and generate LCOV report. +test-coverage-lcov toolchain="": (test-coverage toolchain) + cargo {{ toolchain }} llvm-cov report --doctests --lcov --output-path=lcov.info + +# Document crates in workspace. +doc *args: && doc-set-workspace-crates + RUSTDOCFLAGS="--cfg=docsrs -Dwarnings" cargo +nightly doc --workspace {{ all_crate_features }} {{ args }} + +[private] +doc-set-workspace-crates: + #!/usr/bin/env bash + ( + echo "window.ALL_CRATES =" + cargo metadata --format-version=1 | jq '[.packages[] | select(.source == null) | .name]' + echo ";" + ) > "$(cargo metadata --format-version=1 | jq -r '.target_directory')/doc/crates.js" + +# 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-http-test && cargo rdme --force + cd ./actix-router && cargo rdme --force + cd ./actix-multipart && cargo rdme --force + cd ./actix-test && cargo rdme --force + +# Check for unintentional external type exposure on all crates in workspace. +check-external-types-all toolchain="+nightly": + #!/usr/bin/env bash + set -euo pipefail + exit=0 + for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do + if ! just check-external-types-manifest "$f" {{toolchain}}; then exit=1; fi + echo + echo + done + exit $exit + +# Check for unintentional external type exposure on all crates in workspace. +check-external-types-all-table toolchain="+nightly": + #!/usr/bin/env bash + set -euo pipefail + for f in $(find . -mindepth 2 -maxdepth 2 -name Cargo.toml | grep -vE "\-codegen/|\-derive/|\-macros/"); do + echo + echo "Checking for $f" + just check-external-types-manifest "$f" {{toolchain}} --output-format=markdown-table + done + +# Check for unintentional external type exposure on a crate. +check-external-types-manifest manifest_path toolchain="+nightly" *extra_args="": + cargo {{toolchain}} check-external-types --manifest-path "{{manifest_path}}" {{extra_args}} diff --git a/rustfmt.toml b/rustfmt.toml deleted file mode 100644 index 973e002c..00000000 --- a/rustfmt.toml +++ /dev/null @@ -1,2 +0,0 @@ -max_width = 96 -reorder_imports = true diff --git a/scripts/bump b/scripts/bump index 33ea5201..6fd879ea 100755 --- a/scripts/bump +++ b/scripts/bump @@ -1,11 +1,11 @@ -#!/bin/sh +#!/bin/bash # developed on macOS and probably doesn't work on Linux yet due to minor # differences in flags on sed # requires github cli tool for automatic release draft creation -set -euo pipefail +set -eEuo pipefail DIR=$1 @@ -21,12 +21,9 @@ README_FILE=$DIR/README.md # determine changelog file name if [ -f "$DIR/CHANGES.md" ]; then - CHANGELOG_FILE=$DIR/CHANGES.md + CHANGELOG_FILE="$DIR/CHANGES.md" elif [ -f "$DIR/CHANGELOG.md" ]; then - CHANGELOG_FILE=$DIR/CHANGELOG.md -else - echo "No changelog file found" - exit 1 + CHANGELOG_FILE="$DIR/CHANGELOG.md" fi # get current version @@ -37,21 +34,22 @@ CHANGE_CHUNK_FILE="$(mktemp)" echo saving changelog to $CHANGE_CHUNK_FILE echo -# get changelog chunk and save to temp file -cat "$CHANGELOG_FILE" | - # skip up to unreleased heading - sed '1,/Unreleased/ d' | - # take up to previous version heading - sed "/$CURRENT_VERSION/ q" | - # drop last line - sed '$d' \ - >"$CHANGE_CHUNK_FILE" +if [ -n "${CHANGELOG_FILE-}" ]; then + # get changelog chunk and save to temp file + cat "$CHANGELOG_FILE" | + # skip up to unreleased heading + sed '1,/Unreleased/ d' | + # take up to previous version heading + sed "/$CURRENT_VERSION/ q" | + # drop last line + sed '$d' \ + >"$CHANGE_CHUNK_FILE" +fi # if word count of changelog chunk is 0 then insert filler changelog chunk if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then echo "- No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE" echo >>"$CHANGE_CHUNK_FILE" - echo >>"$CHANGE_CHUNK_FILE" fi if [ -n "${2-}" ]; then @@ -69,8 +67,7 @@ if [ "${NEW_VERSION:0:1}" = "v" ]; then NEW_VERSION="${NEW_VERSION:1}" fi -DATE="$(date -u +"%Y-%m-%d")" -echo "updating from $CURRENT_VERSION => $NEW_VERSION ($DATE)" +echo "updating from $CURRENT_VERSION => $NEW_VERSION" # update package.version field sed -i.bak -E "s/^version ?= ?\"[^\"]+\"$/version = \"$NEW_VERSION\"/" "$CARGO_MANIFEST" @@ -78,23 +75,30 @@ sed -i.bak -E "s/^version ?= ?\"[^\"]+\"$/version = \"$NEW_VERSION\"/" "$CARGO_M # update readme [ -f "$README_FILE" ] && sed -i.bak -E "s#$CURRENT_VERSION([/)])#$NEW_VERSION\1#g" "$README_FILE" -# update changelog file -( - sed '/Unreleased/ q' "$CHANGELOG_FILE" # up to unreleased heading - echo # blank line - echo # blank line - echo "## $NEW_VERSION - $DATE" # new version heading - cat "$CHANGE_CHUNK_FILE" # previously unreleased changes - sed "/$CURRENT_VERSION/ q" "$CHANGELOG_FILE" | tail -n 1 # the previous version heading - sed "1,/$CURRENT_VERSION/ d" "$CHANGELOG_FILE" # everything after previous version heading -) >"$CHANGELOG_FILE.bak" -mv "$CHANGELOG_FILE.bak" "$CHANGELOG_FILE" +if [ -n "${CHANGELOG_FILE-}" ]; then + # update changelog file + ( + sed '/Unreleased/ q' "$CHANGELOG_FILE" # up to unreleased heading + echo # blank line + echo "## $NEW_VERSION" # new version heading + cat "$CHANGE_CHUNK_FILE" # previously unreleased changes + sed "/$CURRENT_VERSION/ q" "$CHANGELOG_FILE" | tail -n 1 # the previous version heading + sed "1,/$CURRENT_VERSION/ d" "$CHANGELOG_FILE" # everything after previous version heading + ) >"$CHANGELOG_FILE.bak" + mv "$CHANGELOG_FILE.bak" "$CHANGELOG_FILE" + + # format CHANGELOG file according to prettier + npx -y prettier --write "$CHANGELOG_FILE" || true +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:" @@ -109,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 @@ -127,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-//')" @@ -139,12 +151,14 @@ GIT_TAG="$(echo $SHORT_PACKAGE_NAME-v$NEW_VERSION)" RELEASE_TITLE="$(echo $PACKAGE_NAME: v$NEW_VERSION)" if [ "$(echo $NEW_VERSION | grep beta)" ] || [ "$(echo $NEW_VERSION | grep rc)" ] || [ "$(echo $NEW_VERSION | grep alpha)" ]; then - PRERELEASE="--prerelease" + FLAGS="--prerelease" +else + FLAGS="--latest" fi echo echo "GitHub release command:" -GH_CMD="gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" ${PRERELEASE:-}" +GH_CMD="gh release create \"$GIT_TAG\" --draft --title \"$RELEASE_TITLE\" --notes-file \"$CHANGE_CHUNK_FILE\" ${FLAGS:-}" echo "$GH_CMD" read -p "Submit draft GH release: (y/N) " GH_RELEASE diff --git a/scripts/free-disk-space.sh b/scripts/free-disk-space.sh new file mode 100755 index 00000000..2946cfcf --- /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