mirror of
https://github.com/fafhrd91/actix-web
synced 2025-08-29 08:09:18 +02:00
Compare commits
1 Commits
actors-v4.
...
files-chun
Author | SHA1 | Date | |
---|---|---|---|
|
c759bdef4c |
3
.github/workflows/bench.yml
vendored
3
.github/workflows/bench.yml
vendored
@@ -5,9 +5,6 @@ on:
|
|||||||
branches:
|
branches:
|
||||||
- master
|
- master
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
check_benchmark:
|
check_benchmark:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
113
.github/workflows/ci-post-merge.yml
vendored
113
.github/workflows/ci-post-merge.yml
vendored
@@ -4,9 +4,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_test_nightly:
|
build_and_test_nightly:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -26,7 +23,6 @@ jobs:
|
|||||||
CI: 1
|
CI: 1
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
VCPKGRS_DYNAMIC: 1
|
VCPKGRS_DYNAMIC: 1
|
||||||
CARGO_UNSTABLE_SPARSE_REGISTRY: true
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@@ -48,15 +44,18 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: taiki-e/install-action@cargo-hack
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: generate-lockfile }
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-check-min }
|
with: { command: ci-check-min }
|
||||||
@@ -79,58 +78,108 @@ jobs:
|
|||||||
cargo test --lib --tests -p=actix-multipart --all-features
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
|
- name: tests (io-uring)
|
||||||
|
if: matrix.target.os == 'ubuntu-latest'
|
||||||
|
timeout-minutes: 60
|
||||||
|
run: >
|
||||||
|
sudo bash -c "ulimit -Sl 512
|
||||||
|
&& ulimit -Hl 512
|
||||||
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
|
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: Clear the cargo caches
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||||
cargo-cache
|
cargo-cache
|
||||||
|
|
||||||
ci_feature_powerset_check:
|
ci_feature_powerset_check:
|
||||||
name: Verify Feature Combinations
|
name: Verify Feature Combinations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
|
||||||
CI: 1
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
- name: Install cargo-hack
|
with:
|
||||||
uses: taiki-e/install-action@cargo-hack
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
run: cargo generate-lockfile
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: check feature combinations
|
- name: Install cargo-hack
|
||||||
run: cargo ci-check-all-feature-powerset
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
- name: check feature combinations
|
- name: check feature combinations
|
||||||
run: cargo ci-check-all-feature-powerset-linux
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset }
|
||||||
|
|
||||||
|
- name: check feature combinations
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: ci-check-all-feature-powerset-linux }
|
||||||
|
|
||||||
|
# job currently (1st Feb 2022) segfaults
|
||||||
|
# coverage:
|
||||||
|
# name: coverage
|
||||||
|
# runs-on: ubuntu-latest
|
||||||
|
# steps:
|
||||||
|
# - uses: actions/checkout@v2
|
||||||
|
|
||||||
|
# - name: Install stable
|
||||||
|
# uses: actions-rs/toolchain@v1
|
||||||
|
# with:
|
||||||
|
# toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
# profile: minimal
|
||||||
|
# override: true
|
||||||
|
|
||||||
|
# - name: Generate Cargo.lock
|
||||||
|
# uses: actions-rs/cargo@v1
|
||||||
|
# with: { command: generate-lockfile }
|
||||||
|
# - name: Cache Dependencies
|
||||||
|
# uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
# - name: Generate coverage file
|
||||||
|
# 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 }
|
||||||
|
|
||||||
nextest:
|
nextest:
|
||||||
name: nextest
|
name: nextest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
env:
|
|
||||||
CI: 1
|
|
||||||
CARGO_INCREMENTAL: 0
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
- name: Install nextest
|
with:
|
||||||
uses: taiki-e/install-action@nextest
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
run: cargo generate-lockfile
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.3.0
|
uses: Swatinem/rust-cache@v1.3.0
|
||||||
|
|
||||||
|
- name: Install cargo-nextest
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: install
|
||||||
|
args: cargo-nextest
|
||||||
|
|
||||||
- name: Test with cargo-nextest
|
- name: Test with cargo-nextest
|
||||||
run: cargo nextest run
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: nextest
|
||||||
|
args: run
|
||||||
|
63
.github/workflows/ci.yml
vendored
63
.github/workflows/ci.yml
vendored
@@ -6,9 +6,6 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: read # to fetch code (actions/checkout)
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build_and_test:
|
build_and_test:
|
||||||
strategy:
|
strategy:
|
||||||
@@ -19,7 +16,7 @@ jobs:
|
|||||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
- { 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-2022, triple: x86_64-pc-windows-msvc }
|
||||||
version:
|
version:
|
||||||
- 1.59.0 # MSRV
|
- 1.54.0 # MSRV
|
||||||
- stable
|
- stable
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
@@ -50,26 +47,17 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: taiki-e/install-action@cargo-hack
|
|
||||||
|
|
||||||
- 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
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: generate-lockfile }
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: workaround MSRV issues
|
- name: Install cargo-hack
|
||||||
if: matrix.version != 'stable'
|
uses: actions-rs/cargo@v1
|
||||||
run: |
|
with:
|
||||||
cargo update -p=zstd-sys --precise=2.0.1+zstd.1.5.2
|
command: install
|
||||||
|
args: cargo-hack
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
@@ -93,31 +81,19 @@ jobs:
|
|||||||
cargo test --lib --tests -p=actix-multipart --all-features
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
|
||||||
run: |
|
|
||||||
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
|
||||||
io-uring:
|
|
||||||
name: io-uring tests
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
run: cargo generate-lockfile
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.3.0
|
|
||||||
|
|
||||||
- name: tests (io-uring)
|
- name: tests (io-uring)
|
||||||
|
if: matrix.target.os == 'ubuntu-latest'
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run: >
|
run: >
|
||||||
sudo bash -c "ulimit -Sl 512
|
sudo bash -c "ulimit -Sl 512
|
||||||
&& ulimit -Hl 512
|
&& ulimit -Hl 512
|
||||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
|
- name: Clear the cargo caches
|
||||||
|
run: |
|
||||||
|
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||||
|
cargo-cache
|
||||||
|
|
||||||
rustdoc:
|
rustdoc:
|
||||||
name: doc tests
|
name: doc tests
|
||||||
@@ -125,13 +101,20 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- name: Install Rust (nightly)
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
run: cargo generate-lockfile
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.3.0
|
uses: Swatinem/rust-cache@v1.3.0
|
||||||
|
|
||||||
- name: doc tests
|
- name: doc tests
|
||||||
run: cargo ci-doctest
|
uses: actions-rs/cargo@v1
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
|
with: { command: ci-doctest }
|
||||||
|
41
.github/workflows/clippy-fmt.yml
vendored
41
.github/workflows/clippy-fmt.yml
vendored
@@ -9,37 +9,54 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
|
||||||
with: { components: rustfmt }
|
- name: Install Rust
|
||||||
- run: cargo fmt --all -- --check
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
components: rustfmt
|
||||||
|
- name: Check with rustfmt
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with:
|
||||||
|
command: fmt
|
||||||
|
args: --all -- --check
|
||||||
|
|
||||||
clippy:
|
clippy:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- name: Install Rust
|
||||||
with: { components: clippy }
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
components: clippy
|
||||||
|
override: true
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
run: cargo generate-lockfile
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: Check with Clippy
|
- name: Check with Clippy
|
||||||
uses: actions-rs/clippy-check@v1
|
uses: actions-rs/clippy-check@v1
|
||||||
with:
|
with:
|
||||||
args: --workspace --tests --examples --all-features
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
args: --workspace --tests --examples --all-features
|
||||||
|
|
||||||
lint-docs:
|
lint-docs:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
- name: Install Rust
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
uses: actions-rs/toolchain@v1
|
||||||
with: { components: rust-docs }
|
with:
|
||||||
|
toolchain: stable
|
||||||
|
profile: minimal
|
||||||
|
components: rust-docs
|
||||||
- name: Check for broken intra-doc links
|
- name: Check for broken intra-doc links
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
env:
|
env:
|
||||||
|
36
.github/workflows/coverage.yml
vendored
36
.github/workflows/coverage.yml
vendored
@@ -1,36 +0,0 @@
|
|||||||
# disabled because `cargo tarpaulin` currently segfaults
|
|
||||||
|
|
||||||
name: Coverage
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches: [master]
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# job currently (1st Feb 2022) segfaults
|
|
||||||
coverage:
|
|
||||||
name: coverage
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
steps:
|
|
||||||
- uses: actions/checkout@v2
|
|
||||||
|
|
||||||
- name: Install stable
|
|
||||||
uses: actions-rs/toolchain@v1
|
|
||||||
with:
|
|
||||||
toolchain: stable-x86_64-unknown-linux-gnu
|
|
||||||
profile: minimal
|
|
||||||
override: true
|
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with: { command: generate-lockfile }
|
|
||||||
- name: Cache Dependencies
|
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
|
||||||
|
|
||||||
- name: Generate coverage file
|
|
||||||
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 }
|
|
25
.github/workflows/upload-doc.yml
vendored
25
.github/workflows/upload-doc.yml
vendored
@@ -4,29 +4,32 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches: [master]
|
branches: [master]
|
||||||
|
|
||||||
permissions: {}
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
permissions:
|
|
||||||
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
|
|
||||||
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
- uses: dtolnay/rust-toolchain@nightly
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: nightly-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
- name: Build Docs
|
- name: Build Docs
|
||||||
run: cargo +nightly doc --no-deps --workspace --all-features
|
uses: actions-rs/cargo@v1
|
||||||
env:
|
with:
|
||||||
RUSTDOCFLAGS: --cfg=docsrs
|
command: doc
|
||||||
|
args: --workspace --all-features --no-deps
|
||||||
|
|
||||||
- name: Tweak HTML
|
- name: Tweak HTML
|
||||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||||
|
|
||||||
- name: Deploy to GitHub Pages
|
- name: Deploy to GitHub Pages
|
||||||
uses: JamesIves/github-pages-deploy-action@v4.4.1
|
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||||
with:
|
with:
|
||||||
folder: target/doc
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
single-commit: true
|
BRANCH: gh-pages
|
||||||
|
FOLDER: target/doc
|
||||||
|
@@ -1,27 +1,6 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
- 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
|
|
||||||
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
|
||||||
|
|
||||||
[#2817]: https://github.com/actix/actix-web/pull/2817
|
|
||||||
|
|
||||||
|
|
||||||
## 0.6.1 - 2022-06-11
|
|
||||||
- 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]
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
|
||||||
|
|
||||||
[#2021]: https://github.com/actix/actix-web/pull/2021
|
|
||||||
[#2645]: https://github.com/actix/actix-web/pull/2645
|
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0 - 2022-02-25
|
## 0.6.0 - 2022-02-25
|
||||||
|
@@ -1,8 +1,9 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.2"
|
version = "0.6.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
|
"fakeshadow <24548779@qq.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
]
|
]
|
||||||
description = "Static file serving for Actix Web"
|
description = "Static file serving for Actix Web"
|
||||||
@@ -26,25 +27,22 @@ actix-service = "2"
|
|||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
|
|
||||||
|
askama_escape = "0.10"
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
http-range = "0.1.4"
|
http-range = "0.1.4"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
mime_guess = "2.0.1"
|
mime_guess = "2.0.1"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2.7"
|
pin-project-lite = "0.2.7"
|
||||||
v_htmlescape= "0.15"
|
|
||||||
|
|
||||||
# experimental-io-uring
|
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||||
[target.'cfg(target_os = "linux")'.dependencies]
|
|
||||||
tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
|
|
||||||
actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.7"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1"
|
actix-test = "0.1.0-beta.13"
|
||||||
actix-web = "4"
|
actix-web = "4.0.0"
|
||||||
tempfile = "3.2"
|
tempfile = "3.2"
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
> Static file serving for Actix Web
|
> Static file serving for Actix Web
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.6.2)
|
[](https://docs.rs/actix-files/0.6.0)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.6.2)
|
[](https://deps.rs/crate/actix-files/0.6.0)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
||||||
|
|
||||||
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
||||||
|
use askama_escape::{escape as escape_html_entity, Html};
|
||||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||||
use v_htmlescape::escape as escape_html_entity;
|
|
||||||
|
|
||||||
/// A directory; responds with the generated directory listing.
|
/// A directory; responds with the generated directory listing.
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
@@ -59,7 +59,7 @@ macro_rules! encode_file_url {
|
|||||||
/// ```
|
/// ```
|
||||||
macro_rules! encode_file_name {
|
macro_rules! encode_file_name {
|
||||||
($entry:ident) => {
|
($entry:ident) => {
|
||||||
escape_html_entity(&$entry.file_name().to_string_lossy())
|
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError};
|
|||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
|
||||||
/// Errors which can occur when serving static files.
|
/// Errors which can occur when serving static files.
|
||||||
#[derive(Debug, PartialEq, Eq, Display)]
|
#[derive(Display, Debug, PartialEq)]
|
||||||
pub enum FilesError {
|
pub enum FilesError {
|
||||||
/// Path is not a directory
|
/// Path is not a directory
|
||||||
#[allow(dead_code)]
|
#[allow(dead_code)]
|
||||||
@@ -22,7 +22,7 @@ impl ResponseError for FilesError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
#[derive(Debug, PartialEq, Eq, Display)]
|
#[derive(Display, Debug, PartialEq)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum UriSegmentError {
|
pub enum UriSegmentError {
|
||||||
/// The segment started with the wrapped invalid character.
|
/// The segment started with the wrapped invalid character.
|
||||||
|
@@ -13,7 +13,6 @@
|
|||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
#![warn(future_incompatible, missing_docs, missing_debug_implementations)]
|
||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
@@ -34,7 +33,6 @@ mod path_buf;
|
|||||||
mod range;
|
mod range;
|
||||||
mod service;
|
mod service;
|
||||||
|
|
||||||
pub use self::chunked::ChunkedReadFile;
|
|
||||||
pub use self::directory::Directory;
|
pub use self::directory::Directory;
|
||||||
pub use self::files::Files;
|
pub use self::files::Files;
|
||||||
pub use self::named::NamedFile;
|
pub use self::named::NamedFile;
|
||||||
@@ -365,43 +363,20 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(deprecated)]
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn status_code_customize_same_output() {
|
async fn test_named_file_status_code_text() {
|
||||||
let file1 = NamedFile::open_async("Cargo.toml")
|
let mut file = NamedFile::open_async("Cargo.toml")
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_status_code(StatusCode::NOT_FOUND);
|
.set_status_code(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let file2 = NamedFile::open_async("Cargo.toml")
|
|
||||||
.await
|
|
||||||
.unwrap()
|
|
||||||
.customize()
|
|
||||||
.with_status(StatusCode::NOT_FOUND);
|
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
|
||||||
let res1 = file1.respond_to(&req);
|
|
||||||
let res2 = file2.respond_to(&req);
|
|
||||||
|
|
||||||
assert_eq!(res1.status(), StatusCode::NOT_FOUND);
|
|
||||||
assert_eq!(res2.status(), StatusCode::NOT_FOUND);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn test_named_file_status_code_text() {
|
|
||||||
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
|
||||||
|
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let _f: &mut File = &mut file;
|
let _f: &mut File = &mut file;
|
||||||
}
|
}
|
||||||
|
|
||||||
let file = file.customize().with_status(StatusCode::NOT_FOUND);
|
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req);
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@@ -21,9 +21,9 @@ use actix_web::{
|
|||||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||||
};
|
};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
use bytes::Bytes;
|
||||||
use derive_more::{Deref, DerefMut};
|
use derive_more::{Deref, DerefMut};
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::{future::LocalBoxFuture, Stream};
|
||||||
use mime::Mime;
|
|
||||||
use mime_guess::from_path;
|
use mime_guess::from_path;
|
||||||
|
|
||||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||||
@@ -77,8 +77,8 @@ pub struct NamedFile {
|
|||||||
pub(crate) md: Metadata,
|
pub(crate) md: Metadata,
|
||||||
pub(crate) flags: Flags,
|
pub(crate) flags: Flags,
|
||||||
pub(crate) status_code: StatusCode,
|
pub(crate) status_code: StatusCode,
|
||||||
pub(crate) content_type: Mime,
|
pub(crate) content_type: mime::Mime,
|
||||||
pub(crate) content_disposition: ContentDisposition,
|
pub(crate) content_disposition: header::ContentDisposition,
|
||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,10 +129,10 @@ impl NamedFile {
|
|||||||
let ct = from_path(&path).first_or_octet_stream();
|
let ct = from_path(&path).first_or_octet_stream();
|
||||||
|
|
||||||
let disposition = match ct.type_() {
|
let disposition = match ct.type_() {
|
||||||
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
||||||
mime::APPLICATION => match ct.subtype() {
|
mime::APPLICATION => match ct.subtype() {
|
||||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||||
name if name == "wasm" || name == "xhtml" => DispositionType::Inline,
|
name if name == "wasm" => DispositionType::Inline,
|
||||||
_ => DispositionType::Attachment,
|
_ => DispositionType::Attachment,
|
||||||
},
|
},
|
||||||
_ => DispositionType::Attachment,
|
_ => DispositionType::Attachment,
|
||||||
@@ -210,10 +210,11 @@ impl NamedFile {
|
|||||||
Self::from_file(file, path)
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(rustdoc::broken_intra_doc_links)]
|
||||||
/// Attempts to open a file asynchronously in read-only mode.
|
/// Attempts to open a file asynchronously in read-only mode.
|
||||||
///
|
///
|
||||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it
|
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||||
/// will behave just like `open`.
|
/// Otherwise, it will be just like [`open`][Self::open].
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
@@ -238,13 +239,13 @@ impl NamedFile {
|
|||||||
Self::from_file(file, path)
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns reference to the underlying file object.
|
/// Returns reference to the underlying `File` object.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn file(&self) -> &File {
|
pub fn file(&self) -> &File {
|
||||||
&self.file
|
&self.file
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the filesystem path to this file.
|
/// Retrieve the path of this file.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
@@ -262,53 +263,16 @@ impl NamedFile {
|
|||||||
self.path.as_path()
|
self.path.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the time the file was last modified.
|
/// Set response **Status Code**
|
||||||
///
|
|
||||||
/// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`].
|
|
||||||
/// Therefore, it is usually safe to unwrap this.
|
|
||||||
#[inline]
|
|
||||||
pub fn modified(&self) -> Option<SystemTime> {
|
|
||||||
self.modified
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the filesystem metadata associated with this file.
|
|
||||||
#[inline]
|
|
||||||
pub fn metadata(&self) -> &Metadata {
|
|
||||||
&self.md
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `Content-Type` header that will be used when serving this file.
|
|
||||||
#[inline]
|
|
||||||
pub fn content_type(&self) -> &Mime {
|
|
||||||
&self.content_type
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `Content-Disposition` that will be used when serving this file.
|
|
||||||
#[inline]
|
|
||||||
pub fn content_disposition(&self) -> &ContentDisposition {
|
|
||||||
&self.content_disposition
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns the `Content-Encoding` that will be used when serving this file.
|
|
||||||
///
|
|
||||||
/// A return value of `None` indicates that the content is not already using a compressed
|
|
||||||
/// representation and may be subject to compression downstream.
|
|
||||||
#[inline]
|
|
||||||
pub fn content_encoding(&self) -> Option<ContentEncoding> {
|
|
||||||
self.encoding
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Set response status code.
|
|
||||||
#[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")]
|
|
||||||
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
||||||
self.status_code = status;
|
self.status_code = status;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets the `Content-Type` header that will be used when serving this file. By default the
|
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
|
||||||
/// `Content-Type` is inferred from the filename extension.
|
/// from the filename extension.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_type(mut self, mime_type: Mime) -> Self {
|
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||||
self.content_type = mime_type;
|
self.content_type = mime_type;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@@ -321,15 +285,15 @@ impl NamedFile {
|
|||||||
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||||
/// (using `to_string_lossy`).
|
/// (using `to_string_lossy`).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self {
|
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||||
self.content_disposition = cd;
|
self.content_disposition = cd;
|
||||||
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disables `Content-Disposition` header.
|
/// Disable `Content-Disposition` header.
|
||||||
///
|
///
|
||||||
/// By default, the `Content-Disposition` header is sent.
|
/// By default Content-Disposition` header is enabled.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn disable_content_disposition(mut self) -> Self {
|
pub fn disable_content_disposition(mut self) -> Self {
|
||||||
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
||||||
@@ -528,26 +492,11 @@ impl NamedFile {
|
|||||||
length = ranges[0].length;
|
length = ranges[0].length;
|
||||||
offset = ranges[0].start;
|
offset = ranges[0].start;
|
||||||
|
|
||||||
// When a Content-Encoding header is present in a 206 partial content response
|
// don't allow compression middleware to modify partial content
|
||||||
// for video content, it prevents browser video players from starting playback
|
res.insert_header((
|
||||||
// before loading the whole video and also prevents seeking.
|
header::CONTENT_ENCODING,
|
||||||
//
|
HeaderValue::from_static("identity"),
|
||||||
// See: https://github.com/actix/actix-web/issues/2815
|
));
|
||||||
//
|
|
||||||
// The assumption of this fix is that the video player knows to not send an
|
|
||||||
// Accept-Encoding header for this request and that downstream middleware will
|
|
||||||
// not attempt compression for requests without it.
|
|
||||||
//
|
|
||||||
// TODO: Solve question around what to do if self.encoding is set and partial
|
|
||||||
// range is requested. Reject request? Ignoring self.encoding seems wrong, too.
|
|
||||||
// In practice, it should not come up.
|
|
||||||
if req.headers().contains_key(&header::ACCEPT_ENCODING) {
|
|
||||||
// don't allow compression middleware to modify partial content
|
|
||||||
res.insert_header((
|
|
||||||
header::CONTENT_ENCODING,
|
|
||||||
HeaderValue::from_static("identity"),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
header::CONTENT_RANGE,
|
header::CONTENT_RANGE,
|
||||||
@@ -579,6 +528,11 @@ impl NamedFile {
|
|||||||
|
|
||||||
res.body(SizedStream::new(length, reader))
|
res.body(SizedStream::new(length, reader))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns stream of chunks for the complete file.
|
||||||
|
pub fn into_chunk_stream(self) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||||
|
chunked::new_chunked_read(self.md.len(), 0, self.file)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||||
|
@@ -23,7 +23,7 @@ impl Deref for FilesService {
|
|||||||
type Target = FilesServiceInner;
|
type Target = FilesServiceInner;
|
||||||
|
|
||||||
fn deref(&self) -> &Self::Target {
|
fn deref(&self) -> &Self::Target {
|
||||||
&self.0
|
&*self.0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,11 +1,11 @@
|
|||||||
use actix_files::{Files, NamedFile};
|
use actix_files::Files;
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
http::{
|
http::{
|
||||||
header::{self, HeaderValue},
|
header::{self, HeaderValue},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
test::{self, TestRequest},
|
test::{self, TestRequest},
|
||||||
web, App,
|
App,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
@@ -36,31 +36,3 @@ async fn test_utf8_file_contents() {
|
|||||||
Some(&HeaderValue::from_static("text/plain")),
|
Some(&HeaderValue::from_static("text/plain")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::test]
|
|
||||||
async fn partial_range_response_encoding() {
|
|
||||||
let srv = test::init_service(App::new().default_service(web::to(|| async {
|
|
||||||
NamedFile::open_async("./tests/test.binary").await.unwrap()
|
|
||||||
})))
|
|
||||||
.await;
|
|
||||||
|
|
||||||
// range request without accept-encoding returns no content-encoding header
|
|
||||||
let req = TestRequest::with_uri("/")
|
|
||||||
.append_header((header::RANGE, "bytes=10-20"))
|
|
||||||
.to_request();
|
|
||||||
let res = test::call_service(&srv, req).await;
|
|
||||||
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
|
|
||||||
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
|
||||||
|
|
||||||
// range request with accept-encoding returns a content-encoding header
|
|
||||||
let req = TestRequest::with_uri("/")
|
|
||||||
.append_header((header::RANGE, "bytes=10-20"))
|
|
||||||
.append_header((header::ACCEPT_ENCODING, "identity"))
|
|
||||||
.to_request();
|
|
||||||
let res = test::call_service(&srv, req).await;
|
|
||||||
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
|
|
||||||
assert_eq!(
|
|
||||||
res.headers().get(header::CONTENT_ENCODING).unwrap(),
|
|
||||||
"identity"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
@@ -1,28 +1,8 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 3.1.0 - 2023-01-21
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59.
|
|
||||||
|
|
||||||
|
|
||||||
## 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`.
|
|
||||||
- Update `actix-tls` dependency to `3`.
|
|
||||||
- Update `bytes` to `1.0`. [#1813]
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57.
|
|
||||||
|
|
||||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
|
||||||
[#2097]: https://github.com/actix/actix-web/pull/2097
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
|
||||||
|
|
||||||
|
|
||||||
<details>
|
|
||||||
<summary>3.0.0 Pre-Releases</summary>
|
|
||||||
|
|
||||||
## 3.0.0-beta.13 - 2022-02-16
|
## 3.0.0-beta.13 - 2022-02-16
|
||||||
- No significant changes since `3.0.0-beta.12`.
|
- No significant changes since `3.0.0-beta.12`.
|
||||||
|
|
||||||
@@ -88,7 +68,6 @@
|
|||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
|
||||||
</details>
|
|
||||||
|
|
||||||
## 2.1.0 - 2020-11-25
|
## 2.1.0 - 2020-11-25
|
||||||
- Add ability to set address for `TestServer`. [#1645]
|
- Add ability to set address for `TestServer`. [#1645]
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http-test"
|
name = "actix-http-test"
|
||||||
version = "3.1.0"
|
version = "3.0.0-beta.13"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Various helpers for Actix applications to use during testing"
|
description = "Various helpers for Actix applications to use during testing"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
@@ -29,16 +29,17 @@ default = []
|
|||||||
openssl = ["tls-openssl", "awc/openssl"]
|
openssl = ["tls-openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2"
|
actix-service = "2.0.0"
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-tls = "3"
|
actix-tls = "3"
|
||||||
actix-utils = "3"
|
actix-utils = "3.0.0"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
awc = { version = "3", default-features = false }
|
awc = { version = "3.0.0-beta.21", default-features = false }
|
||||||
|
|
||||||
|
base64 = "0.13"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
futures-core = { version = "0.3.17", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
socket2 = "0.4"
|
socket2 = "0.4"
|
||||||
@@ -47,8 +48,8 @@ serde_json = "1.0"
|
|||||||
slab = "0.4"
|
slab = "0.4"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||||
tokio = { version = "1.18.4", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
||||||
actix-http = "3"
|
actix-http = "3.0.0"
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
> Various helpers for Actix applications to use during testing.
|
> Various helpers for Actix applications to use during testing.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://docs.rs/actix-http-test/3.1.0)
|
[](https://docs.rs/actix-http-test/3.0.0-beta.13)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br>
|
<br>
|
||||||
[](https://deps.rs/crate/actix-http-test/3.1.0)
|
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.13)
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
@@ -88,7 +87,6 @@ pub async fn test_server_with_addr<F: ServerServiceFactory<TcpStream>>(
|
|||||||
|
|
||||||
// notify TestServer that server and system have shut down
|
// notify TestServer that server and system have shut down
|
||||||
// all thread managed resources should be dropped at this point
|
// all thread managed resources should be dropped at this point
|
||||||
#[allow(clippy::let_underscore_future)]
|
|
||||||
let _ = thread_stop_tx.send(());
|
let _ = thread_stop_tx.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -296,7 +294,6 @@ impl Drop for TestServer {
|
|||||||
// without needing to await anything
|
// without needing to await anything
|
||||||
|
|
||||||
// signal server to stop
|
// signal server to stop
|
||||||
#[allow(clippy::let_underscore_future)]
|
|
||||||
let _ = self.server.stop(true);
|
let _ = self.server.stop(true);
|
||||||
|
|
||||||
// signal system to stop
|
// signal system to stop
|
||||||
|
@@ -1,106 +1,8 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 3.3.0 - 2023-01-21
|
|
||||||
### 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<B>` 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] [#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`
|
|
||||||
|
|
||||||
### 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
|
|
||||||
[#2968]: https://github.com/actix/actix-web/pull/2968
|
|
||||||
|
|
||||||
|
|
||||||
## 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 - 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 - 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 - 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 - 2022-03-09
|
|
||||||
### Fixed
|
|
||||||
- Document on docs.rs with `ws` feature enabled.
|
|
||||||
|
|
||||||
|
|
||||||
## 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 - 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 - 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 - 2022-02-25
|
||||||
### Dependencies
|
### Dependencies
|
||||||
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
||||||
@@ -843,10 +745,10 @@
|
|||||||
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||||
due to deprecate of resolver actor. [#1813]
|
due to deprecate of resolver actor. [#1813]
|
||||||
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||||
error would return as `ConnectError::SslError`. [#1813]
|
error would return as `ConnectError::SslError`. [#1813]
|
||||||
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
||||||
Due to this change `actix_threadpool::BlockingError` type is moved into
|
Due to this change `actix_threadpool::BlockingError` type is moved into
|
||||||
`actix_http::error` module. [#1878]
|
`actix_http::error` module. [#1878]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.3.0"
|
version = "3.0.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
@@ -20,7 +20,7 @@ edition = "2018"
|
|||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
# features that docs.rs will build with
|
# features that docs.rs will build with
|
||||||
features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
features = ["http2", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_http"
|
name = "actix_http"
|
||||||
@@ -37,7 +37,7 @@ ws = [
|
|||||||
"local-channel",
|
"local-channel",
|
||||||
"base64",
|
"base64",
|
||||||
"rand",
|
"rand",
|
||||||
"sha1",
|
"sha-1",
|
||||||
]
|
]
|
||||||
|
|
||||||
# TLS via OpenSSL
|
# TLS via OpenSSL
|
||||||
@@ -67,28 +67,26 @@ bytes = "1"
|
|||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
httparse = "1.5.1"
|
httparse = "1.5.1"
|
||||||
httpdate = "1.0.1"
|
httpdate = "1.0.1"
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
tokio = { version = "1.18.4", features = [] }
|
|
||||||
tokio-util = { version = "0.7", features = ["io", "codec"] }
|
|
||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
|
||||||
|
|
||||||
# http2
|
# http2
|
||||||
h2 = { version = "0.3.9", optional = true }
|
h2 = { version = "0.3.9", optional = true }
|
||||||
|
|
||||||
# websockets
|
# websockets
|
||||||
local-channel = { version = "0.1", optional = true }
|
local-channel = { version = "0.1", optional = true }
|
||||||
base64 = { version = "0.21", optional = true }
|
base64 = { version = "0.13", optional = true }
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
sha1 = { version = "0.10", optional = true }
|
sha-1 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
# openssl/rustls
|
# openssl/rustls
|
||||||
actix-tls = { version = "3", default-features = false, optional = true }
|
actix-tls = { version = "3", default-features = false, optional = true }
|
||||||
@@ -96,34 +94,33 @@ actix-tls = { version = "3", default-features = false, optional = true }
|
|||||||
# compress-*
|
# compress-*
|
||||||
brotli = { version = "3.3.3", optional = true }
|
brotli = { version = "3.3.3", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.12", optional = true }
|
zstd = { version = "0.10", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http-test = { version = "3", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
actix-tls = { version = "3", features = ["openssl"] }
|
actix-tls = { version = "3", features = ["openssl"] }
|
||||||
actix-web = "4"
|
actix-web = "4.0.0"
|
||||||
|
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
criterion = { version = "0.4", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
memchr = "2.4"
|
memchr = "2.4"
|
||||||
once_cell = "1.9"
|
once_cell = "1.9"
|
||||||
rcgen = "0.9"
|
rcgen = "0.8"
|
||||||
regex = "1.3"
|
regex = "1.3"
|
||||||
rustversion = "1"
|
rustls-pemfile = "0.2"
|
||||||
rustls-pemfile = "1"
|
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
tokio = { version = "1.18.4", features = ["net", "rt", "macros"] }
|
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
required-features = ["ws", "rustls"]
|
required-features = ["rustls"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "write-camel-case"
|
name = "write-camel-case"
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
> HTTP primitives for the Actix ecosystem.
|
> HTTP primitives for the Actix ecosystem.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.3.0)
|
[](https://docs.rs/actix-http/3.0.0)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.3.0)
|
[](https://deps.rs/crate/actix-http/3.0.0)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
@@ -25,7 +25,7 @@ use actix_http::{HttpService, Response};
|
|||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use futures_util::future;
|
use futures_util::future;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use tracing::info;
|
use log::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||||
|
|
||||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||||
|
@@ -114,12 +114,11 @@ mod _original {
|
|||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
|
|
||||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||||
#![allow(invalid_value, clippy::uninit_assumed_init)]
|
#![allow(clippy::uninit_assumed_init)]
|
||||||
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
#[allow(invalid_value)]
|
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
@@ -18,8 +18,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
HttpService::build()
|
HttpService::build()
|
||||||
// pass the app to service builder
|
// pass the app to service builder
|
||||||
// map_config is used to map App's configuration to ServiceBuilder
|
// map_config is used to map App's configuration to ServiceBuilder
|
||||||
// h1 will configure server to only use HTTP/1.1
|
.finish(map_config(app, |_| AppConfig::default()))
|
||||||
.h1(map_config(app, |_| AppConfig::default()))
|
|
||||||
.tcp()
|
.tcp()
|
||||||
})?
|
})?
|
||||||
.run()
|
.run()
|
||||||
|
@@ -5,7 +5,6 @@ use actix_server::Server;
|
|||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@@ -23,7 +22,7 @@ async fn main() -> io::Result<()> {
|
|||||||
body.extend_from_slice(&item?);
|
body.extend_from_slice(&item?);
|
||||||
}
|
}
|
||||||
|
|
||||||
info!("request body: {:?}", body);
|
log::info!("request body: {:?}", body);
|
||||||
|
|
||||||
let res = Response::build(StatusCode::OK)
|
let res = Response::build(StatusCode::OK)
|
||||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||||
|
@@ -1,29 +0,0 @@
|
|||||||
//! An example that supports automatic selection of plaintext h1/h2c connections.
|
|
||||||
//!
|
|
||||||
//! Notably, both the following commands will work.
|
|
||||||
//! ```console
|
|
||||||
//! $ curl --http1.1 'http://localhost:8080/'
|
|
||||||
//! $ curl --http2-prior-knowledge 'http://localhost:8080/'
|
|
||||||
//! ```
|
|
||||||
|
|
||||||
use std::{convert::Infallible, io};
|
|
||||||
|
|
||||||
use actix_http::{HttpService, Request, Response, StatusCode};
|
|
||||||
use actix_server::Server;
|
|
||||||
|
|
||||||
#[tokio::main(flavor = "current_thread")]
|
|
||||||
async fn main() -> io::Result<()> {
|
|
||||||
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
|
|
||||||
|
|
||||||
Server::build()
|
|
||||||
.bind("h2c-detect", ("127.0.0.1", 8080), || {
|
|
||||||
HttpService::build()
|
|
||||||
.finish(|_req: Request| async move {
|
|
||||||
Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
|
|
||||||
})
|
|
||||||
.tcp_auto_h2c()
|
|
||||||
})?
|
|
||||||
.workers(2)
|
|
||||||
.run()
|
|
||||||
.await
|
|
||||||
}
|
|
@@ -1,8 +1,9 @@
|
|||||||
use std::{convert::Infallible, io, time::Duration};
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode};
|
use actix_http::{
|
||||||
|
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
||||||
|
};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@@ -17,12 +18,12 @@ async fn main() -> io::Result<()> {
|
|||||||
ext.insert(42u32);
|
ext.insert(42u32);
|
||||||
})
|
})
|
||||||
.finish(|req: Request| async move {
|
.finish(|req: Request| async move {
|
||||||
info!("{:?}", req);
|
log::info!("{:?}", req);
|
||||||
|
|
||||||
let mut res = Response::build(StatusCode::OK);
|
let mut res = Response::build(StatusCode::OK);
|
||||||
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
||||||
|
|
||||||
let forty_two = req.conn_data::<u32>().unwrap().to_string();
|
let forty_two = req.extensions().get::<u32>().unwrap().to_string();
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
"x-forty-two",
|
"x-forty-two",
|
||||||
HeaderValue::from_str(&forty_two).unwrap(),
|
HeaderValue::from_str(&forty_two).unwrap(),
|
||||||
|
@@ -12,7 +12,6 @@ use actix_http::{body::BodyStream, HttpService, Response};
|
|||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use tracing::info;
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@@ -22,7 +21,7 @@ async fn main() -> io::Result<()> {
|
|||||||
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.finish(|req| async move {
|
.finish(|req| async move {
|
||||||
info!("{:?}", req);
|
log::info!("{:?}", req);
|
||||||
let res = Response::ok();
|
let res = Response::ok();
|
||||||
|
|
||||||
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
||||||
|
@@ -10,14 +10,13 @@ use std::{
|
|||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use actix_codec::Encoder;
|
||||||
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
|
||||||
use actix_rt::time::{interval, Interval};
|
use actix_rt::time::{interval, Interval};
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
use tokio_util::codec::Encoder;
|
|
||||||
use tracing::{info, trace};
|
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@@ -35,13 +34,13 @@ async fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
||||||
info!("handshaking");
|
log::info!("handshaking");
|
||||||
let mut res = ws::handshake(req.head())?;
|
let mut res = ws::handshake(req.head())?;
|
||||||
|
|
||||||
// handshake will always fail under HTTP/2
|
// handshake will always fail under HTTP/2
|
||||||
|
|
||||||
info!("responding");
|
log::info!("responding");
|
||||||
res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
|
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Heartbeat {
|
struct Heartbeat {
|
||||||
@@ -62,7 +61,7 @@ impl Stream for Heartbeat {
|
|||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
trace!("poll");
|
log::trace!("poll");
|
||||||
|
|
||||||
ready!(self.as_mut().interval.poll_tick(cx));
|
ready!(self.as_mut().interval.poll_tick(cx));
|
||||||
|
|
||||||
|
@@ -120,28 +120,8 @@ pub trait MessageBody {
|
|||||||
}
|
}
|
||||||
|
|
||||||
mod foreign_impls {
|
mod foreign_impls {
|
||||||
use std::{borrow::Cow, ops::DerefMut};
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
impl<B> MessageBody for &mut B
|
|
||||||
where
|
|
||||||
B: MessageBody + Unpin + ?Sized,
|
|
||||||
{
|
|
||||||
type Error = B::Error;
|
|
||||||
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
(**self).size()
|
|
||||||
}
|
|
||||||
|
|
||||||
fn poll_next(
|
|
||||||
mut self: Pin<&mut Self>,
|
|
||||||
cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
Pin::new(&mut **self).poll_next(cx)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for Infallible {
|
impl MessageBody for Infallible {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
@@ -199,9 +179,8 @@ mod foreign_impls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, B> MessageBody for Pin<T>
|
impl<B> MessageBody for Pin<Box<B>>
|
||||||
where
|
where
|
||||||
T: DerefMut<Target = B> + Unpin,
|
|
||||||
B: MessageBody + ?Sized,
|
B: MessageBody + ?Sized,
|
||||||
{
|
{
|
||||||
type Error = B::Error;
|
type Error = B::Error;
|
||||||
@@ -324,39 +303,6 @@ mod foreign_impls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for Cow<'static, [u8]> {
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
let bytes = match mem::take(self.get_mut()) {
|
|
||||||
Cow::Borrowed(b) => Bytes::from_static(b),
|
|
||||||
Cow::Owned(b) => Bytes::from(b),
|
|
||||||
};
|
|
||||||
Poll::Ready(Some(Ok(bytes)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
|
||||||
match self {
|
|
||||||
Cow::Borrowed(b) => Ok(Bytes::from_static(b)),
|
|
||||||
Cow::Owned(b) => Ok(Bytes::from(b)),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for &'static str {
|
impl MessageBody for &'static str {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
@@ -412,39 +358,6 @@ mod foreign_impls {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MessageBody for Cow<'static, str> {
|
|
||||||
type Error = Infallible;
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn size(&self) -> BodySize {
|
|
||||||
BodySize::Sized(self.len() as u64)
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn poll_next(
|
|
||||||
self: Pin<&mut Self>,
|
|
||||||
_cx: &mut Context<'_>,
|
|
||||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
|
||||||
if self.is_empty() {
|
|
||||||
Poll::Ready(None)
|
|
||||||
} else {
|
|
||||||
let bytes = match mem::take(self.get_mut()) {
|
|
||||||
Cow::Borrowed(s) => Bytes::from_static(s.as_bytes()),
|
|
||||||
Cow::Owned(s) => Bytes::from(s.into_bytes()),
|
|
||||||
};
|
|
||||||
Poll::Ready(Some(Ok(bytes)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[inline]
|
|
||||||
fn try_into_bytes(self) -> Result<Bytes, Self> {
|
|
||||||
match self {
|
|
||||||
Cow::Borrowed(s) => Ok(Bytes::from_static(s.as_bytes())),
|
|
||||||
Cow::Owned(s) => Ok(Bytes::from(s.into_bytes())),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl MessageBody for bytestring::ByteString {
|
impl MessageBody for bytestring::ByteString {
|
||||||
type Error = Infallible;
|
type Error = Infallible;
|
||||||
|
|
||||||
@@ -532,7 +445,6 @@ mod tests {
|
|||||||
use actix_rt::pin;
|
use actix_rt::pin;
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use futures_util::stream;
|
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::body::{self, EitherBody};
|
use crate::body::{self, EitherBody};
|
||||||
@@ -569,35 +481,6 @@ mod tests {
|
|||||||
assert_poll_next_none!(pl);
|
assert_poll_next_none!(pl);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn mut_equivalence() {
|
|
||||||
assert_eq!(().size(), BodySize::Sized(0));
|
|
||||||
assert_eq!(().size(), (&(&mut ())).size());
|
|
||||||
|
|
||||||
let pl = &mut ();
|
|
||||||
pin!(pl);
|
|
||||||
assert_poll_next_none!(pl);
|
|
||||||
|
|
||||||
let pl = &mut Box::new(());
|
|
||||||
pin!(pl);
|
|
||||||
assert_poll_next_none!(pl);
|
|
||||||
|
|
||||||
let mut body = body::SizedStream::new(
|
|
||||||
8,
|
|
||||||
stream::iter([
|
|
||||||
Ok::<_, std::io::Error>(Bytes::from("1234")),
|
|
||||||
Ok(Bytes::from("5678")),
|
|
||||||
]),
|
|
||||||
);
|
|
||||||
let body = &mut body;
|
|
||||||
assert_eq!(body.size(), BodySize::Sized(8));
|
|
||||||
pin!(body);
|
|
||||||
assert_poll_next!(body, Bytes::from_static(b"1234"));
|
|
||||||
assert_poll_next!(body, Bytes::from_static(b"5678"));
|
|
||||||
assert_poll_next_none!(body);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[allow(clippy::let_unit_value)]
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unit() {
|
async fn test_unit() {
|
||||||
let pl = ();
|
let pl = ();
|
||||||
@@ -723,18 +606,4 @@ mod tests {
|
|||||||
let not_body = resp_body.downcast_ref::<()>();
|
let not_body = resp_body.downcast_ref::<()>();
|
||||||
assert!(not_body.is_none());
|
assert!(not_body.is_none());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn non_owning_to_bytes() {
|
|
||||||
let mut body = BoxBody::new(());
|
|
||||||
let bytes = body::to_bytes(&mut body).await.unwrap();
|
|
||||||
assert_eq!(bytes, Bytes::new());
|
|
||||||
|
|
||||||
let mut body = body::BodyStream::new(stream::iter([
|
|
||||||
Ok::<_, std::io::Error>(Bytes::from("1234")),
|
|
||||||
Ok(Bytes::from("5678")),
|
|
||||||
]));
|
|
||||||
let bytes = body::to_bytes(&mut body).await.unwrap();
|
|
||||||
assert_eq!(bytes, Bytes::from_static(b"12345678"));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
@@ -44,7 +44,7 @@ where
|
|||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
fn size(&self) -> BodySize {
|
fn size(&self) -> BodySize {
|
||||||
BodySize::Sized(self.size)
|
BodySize::Sized(self.size as u64)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to pull out the next value of the underlying [`Stream`].
|
/// Attempts to pull out the next value of the underlying [`Stream`].
|
||||||
|
@@ -42,7 +42,7 @@ pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
|||||||
let body = body.as_mut();
|
let body = body.as_mut();
|
||||||
|
|
||||||
match ready!(body.poll_next(cx)) {
|
match ready!(body.poll_next(cx)) {
|
||||||
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||||
None => return Poll::Ready(Ok(())),
|
None => return Poll::Ready(Ok(())),
|
||||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
}
|
}
|
||||||
|
@@ -186,7 +186,7 @@ where
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish service configuration and create a service for the HTTP/1 protocol.
|
/// Finish service configuration and create a HTTP Service for HTTP/1 protocol.
|
||||||
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -209,9 +209,8 @@ where
|
|||||||
.on_connect_ext(self.on_connect_ext)
|
.on_connect_ext(self.on_connect_ext)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Finish service configuration and create a service for the HTTP/2 protocol.
|
/// Finish service configuration and create a HTTP service for HTTP/2 protocol.
|
||||||
#[cfg(feature = "http2")]
|
#[cfg(feature = "http2")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
|
||||||
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
pub fn h2<F, B>(self, service: F) -> crate::h2::H2Service<T, S, B>
|
||||||
where
|
where
|
||||||
F: IntoServiceFactory<S, Request>,
|
F: IntoServiceFactory<S, Request>,
|
||||||
|
@@ -35,7 +35,7 @@ impl Default for ServiceConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceConfig {
|
impl ServiceConfig {
|
||||||
/// Create instance of `ServiceConfig`.
|
/// Create instance of `ServiceConfig`
|
||||||
pub fn new(
|
pub fn new(
|
||||||
keep_alive: KeepAlive,
|
keep_alive: KeepAlive,
|
||||||
client_request_timeout: Duration,
|
client_request_timeout: Duration,
|
||||||
|
@@ -17,7 +17,6 @@ use pin_project_lite::pin_project;
|
|||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||||
|
|
||||||
use tracing::trace;
|
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
use zstd::stream::write::Encoder as ZstdEncoder;
|
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
@@ -257,7 +256,7 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
|||||||
head.headers_mut()
|
head.headers_mut()
|
||||||
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
||||||
head.headers_mut()
|
head.headers_mut()
|
||||||
.append(header::VARY, HeaderValue::from_static("accept-encoding"));
|
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
|
||||||
|
|
||||||
head.no_chunking(false);
|
head.no_chunking(false);
|
||||||
}
|
}
|
||||||
@@ -357,7 +356,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding br encoding: {}", err);
|
log::trace!("Error decoding br encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -366,7 +365,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding gzip encoding: {}", err);
|
log::trace!("Error decoding gzip encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -375,7 +374,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding deflate encoding: {}", err);
|
log::trace!("Error decoding deflate encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@@ -384,7 +383,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("Error decoding ztsd encoding: {}", err);
|
log::trace!("Error decoding ztsd encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -294,7 +294,6 @@ impl std::error::Error for PayloadError {
|
|||||||
PayloadError::Overflow => None,
|
PayloadError::Overflow => None,
|
||||||
PayloadError::UnknownLength => None,
|
PayloadError::UnknownLength => None,
|
||||||
#[cfg(feature = "http2")]
|
#[cfg(feature = "http2")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
|
||||||
PayloadError::Http2Payload(err) => Some(err),
|
PayloadError::Http2Payload(err) => Some(err),
|
||||||
PayloadError::Io(err) => Some(err),
|
PayloadError::Io(err) => Some(err),
|
||||||
}
|
}
|
||||||
@@ -352,7 +351,6 @@ pub enum DispatchError {
|
|||||||
/// HTTP/2 error.
|
/// HTTP/2 error.
|
||||||
#[display(fmt = "{}", _0)]
|
#[display(fmt = "{}", _0)]
|
||||||
#[cfg(feature = "http2")]
|
#[cfg(feature = "http2")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
|
||||||
H2(h2::Error),
|
H2(h2::Error),
|
||||||
|
|
||||||
/// The first request did not complete within the specified timeout.
|
/// The first request did not complete within the specified timeout.
|
||||||
@@ -390,7 +388,7 @@ impl StdError for DispatchError {
|
|||||||
|
|
||||||
/// A set of error that can occur during parsing content type.
|
/// A set of error that can occur during parsing content type.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[cfg_attr(test, derive(PartialEq, Eq))]
|
#[cfg_attr(test, derive(PartialEq))]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum ContentTypeError {
|
pub enum ContentTypeError {
|
||||||
/// Can not parse content type
|
/// Can not parse content type
|
||||||
|
@@ -1,30 +1,9 @@
|
|||||||
use std::{
|
use std::{
|
||||||
any::{Any, TypeId},
|
any::{Any, TypeId},
|
||||||
collections::HashMap,
|
|
||||||
fmt,
|
fmt,
|
||||||
hash::{BuildHasherDefault, Hasher},
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A hasher for `TypeId`s that takes advantage of its known characteristics.
|
use ahash::AHashMap;
|
||||||
///
|
|
||||||
/// Author of `anymap` crate has done research on the topic:
|
|
||||||
/// https://github.com/chris-morgan/anymap/blob/2e9a5704/src/lib.rs#L599
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
struct NoOpHasher(u64);
|
|
||||||
|
|
||||||
impl Hasher for NoOpHasher {
|
|
||||||
fn write(&mut self, _bytes: &[u8]) {
|
|
||||||
unimplemented!("This NoOpHasher can only handle u64s")
|
|
||||||
}
|
|
||||||
|
|
||||||
fn write_u64(&mut self, i: u64) {
|
|
||||||
self.0 = i;
|
|
||||||
}
|
|
||||||
|
|
||||||
fn finish(&self) -> u64 {
|
|
||||||
self.0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A type map for request extensions.
|
/// A type map for request extensions.
|
||||||
///
|
///
|
||||||
@@ -32,7 +11,7 @@ impl Hasher for NoOpHasher {
|
|||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Extensions {
|
pub struct Extensions {
|
||||||
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
|
||||||
map: HashMap<TypeId, Box<dyn Any>, BuildHasherDefault<NoOpHasher>>,
|
map: AHashMap<TypeId, Box<dyn Any>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Extensions {
|
impl Extensions {
|
||||||
@@ -40,7 +19,7 @@ impl Extensions {
|
|||||||
#[inline]
|
#[inline]
|
||||||
pub fn new() -> Extensions {
|
pub fn new() -> Extensions {
|
||||||
Extensions {
|
Extensions {
|
||||||
map: HashMap::default(),
|
map: AHashMap::new(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
use std::{io, task::Poll};
|
use std::{io, task::Poll};
|
||||||
|
|
||||||
use bytes::{Buf as _, Bytes, BytesMut};
|
use bytes::{Buf as _, Bytes, BytesMut};
|
||||||
use tracing::{debug, trace};
|
|
||||||
|
|
||||||
macro_rules! byte (
|
macro_rules! byte (
|
||||||
($rdr:ident) => ({
|
($rdr:ident) => ({
|
||||||
@@ -15,7 +14,7 @@ macro_rules! byte (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Clone)]
|
||||||
pub(super) enum ChunkedState {
|
pub(super) enum ChunkedState {
|
||||||
Size,
|
Size,
|
||||||
SizeLws,
|
SizeLws,
|
||||||
@@ -71,13 +70,13 @@ impl ChunkedState {
|
|||||||
|
|
||||||
match size.checked_mul(radix) {
|
match size.checked_mul(radix) {
|
||||||
Some(n) => {
|
Some(n) => {
|
||||||
*size = n;
|
*size = n as u64;
|
||||||
*size += rem as u64;
|
*size += rem as u64;
|
||||||
|
|
||||||
Poll::Ready(Ok(ChunkedState::Size))
|
Poll::Ready(Ok(ChunkedState::Size))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
debug!("chunk size would overflow u64");
|
log::debug!("chunk size would overflow u64");
|
||||||
Poll::Ready(Err(io::Error::new(
|
Poll::Ready(Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
"Invalid chunk size line: Size is too big",
|
"Invalid chunk size line: Size is too big",
|
||||||
@@ -125,7 +124,7 @@ impl ChunkedState {
|
|||||||
rem: &mut u64,
|
rem: &mut u64,
|
||||||
buf: &mut Option<Bytes>,
|
buf: &mut Option<Bytes>,
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
trace!("Chunked read, remaining={:?}", rem);
|
log::trace!("Chunked read, remaining={:?}", rem);
|
||||||
|
|
||||||
let len = rdr.len() as u64;
|
let len = rdr.len() as u64;
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use actix_codec::{Decoder, Encoder};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||||
|
@@ -1,9 +1,9 @@
|
|||||||
use std::{fmt, io};
|
use std::{fmt, io};
|
||||||
|
|
||||||
|
use actix_codec::{Decoder, Encoder};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use http::{Method, Version};
|
use http::{Method, Version};
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
|
||||||
|
@@ -6,7 +6,7 @@ use http::{
|
|||||||
header::{self, HeaderName, HeaderValue},
|
header::{self, HeaderName, HeaderValue},
|
||||||
Method, StatusCode, Uri, Version,
|
Method, StatusCode, Uri, Version,
|
||||||
};
|
};
|
||||||
use tracing::{debug, error, trace};
|
use log::{debug, error, trace};
|
||||||
|
|
||||||
use super::chunked::ChunkedState;
|
use super::chunked::ChunkedState;
|
||||||
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
||||||
@@ -46,23 +46,6 @@ pub(crate) enum PayloadLength {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PayloadLength {
|
|
||||||
/// Returns true if variant is `None`.
|
|
||||||
fn is_none(&self) -> bool {
|
|
||||||
matches!(self, Self::None)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if variant is represents zero-length (not none) payload.
|
|
||||||
fn is_zero(&self) -> bool {
|
|
||||||
matches!(
|
|
||||||
self,
|
|
||||||
PayloadLength::Payload(PayloadType::Payload(PayloadDecoder {
|
|
||||||
kind: Kind::Length(0)
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) trait MessageType: Sized {
|
pub(crate) trait MessageType: Sized {
|
||||||
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
||||||
|
|
||||||
@@ -76,7 +59,6 @@ pub(crate) trait MessageType: Sized {
|
|||||||
&mut self,
|
&mut self,
|
||||||
slice: &Bytes,
|
slice: &Bytes,
|
||||||
raw_headers: &[HeaderIndex],
|
raw_headers: &[HeaderIndex],
|
||||||
version: Version,
|
|
||||||
) -> Result<PayloadLength, ParseError> {
|
) -> Result<PayloadLength, ParseError> {
|
||||||
let mut ka = None;
|
let mut ka = None;
|
||||||
let mut has_upgrade_websocket = false;
|
let mut has_upgrade_websocket = false;
|
||||||
@@ -105,23 +87,21 @@ pub(crate) trait MessageType: Sized {
|
|||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
header::CONTENT_LENGTH => match value.to_str().map(str::trim) {
|
header::CONTENT_LENGTH => match value.to_str() {
|
||||||
Ok(val) if val.starts_with('+') => {
|
Ok(s) if s.trim().starts_with('+') => {
|
||||||
debug!("illegal Content-Length: {:?}", val);
|
debug!("illegal Content-Length: {:?}", s);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
Ok(s) => {
|
||||||
Ok(val) => {
|
if let Ok(len) = s.parse::<u64>() {
|
||||||
if let Ok(len) = val.parse::<u64>() {
|
if len != 0 {
|
||||||
// accept 0 lengths here and remove them in `decode` after all
|
content_length = Some(len);
|
||||||
// headers have been processed to prevent request smuggling issues
|
}
|
||||||
content_length = Some(len);
|
|
||||||
} else {
|
} else {
|
||||||
debug!("illegal Content-Length: {:?}", val);
|
debug!("illegal Content-Length: {:?}", s);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!("illegal Content-Length: {:?}", value);
|
debug!("illegal Content-Length: {:?}", value);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
@@ -134,23 +114,22 @@ pub(crate) trait MessageType: Sized {
|
|||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
header::TRANSFER_ENCODING if version == Version::HTTP_11 => {
|
header::TRANSFER_ENCODING => {
|
||||||
seen_te = true;
|
seen_te = true;
|
||||||
|
|
||||||
if let Ok(val) = value.to_str().map(str::trim) {
|
if let Ok(s) = value.to_str().map(str::trim) {
|
||||||
if val.eq_ignore_ascii_case("chunked") {
|
if s.eq_ignore_ascii_case("chunked") {
|
||||||
chunked = true;
|
chunked = true;
|
||||||
} else if val.eq_ignore_ascii_case("identity") {
|
} else if s.eq_ignore_ascii_case("identity") {
|
||||||
// allow silently since multiple TE headers are already checked
|
// allow silently since multiple TE headers are already checked
|
||||||
} else {
|
} else {
|
||||||
debug!("illegal Transfer-Encoding: {:?}", val);
|
debug!("illegal Transfer-Encoding: {:?}", s);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// connection keep-alive state
|
// connection keep-alive state
|
||||||
header::CONNECTION => {
|
header::CONNECTION => {
|
||||||
ka = if let Ok(conn) = value.to_str().map(str::trim) {
|
ka = if let Ok(conn) = value.to_str().map(str::trim) {
|
||||||
@@ -167,7 +146,6 @@ pub(crate) trait MessageType: Sized {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
header::UPGRADE => {
|
header::UPGRADE => {
|
||||||
if let Ok(val) = value.to_str().map(str::trim) {
|
if let Ok(val) = value.to_str().map(str::trim) {
|
||||||
if val.eq_ignore_ascii_case("websocket") {
|
if val.eq_ignore_ascii_case("websocket") {
|
||||||
@@ -175,23 +153,19 @@ pub(crate) trait MessageType: Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header::EXPECT => {
|
header::EXPECT => {
|
||||||
let bytes = value.as_bytes();
|
let bytes = value.as_bytes();
|
||||||
if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
|
if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
|
||||||
expect = true;
|
expect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.append(name, value);
|
headers.append(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_connection_type(ka);
|
self.set_connection_type(ka);
|
||||||
|
|
||||||
if expect {
|
if expect {
|
||||||
self.set_expect()
|
self.set_expect()
|
||||||
}
|
}
|
||||||
@@ -275,22 +249,7 @@ impl MessageType for Request {
|
|||||||
let mut msg = Request::new();
|
let mut msg = Request::new();
|
||||||
|
|
||||||
// convert headers
|
// convert headers
|
||||||
let mut length =
|
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
|
||||||
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
|
|
||||||
if ver == Version::HTTP_10 && method == Method::POST && length.is_none() {
|
|
||||||
debug!("no Content-Length specified for HTTP/1.0 POST request");
|
|
||||||
return Err(ParseError::Header);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
|
|
||||||
// Protects against some request smuggling attacks.
|
|
||||||
// See https://github.com/actix/actix-web/issues/2767.
|
|
||||||
if length.is_zero() {
|
|
||||||
length = PayloadLength::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// payload decoder
|
// payload decoder
|
||||||
let decoder = match length {
|
let decoder = match length {
|
||||||
@@ -334,35 +293,22 @@ impl MessageType for ResponseHead {
|
|||||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
|
|
||||||
let (len, ver, status, h_len) = {
|
let (len, ver, status, h_len) = {
|
||||||
// SAFETY:
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
||||||
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the
|
|
||||||
// type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which
|
|
||||||
// do not require initialization.
|
|
||||||
let mut parsed = unsafe {
|
|
||||||
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
|
|
||||||
.assume_init()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut res = httparse::Response::new(&mut []);
|
let mut res = httparse::Response::new(&mut parsed);
|
||||||
|
match res.parse(src)? {
|
||||||
let mut config = httparse::ParserConfig::default();
|
|
||||||
config.allow_spaces_after_header_name_in_responses(true);
|
|
||||||
|
|
||||||
match config.parse_response_with_uninit_headers(&mut res, src, &mut parsed)? {
|
|
||||||
httparse::Status::Complete(len) => {
|
httparse::Status::Complete(len) => {
|
||||||
let version = if res.version.unwrap() == 1 {
|
let version = if res.version.unwrap() == 1 {
|
||||||
Version::HTTP_11
|
Version::HTTP_11
|
||||||
} else {
|
} else {
|
||||||
Version::HTTP_10
|
Version::HTTP_10
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = StatusCode::from_u16(res.code.unwrap())
|
let status = StatusCode::from_u16(res.code.unwrap())
|
||||||
.map_err(|_| ParseError::Status)?;
|
.map_err(|_| ParseError::Status)?;
|
||||||
HeaderIndex::record(src, res.headers, &mut headers);
|
HeaderIndex::record(src, res.headers, &mut headers);
|
||||||
|
|
||||||
(len, version, status, res.headers.len())
|
(len, version, status, res.headers.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
httparse::Status::Partial => {
|
httparse::Status::Partial => {
|
||||||
return if src.len() >= MAX_BUFFER_SIZE {
|
return if src.len() >= MAX_BUFFER_SIZE {
|
||||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||||
@@ -378,15 +324,7 @@ impl MessageType for ResponseHead {
|
|||||||
msg.version = ver;
|
msg.version = ver;
|
||||||
|
|
||||||
// convert headers
|
// convert headers
|
||||||
let mut length =
|
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
|
||||||
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.
|
|
||||||
// See https://github.com/actix/actix-web/issues/2767.
|
|
||||||
if length.is_zero() {
|
|
||||||
length = PayloadLength::None;
|
|
||||||
}
|
|
||||||
|
|
||||||
// message payload
|
// message payload
|
||||||
let decoder = if let PayloadLength::Payload(pl) = length {
|
let decoder = if let PayloadLength::Payload(pl) = length {
|
||||||
@@ -422,6 +360,9 @@ pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
|||||||
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||||
|
|
||||||
|
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
||||||
|
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
||||||
|
|
||||||
impl HeaderIndex {
|
impl HeaderIndex {
|
||||||
pub(crate) fn record(
|
pub(crate) fn record(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
@@ -440,7 +381,7 @@ impl HeaderIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
/// Chunk type yielded while decoding a payload.
|
/// Chunk type yielded while decoding a payload.
|
||||||
pub enum PayloadItem {
|
pub enum PayloadItem {
|
||||||
Chunk(Bytes),
|
Chunk(Bytes),
|
||||||
@@ -450,7 +391,7 @@ pub enum PayloadItem {
|
|||||||
/// Decoder that can handle different payload types.
|
/// Decoder that can handle different payload types.
|
||||||
///
|
///
|
||||||
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
pub struct PayloadDecoder {
|
pub struct PayloadDecoder {
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
}
|
}
|
||||||
@@ -476,7 +417,7 @@ impl PayloadDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq)]
|
||||||
enum Kind {
|
enum Kind {
|
||||||
/// A reader used when a `Content-Length` header is passed with a positive integer.
|
/// A reader used when a `Content-Length` header is passed with a positive integer.
|
||||||
Length(u64),
|
Length(u64),
|
||||||
@@ -655,100 +596,14 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn parse_h09_reject() {
|
fn test_parse_post() {
|
||||||
let mut buf = BytesMut::from(
|
let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n");
|
||||||
"GET /test1 HTTP/0.9\r\n\
|
|
||||||
\r\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
reader.decode(&mut buf).unwrap_err();
|
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"POST /test2 HTTP/0.9\r\n\
|
|
||||||
Content-Length: 3\r\n\
|
|
||||||
\r\n
|
|
||||||
abc",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
reader.decode(&mut buf).unwrap_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_h10_get() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test1 HTTP/1.0\r\n\
|
|
||||||
\r\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(req.version(), Version::HTTP_10);
|
|
||||||
assert_eq!(*req.method(), Method::GET);
|
|
||||||
assert_eq!(req.path(), "/test1");
|
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test2 HTTP/1.0\r\n\
|
|
||||||
Content-Length: 0\r\n\
|
|
||||||
\r\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(req.version(), Version::HTTP_10);
|
|
||||||
assert_eq!(*req.method(), Method::GET);
|
|
||||||
assert_eq!(req.path(), "/test2");
|
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET /test3 HTTP/1.0\r\n\
|
|
||||||
Content-Length: 3\r\n\
|
|
||||||
\r\n
|
|
||||||
abc",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(req.version(), Version::HTTP_10);
|
|
||||||
assert_eq!(*req.method(), Method::GET);
|
|
||||||
assert_eq!(req.path(), "/test3");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn parse_h10_post() {
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"POST /test1 HTTP/1.0\r\n\
|
|
||||||
Content-Length: 3\r\n\
|
|
||||||
\r\n\
|
|
||||||
abc",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
|
||||||
assert_eq!(req.version(), Version::HTTP_10);
|
|
||||||
assert_eq!(*req.method(), Method::POST);
|
|
||||||
assert_eq!(req.path(), "/test1");
|
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"POST /test2 HTTP/1.0\r\n\
|
|
||||||
Content-Length: 0\r\n\
|
|
||||||
\r\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
assert_eq!(req.version(), Version::HTTP_10);
|
assert_eq!(req.version(), Version::HTTP_10);
|
||||||
assert_eq!(*req.method(), Method::POST);
|
assert_eq!(*req.method(), Method::POST);
|
||||||
assert_eq!(req.path(), "/test2");
|
assert_eq!(req.path(), "/test2");
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"POST /test3 HTTP/1.0\r\n\
|
|
||||||
\r\n",
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let err = reader.decode(&mut buf).unwrap_err();
|
|
||||||
assert!(err.to_string().contains("Header"))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -844,98 +699,121 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_default_1_0() {
|
fn test_conn_default_1_0() {
|
||||||
let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.0\r\n\r\n"));
|
let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n");
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_default_1_1() {
|
fn test_conn_default_1_1() {
|
||||||
let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.1\r\n\r\n"));
|
let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n");
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_close() {
|
fn test_conn_close() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: close\r\n\r\n",
|
connection: close\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
|
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: Close\r\n\r\n",
|
connection: Close\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_close_1_0() {
|
fn test_conn_close_1_0() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: close\r\n\r\n",
|
connection: close\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_keep_alive_1_0() {
|
fn test_conn_keep_alive_1_0() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: keep-alive\r\n\r\n",
|
connection: keep-alive\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
|
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: Keep-Alive\r\n\r\n",
|
connection: Keep-Alive\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_keep_alive_1_1() {
|
fn test_conn_keep_alive_1_1() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: keep-alive\r\n\r\n",
|
connection: keep-alive\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_other_1_0() {
|
fn test_conn_other_1_0() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: other\r\n\r\n",
|
connection: other\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_other_1_1() {
|
fn test_conn_other_1_1() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: other\r\n\r\n",
|
connection: other\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_upgrade() {
|
fn test_conn_upgrade() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
upgrade: websockets\r\n\
|
upgrade: websockets\r\n\
|
||||||
connection: upgrade\r\n\r\n",
|
connection: upgrade\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||||
|
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
upgrade: Websockets\r\n\
|
upgrade: Websockets\r\n\
|
||||||
connection: Upgrade\r\n\r\n",
|
connection: Upgrade\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||||
@@ -943,62 +821,59 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_upgrade_connect_method() {
|
fn test_conn_upgrade_connect_method() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"CONNECT /test HTTP/1.1\r\n\
|
"CONNECT /test HTTP/1.1\r\n\
|
||||||
content-type: text/plain\r\n\r\n",
|
content-type: text/plain\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_headers_bad_content_length() {
|
fn test_headers_content_length_err_1() {
|
||||||
// string CL
|
let mut buf = BytesMut::from(
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
content-length: line\r\n\r\n",
|
content-length: line\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
|
||||||
// negative CL
|
expect_parse_err!(&mut buf)
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
|
||||||
"GET /test HTTP/1.1\r\n\
|
|
||||||
content-length: -1\r\n\r\n",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn octal_ish_cl_parsed_as_decimal() {
|
fn test_headers_content_length_err_2() {
|
||||||
let mut buf = BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"POST /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
content-length: 011\r\n\r\n",
|
content-length: -1\r\n\r\n",
|
||||||
);
|
);
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
|
||||||
let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
expect_parse_err!(&mut buf);
|
||||||
assert!(matches!(
|
|
||||||
pl,
|
|
||||||
PayloadType::Payload(pl) if pl == PayloadDecoder::length(11)
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_header() {
|
fn test_invalid_header() {
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
test line\r\n\r\n",
|
test line\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_name() {
|
fn test_invalid_name() {
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
test[]: line\r\n\r\n",
|
test[]: line\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_bad_status_line() {
|
fn test_http_request_bad_status_line() {
|
||||||
expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n"));
|
let mut buf = BytesMut::from("getpath \r\n\r\n");
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1038,10 +913,11 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_utf8() {
|
fn test_http_request_parser_utf8() {
|
||||||
let req = parse_ready!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
x-test: тест\r\n\r\n",
|
x-test: тест\r\n\r\n",
|
||||||
));
|
);
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
req.headers().get("x-test").unwrap().as_bytes(),
|
req.headers().get("x-test").unwrap().as_bytes(),
|
||||||
@@ -1051,18 +927,24 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_two_slashes() {
|
fn test_http_request_parser_two_slashes() {
|
||||||
let req = parse_ready!(&mut BytesMut::from("GET //path HTTP/1.1\r\n\r\n"));
|
let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n");
|
||||||
|
let req = parse_ready!(&mut buf);
|
||||||
|
|
||||||
assert_eq!(req.path(), "//path");
|
assert_eq!(req.path(), "//path");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_bad_method() {
|
fn test_http_request_parser_bad_method() {
|
||||||
expect_parse_err!(&mut BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"));
|
let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n");
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_bad_version() {
|
fn test_http_request_parser_bad_version() {
|
||||||
expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n"));
|
let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n");
|
||||||
|
|
||||||
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@@ -1079,66 +961,29 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hrs_multiple_content_length() {
|
fn hrs_multiple_content_length() {
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET / HTTP/1.1\r\n\
|
"GET / HTTP/1.1\r\n\
|
||||||
Host: example.com\r\n\
|
Host: example.com\r\n\
|
||||||
Content-Length: 4\r\n\
|
Content-Length: 4\r\n\
|
||||||
Content-Length: 2\r\n\
|
Content-Length: 2\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
abcd",
|
abcd",
|
||||||
));
|
);
|
||||||
|
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
expect_parse_err!(&mut buf);
|
||||||
"GET / HTTP/1.1\r\n\
|
|
||||||
Host: example.com\r\n\
|
|
||||||
Content-Length: 0\r\n\
|
|
||||||
Content-Length: 2\r\n\
|
|
||||||
\r\n\
|
|
||||||
ab",
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hrs_content_length_plus() {
|
fn hrs_content_length_plus() {
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET / HTTP/1.1\r\n\
|
"GET / HTTP/1.1\r\n\
|
||||||
Host: example.com\r\n\
|
Host: example.com\r\n\
|
||||||
Content-Length: +3\r\n\
|
Content-Length: +3\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
000",
|
000",
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hrs_te_http10() {
|
|
||||||
// in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header
|
|
||||||
|
|
||||||
expect_parse_err!(&mut BytesMut::from(
|
|
||||||
"POST / HTTP/1.0\r\n\
|
|
||||||
Host: example.com\r\n\
|
|
||||||
Transfer-Encoding: chunked\r\n\
|
|
||||||
\r\n\
|
|
||||||
3\r\n\
|
|
||||||
aaa\r\n\
|
|
||||||
0\r\n\
|
|
||||||
",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hrs_cl_and_te_http10() {
|
|
||||||
// in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both
|
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
|
||||||
"GET / HTTP/1.0\r\n\
|
|
||||||
Host: example.com\r\n\
|
|
||||||
Content-Length: 3\r\n\
|
|
||||||
Transfer-Encoding: chunked\r\n\
|
|
||||||
\r\n\
|
|
||||||
000",
|
|
||||||
);
|
);
|
||||||
|
|
||||||
parse_ready!(&mut buf);
|
expect_parse_err!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@@ -8,23 +8,20 @@ use std::{
|
|||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_codec::{Framed, FramedParts};
|
use actix_codec::{AsyncRead, AsyncWrite, Decoder as _, Encoder as _, Framed, FramedParts};
|
||||||
use actix_rt::time::sleep_until;
|
use actix_rt::time::sleep_until;
|
||||||
use actix_service::Service;
|
use actix_service::Service;
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
|
||||||
use tokio_util::codec::{Decoder as _, Encoder as _};
|
|
||||||
use tracing::{error, trace};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BodySize, BoxBody, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
config::ServiceConfig,
|
config::ServiceConfig,
|
||||||
error::{DispatchError, ParseError, PayloadError},
|
error::{DispatchError, ParseError, PayloadError},
|
||||||
service::HttpFlow,
|
service::HttpFlow,
|
||||||
Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
ConnectionType, Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@@ -339,7 +336,7 @@ where
|
|||||||
while written < len {
|
while written < len {
|
||||||
match io.as_mut().poll_write(cx, &write_buf[written..])? {
|
match io.as_mut().poll_write(cx, &write_buf[written..])? {
|
||||||
Poll::Ready(0) => {
|
Poll::Ready(0) => {
|
||||||
error!("write zero; closing");
|
log::error!("write zero; closing");
|
||||||
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
|
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -378,6 +375,8 @@ where
|
|||||||
DispatchError::Io(err)
|
DispatchError::Io(err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
||||||
|
|
||||||
Ok(size)
|
Ok(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -460,12 +459,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// all messages are dealt with
|
// all messages are dealt with
|
||||||
None => {
|
None => return Ok(PollResponse::DoNothing),
|
||||||
// start keep-alive if last request allowed it
|
|
||||||
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
|
||||||
|
|
||||||
return Ok(PollResponse::DoNothing);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
StateProj::ServiceCall { fut } => {
|
StateProj::ServiceCall { fut } => {
|
||||||
@@ -571,7 +565,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateProj::ExpectCall { fut } => {
|
StateProj::ExpectCall { fut } => {
|
||||||
trace!(" calling expect service");
|
log::trace!(" calling expect service");
|
||||||
|
|
||||||
match fut.poll(cx) {
|
match fut.poll(cx) {
|
||||||
// expect resolved. write continue to buffer and set InnerDispatcher state
|
// expect resolved. write continue to buffer and set InnerDispatcher state
|
||||||
@@ -693,15 +687,76 @@ where
|
|||||||
let can_not_read = !self.can_read(cx);
|
let can_not_read = !self.can_read(cx);
|
||||||
|
|
||||||
// limit amount of non-processed requests
|
// limit amount of non-processed requests
|
||||||
if pipeline_queue_full || can_not_read {
|
if pipeline_queue_full {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut this = self.as_mut().project();
|
let mut this = self.as_mut().project();
|
||||||
|
|
||||||
|
if can_not_read {
|
||||||
|
log::debug!("cannot read request payload");
|
||||||
|
|
||||||
|
if let Some(sender) = &this.payload {
|
||||||
|
// ...maybe handler does not want to read any more payload...
|
||||||
|
if let PayloadStatus::Dropped = sender.need_read(cx) {
|
||||||
|
log::debug!("handler dropped payload early; attempt to clean connection");
|
||||||
|
// ...in which case poll request payload a few times
|
||||||
|
loop {
|
||||||
|
match this.codec.decode(this.read_buf)? {
|
||||||
|
Some(msg) => {
|
||||||
|
match msg {
|
||||||
|
// payload decoded did not yield EOF yet
|
||||||
|
Message::Chunk(Some(_)) => {
|
||||||
|
// if non-clean connection, next loop iter will detect empty
|
||||||
|
// read buffer and close connection
|
||||||
|
}
|
||||||
|
|
||||||
|
// connection is in clean state for next request
|
||||||
|
Message::Chunk(None) => {
|
||||||
|
log::debug!("connection successfully cleaned");
|
||||||
|
|
||||||
|
// reset dispatcher state
|
||||||
|
let _ = this.payload.take();
|
||||||
|
this.state.set(State::None);
|
||||||
|
|
||||||
|
// break out of payload decode loop
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Either whole payload is read and loop is broken or more data
|
||||||
|
// was expected in which case connection is closed. In both
|
||||||
|
// situations dispatcher cannot get here.
|
||||||
|
Message::Item(_) => {
|
||||||
|
unreachable!("dispatcher is in payload receive state")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// not enough info to decide if connection is going to be clean or not
|
||||||
|
None => {
|
||||||
|
log::error!(
|
||||||
|
"handler did not read whole payload and dispatcher could not \
|
||||||
|
drain read buf; return 500 and close connection"
|
||||||
|
);
|
||||||
|
|
||||||
|
this.flags.insert(Flags::SHUTDOWN);
|
||||||
|
let mut res = Response::internal_server_error().drop_body();
|
||||||
|
res.head_mut().set_connection_type(ConnectionType::Close);
|
||||||
|
this.messages.push_back(DispatcherMessage::Error(res));
|
||||||
|
*this.error = Some(DispatchError::HandlerDroppedPayload);
|
||||||
|
return Ok(true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// can_not_read and no request payload
|
||||||
|
return Ok(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
|
|
||||||
// decode from read buf as many full requests as possible
|
|
||||||
loop {
|
loop {
|
||||||
match this.codec.decode(this.read_buf) {
|
match this.codec.decode(this.read_buf) {
|
||||||
Ok(Some(msg)) => {
|
Ok(Some(msg)) => {
|
||||||
@@ -754,7 +809,7 @@ where
|
|||||||
if let Some(ref mut payload) = this.payload {
|
if let Some(ref mut payload) = this.payload {
|
||||||
payload.feed_data(chunk);
|
payload.feed_data(chunk);
|
||||||
} else {
|
} else {
|
||||||
error!("Internal server error: unexpected payload chunk");
|
log::error!("Internal server error: unexpected payload chunk");
|
||||||
this.flags.insert(Flags::READ_DISCONNECT);
|
this.flags.insert(Flags::READ_DISCONNECT);
|
||||||
this.messages.push_back(DispatcherMessage::Error(
|
this.messages.push_back(DispatcherMessage::Error(
|
||||||
Response::internal_server_error().drop_body(),
|
Response::internal_server_error().drop_body(),
|
||||||
@@ -768,7 +823,7 @@ where
|
|||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.feed_eof();
|
payload.feed_eof();
|
||||||
} else {
|
} else {
|
||||||
error!("Internal server error: unexpected eof");
|
log::error!("Internal server error: unexpected eof");
|
||||||
this.flags.insert(Flags::READ_DISCONNECT);
|
this.flags.insert(Flags::READ_DISCONNECT);
|
||||||
this.messages.push_back(DispatcherMessage::Error(
|
this.messages.push_back(DispatcherMessage::Error(
|
||||||
Response::internal_server_error().drop_body(),
|
Response::internal_server_error().drop_body(),
|
||||||
@@ -785,7 +840,7 @@ where
|
|||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
|
|
||||||
Err(ParseError::Io(err)) => {
|
Err(ParseError::Io(err)) => {
|
||||||
trace!("I/O error: {}", &err);
|
log::trace!("I/O error: {}", &err);
|
||||||
self.as_mut().client_disconnected();
|
self.as_mut().client_disconnected();
|
||||||
this = self.as_mut().project();
|
this = self.as_mut().project();
|
||||||
*this.error = Some(DispatchError::Io(err));
|
*this.error = Some(DispatchError::Io(err));
|
||||||
@@ -793,7 +848,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(ParseError::TooLarge) => {
|
Err(ParseError::TooLarge) => {
|
||||||
trace!("request head was too big; returning 431 response");
|
log::trace!("request head was too big; returning 431 response");
|
||||||
|
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.set_error(PayloadError::Overflow);
|
payload.set_error(PayloadError::Overflow);
|
||||||
@@ -813,7 +868,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("parse error {}", &err);
|
log::trace!("parse error {}", &err);
|
||||||
|
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.set_error(PayloadError::EncodingCorrupted);
|
payload.set_error(PayloadError::EncodingCorrupted);
|
||||||
@@ -844,7 +899,10 @@ where
|
|||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
// timeout on first request (slow request) return 408
|
// timeout on first request (slow request) return 408
|
||||||
|
|
||||||
trace!("timed out on slow request; replying with 408 and closing connection");
|
log::trace!(
|
||||||
|
"timed out on slow request; \
|
||||||
|
replying with 408 and closing connection"
|
||||||
|
);
|
||||||
|
|
||||||
let _ = self.as_mut().send_error_response(
|
let _ = self.as_mut().send_error_response(
|
||||||
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
|
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
|
||||||
@@ -887,7 +945,7 @@ where
|
|||||||
// keep-alive timer has timed out
|
// keep-alive timer has timed out
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
// no tasks at hand
|
// no tasks at hand
|
||||||
trace!("timer timed out; closing connection");
|
log::trace!("timer timed out; closing connection");
|
||||||
this.flags.insert(Flags::SHUTDOWN);
|
this.flags.insert(Flags::SHUTDOWN);
|
||||||
|
|
||||||
if let Some(deadline) = this.config.client_disconnect_deadline() {
|
if let Some(deadline) = this.config.client_disconnect_deadline() {
|
||||||
@@ -917,7 +975,7 @@ where
|
|||||||
|
|
||||||
// timed-out during shutdown; drop connection
|
// timed-out during shutdown; drop connection
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
trace!("timed-out during shutdown");
|
log::trace!("timed-out during shutdown");
|
||||||
return Err(DispatchError::DisconnectTimeout);
|
return Err(DispatchError::DisconnectTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -978,11 +1036,9 @@ where
|
|||||||
//
|
//
|
||||||
// A Request head too large to parse is only checked on `httparse::Status::Partial`.
|
// A Request head too large to parse is only checked on `httparse::Status::Partial`.
|
||||||
|
|
||||||
match this.payload {
|
if this.payload.is_none() {
|
||||||
// When dispatcher has a payload the responsibility of wake ups is shifted to
|
// When dispatcher has a payload the responsibility of wake up it would be shift
|
||||||
// `h1::payload::Payload` unless the payload is needing a read, in which case it
|
// to h1::payload::Payload.
|
||||||
// might not have access to the waker and could result in the dispatcher
|
|
||||||
// getting stuck until timeout.
|
|
||||||
//
|
//
|
||||||
// Reason:
|
// Reason:
|
||||||
// Self wake up when there is payload would waste poll and/or result in
|
// Self wake up when there is payload would waste poll and/or result in
|
||||||
@@ -993,8 +1049,7 @@ where
|
|||||||
// read anymore. At this case read_buf could always remain beyond
|
// read anymore. At this case read_buf could always remain beyond
|
||||||
// MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and
|
// MAX_BUFFER_SIZE and self wake up would be busy poll dispatcher and
|
||||||
// waste resources.
|
// waste resources.
|
||||||
Some(ref p) if p.need_read(cx) != PayloadStatus::Read => {}
|
cx.waker().wake_by_ref();
|
||||||
_ => cx.waker().wake_by_ref(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
@@ -1006,7 +1061,7 @@ where
|
|||||||
this.read_buf.reserve(HW_BUFFER_SIZE - remaining);
|
this.read_buf.reserve(HW_BUFFER_SIZE - remaining);
|
||||||
}
|
}
|
||||||
|
|
||||||
match tokio_util::io::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
match actix_codec::poll_read_buf(io.as_mut(), cx, this.read_buf) {
|
||||||
Poll::Ready(Ok(n)) => {
|
Poll::Ready(Ok(n)) => {
|
||||||
this.flags.remove(Flags::FINISHED);
|
this.flags.remove(Flags::FINISHED);
|
||||||
|
|
||||||
@@ -1079,12 +1134,12 @@ where
|
|||||||
|
|
||||||
match this.inner.project() {
|
match this.inner.project() {
|
||||||
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
||||||
error!("Upgrade handler error: {}", err);
|
log::error!("Upgrade handler error: {}", err);
|
||||||
DispatchError::Upgrade
|
DispatchError::Upgrade
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DispatcherStateProj::Normal { mut inner } => {
|
DispatcherStateProj::Normal { mut inner } => {
|
||||||
trace!("start flags: {:?}", &inner.flags);
|
log::trace!("start flags: {:?}", &inner.flags);
|
||||||
|
|
||||||
trace_timer_states(
|
trace_timer_states(
|
||||||
"start",
|
"start",
|
||||||
@@ -1191,7 +1246,7 @@ where
|
|||||||
|
|
||||||
// client is gone
|
// client is gone
|
||||||
if inner.flags.contains(Flags::WRITE_DISCONNECT) {
|
if inner.flags.contains(Flags::WRITE_DISCONNECT) {
|
||||||
trace!("client is gone; disconnecting");
|
log::trace!("client is gone; disconnecting");
|
||||||
return Poll::Ready(Ok(()));
|
return Poll::Ready(Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1200,14 +1255,14 @@ where
|
|||||||
|
|
||||||
// read half is closed; we do not process any responses
|
// read half is closed; we do not process any responses
|
||||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
||||||
trace!("read half closed; start shutdown");
|
log::trace!("read half closed; start shutdown");
|
||||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-alive and stream errors
|
// keep-alive and stream errors
|
||||||
if state_is_none && inner_p.write_buf.is_empty() {
|
if state_is_none && inner_p.write_buf.is_empty() {
|
||||||
if let Some(err) = inner_p.error.take() {
|
if let Some(err) = inner_p.error.take() {
|
||||||
error!("stream error: {}", &err);
|
log::error!("stream error: {}", &err);
|
||||||
return Poll::Ready(Err(err));
|
return Poll::Ready(Err(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1236,7 +1291,7 @@ where
|
|||||||
Poll::Pending
|
Poll::Pending
|
||||||
};
|
};
|
||||||
|
|
||||||
trace!("end flags: {:?}", &inner.flags);
|
log::trace!("end flags: {:?}", &inner.flags);
|
||||||
|
|
||||||
poll
|
poll
|
||||||
}
|
}
|
||||||
@@ -1251,17 +1306,17 @@ fn trace_timer_states(
|
|||||||
ka_timer: &TimerState,
|
ka_timer: &TimerState,
|
||||||
shutdown_timer: &TimerState,
|
shutdown_timer: &TimerState,
|
||||||
) {
|
) {
|
||||||
trace!("{} timers:", label);
|
log::trace!("{} timers:", label);
|
||||||
|
|
||||||
if head_timer.is_enabled() {
|
if head_timer.is_enabled() {
|
||||||
trace!(" head {}", &head_timer);
|
log::trace!(" head {}", &head_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ka_timer.is_enabled() {
|
if ka_timer.is_enabled() {
|
||||||
trace!(" keep-alive {}", &ka_timer);
|
log::trace!(" keep-alive {}", &ka_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if shutdown_timer.is_enabled() {
|
if shutdown_timer.is_enabled() {
|
||||||
trace!(" shutdown {}", &shutdown_timer);
|
log::trace!(" shutdown {}", &shutdown_timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -64,7 +64,7 @@ fn drop_payload_service(
|
|||||||
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
|
fn echo_payload_service() -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
|
||||||
fn_service(|mut req: Request| {
|
fn_service(|mut req: Request| {
|
||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::stream::StreamExt as _;
|
||||||
|
|
||||||
let mut pl = req.take_payload();
|
let mut pl = req.take_payload();
|
||||||
let mut body = BytesMut::new();
|
let mut body = BytesMut::new();
|
||||||
@@ -637,7 +637,7 @@ async fn expect_handling() {
|
|||||||
|
|
||||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
let io = inner.io.as_ref().unwrap();
|
let io = inner.io.as_ref().unwrap();
|
||||||
let mut res = io.write_buf()[..].to_owned();
|
let mut res = (&io.write_buf()[..]).to_owned();
|
||||||
stabilize_date_header(&mut res);
|
stabilize_date_header(&mut res);
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
@@ -699,7 +699,7 @@ async fn expect_eager() {
|
|||||||
|
|
||||||
if let DispatcherState::Normal { ref inner } = h1.inner {
|
if let DispatcherState::Normal { ref inner } = h1.inner {
|
||||||
let io = inner.io.as_ref().unwrap();
|
let io = inner.io.as_ref().unwrap();
|
||||||
let mut res = io.write_buf()[..].to_owned();
|
let mut res = (&io.write_buf()[..]).to_owned();
|
||||||
stabilize_date_header(&mut res);
|
stabilize_date_header(&mut res);
|
||||||
|
|
||||||
// Despite the content-length header and even though the request payload has not
|
// Despite the content-length header and even though the request payload has not
|
||||||
@@ -783,9 +783,6 @@ async fn upgrade_handling() {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
// fix in #2624 reverted temporarily
|
|
||||||
// complete fix tracked in #2745
|
|
||||||
#[ignore]
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn handler_drop_payload() {
|
async fn handler_drop_payload() {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
@@ -450,7 +450,7 @@ impl TransferEncoding {
|
|||||||
|
|
||||||
buf.extend_from_slice(&msg[..len as usize]);
|
buf.extend_from_slice(&msg[..len as usize]);
|
||||||
|
|
||||||
*remaining -= len;
|
*remaining -= len as u64;
|
||||||
Ok(*remaining == 0)
|
Ok(*remaining == 0)
|
||||||
} else {
|
} else {
|
||||||
Ok(true)
|
Ok(true)
|
||||||
@@ -517,7 +517,6 @@ unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
|||||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||||
buffer[index] = c & 0b1101_1111;
|
buffer[index] = c & 0b1101_1111;
|
||||||
}
|
}
|
||||||
index += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
@@ -529,7 +528,7 @@ mod tests {
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS};
|
use http::header::AUTHORIZATION;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -560,9 +559,6 @@ mod tests {
|
|||||||
head.headers
|
head.headers
|
||||||
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
||||||
|
|
||||||
head.headers
|
|
||||||
.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
|
|
||||||
|
|
||||||
let mut head = RequestHeadType::Owned(head);
|
let mut head = RequestHeadType::Owned(head);
|
||||||
|
|
||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
@@ -578,7 +574,6 @@ mod tests {
|
|||||||
assert!(data.contains("Connection: close\r\n"));
|
assert!(data.contains("Connection: close\r\n"));
|
||||||
assert!(data.contains("Content-Type: plain/text\r\n"));
|
assert!(data.contains("Content-Type: plain/text\r\n"));
|
||||||
assert!(data.contains("Date: date\r\n"));
|
assert!(data.contains("Date: date\r\n"));
|
||||||
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
|
|
||||||
|
|
||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
|
@@ -16,7 +16,7 @@ use crate::error::PayloadError;
|
|||||||
/// max buffer size 32k
|
/// max buffer size 32k
|
||||||
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum PayloadStatus {
|
pub enum PayloadStatus {
|
||||||
Read,
|
Read,
|
||||||
Pause,
|
Pause,
|
||||||
@@ -252,15 +252,18 @@ impl Inner {
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
use actix_utils::future::poll_fn;
|
use actix_utils::future::poll_fn;
|
||||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
assert_impl_all!(Payload: Unpin);
|
assert_impl_all!(Payload: Unpin);
|
||||||
assert_not_impl_any!(Payload: Send, Sync);
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
assert_impl_all!(Inner: Unpin, Send, Sync);
|
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||||
|
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
|
@@ -13,7 +13,6 @@ use actix_service::{
|
|||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
@@ -134,7 +133,6 @@ mod openssl {
|
|||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create OpenSSL based service.
|
/// Create OpenSSL based service.
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -197,7 +195,6 @@ mod rustls {
|
|||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create Rustls based service.
|
/// Create Rustls based service.
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
self,
|
||||||
config: ServerConfig,
|
config: ServerConfig,
|
||||||
@@ -308,13 +305,13 @@ where
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let expect = expect
|
let expect = expect
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade
|
let upgrade = upgrade
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@@ -322,7 +319,7 @@ where
|
|||||||
|
|
||||||
let service = service
|
let service = service
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
Ok(H1ServiceHandler::new(
|
Ok(H1ServiceHandler::new(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -360,7 +357,7 @@ where
|
|||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|err| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
error!("HTTP/1 service readiness error: {:?}", err);
|
log::error!("HTTP/1 service readiness error: {:?}", err);
|
||||||
DispatchError::Service(err)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
use std::{fmt, future::Future, pin::Pin, task::Context};
|
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||||
|
|
||||||
use actix_rt::time::{Instant, Sleep};
|
use actix_rt::time::{Instant, Sleep};
|
||||||
use tracing::trace;
|
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) enum TimerState {
|
pub(super) enum TimerState {
|
||||||
@@ -25,7 +24,7 @@ impl TimerState {
|
|||||||
|
|
||||||
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
||||||
if matches!(self, Self::Disabled) {
|
if matches!(self, Self::Disabled) {
|
||||||
trace!("setting disabled timer from line {}", line);
|
log::trace!("setting disabled timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = Self::Active {
|
*self = Self::Active {
|
||||||
@@ -40,11 +39,11 @@ impl TimerState {
|
|||||||
|
|
||||||
pub(super) fn clear(&mut self, line: u32) {
|
pub(super) fn clear(&mut self, line: u32) {
|
||||||
if matches!(self, Self::Disabled) {
|
if matches!(self, Self::Disabled) {
|
||||||
trace!("trying to clear a disabled timer from line {}", line);
|
log::trace!("trying to clear a disabled timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(self, Self::Inactive) {
|
if matches!(self, Self::Inactive) {
|
||||||
trace!("trying to clear an inactive timer from line {}", line);
|
log::trace!("trying to clear an inactive timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = Self::Inactive;
|
*self = Self::Inactive;
|
||||||
|
@@ -19,8 +19,8 @@ use h2::{
|
|||||||
server::{Connection, SendResponse},
|
server::{Connection, SendResponse},
|
||||||
Ping, PingPong,
|
Ping, PingPong,
|
||||||
};
|
};
|
||||||
|
use log::{error, trace};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use tracing::{error, trace, warn};
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BodySize, BoxBody, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
@@ -29,7 +29,7 @@ use crate::{
|
|||||||
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
HeaderName, HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, UPGRADE,
|
||||||
},
|
},
|
||||||
service::HttpFlow,
|
service::HttpFlow,
|
||||||
Extensions, Method, OnConnectData, Payload, Request, Response, ResponseHead,
|
Extensions, OnConnectData, Payload, Request, Response, ResponseHead,
|
||||||
};
|
};
|
||||||
|
|
||||||
const CHUNK_SIZE: usize = 16_384;
|
const CHUNK_SIZE: usize = 16_384;
|
||||||
@@ -67,7 +67,7 @@ where
|
|||||||
timer
|
timer
|
||||||
})
|
})
|
||||||
.unwrap_or_else(|| Box::pin(sleep(dur))),
|
.unwrap_or_else(|| Box::pin(sleep(dur))),
|
||||||
in_flight: false,
|
on_flight: false,
|
||||||
ping_pong: conn.ping_pong().unwrap(),
|
ping_pong: conn.ping_pong().unwrap(),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -84,14 +84,9 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
struct H2PingPong {
|
struct H2PingPong {
|
||||||
/// Handle to send ping frames from the peer.
|
|
||||||
ping_pong: PingPong,
|
|
||||||
|
|
||||||
/// True when a ping has been sent and is waiting for a reply.
|
|
||||||
in_flight: bool,
|
|
||||||
|
|
||||||
/// Timeout for pong response.
|
|
||||||
timer: Pin<Box<Sleep>>,
|
timer: Pin<Box<Sleep>>,
|
||||||
|
on_flight: bool,
|
||||||
|
ping_pong: PingPong,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
||||||
@@ -118,7 +113,6 @@ where
|
|||||||
let payload = crate::h2::Payload::new(body);
|
let payload = crate::h2::Payload::new(body);
|
||||||
let pl = Payload::H2 { payload };
|
let pl = Payload::H2 { payload };
|
||||||
let mut req = Request::with_payload(pl);
|
let mut req = Request::with_payload(pl);
|
||||||
let head_req = parts.method == Method::HEAD;
|
|
||||||
|
|
||||||
let head = req.head_mut();
|
let head = req.head_mut();
|
||||||
head.uri = parts.uri;
|
head.uri = parts.uri;
|
||||||
@@ -136,10 +130,10 @@ where
|
|||||||
actix_rt::spawn(async move {
|
actix_rt::spawn(async move {
|
||||||
// resolve service call and send response.
|
// resolve service call and send response.
|
||||||
let res = match fut.await {
|
let res = match fut.await {
|
||||||
Ok(res) => handle_response(res.into(), tx, config, head_req).await,
|
Ok(res) => handle_response(res.into(), tx, config).await,
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
let res: Response<BoxBody> = err.into();
|
let res: Response<BoxBody> = err.into();
|
||||||
handle_response(res, tx, config, head_req).await
|
handle_response(res, tx, config).await
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -149,7 +143,7 @@ where
|
|||||||
DispatchError::SendResponse(err) => {
|
DispatchError::SendResponse(err) => {
|
||||||
trace!("Error sending HTTP/2 response: {:?}", err)
|
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||||
}
|
}
|
||||||
DispatchError::SendData(err) => warn!("{:?}", err),
|
DispatchError::SendData(err) => log::warn!("{:?}", err),
|
||||||
DispatchError::ResponseBody(err) => {
|
DispatchError::ResponseBody(err) => {
|
||||||
error!("Response payload stream error: {:?}", err)
|
error!("Response payload stream error: {:?}", err)
|
||||||
}
|
}
|
||||||
@@ -158,28 +152,26 @@ where
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
Poll::Ready(None) => return Poll::Ready(Ok(())),
|
Poll::Ready(None) => return Poll::Ready(Ok(())),
|
||||||
|
|
||||||
Poll::Pending => match this.ping_pong.as_mut() {
|
Poll::Pending => match this.ping_pong.as_mut() {
|
||||||
Some(ping_pong) => loop {
|
Some(ping_pong) => loop {
|
||||||
if ping_pong.in_flight {
|
if ping_pong.on_flight {
|
||||||
// When there is an in-flight ping-pong, poll pong and and keep-alive
|
// When have on flight ping pong. poll pong and and keep alive timer.
|
||||||
// timer. On successful pong received, update keep-alive timer to
|
// on success pong received update keep alive timer to determine the next timing of
|
||||||
// determine the next timing of ping pong.
|
// ping pong.
|
||||||
match ping_pong.ping_pong.poll_pong(cx)? {
|
match ping_pong.ping_pong.poll_pong(cx)? {
|
||||||
Poll::Ready(_) => {
|
Poll::Ready(_) => {
|
||||||
ping_pong.in_flight = false;
|
ping_pong.on_flight = false;
|
||||||
|
|
||||||
let dead_line = this.config.keep_alive_deadline().unwrap();
|
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||||
ping_pong.timer.as_mut().reset(dead_line.into());
|
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||||
}
|
}
|
||||||
Poll::Pending => {
|
Poll::Pending => {
|
||||||
return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(()));
|
return ping_pong.timer.as_mut().poll(cx).map(|_| Ok(()))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// When there is no in-flight ping-pong, keep-alive timer is used to
|
// When there is no on flight ping pong. keep alive timer is used to wait for next
|
||||||
// wait for next timing of ping-pong. Therefore, at this point it serves
|
// timing of ping pong. Therefore at this point it serves as an interval instead.
|
||||||
// as an interval instead.
|
|
||||||
ready!(ping_pong.timer.as_mut().poll(cx));
|
ready!(ping_pong.timer.as_mut().poll(cx));
|
||||||
|
|
||||||
ping_pong.ping_pong.send_ping(Ping::opaque())?;
|
ping_pong.ping_pong.send_ping(Ping::opaque())?;
|
||||||
@@ -187,7 +179,7 @@ where
|
|||||||
let dead_line = this.config.keep_alive_deadline().unwrap();
|
let dead_line = this.config.keep_alive_deadline().unwrap();
|
||||||
ping_pong.timer.as_mut().reset(dead_line.into());
|
ping_pong.timer.as_mut().reset(dead_line.into());
|
||||||
|
|
||||||
ping_pong.in_flight = true;
|
ping_pong.on_flight = true;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
None => return Poll::Pending,
|
None => return Poll::Pending,
|
||||||
@@ -207,7 +199,6 @@ async fn handle_response<B>(
|
|||||||
res: Response<B>,
|
res: Response<B>,
|
||||||
mut tx: SendResponse<Bytes>,
|
mut tx: SendResponse<Bytes>,
|
||||||
config: ServiceConfig,
|
config: ServiceConfig,
|
||||||
head_req: bool,
|
|
||||||
) -> Result<(), DispatchError>
|
) -> Result<(), DispatchError>
|
||||||
where
|
where
|
||||||
B: MessageBody,
|
B: MessageBody,
|
||||||
@@ -217,14 +208,14 @@ where
|
|||||||
// prepare response.
|
// prepare response.
|
||||||
let mut size = body.size();
|
let mut size = body.size();
|
||||||
let res = prepare_response(config, res.head(), &mut size);
|
let res = prepare_response(config, res.head(), &mut size);
|
||||||
let eof_or_head = size.is_eof() || head_req;
|
let eof = size.is_eof();
|
||||||
|
|
||||||
// send response head and return on eof.
|
// send response head and return on eof.
|
||||||
let mut stream = tx
|
let mut stream = tx
|
||||||
.send_response(res, eof_or_head)
|
.send_response(res, eof)
|
||||||
.map_err(DispatchError::SendResponse)?;
|
.map_err(DispatchError::SendResponse)?;
|
||||||
|
|
||||||
if eof_or_head {
|
if eof {
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,13 +287,13 @@ fn prepare_response(
|
|||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
match size {
|
let _ = match size {
|
||||||
BodySize::None | BodySize::Stream => {}
|
BodySize::None | BodySize::Stream => None,
|
||||||
|
|
||||||
BodySize::Sized(0) => {
|
BodySize::Sized(0) => {
|
||||||
#[allow(clippy::declare_interior_mutable_const)]
|
#[allow(clippy::declare_interior_mutable_const)]
|
||||||
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
const HV_ZERO: HeaderValue = HeaderValue::from_static("0");
|
||||||
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO);
|
res.headers_mut().insert(CONTENT_LENGTH, HV_ZERO)
|
||||||
}
|
}
|
||||||
|
|
||||||
BodySize::Sized(len) => {
|
BodySize::Sized(len) => {
|
||||||
@@ -311,7 +302,7 @@ fn prepare_response(
|
|||||||
res.headers_mut().insert(
|
res.headers_mut().insert(
|
||||||
CONTENT_LENGTH,
|
CONTENT_LENGTH,
|
||||||
HeaderValue::from_str(buf.format(*len)).unwrap(),
|
HeaderValue::from_str(buf.format(*len)).unwrap(),
|
||||||
);
|
)
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@@ -103,9 +103,11 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
use static_assertions::assert_impl_all;
|
use static_assertions::assert_impl_all;
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
assert_impl_all!(Payload: Unpin, Send, Sync);
|
assert_impl_all!(Payload: Unpin, Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
}
|
}
|
||||||
|
@@ -14,7 +14,7 @@ use actix_service::{
|
|||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use tracing::{error, trace};
|
use log::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
@@ -117,7 +117,6 @@ mod openssl {
|
|||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
/// Create OpenSSL based service.
|
/// Create OpenSSL based service.
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -165,7 +164,6 @@ mod rustls {
|
|||||||
B: MessageBody + 'static,
|
B: MessageBody + 'static,
|
||||||
{
|
{
|
||||||
/// Create Rustls based service.
|
/// Create Rustls based service.
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
self,
|
||||||
mut config: ServerConfig,
|
mut config: ServerConfig,
|
||||||
@@ -357,7 +355,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
trace!("H2 handshake error: {}", err);
|
log::trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@@ -1,51 +0,0 @@
|
|||||||
//! Common header names not defined in [`http`].
|
|
||||||
//!
|
|
||||||
//! Any headers added to this file will need to be re-exported from the list at `crate::headers`.
|
|
||||||
|
|
||||||
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.
|
|
||||||
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.
|
|
||||||
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]).
|
|
||||||
///
|
|
||||||
/// [CORP]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Cross-Origin_Resource_Policy_(CORP)
|
|
||||||
/// [CORS]: https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS
|
|
||||||
pub const CROSS_ORIGIN_EMBEDDER_POLICY: HeaderName =
|
|
||||||
HeaderName::from_static("cross-origin-embedder-policy");
|
|
||||||
|
|
||||||
/// Response header that allows you to ensure a top-level document does not share a browsing context
|
|
||||||
/// group with cross-origin documents.
|
|
||||||
pub const CROSS_ORIGIN_OPENER_POLICY: HeaderName =
|
|
||||||
HeaderName::from_static("cross-origin-opener-policy");
|
|
||||||
|
|
||||||
/// Response header that conveys a desire that the browser blocks no-cors cross-origin/cross-site
|
|
||||||
/// requests to the given resource.
|
|
||||||
pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
|
|
||||||
HeaderName::from_static("cross-origin-resource-policy");
|
|
||||||
|
|
||||||
/// Response header that provides a mechanism to allow and deny the use of browser features in a
|
|
||||||
/// document or within any `<iframe>` elements in the document.
|
|
||||||
pub const PERMISSIONS_POLICY: HeaderName = HeaderName::from_static("permissions-policy");
|
|
||||||
|
|
||||||
/// Request header (de-facto standard) for identifying the originating IP address of a client
|
|
||||||
/// connecting to a web server through a proxy server.
|
|
||||||
pub const X_FORWARDED_FOR: HeaderName = HeaderName::from_static("x-forwarded-for");
|
|
||||||
|
|
||||||
/// Request header (de-facto standard) for identifying the original host requested by the client in
|
|
||||||
/// the `Host` HTTP request header.
|
|
||||||
pub const X_FORWARDED_HOST: HeaderName = HeaderName::from_static("x-forwarded-host");
|
|
||||||
|
|
||||||
/// Request header (de-facto standard) for identifying the protocol that a client used to connect to
|
|
||||||
/// your proxy or load balancer.
|
|
||||||
pub const X_FORWARDED_PROTO: HeaderName = HeaderName::from_static("x-forwarded-proto");
|
|
@@ -150,7 +150,9 @@ impl HeaderMap {
|
|||||||
/// assert_eq!(map.len(), 3);
|
/// assert_eq!(map.len(), 3);
|
||||||
/// ```
|
/// ```
|
||||||
pub fn len(&self) -> usize {
|
pub fn len(&self) -> usize {
|
||||||
self.inner.values().map(|vals| vals.len()).sum()
|
self.inner
|
||||||
|
.iter()
|
||||||
|
.fold(0, |acc, (_, values)| acc + values.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of _keys_ stored in the map.
|
/// Returns the number of _keys_ stored in the map.
|
||||||
@@ -307,7 +309,7 @@ impl HeaderMap {
|
|||||||
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
pub fn get_all(&self, key: impl AsHeaderName) -> std::slice::Iter<'_, HeaderValue> {
|
||||||
match self.get_value(key) {
|
match self.get_value(key) {
|
||||||
Some(value) => value.iter(),
|
Some(value) => value.iter(),
|
||||||
None => [].iter(),
|
None => (&[]).iter(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -550,39 +552,6 @@ impl HeaderMap {
|
|||||||
Keys(self.inner.keys())
|
Keys(self.inner.keys())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retains only the headers specified by the predicate.
|
|
||||||
///
|
|
||||||
/// In other words, removes all headers `(name, val)` for which `retain_fn(&name, &mut val)`
|
|
||||||
/// returns false.
|
|
||||||
///
|
|
||||||
/// The order in which headers are visited should be considered arbitrary.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
|
|
||||||
/// let mut map = HeaderMap::new();
|
|
||||||
///
|
|
||||||
/// map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
|
||||||
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
|
|
||||||
/// map.append(header::SET_COOKIE, HeaderValue::from_static("two=2"));
|
|
||||||
///
|
|
||||||
/// map.retain(|name, val| val.as_bytes().starts_with(b"one"));
|
|
||||||
///
|
|
||||||
/// assert_eq!(map.len(), 1);
|
|
||||||
/// assert!(map.contains_key(&header::SET_COOKIE));
|
|
||||||
/// ```
|
|
||||||
pub fn retain<F>(&mut self, mut retain_fn: F)
|
|
||||||
where
|
|
||||||
F: FnMut(&HeaderName, &mut HeaderValue) -> bool,
|
|
||||||
{
|
|
||||||
self.inner.retain(|name, vals| {
|
|
||||||
vals.inner.retain(|val| retain_fn(name, val));
|
|
||||||
|
|
||||||
// invariant: make sure newly empty value lists are removed
|
|
||||||
!vals.is_empty()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Clears the map, returning all name-value sets as an iterator.
|
/// Clears the map, returning all name-value sets as an iterator.
|
||||||
///
|
///
|
||||||
/// Header names will only be yielded for the first value in each set. All items that are
|
/// Header names will only be yielded for the first value in each set. All items that are
|
||||||
@@ -974,55 +943,6 @@ mod tests {
|
|||||||
assert!(map.is_empty());
|
assert!(map.is_empty());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn retain() {
|
|
||||||
let mut map = HeaderMap::new();
|
|
||||||
|
|
||||||
map.append(header::LOCATION, HeaderValue::from_static("/test"));
|
|
||||||
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
|
||||||
map.append(header::COOKIE, HeaderValue::from_static("one=1"));
|
|
||||||
map.append(header::COOKIE, HeaderValue::from_static("two=2"));
|
|
||||||
|
|
||||||
assert_eq!(map.len(), 4);
|
|
||||||
|
|
||||||
// by value
|
|
||||||
map.retain(|_, val| !val.as_bytes().contains(&b'/'));
|
|
||||||
assert_eq!(map.len(), 3);
|
|
||||||
|
|
||||||
// by name
|
|
||||||
map.retain(|name, _| name.as_str() != "cookie");
|
|
||||||
assert_eq!(map.len(), 1);
|
|
||||||
|
|
||||||
// keep but mutate value
|
|
||||||
map.retain(|_, val| {
|
|
||||||
*val = HeaderValue::from_static("replaced");
|
|
||||||
true
|
|
||||||
});
|
|
||||||
assert_eq!(map.len(), 1);
|
|
||||||
assert_eq!(map.get("host").unwrap(), "replaced");
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn retain_removes_empty_value_lists() {
|
|
||||||
let mut map = HeaderMap::with_capacity(3);
|
|
||||||
|
|
||||||
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
|
||||||
map.append(header::HOST, HeaderValue::from_static("duck.com"));
|
|
||||||
|
|
||||||
assert_eq!(map.len(), 2);
|
|
||||||
assert_eq!(map.len_keys(), 1);
|
|
||||||
assert_eq!(map.inner.len(), 1);
|
|
||||||
assert_eq!(map.capacity(), 3);
|
|
||||||
|
|
||||||
// remove everything
|
|
||||||
map.retain(|_n, _v| false);
|
|
||||||
|
|
||||||
assert_eq!(map.len(), 0);
|
|
||||||
assert_eq!(map.len_keys(), 0);
|
|
||||||
assert_eq!(map.inner.len(), 0);
|
|
||||||
assert_eq!(map.capacity(), 3);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn entries_into_iter() {
|
fn entries_into_iter() {
|
||||||
let mut map = HeaderMap::new();
|
let mut map = HeaderMap::new();
|
||||||
|
@@ -1,18 +1,14 @@
|
|||||||
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
|
//! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
|
||||||
|
|
||||||
// declaring new header consts will yield this error
|
|
||||||
#![allow(clippy::declare_interior_mutable_const)]
|
|
||||||
|
|
||||||
use percent_encoding::{AsciiSet, CONTROLS};
|
use percent_encoding::{AsciiSet, CONTROLS};
|
||||||
|
|
||||||
// re-export from http except header map related items
|
// re-export from http except header map related items
|
||||||
pub use ::http::header::{
|
pub use http::header::{
|
||||||
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
|
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
|
||||||
};
|
};
|
||||||
|
|
||||||
// re-export const header names, list is explicit so that any updates to `common` module do not
|
// re-export const header names
|
||||||
// conflict with this set
|
pub use http::header::{
|
||||||
pub use ::http::header::{
|
|
||||||
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
|
||||||
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
|
||||||
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
|
||||||
@@ -34,30 +30,22 @@ pub use ::http::header::{
|
|||||||
use crate::{error::ParseError, HttpMessage};
|
use crate::{error::ParseError, HttpMessage};
|
||||||
|
|
||||||
mod as_name;
|
mod as_name;
|
||||||
mod common;
|
|
||||||
mod into_pair;
|
mod into_pair;
|
||||||
mod into_value;
|
mod into_value;
|
||||||
pub mod map;
|
pub mod map;
|
||||||
mod shared;
|
mod shared;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::as_name::AsHeaderName;
|
||||||
as_name::AsHeaderName,
|
pub use self::into_pair::TryIntoHeaderPair;
|
||||||
into_pair::TryIntoHeaderPair,
|
pub use self::into_value::TryIntoHeaderValue;
|
||||||
into_value::TryIntoHeaderValue,
|
pub use self::map::HeaderMap;
|
||||||
map::HeaderMap,
|
pub use self::shared::{
|
||||||
shared::{
|
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate, LanguageTag,
|
||||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
|
Quality, QualityItem,
|
||||||
LanguageTag, Quality, QualityItem,
|
|
||||||
},
|
|
||||||
utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
|
|
||||||
};
|
};
|
||||||
|
pub use self::utils::{
|
||||||
// re-export list is explicit so that any updates to `http` do not conflict with this set
|
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
||||||
pub use self::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,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/// An interface for types that already represent a valid header.
|
/// An interface for types that already represent a valid header.
|
||||||
|
@@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE};
|
|||||||
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||||
///
|
///
|
||||||
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
||||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
#[derive(Clone, Debug, PartialEq)]
|
||||||
pub struct ExtendedValue {
|
pub struct ExtendedValue {
|
||||||
/// The character set that is used to encode the `value` to a string.
|
/// The character set that is used to encode the `value` to a string.
|
||||||
pub charset: Charset,
|
pub charset: Charset,
|
||||||
|
@@ -147,7 +147,7 @@ mod tests {
|
|||||||
|
|
||||||
// copy of encoding from actix-web headers
|
// copy of encoding from actix-web headers
|
||||||
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Clone, PartialEq, Debug)]
|
||||||
pub enum Encoding {
|
pub enum Encoding {
|
||||||
Chunked,
|
Chunked,
|
||||||
Brotli,
|
Brotli,
|
||||||
|
@@ -21,12 +21,10 @@
|
|||||||
#![allow(
|
#![allow(
|
||||||
clippy::type_complexity,
|
clippy::type_complexity,
|
||||||
clippy::too_many_arguments,
|
clippy::too_many_arguments,
|
||||||
clippy::borrow_interior_mutable_const,
|
clippy::borrow_interior_mutable_const
|
||||||
clippy::uninlined_format_args
|
|
||||||
)]
|
)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
#![cfg_attr(docsrs, feature(doc_cfg))]
|
|
||||||
|
|
||||||
pub use ::http::{uri, uri::Uri};
|
pub use ::http::{uri, uri::Uri};
|
||||||
pub use ::http::{Method, StatusCode, Version};
|
pub use ::http::{Method, StatusCode, Version};
|
||||||
@@ -41,7 +39,6 @@ pub mod error;
|
|||||||
mod extensions;
|
mod extensions;
|
||||||
pub mod h1;
|
pub mod h1;
|
||||||
#[cfg(feature = "http2")]
|
#[cfg(feature = "http2")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
|
|
||||||
pub mod h2;
|
pub mod h2;
|
||||||
pub mod header;
|
pub mod header;
|
||||||
mod helpers;
|
mod helpers;
|
||||||
@@ -56,7 +53,6 @@ mod responses;
|
|||||||
mod service;
|
mod service;
|
||||||
pub mod test;
|
pub mod test;
|
||||||
#[cfg(feature = "ws")]
|
#[cfg(feature = "ws")]
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "ws")))]
|
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
|
||||||
pub use self::builder::HttpServiceBuilder;
|
pub use self::builder::HttpServiceBuilder;
|
||||||
@@ -73,9 +69,6 @@ pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
|
|||||||
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||||
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||||
pub use self::service::HttpService;
|
pub use self::service::HttpService;
|
||||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
|
||||||
pub use self::service::TlsAcceptorConfig;
|
|
||||||
|
|
||||||
/// A major HTTP protocol version.
|
/// A major HTTP protocol version.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
@@ -3,7 +3,7 @@ use std::{cell::RefCell, ops, rc::Rc};
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
/// Represents various types of connection
|
/// Represents various types of connection
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||||
pub enum ConnectionType {
|
pub enum ConnectionType {
|
||||||
/// Close connection after response.
|
/// Close connection after response.
|
||||||
Close,
|
Close,
|
||||||
|
@@ -13,8 +13,7 @@ use crate::error::PayloadError;
|
|||||||
/// A boxed payload stream.
|
/// A boxed payload stream.
|
||||||
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||||
#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
|
||||||
pub type PayloadStream = BoxedPayloadStream;
|
pub type PayloadStream = BoxedPayloadStream;
|
||||||
|
|
||||||
#[cfg(not(feature = "http2"))]
|
#[cfg(not(feature = "http2"))]
|
||||||
@@ -97,10 +96,12 @@ where
|
|||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
|
use std::panic::{RefUnwindSafe, UnwindSafe};
|
||||||
|
|
||||||
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
use static_assertions::{assert_impl_all, assert_not_impl_any};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
assert_impl_all!(Payload: Unpin);
|
assert_impl_all!(Payload: Unpin);
|
||||||
assert_not_impl_any!(Payload: Send, Sync);
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
}
|
}
|
||||||
|
@@ -113,14 +113,14 @@ impl<P> Request<P> {
|
|||||||
#[inline]
|
#[inline]
|
||||||
/// Http message part of the request
|
/// Http message part of the request
|
||||||
pub fn head(&self) -> &RequestHead {
|
pub fn head(&self) -> &RequestHead {
|
||||||
&self.head
|
&*self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
#[inline]
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
/// Mutable reference to a HTTP message part of the request
|
/// Mutable reference to a HTTP message part of the request
|
||||||
pub fn head_mut(&mut self) -> &mut RequestHead {
|
pub fn head_mut(&mut self) -> &mut RequestHead {
|
||||||
&mut self.head
|
&mut *self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Mutable reference to the message's headers.
|
/// Mutable reference to the message's headers.
|
||||||
|
@@ -144,7 +144,7 @@ impl ResponseBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set connection type to `Upgrade`.
|
/// Set connection type to Upgrade
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
||||||
where
|
where
|
||||||
@@ -161,7 +161,7 @@ impl ResponseBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force-close connection, even if it is marked as keep-alive.
|
/// Force close connection, even if it is marked as keep-alive
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn force_close(&mut self) -> &mut Self {
|
pub fn force_close(&mut self) -> &mut Self {
|
||||||
if let Some(parts) = self.inner() {
|
if let Some(parts) = self.inner() {
|
||||||
|
@@ -237,7 +237,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||||
stream
|
let _ = stream
|
||||||
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut data = vec![];
|
let mut data = vec![];
|
||||||
@@ -251,7 +251,7 @@ mod tests {
|
|||||||
assert!(memmem::find(&data, b"content-length").is_none());
|
assert!(memmem::find(&data, b"content-length").is_none());
|
||||||
|
|
||||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||||
stream
|
let _ = stream
|
||||||
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut data = vec![];
|
let mut data = vec![];
|
||||||
|
@@ -83,13 +83,13 @@ impl<B> Response<B> {
|
|||||||
/// Returns a reference to the head of this response.
|
/// Returns a reference to the head of this response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn head(&self) -> &ResponseHead {
|
pub fn head(&self) -> &ResponseHead {
|
||||||
&self.head
|
&*self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a mutable reference to the head of this response.
|
/// Returns a mutable reference to the head of this response.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn head_mut(&mut self) -> &mut ResponseHead {
|
pub fn head_mut(&mut self) -> &mut ResponseHead {
|
||||||
&mut self.head
|
&mut *self.head
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the status code of this response.
|
/// Returns the status code of this response.
|
||||||
|
@@ -15,7 +15,6 @@ use actix_service::{
|
|||||||
};
|
};
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
@@ -24,39 +23,7 @@ use crate::{
|
|||||||
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
h1, ConnectCallback, OnConnectData, Protocol, Request, Response, ServiceConfig,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// A [`ServiceFactory`] for HTTP/1.1 and HTTP/2 connections.
|
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
|
||||||
///
|
|
||||||
/// Use [`build`](Self::build) to begin constructing service. Also see [`HttpServiceBuilder`].
|
|
||||||
///
|
|
||||||
/// # 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.
|
|
||||||
/// - 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.
|
|
||||||
///
|
|
||||||
/// # Examples
|
|
||||||
/// ```
|
|
||||||
/// # use std::convert::Infallible;
|
|
||||||
/// use actix_http::{HttpService, Request, Response, StatusCode};
|
|
||||||
///
|
|
||||||
/// // this service would constructed in an actix_server::Server
|
|
||||||
///
|
|
||||||
/// # actix_rt::System::new().block_on(async {
|
|
||||||
/// HttpService::build()
|
|
||||||
/// // the builder finalizing method, other finalizers would not return an `HttpService`
|
|
||||||
/// .finish(|_req: Request| async move {
|
|
||||||
/// Ok::<_, Infallible>(
|
|
||||||
/// Response::build(StatusCode::OK).body("Hello!")
|
|
||||||
/// )
|
|
||||||
/// })
|
|
||||||
/// // the service finalizing method method
|
|
||||||
/// // you can use `.tcp_auto_h2c()`, `.rustls()`, or `.openssl()` instead of `.tcp()`
|
|
||||||
/// .tcp();
|
|
||||||
/// # })
|
|
||||||
/// ```
|
|
||||||
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
||||||
srv: S,
|
srv: S,
|
||||||
cfg: ServiceConfig,
|
cfg: ServiceConfig,
|
||||||
@@ -195,9 +162,7 @@ where
|
|||||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Creates TCP stream service from HTTP service.
|
/// Create simple tcp stream service
|
||||||
///
|
|
||||||
/// The resulting service only supports HTTP/1.x.
|
|
||||||
pub fn tcp(
|
pub fn tcp(
|
||||||
self,
|
self,
|
||||||
) -> impl ServiceFactory<
|
) -> impl ServiceFactory<
|
||||||
@@ -213,61 +178,6 @@ where
|
|||||||
})
|
})
|
||||||
.and_then(self)
|
.and_then(self)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// 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 = (),
|
|
||||||
> {
|
|
||||||
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
|
|
||||||
// reliably determine version or else it should fallback to h1 and fail quickly if data
|
|
||||||
// on the wire is junk
|
|
||||||
const H2_PREFACE: &[u8] = b"PRI * HTTP/2";
|
|
||||||
|
|
||||||
let mut buf = [0; 12];
|
|
||||||
|
|
||||||
io.peek(&mut buf).await?;
|
|
||||||
|
|
||||||
let proto = if buf == H2_PREFACE {
|
|
||||||
Protocol::Http2
|
|
||||||
} else {
|
|
||||||
Protocol::Http1
|
|
||||||
};
|
|
||||||
|
|
||||||
let peer_addr = io.peer_addr().ok();
|
|
||||||
Ok((io, proto, peer_addr))
|
|
||||||
})
|
|
||||||
.and_then(self)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Configuration options used when accepting TLS connection.
|
|
||||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
|
||||||
#[derive(Debug, Default)]
|
|
||||||
pub struct TlsAcceptorConfig {
|
|
||||||
pub(crate) handshake_timeout: Option<std::time::Duration>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
|
||||||
impl TlsAcceptorConfig {
|
|
||||||
/// Set TLS handshake timeout duration.
|
|
||||||
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
|
|
||||||
Self {
|
|
||||||
handshake_timeout: Some(dur),
|
|
||||||
// ..self
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
@@ -309,7 +219,6 @@ mod openssl {
|
|||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create OpenSSL based service.
|
/// Create OpenSSL based service.
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
|
||||||
pub fn openssl(
|
pub fn openssl(
|
||||||
self,
|
self,
|
||||||
acceptor: SslAcceptor,
|
acceptor: SslAcceptor,
|
||||||
@@ -320,29 +229,7 @@ mod openssl {
|
|||||||
Error = TlsError<SslError, DispatchError>,
|
Error = TlsError<SslError, DispatchError>,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
self.openssl_with_config(acceptor, TlsAcceptorConfig::default())
|
Acceptor::new(acceptor)
|
||||||
}
|
|
||||||
|
|
||||||
/// Create OpenSSL based service with custom TLS acceptor configuration.
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
|
|
||||||
pub fn openssl_with_config(
|
|
||||||
self,
|
|
||||||
acceptor: SslAcceptor,
|
|
||||||
tls_acceptor_config: TlsAcceptorConfig,
|
|
||||||
) -> impl ServiceFactory<
|
|
||||||
TcpStream,
|
|
||||||
Config = (),
|
|
||||||
Response = (),
|
|
||||||
Error = TlsError<SslError, DispatchError>,
|
|
||||||
InitError = (),
|
|
||||||
> {
|
|
||||||
let mut acceptor = Acceptor::new(acceptor);
|
|
||||||
|
|
||||||
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
|
|
||||||
acceptor.set_handshake_timeout(handshake_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptor
|
|
||||||
.map_init_err(|_| {
|
.map_init_err(|_| {
|
||||||
unreachable!("TLS acceptor service factory does not error on init")
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
})
|
})
|
||||||
@@ -404,26 +291,9 @@ mod rustls {
|
|||||||
U::InitError: fmt::Debug,
|
U::InitError: fmt::Debug,
|
||||||
{
|
{
|
||||||
/// Create Rustls based service.
|
/// Create Rustls based service.
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
self,
|
|
||||||
config: ServerConfig,
|
|
||||||
) -> impl ServiceFactory<
|
|
||||||
TcpStream,
|
|
||||||
Config = (),
|
|
||||||
Response = (),
|
|
||||||
Error = TlsError<io::Error, DispatchError>,
|
|
||||||
InitError = (),
|
|
||||||
> {
|
|
||||||
self.rustls_with_config(config, TlsAcceptorConfig::default())
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Create Rustls based service with custom TLS acceptor configuration.
|
|
||||||
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
|
|
||||||
pub fn rustls_with_config(
|
|
||||||
self,
|
self,
|
||||||
mut config: ServerConfig,
|
mut config: ServerConfig,
|
||||||
tls_acceptor_config: TlsAcceptorConfig,
|
|
||||||
) -> impl ServiceFactory<
|
) -> impl ServiceFactory<
|
||||||
TcpStream,
|
TcpStream,
|
||||||
Config = (),
|
Config = (),
|
||||||
@@ -435,13 +305,7 @@ mod rustls {
|
|||||||
protos.extend_from_slice(&config.alpn_protocols);
|
protos.extend_from_slice(&config.alpn_protocols);
|
||||||
config.alpn_protocols = protos;
|
config.alpn_protocols = protos;
|
||||||
|
|
||||||
let mut acceptor = Acceptor::new(config);
|
Acceptor::new(config)
|
||||||
|
|
||||||
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
|
|
||||||
acceptor.set_handshake_timeout(handshake_timeout);
|
|
||||||
}
|
|
||||||
|
|
||||||
acceptor
|
|
||||||
.map_init_err(|_| {
|
.map_init_err(|_| {
|
||||||
unreachable!("TLS acceptor service factory does not error on init")
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
})
|
})
|
||||||
@@ -505,13 +369,13 @@ where
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let expect = expect
|
let expect = expect
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade
|
let upgrade = upgrade
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@@ -519,7 +383,7 @@ where
|
|||||||
|
|
||||||
let service = service
|
let service = service
|
||||||
.await
|
.await
|
||||||
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
Ok(HttpServiceHandler::new(
|
Ok(HttpServiceHandler::new(
|
||||||
cfg,
|
cfg,
|
||||||
@@ -626,7 +490,7 @@ where
|
|||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|err| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
error!("HTTP service readiness error: {:?}", err);
|
log::error!("HTTP service readiness error: {:?}", err);
|
||||||
DispatchError::Service(err)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -802,7 +666,7 @@ where
|
|||||||
self.poll(cx)
|
self.poll(cx)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
tracing::trace!("H2 handshake error: {}", err);
|
log::trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,29 @@ use crate::{
|
|||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Test `Request` builder.
|
/// Test `Request` builder
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// # use http::{header, StatusCode};
|
||||||
|
/// # use actix_web::*;
|
||||||
|
/// use actix_web::test::TestRequest;
|
||||||
|
///
|
||||||
|
/// fn index(req: &HttpRequest) -> Response {
|
||||||
|
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
||||||
|
/// Response::Ok().into()
|
||||||
|
/// } else {
|
||||||
|
/// Response::BadRequest().into()
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// let resp = TestRequest::default().insert_header("content-type", "text/plain")
|
||||||
|
/// .run(&index)
|
||||||
|
/// .unwrap();
|
||||||
|
/// assert_eq!(resp.status(), StatusCode::OK);
|
||||||
|
///
|
||||||
|
/// let resp = TestRequest::default().run(&index).unwrap();
|
||||||
|
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||||
|
/// ```
|
||||||
pub struct TestRequest(Option<Inner>);
|
pub struct TestRequest(Option<Inner>);
|
||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
|
@@ -1,8 +1,7 @@
|
|||||||
|
use actix_codec::{Decoder, Encoder};
|
||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
frame::Parser,
|
frame::Parser,
|
||||||
@@ -11,7 +10,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// A WebSocket message.
|
/// A WebSocket message.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
/// Text message.
|
/// Text message.
|
||||||
Text(ByteString),
|
Text(ByteString),
|
||||||
@@ -36,7 +35,7 @@ pub enum Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A WebSocket frame.
|
/// A WebSocket frame.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Frame {
|
pub enum Frame {
|
||||||
/// Text frame. Note that the codec does not validate UTF-8 encoding.
|
/// Text frame. Note that the codec does not validate UTF-8 encoding.
|
||||||
Text(Bytes),
|
Text(Bytes),
|
||||||
@@ -58,7 +57,7 @@ pub enum Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A WebSocket continuation item.
|
/// A WebSocket continuation item.
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq)]
|
||||||
pub enum Item {
|
pub enum Item {
|
||||||
FirstText(Bytes),
|
FirstText(Bytes),
|
||||||
FirstBinary(Bytes),
|
FirstBinary(Bytes),
|
||||||
@@ -254,7 +253,7 @@ impl Decoder for Codec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
error!("Unfinished fragment {:?}", opcode);
|
log::error!("Unfinished fragment {:?}", opcode);
|
||||||
Err(ProtocolError::ContinuationFragment(opcode))
|
Err(ProtocolError::ContinuationFragment(opcode))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@@ -73,12 +73,10 @@ mod inner {
|
|||||||
use actix_service::{IntoService, Service};
|
use actix_service::{IntoService, Service};
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use local_channel::mpsc;
|
use local_channel::mpsc;
|
||||||
|
use log::debug;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
use tracing::debug;
|
|
||||||
|
|
||||||
use actix_codec::Framed;
|
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
||||||
use tokio::io::{AsyncRead, AsyncWrite};
|
|
||||||
use tokio_util::codec::{Decoder, Encoder};
|
|
||||||
|
|
||||||
use crate::{body::BoxBody, Response};
|
use crate::{body::BoxBody, Response};
|
||||||
|
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use tracing::debug;
|
use log::debug;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
mask::apply_mask,
|
mask::apply_mask,
|
||||||
@@ -17,6 +17,7 @@ impl Parser {
|
|||||||
fn parse_metadata(
|
fn parse_metadata(
|
||||||
src: &[u8],
|
src: &[u8],
|
||||||
server: bool,
|
server: bool,
|
||||||
|
max_size: usize,
|
||||||
) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
|
) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
|
||||||
let chunk_len = src.len();
|
let chunk_len = src.len();
|
||||||
|
|
||||||
@@ -59,12 +60,20 @@ impl Parser {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
|
let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
|
||||||
|
if len > max_size as u64 {
|
||||||
|
return Err(ProtocolError::Overflow);
|
||||||
|
}
|
||||||
idx += 8;
|
idx += 8;
|
||||||
len as usize
|
len as usize
|
||||||
} else {
|
} else {
|
||||||
len as usize
|
len as usize
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// check for max allowed size
|
||||||
|
if length > max_size {
|
||||||
|
return Err(ProtocolError::Overflow);
|
||||||
|
}
|
||||||
|
|
||||||
let mask = if server {
|
let mask = if server {
|
||||||
if chunk_len < idx + 4 {
|
if chunk_len < idx + 4 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@@ -89,10 +98,11 @@ impl Parser {
|
|||||||
max_size: usize,
|
max_size: usize,
|
||||||
) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
|
) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
|
||||||
// try to parse ws frame metadata
|
// try to parse ws frame metadata
|
||||||
let (idx, finished, opcode, length, mask) = match Parser::parse_metadata(src, server)? {
|
let (idx, finished, opcode, length, mask) =
|
||||||
None => return Ok(None),
|
match Parser::parse_metadata(src, server, max_size)? {
|
||||||
Some(res) => res,
|
None => return Ok(None),
|
||||||
};
|
Some(res) => res,
|
||||||
|
};
|
||||||
|
|
||||||
// not enough data
|
// not enough data
|
||||||
if src.len() < idx + length {
|
if src.len() < idx + length {
|
||||||
@@ -102,13 +112,6 @@ impl Parser {
|
|||||||
// remove prefix
|
// remove prefix
|
||||||
src.advance(idx);
|
src.advance(idx);
|
||||||
|
|
||||||
// check for max allowed size
|
|
||||||
if length > max_size {
|
|
||||||
// drop the payload
|
|
||||||
src.advance(length);
|
|
||||||
return Err(ProtocolError::Overflow);
|
|
||||||
}
|
|
||||||
|
|
||||||
// no need for body
|
// no need for body
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return Ok(Some((finished, opcode, None)));
|
return Ok(Some((finished, opcode, None)));
|
||||||
@@ -313,7 +316,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_frame_no_mask() {
|
fn test_parse_frame_no_mask() {
|
||||||
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
|
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0001u8][..]);
|
||||||
buf.extend([1u8]);
|
buf.extend(&[1u8]);
|
||||||
|
|
||||||
assert!(Parser::parse(&mut buf, true, 1024).is_err());
|
assert!(Parser::parse(&mut buf, true, 1024).is_err());
|
||||||
|
|
||||||
@@ -326,7 +329,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn test_parse_frame_max_size() {
|
fn test_parse_frame_max_size() {
|
||||||
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]);
|
let mut buf = BytesMut::from(&[0b0000_0001u8, 0b0000_0010u8][..]);
|
||||||
buf.extend([1u8, 1u8]);
|
buf.extend(&[1u8, 1u8]);
|
||||||
|
|
||||||
assert!(Parser::parse(&mut buf, true, 1).is_err());
|
assert!(Parser::parse(&mut buf, true, 1).is_err());
|
||||||
|
|
||||||
@@ -336,30 +339,6 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_frame_max_size_recoverability() {
|
|
||||||
let mut buf = BytesMut::new();
|
|
||||||
// The first text frame with length == 2, payload doesn't matter.
|
|
||||||
buf.extend([0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
|
||||||
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
|
|
||||||
buf.extend([0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
|
||||||
|
|
||||||
assert_eq!(buf.len(), 8);
|
|
||||||
assert!(matches!(
|
|
||||||
Parser::parse(&mut buf, false, 1),
|
|
||||||
Err(ProtocolError::Overflow)
|
|
||||||
));
|
|
||||||
assert_eq!(buf.len(), 4);
|
|
||||||
let frame = extract(Parser::parse(&mut buf, false, 2));
|
|
||||||
assert!(!frame.finished);
|
|
||||||
assert_eq!(frame.opcode, OpCode::Binary);
|
|
||||||
assert_eq!(
|
|
||||||
frame.payload,
|
|
||||||
Bytes::from(vec![0b1111_1111u8, 0b1111_1111u8])
|
|
||||||
);
|
|
||||||
assert_eq!(buf.len(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ping_frame() {
|
fn test_ping_frame() {
|
||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
|
@@ -67,7 +67,7 @@ pub enum ProtocolError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// WebSocket handshake errors
|
/// WebSocket handshake errors
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)]
|
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)]
|
||||||
pub enum HandshakeError {
|
pub enum HandshakeError {
|
||||||
/// Only get method is allowed.
|
/// Only get method is allowed.
|
||||||
#[display(fmt = "Method not allowed.")]
|
#[display(fmt = "Method not allowed.")]
|
||||||
|
@@ -3,9 +3,6 @@ use std::{
|
|||||||
fmt,
|
fmt,
|
||||||
};
|
};
|
||||||
|
|
||||||
use base64::prelude::*;
|
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
/// Operation codes defined in [RFC 6455 §11.8].
|
/// Operation codes defined in [RFC 6455 §11.8].
|
||||||
///
|
///
|
||||||
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
||||||
@@ -61,7 +58,7 @@ impl From<OpCode> for u8 {
|
|||||||
Ping => 9,
|
Ping => 9,
|
||||||
Pong => 10,
|
Pong => 10,
|
||||||
Bad => {
|
Bad => {
|
||||||
error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
log::error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
||||||
8 // if this somehow happens, a close frame will help us tear down quickly
|
8 // if this somehow happens, a close frame will help us tear down quickly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -245,7 +242,7 @@ pub fn hash_key(key: &[u8]) -> [u8; 28] {
|
|||||||
};
|
};
|
||||||
|
|
||||||
let mut hash_b64 = [0; 28];
|
let mut hash_b64 = [0; 28];
|
||||||
let n = BASE64_STANDARD.encode_slice(hash, &mut hash_b64).unwrap();
|
let n = base64::encode_config_slice(&hash, base64::STANDARD, &mut hash_b64);
|
||||||
assert_eq!(n, 28);
|
assert_eq!(n, 28);
|
||||||
|
|
||||||
hash_b64
|
hash_b64
|
||||||
|
@@ -1,15 +1,14 @@
|
|||||||
#![cfg(feature = "openssl")]
|
#![cfg(feature = "openssl")]
|
||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
extern crate tls_openssl as openssl;
|
extern crate tls_openssl as openssl;
|
||||||
|
|
||||||
use std::{convert::Infallible, io, time::Duration};
|
use std::{convert::Infallible, io};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{BodyStream, BoxBody, SizedStream},
|
body::{BodyStream, BoxBody, SizedStream},
|
||||||
error::PayloadError,
|
error::PayloadError,
|
||||||
header::{self, HeaderValue},
|
header::{self, HeaderValue},
|
||||||
Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
|
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
use actix_service::{fn_service, ServiceFactoryExt};
|
use actix_service::{fn_service, ServiceFactoryExt};
|
||||||
@@ -17,7 +16,7 @@ use actix_utils::future::{err, ok, ready};
|
|||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use derive_more::{Display, Error};
|
use derive_more::{Display, Error};
|
||||||
use futures_core::Stream;
|
use futures_core::Stream;
|
||||||
use futures_util::{stream::once, StreamExt as _};
|
use futures_util::stream::{once, StreamExt as _};
|
||||||
use openssl::{
|
use openssl::{
|
||||||
pkey::PKey,
|
pkey::PKey,
|
||||||
ssl::{SslAcceptor, SslMethod},
|
ssl::{SslAcceptor, SslMethod},
|
||||||
@@ -90,10 +89,7 @@ async fn h2_1() -> io::Result<()> {
|
|||||||
assert_eq!(req.version(), Version::HTTP_2);
|
assert_eq!(req.version(), Version::HTTP_2);
|
||||||
ok::<_, Error>(Response::ok())
|
ok::<_, Error>(Response::ok())
|
||||||
})
|
})
|
||||||
.openssl_with_config(
|
.openssl(tls_config())
|
||||||
tls_config(),
|
|
||||||
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
|
|
||||||
)
|
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@@ -1,5 +1,4 @@
|
|||||||
#![cfg(feature = "rustls")]
|
#![cfg(feature = "rustls")]
|
||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
extern crate tls_rustls as rustls;
|
extern crate tls_rustls as rustls;
|
||||||
|
|
||||||
@@ -9,14 +8,13 @@ use std::{
|
|||||||
net::{SocketAddr, TcpStream as StdTcpStream},
|
net::{SocketAddr, TcpStream as StdTcpStream},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
task::Poll,
|
task::Poll,
|
||||||
time::Duration,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{BodyStream, BoxBody, SizedStream},
|
body::{BodyStream, BoxBody, SizedStream},
|
||||||
error::PayloadError,
|
error::PayloadError,
|
||||||
header::{self, HeaderName, HeaderValue},
|
header::{self, HeaderName, HeaderValue},
|
||||||
Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
|
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
use actix_rt::pin;
|
use actix_rt::pin;
|
||||||
@@ -42,7 +40,7 @@ where
|
|||||||
let body = stream.as_mut();
|
let body = stream.as_mut();
|
||||||
|
|
||||||
match ready!(body.poll_next(cx)) {
|
match ready!(body.poll_next(cx)) {
|
||||||
Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
|
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||||
None => return Poll::Ready(Ok(())),
|
None => return Poll::Ready(Ok(())),
|
||||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||||
}
|
}
|
||||||
@@ -162,10 +160,7 @@ async fn h2_1() -> io::Result<()> {
|
|||||||
assert_eq!(req.version(), Version::HTTP_2);
|
assert_eq!(req.version(), Version::HTTP_2);
|
||||||
ok::<_, Error>(Response::ok())
|
ok::<_, Error>(Response::ok())
|
||||||
})
|
})
|
||||||
.rustls_with_config(
|
.rustls(tls_config())
|
||||||
tls_config(),
|
|
||||||
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
|
|
||||||
)
|
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@@ -217,7 +212,6 @@ async fn h2_content_length() {
|
|||||||
let value = HeaderValue::from_static("0");
|
let value = HeaderValue::from_static("0");
|
||||||
|
|
||||||
{
|
{
|
||||||
#[allow(clippy::single_element_loop)]
|
|
||||||
for &i in &[0] {
|
for &i in &[0] {
|
||||||
let req = srv
|
let req = srv
|
||||||
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
||||||
@@ -232,7 +226,6 @@ async fn h2_content_length() {
|
|||||||
// assert_eq!(response.headers().get(&header), None);
|
// assert_eq!(response.headers().get(&header), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::single_element_loop)]
|
|
||||||
for &i in &[1] {
|
for &i in &[1] {
|
||||||
let req = srv
|
let req = srv
|
||||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
io::{Read, Write},
|
io::{Read, Write},
|
||||||
@@ -9,15 +7,18 @@ use std::{
|
|||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{self, BodyStream, BoxBody, SizedStream},
|
body::{self, BodyStream, BoxBody, SizedStream},
|
||||||
header, Error, HttpService, KeepAlive, Request, Response, StatusCode, Version,
|
header, Error, HttpService, KeepAlive, Request, Response, StatusCode,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
use actix_rt::{net::TcpStream, time::sleep};
|
use actix_rt::time::sleep;
|
||||||
use actix_service::fn_service;
|
use actix_service::fn_service;
|
||||||
use actix_utils::future::{err, ok, ready};
|
use actix_utils::future::{err, ok, ready};
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use derive_more::{Display, Error};
|
use derive_more::{Display, Error};
|
||||||
use futures_util::{stream::once, FutureExt as _, StreamExt as _};
|
use futures_util::{
|
||||||
|
stream::{once, StreamExt as _},
|
||||||
|
FutureExt as _,
|
||||||
|
};
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
@@ -857,44 +858,3 @@ async fn not_modified_spec_h1() {
|
|||||||
|
|
||||||
srv.stop().await;
|
srv.stop().await;
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
|
||||||
async fn h2c_auto() {
|
|
||||||
let mut srv = test_server(|| {
|
|
||||||
HttpService::build()
|
|
||||||
.keep_alive(KeepAlive::Disabled)
|
|
||||||
.finish(|req: Request| {
|
|
||||||
let body = match req.version() {
|
|
||||||
Version::HTTP_11 => "h1",
|
|
||||||
Version::HTTP_2 => "h2",
|
|
||||||
_ => unreachable!(),
|
|
||||||
};
|
|
||||||
ok::<_, Infallible>(Response::ok().set_body(body))
|
|
||||||
})
|
|
||||||
.tcp_auto_h2c()
|
|
||||||
})
|
|
||||||
.await;
|
|
||||||
|
|
||||||
let req = srv.get("/");
|
|
||||||
assert_eq!(req.get_version(), &Version::HTTP_11);
|
|
||||||
let mut res = req.send().await.unwrap();
|
|
||||||
assert!(res.status().is_success());
|
|
||||||
assert_eq!(res.body().await.unwrap(), &b"h1"[..]);
|
|
||||||
|
|
||||||
// awc doesn't support forcing the version to http/2 so use h2 manually
|
|
||||||
|
|
||||||
let tcp = TcpStream::connect(srv.addr()).await.unwrap();
|
|
||||||
let (h2, connection) = h2::client::handshake(tcp).await.unwrap();
|
|
||||||
tokio::spawn(async move { connection.await.unwrap() });
|
|
||||||
let mut h2 = h2.ready().await.unwrap();
|
|
||||||
|
|
||||||
let request = ::http::Request::new(());
|
|
||||||
let (response, _) = h2.send_request(request, true).unwrap();
|
|
||||||
let (head, mut body) = response.await.unwrap().into_parts();
|
|
||||||
let body = body.data().await.unwrap().unwrap();
|
|
||||||
|
|
||||||
assert!(head.status.is_success());
|
|
||||||
assert_eq!(body, &b"h2"[..]);
|
|
||||||
|
|
||||||
srv.stop().await;
|
|
||||||
}
|
|
||||||
|
@@ -1,5 +1,3 @@
|
|||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
cell::Cell,
|
cell::Cell,
|
||||||
convert::Infallible,
|
convert::Infallible,
|
||||||
|
@@ -1,13 +1,6 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0 - 2023-01-21
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
|
||||||
- `Field::content_type()` now returns `Option<&mime::Mime>` [#2885]
|
|
||||||
|
|
||||||
[#2885]: https://github.com/actix/actix-web/pull/2885
|
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0 - 2022-02-25
|
## 0.4.0 - 2022-02-25
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-multipart"
|
name = "actix-multipart"
|
||||||
version = "0.5.0"
|
version = "0.4.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Multipart form support for Actix Web"
|
description = "Multipart form support for Actix Web"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
@@ -14,21 +14,21 @@ name = "actix_multipart"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-utils = "3"
|
actix-utils = "3.0.0"
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4.0.0", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
futures-core = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
httparse = "1.3"
|
httparse = "1.3"
|
||||||
local-waker = "0.1"
|
local-waker = "0.1"
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
memchr = "2.5"
|
twoway = "0.2"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-http = "3"
|
actix-http = "3.0.0"
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
tokio = { version = "1.18.4", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
tokio-stream = "0.1"
|
tokio-stream = "0.1"
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
> Multipart form support for Actix Web.
|
> Multipart form support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://docs.rs/actix-multipart/0.5.0)
|
[](https://docs.rs/actix-multipart/0.4.0)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-multipart/0.5.0)
|
[](https://deps.rs/crate/actix-multipart/0.4.0)
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@@ -14,7 +14,7 @@ use crate::server::Multipart;
|
|||||||
/// ```
|
/// ```
|
||||||
/// use actix_web::{web, HttpResponse, Error};
|
/// use actix_web::{web, HttpResponse, Error};
|
||||||
/// use actix_multipart::Multipart;
|
/// use actix_multipart::Multipart;
|
||||||
/// use futures_util::StreamExt as _;
|
/// use futures_util::stream::StreamExt as _;
|
||||||
///
|
///
|
||||||
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
/// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
|
||||||
/// // iterate over multipart stream
|
/// // iterate over multipart stream
|
||||||
|
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
#![allow(clippy::borrow_interior_mutable_const, clippy::uninlined_format_args)]
|
#![allow(clippy::borrow_interior_mutable_const)]
|
||||||
|
|
||||||
mod error;
|
mod error;
|
||||||
mod extractor;
|
mod extractor;
|
||||||
|
@@ -289,8 +289,10 @@ impl InnerMultipart {
|
|||||||
match self.state {
|
match self.state {
|
||||||
// read until first boundary
|
// read until first boundary
|
||||||
InnerState::FirstBoundary => {
|
InnerState::FirstBoundary => {
|
||||||
match InnerMultipart::skip_until_boundary(&mut payload, &self.boundary)?
|
match InnerMultipart::skip_until_boundary(
|
||||||
{
|
&mut *payload,
|
||||||
|
&self.boundary,
|
||||||
|
)? {
|
||||||
Some(eof) => {
|
Some(eof) => {
|
||||||
if eof {
|
if eof {
|
||||||
self.state = InnerState::Eof;
|
self.state = InnerState::Eof;
|
||||||
@@ -304,7 +306,7 @@ impl InnerMultipart {
|
|||||||
}
|
}
|
||||||
// read boundary
|
// read boundary
|
||||||
InnerState::Boundary => {
|
InnerState::Boundary => {
|
||||||
match InnerMultipart::read_boundary(&mut payload, &self.boundary)? {
|
match InnerMultipart::read_boundary(&mut *payload, &self.boundary)? {
|
||||||
None => return Poll::Pending,
|
None => return Poll::Pending,
|
||||||
Some(eof) => {
|
Some(eof) => {
|
||||||
if eof {
|
if eof {
|
||||||
@@ -321,7 +323,7 @@ impl InnerMultipart {
|
|||||||
|
|
||||||
// read field headers for next field
|
// read field headers for next field
|
||||||
if self.state == InnerState::Headers {
|
if self.state == InnerState::Headers {
|
||||||
if let Some(headers) = InnerMultipart::read_headers(&mut payload)? {
|
if let Some(headers) = InnerMultipart::read_headers(&mut *payload)? {
|
||||||
self.state = InnerState::Boundary;
|
self.state = InnerState::Boundary;
|
||||||
headers
|
headers
|
||||||
} else {
|
} else {
|
||||||
@@ -359,18 +361,17 @@ impl InnerMultipart {
|
|||||||
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
|
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
|
||||||
};
|
};
|
||||||
|
|
||||||
let ct: Option<mime::Mime> = headers
|
let ct: mime::Mime = headers
|
||||||
.get(&header::CONTENT_TYPE)
|
.get(&header::CONTENT_TYPE)
|
||||||
.and_then(|ct| ct.to_str().ok())
|
.and_then(|ct| ct.to_str().ok())
|
||||||
.and_then(|ct| ct.parse().ok());
|
.and_then(|ct| ct.parse().ok())
|
||||||
|
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
|
||||||
|
|
||||||
self.state = InnerState::Boundary;
|
self.state = InnerState::Boundary;
|
||||||
|
|
||||||
// nested multipart stream is not supported
|
// nested multipart stream is not supported
|
||||||
if let Some(mime) = &ct {
|
if ct.type_() == mime::MULTIPART {
|
||||||
if mime.type_() == mime::MULTIPART {
|
return Poll::Ready(Some(Err(MultipartError::Nested)));
|
||||||
return Poll::Ready(Some(Err(MultipartError::Nested)));
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
let field =
|
let field =
|
||||||
@@ -398,7 +399,7 @@ impl Drop for InnerMultipart {
|
|||||||
|
|
||||||
/// A single field in a multipart stream
|
/// A single field in a multipart stream
|
||||||
pub struct Field {
|
pub struct Field {
|
||||||
ct: Option<mime::Mime>,
|
ct: mime::Mime,
|
||||||
cd: ContentDisposition,
|
cd: ContentDisposition,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
inner: Rc<RefCell<InnerField>>,
|
inner: Rc<RefCell<InnerField>>,
|
||||||
@@ -409,7 +410,7 @@ impl Field {
|
|||||||
fn new(
|
fn new(
|
||||||
safety: Safety,
|
safety: Safety,
|
||||||
headers: HeaderMap,
|
headers: HeaderMap,
|
||||||
ct: Option<mime::Mime>,
|
ct: mime::Mime,
|
||||||
cd: ContentDisposition,
|
cd: ContentDisposition,
|
||||||
inner: Rc<RefCell<InnerField>>,
|
inner: Rc<RefCell<InnerField>>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
@@ -427,13 +428,9 @@ impl Field {
|
|||||||
&self.headers
|
&self.headers
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns a reference to the field's content (mime) type, if it is supplied by the client.
|
/// Returns a reference to the field's content (mime) type.
|
||||||
///
|
pub fn content_type(&self) -> &mime::Mime {
|
||||||
/// According to [RFC 7578](https://www.rfc-editor.org/rfc/rfc7578#section-4.4), if it is not
|
&self.ct
|
||||||
/// present, it should default to "text/plain". Note it is the responsibility of the client to
|
|
||||||
/// provide the appropriate content type, there is no attempt to validate this by the server.
|
|
||||||
pub fn content_type(&self) -> Option<&mime::Mime> {
|
|
||||||
self.ct.as_ref()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the field's Content-Disposition.
|
/// Returns the field's Content-Disposition.
|
||||||
@@ -485,11 +482,7 @@ impl Stream for Field {
|
|||||||
|
|
||||||
impl fmt::Debug for Field {
|
impl fmt::Debug for Field {
|
||||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||||
if let Some(ct) = &self.ct {
|
writeln!(f, "\nField: {}", self.ct)?;
|
||||||
writeln!(f, "\nField: {}", ct)?;
|
|
||||||
} else {
|
|
||||||
writeln!(f, "\nField:")?;
|
|
||||||
}
|
|
||||||
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
writeln!(f, " boundary: {}", self.inner.borrow().boundary)?;
|
||||||
writeln!(f, " headers:")?;
|
writeln!(f, " headers:")?;
|
||||||
for (key, val) in self.headers.iter() {
|
for (key, val) in self.headers.iter() {
|
||||||
@@ -606,7 +599,7 @@ impl InnerField {
|
|||||||
}
|
}
|
||||||
|
|
||||||
loop {
|
loop {
|
||||||
return if let Some(idx) = memchr::memmem::find(&payload.buf[pos..], b"\r") {
|
return if let Some(idx) = twoway::find_bytes(&payload.buf[pos..], b"\r") {
|
||||||
let cur = pos + idx;
|
let cur = pos + idx;
|
||||||
|
|
||||||
// check if we have enough data for boundary detection
|
// check if we have enough data for boundary detection
|
||||||
@@ -650,9 +643,9 @@ impl InnerField {
|
|||||||
let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
let result = if let Some(mut payload) = self.payload.as_ref().unwrap().get_mut(s) {
|
||||||
if !self.eof {
|
if !self.eof {
|
||||||
let res = if let Some(ref mut len) = self.length {
|
let res = if let Some(ref mut len) = self.length {
|
||||||
InnerField::read_len(&mut payload, len)
|
InnerField::read_len(&mut *payload, len)
|
||||||
} else {
|
} else {
|
||||||
InnerField::read_stream(&mut payload, &self.boundary)
|
InnerField::read_stream(&mut *payload, &self.boundary)
|
||||||
};
|
};
|
||||||
|
|
||||||
match res {
|
match res {
|
||||||
@@ -827,7 +820,7 @@ impl PayloadBuffer {
|
|||||||
|
|
||||||
/// Read until specified ending
|
/// Read until specified ending
|
||||||
fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
|
fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
|
||||||
let res = memchr::memmem::find(&self.buf, line)
|
let res = twoway::find_bytes(&self.buf, line)
|
||||||
.map(|idx| self.buf.split_to(idx + line.len()).freeze());
|
.map(|idx| self.buf.split_to(idx + line.len()).freeze());
|
||||||
|
|
||||||
if res.is_none() && self.eof {
|
if res.is_none() && self.eof {
|
||||||
@@ -868,7 +861,7 @@ mod tests {
|
|||||||
use actix_web::test::TestRequest;
|
use actix_web::test::TestRequest;
|
||||||
use actix_web::FromRequest;
|
use actix_web::FromRequest;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use futures_util::{future::lazy, StreamExt as _};
|
use futures_util::{future::lazy, StreamExt};
|
||||||
use std::time::Duration;
|
use std::time::Duration;
|
||||||
use tokio::sync::mpsc;
|
use tokio::sync::mpsc;
|
||||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
@@ -1031,8 +1024,8 @@ mod tests {
|
|||||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||||
|
|
||||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
match field.next().await.unwrap() {
|
match field.next().await.unwrap() {
|
||||||
Ok(chunk) => assert_eq!(chunk, "test"),
|
Ok(chunk) => assert_eq!(chunk, "test"),
|
||||||
@@ -1048,8 +1041,8 @@ mod tests {
|
|||||||
|
|
||||||
match multipart.next().await.unwrap() {
|
match multipart.next().await.unwrap() {
|
||||||
Ok(mut field) => {
|
Ok(mut field) => {
|
||||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
match field.next().await {
|
match field.next().await {
|
||||||
Some(Ok(chunk)) => assert_eq!(chunk, "data"),
|
Some(Ok(chunk)) => assert_eq!(chunk, "data"),
|
||||||
@@ -1093,8 +1086,8 @@ mod tests {
|
|||||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||||
|
|
||||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
assert_eq!(get_whole_field(&mut field).await, "test");
|
assert_eq!(get_whole_field(&mut field).await, "test");
|
||||||
}
|
}
|
||||||
@@ -1103,8 +1096,8 @@ mod tests {
|
|||||||
|
|
||||||
match multipart.next().await {
|
match multipart.next().await {
|
||||||
Some(Ok(mut field)) => {
|
Some(Ok(mut field)) => {
|
||||||
assert_eq!(field.content_type().unwrap().type_(), mime::TEXT);
|
assert_eq!(field.content_type().type_(), mime::TEXT);
|
||||||
assert_eq!(field.content_type().unwrap().subtype(), mime::PLAIN);
|
assert_eq!(field.content_type().subtype(), mime::PLAIN);
|
||||||
|
|
||||||
assert_eq!(get_whole_field(&mut field).await, "data");
|
assert_eq!(get_whole_field(&mut field).await, "data");
|
||||||
}
|
}
|
||||||
|
@@ -1,13 +1,6 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 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 - 2022-02-22
|
## 0.5.0 - 2022-02-22
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-router"
|
name = "actix-router"
|
||||||
version = "0.5.1"
|
version = "0.5.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||||
@@ -21,21 +21,18 @@ default = ["http"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytestring = ">=0.1.5, <2"
|
bytestring = ">=0.1.5, <2"
|
||||||
http = { version = "0.2.5", optional = true }
|
firestorm = "0.5"
|
||||||
|
http = { version = "0.2.3", optional = true }
|
||||||
|
log = "0.4"
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.4", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
|
firestorm = { version = "0.5", features = ["enable_system_time"] }
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
percent-encoding = "2.1"
|
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "router"
|
name = "router"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
[[bench]]
|
|
||||||
name = "quoter"
|
|
||||||
harness = false
|
|
||||||
|
@@ -1,54 +0,0 @@
|
|||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
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::<String>();
|
|
||||||
let path_unquoted = ('\u{00}'..='\u{7f}').collect::<String>();
|
|
||||||
|
|
||||||
group.bench_function("quoter_unquoted", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
for _ in 0..10 {
|
|
||||||
black_box(quoter.requote(path_unquoted.as_bytes()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("percent_encode_unquoted", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
for _ in 0..10 {
|
|
||||||
let decode = percent_encoding::percent_decode(path_unquoted.as_bytes());
|
|
||||||
black_box(Into::<Cow<'_, [u8]>>::into(decode));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("quoter_quoted", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
for _ in 0..10 {
|
|
||||||
black_box(quoter.requote(path_quoted.as_bytes()));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.bench_function("percent_encode_quoted", |b| {
|
|
||||||
b.iter(|| {
|
|
||||||
for _ in 0..10 {
|
|
||||||
let decode = percent_encoding::percent_decode(path_quoted.as_bytes());
|
|
||||||
black_box(Into::<Cow<'_, [u8]>>::into(decode));
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
group.finish();
|
|
||||||
}
|
|
||||||
|
|
||||||
criterion_group!(benches, compare_quoters);
|
|
||||||
criterion_main!(benches);
|
|
169
actix-router/examples/flamegraph.rs
Normal file
169
actix-router/examples/flamegraph.rs
Normal file
@@ -0,0 +1,169 @@
|
|||||||
|
macro_rules! register {
|
||||||
|
(brackets) => {{
|
||||||
|
register!(finish => "{p1}", "{p2}", "{p3}", "{p4}")
|
||||||
|
}};
|
||||||
|
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
|
||||||
|
let arr = [
|
||||||
|
concat!("/authorizations"),
|
||||||
|
concat!("/authorizations/", $p1),
|
||||||
|
concat!("/applications/", $p1, "/tokens/", $p2),
|
||||||
|
concat!("/events"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/events"),
|
||||||
|
concat!("/networks/", $p1, "/", $p2, "/events"),
|
||||||
|
concat!("/orgs/", $p1, "/events"),
|
||||||
|
concat!("/users/", $p1, "/received_events"),
|
||||||
|
concat!("/users/", $p1, "/received_events/public"),
|
||||||
|
concat!("/users/", $p1, "/events"),
|
||||||
|
concat!("/users/", $p1, "/events/public"),
|
||||||
|
concat!("/users/", $p1, "/events/orgs/", $p2),
|
||||||
|
concat!("/feeds"),
|
||||||
|
concat!("/notifications"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/notifications"),
|
||||||
|
concat!("/notifications/threads/", $p1),
|
||||||
|
concat!("/notifications/threads/", $p1, "/subscription"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/stargazers"),
|
||||||
|
concat!("/users/", $p1, "/starred"),
|
||||||
|
concat!("/user/starred"),
|
||||||
|
concat!("/user/starred/", $p1, "/", $p2),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/subscribers"),
|
||||||
|
concat!("/users/", $p1, "/subscriptions"),
|
||||||
|
concat!("/user/subscriptions"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/subscription"),
|
||||||
|
concat!("/user/subscriptions/", $p1, "/", $p2),
|
||||||
|
concat!("/users/", $p1, "/gists"),
|
||||||
|
concat!("/gists"),
|
||||||
|
concat!("/gists/", $p1),
|
||||||
|
concat!("/gists/", $p1, "/star"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/git/refs"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3),
|
||||||
|
concat!("/issues"),
|
||||||
|
concat!("/user/issues"),
|
||||||
|
concat!("/orgs/", $p1, "/issues"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/issues"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/assignees"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/labels"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/labels/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/milestones/"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3),
|
||||||
|
concat!("/emojis"),
|
||||||
|
concat!("/gitignore/templates"),
|
||||||
|
concat!("/gitignore/templates/", $p1),
|
||||||
|
concat!("/meta"),
|
||||||
|
concat!("/rate_limit"),
|
||||||
|
concat!("/users/", $p1, "/orgs"),
|
||||||
|
concat!("/user/orgs"),
|
||||||
|
concat!("/orgs/", $p1),
|
||||||
|
concat!("/orgs/", $p1, "/members"),
|
||||||
|
concat!("/orgs/", $p1, "/members", $p2),
|
||||||
|
concat!("/orgs/", $p1, "/public_members"),
|
||||||
|
concat!("/orgs/", $p1, "/public_members/", $p2),
|
||||||
|
concat!("/orgs/", $p1, "/teams"),
|
||||||
|
concat!("/teams/", $p1),
|
||||||
|
concat!("/teams/", $p1, "/members"),
|
||||||
|
concat!("/teams/", $p1, "/members", $p2),
|
||||||
|
concat!("/teams/", $p1, "/repos"),
|
||||||
|
concat!("/teams/", $p1, "/repos/", $p2, "/", $p3),
|
||||||
|
concat!("/user/teams"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/pulls"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"),
|
||||||
|
concat!("/user/repos"),
|
||||||
|
concat!("/users/", $p1, "/repos"),
|
||||||
|
concat!("/orgs/", $p1, "/repos"),
|
||||||
|
concat!("/repositories"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/contributors"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/languages"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/teams"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/tags"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/branches"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/branches/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/collaborators"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/comments"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/commits"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/commits/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/readme"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/keys"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/keys", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/downloads"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/downloads", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/forks"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/hooks"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/hooks", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/releases"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/releases/", $p3),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/stats/contributors"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/stats/participation"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"),
|
||||||
|
concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3),
|
||||||
|
concat!("/search/repositories"),
|
||||||
|
concat!("/search/code"),
|
||||||
|
concat!("/search/issues"),
|
||||||
|
concat!("/search/users"),
|
||||||
|
concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4),
|
||||||
|
concat!("/legacy/repos/search/", $p1),
|
||||||
|
concat!("/legacy/user/search/", $p1),
|
||||||
|
concat!("/legacy/user/email/", $p1),
|
||||||
|
concat!("/users/", $p1),
|
||||||
|
concat!("/user"),
|
||||||
|
concat!("/users"),
|
||||||
|
concat!("/user/emails"),
|
||||||
|
concat!("/users/", $p1, "/followers"),
|
||||||
|
concat!("/user/followers"),
|
||||||
|
concat!("/users/", $p1, "/following"),
|
||||||
|
concat!("/user/following"),
|
||||||
|
concat!("/user/following/", $p1),
|
||||||
|
concat!("/users/", $p1, "/following", $p2),
|
||||||
|
concat!("/users/", $p1, "/keys"),
|
||||||
|
concat!("/user/keys"),
|
||||||
|
concat!("/user/keys/", $p1),
|
||||||
|
];
|
||||||
|
|
||||||
|
arr.to_vec()
|
||||||
|
}};
|
||||||
|
}
|
||||||
|
|
||||||
|
static PATHS: [&str; 5] = [
|
||||||
|
"/authorizations",
|
||||||
|
"/user/repos",
|
||||||
|
"/repos/rust-lang/rust/stargazers",
|
||||||
|
"/orgs/rust-lang/public_members/nikomatsakis",
|
||||||
|
"/repos/rust-lang/rust/releases/1.51.0",
|
||||||
|
];
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let mut router = actix_router::Router::<bool>::build();
|
||||||
|
|
||||||
|
for route in register!(brackets) {
|
||||||
|
router.path(route, true);
|
||||||
|
}
|
||||||
|
|
||||||
|
let actix = router.finish();
|
||||||
|
|
||||||
|
if firestorm::enabled() {
|
||||||
|
firestorm::bench("target", || {
|
||||||
|
for &route in &PATHS {
|
||||||
|
let mut path = actix_router::Path::new(route);
|
||||||
|
actix.recognize(&mut path).unwrap();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.unwrap();
|
||||||
|
}
|
||||||
|
}
|
@@ -7,7 +7,7 @@ use crate::path::{Path, PathIter};
|
|||||||
use crate::{Quoter, ResourcePath};
|
use crate::{Quoter, ResourcePath};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static FULL_QUOTER: Quoter = Quoter::new(b"", b"");
|
static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b"");
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! unsupported_type {
|
macro_rules! unsupported_type {
|
||||||
@@ -293,7 +293,7 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
|||||||
parse_value!(deserialize_bool, visit_bool, "bool");
|
parse_value!(deserialize_bool, visit_bool, "bool");
|
||||||
parse_value!(deserialize_i8, visit_i8, "i8");
|
parse_value!(deserialize_i8, visit_i8, "i8");
|
||||||
parse_value!(deserialize_i16, visit_i16, "i16");
|
parse_value!(deserialize_i16, visit_i16, "i16");
|
||||||
parse_value!(deserialize_i32, visit_i32, "i32");
|
parse_value!(deserialize_i32, visit_i32, "i16");
|
||||||
parse_value!(deserialize_i64, visit_i64, "i64");
|
parse_value!(deserialize_i64, visit_i64, "i64");
|
||||||
parse_value!(deserialize_u8, visit_u8, "u8");
|
parse_value!(deserialize_u8, visit_u8, "u8");
|
||||||
parse_value!(deserialize_u16, visit_u16, "u16");
|
parse_value!(deserialize_u16, visit_u16, "u16");
|
||||||
|
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
|
||||||
|
@@ -1,6 +1,7 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::ops::{DerefMut, Index};
|
use std::ops::{DerefMut, Index};
|
||||||
|
|
||||||
|
use firestorm::profile_method;
|
||||||
use serde::de;
|
use serde::de;
|
||||||
|
|
||||||
use crate::{de::PathDeserializer, Resource, ResourcePath};
|
use crate::{de::PathDeserializer, Resource, ResourcePath};
|
||||||
@@ -51,6 +52,7 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Returns full path as a string.
|
/// Returns full path as a string.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
|
profile_method!(as_str);
|
||||||
self.path.path()
|
self.path.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -59,6 +61,7 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Returns empty string if no more is to be processed.
|
/// Returns empty string if no more is to be processed.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn unprocessed(&self) -> &str {
|
pub fn unprocessed(&self) -> &str {
|
||||||
|
profile_method!(unprocessed);
|
||||||
// clamp skip to path length
|
// clamp skip to path length
|
||||||
let skip = (self.skip as usize).min(self.as_str().len());
|
let skip = (self.skip as usize).min(self.as_str().len());
|
||||||
&self.path.path()[skip..]
|
&self.path.path()[skip..]
|
||||||
@@ -69,6 +72,8 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
|
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
|
profile_method!(path);
|
||||||
|
|
||||||
let skip = self.skip as usize;
|
let skip = self.skip as usize;
|
||||||
let path = self.path.path();
|
let path = self.path.path();
|
||||||
if skip <= path.len() {
|
if skip <= path.len() {
|
||||||
@@ -81,6 +86,8 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Set new path.
|
/// Set new path.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set(&mut self, path: T) {
|
pub fn set(&mut self, path: T) {
|
||||||
|
profile_method!(set);
|
||||||
|
|
||||||
self.skip = 0;
|
self.skip = 0;
|
||||||
self.path = path;
|
self.path = path;
|
||||||
self.segments.clear();
|
self.segments.clear();
|
||||||
@@ -89,6 +96,8 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Reset state.
|
/// Reset state.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
|
profile_method!(reset);
|
||||||
|
|
||||||
self.skip = 0;
|
self.skip = 0;
|
||||||
self.segments.clear();
|
self.segments.clear();
|
||||||
}
|
}
|
||||||
@@ -96,10 +105,13 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Skip first `n` chars in path.
|
/// Skip first `n` chars in path.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn skip(&mut self, n: u16) {
|
pub fn skip(&mut self, n: u16) {
|
||||||
|
profile_method!(skip);
|
||||||
self.skip += n;
|
self.skip += n;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add(&mut self, name: impl Into<Cow<'static, str>>, value: PathItem) {
|
pub(crate) fn add(&mut self, name: impl Into<Cow<'static, str>>, value: PathItem) {
|
||||||
|
profile_method!(add);
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))),
|
PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))),
|
||||||
PathItem::Segment(begin, end) => self.segments.push((
|
PathItem::Segment(begin, end) => self.segments.push((
|
||||||
@@ -115,6 +127,8 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
name: impl Into<Cow<'static, str>>,
|
name: impl Into<Cow<'static, str>>,
|
||||||
value: impl Into<Cow<'static, str>>,
|
value: impl Into<Cow<'static, str>>,
|
||||||
) {
|
) {
|
||||||
|
profile_method!(add_static);
|
||||||
|
|
||||||
self.segments
|
self.segments
|
||||||
.push((name.into(), PathItem::Static(value.into())));
|
.push((name.into(), PathItem::Static(value.into())));
|
||||||
}
|
}
|
||||||
@@ -133,6 +147,8 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
|
|
||||||
/// Get matched parameter by name without type conversion
|
/// Get matched parameter by name without type conversion
|
||||||
pub fn get(&self, name: &str) -> Option<&str> {
|
pub fn get(&self, name: &str) -> Option<&str> {
|
||||||
|
profile_method!(get);
|
||||||
|
|
||||||
for (seg_name, val) in self.segments.iter() {
|
for (seg_name, val) in self.segments.iter() {
|
||||||
if name == seg_name {
|
if name == seg_name {
|
||||||
return match val {
|
return match val {
|
||||||
@@ -151,6 +167,8 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
///
|
///
|
||||||
/// If keyed parameter is not available empty string is used as default value.
|
/// If keyed parameter is not available empty string is used as default value.
|
||||||
pub fn query(&self, key: &str) -> &str {
|
pub fn query(&self, key: &str) -> &str {
|
||||||
|
profile_method!(query);
|
||||||
|
|
||||||
if let Some(s) = self.get(key) {
|
if let Some(s) = self.get(key) {
|
||||||
s
|
s
|
||||||
} else {
|
} else {
|
||||||
@@ -168,6 +186,7 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
|
|
||||||
/// Try to deserialize matching parameters to a specified type `U`
|
/// Try to deserialize matching parameters to a specified type `U`
|
||||||
pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result<U, de::value::Error> {
|
pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result<U, de::value::Error> {
|
||||||
|
profile_method!(load);
|
||||||
de::Deserialize::deserialize(PathDeserializer::new(self))
|
de::Deserialize::deserialize(PathDeserializer::new(self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -242,7 +261,6 @@ mod tests {
|
|||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[allow(clippy::needless_borrow)]
|
|
||||||
#[test]
|
#[test]
|
||||||
fn deref_impls() {
|
fn deref_impls() {
|
||||||
let mut foo = Path::new("/foo");
|
let mut foo = Path::new("/foo");
|
||||||
|
@@ -1,89 +1,132 @@
|
|||||||
/// Partial percent-decoding.
|
#[allow(dead_code)]
|
||||||
///
|
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
||||||
/// Performs percent-decoding on a slice but can selectively skip decoding certain sequences.
|
|
||||||
///
|
#[allow(dead_code)]
|
||||||
/// # Examples
|
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
||||||
/// ```
|
|
||||||
/// # use actix_router::Quoter;
|
#[allow(dead_code)]
|
||||||
/// // + is set as a protected character and will not be decoded...
|
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
||||||
/// let q = Quoter::new(&[], b"+");
|
|
||||||
///
|
#[allow(dead_code)]
|
||||||
/// // ...but the other encoded characters (like the hyphen below) will.
|
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
||||||
/// assert_eq!(q.requote(b"/a%2Db%2Bc").unwrap(), b"/a-b%2Bc");
|
|
||||||
/// ```
|
#[allow(dead_code)]
|
||||||
|
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||||
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
1234567890
|
||||||
|
-._~";
|
||||||
|
|
||||||
|
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||||
|
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||||
|
1234567890
|
||||||
|
-._~
|
||||||
|
!$'()*,";
|
||||||
|
|
||||||
|
const QS: &[u8] = b"+&=;b";
|
||||||
|
|
||||||
|
/// A quoter
|
||||||
pub struct Quoter {
|
pub struct Quoter {
|
||||||
|
/// Simple bit-map of safe values in the 0-127 ASCII range.
|
||||||
|
safe_table: [u8; 16],
|
||||||
|
|
||||||
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
||||||
protected_table: AsciiBitmap,
|
protected_table: [u8; 16],
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Quoter {
|
impl Quoter {
|
||||||
/// Constructs a new `Quoter` instance given a set of protected ASCII bytes.
|
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
||||||
///
|
let mut quoter = Quoter {
|
||||||
/// The first argument is ignored but is kept for backward compatibility.
|
safe_table: [0; 16],
|
||||||
///
|
protected_table: [0; 16],
|
||||||
/// # Panics
|
};
|
||||||
/// Panics if any of the `protected` bytes are not in the 0-127 ASCII range.
|
|
||||||
pub fn new(_: &[u8], protected: &[u8]) -> Quoter {
|
// prepare safe table
|
||||||
let mut protected_table = AsciiBitmap::default();
|
for ch in 0..128 {
|
||||||
|
if ALLOWED.contains(&ch) {
|
||||||
|
set_bit(&mut quoter.safe_table, ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
if QS.contains(&ch) {
|
||||||
|
set_bit(&mut quoter.safe_table, ch);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for &ch in safe {
|
||||||
|
set_bit(&mut quoter.safe_table, ch)
|
||||||
|
}
|
||||||
|
|
||||||
// prepare protected table
|
// prepare protected table
|
||||||
for &ch in protected {
|
for &ch in protected {
|
||||||
protected_table.set_bit(ch);
|
set_bit(&mut quoter.safe_table, ch);
|
||||||
|
set_bit(&mut quoter.protected_table, ch);
|
||||||
}
|
}
|
||||||
|
|
||||||
Quoter { protected_table }
|
quoter
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes the next escape sequence, if any, and advances `val`.
|
/// Decodes safe percent-encoded sequences from `val`.
|
||||||
#[inline(always)]
|
|
||||||
fn decode_next<'a>(&self, val: &mut &'a [u8]) -> Option<(&'a [u8], u8)> {
|
|
||||||
for i in 0..val.len() {
|
|
||||||
if let (prev, [b'%', p1, p2, rem @ ..]) = val.split_at(i) {
|
|
||||||
if let Some(ch) = hex_pair_to_char(*p1, *p2)
|
|
||||||
// ignore protected ascii bytes
|
|
||||||
.filter(|&ch| !(ch < 128 && self.protected_table.bit_at(ch)))
|
|
||||||
{
|
|
||||||
*val = rem;
|
|
||||||
return Some((prev, ch));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Partially percent-decodes the given bytes.
|
|
||||||
///
|
///
|
||||||
/// Escape sequences of the protected set are *not* decoded.
|
/// Returns `None` when no modification to the original byte string was required.
|
||||||
///
|
///
|
||||||
/// Returns `None` when no modification to the original bytes was required.
|
/// Non-ASCII bytes are accepted as valid input.
|
||||||
///
|
///
|
||||||
/// Invalid/incomplete percent-encoding sequences are passed unmodified.
|
/// Behavior for invalid/incomplete percent-encoding sequences is unspecified and may include
|
||||||
|
/// removing the invalid sequence from the output or passing it as-is.
|
||||||
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
||||||
let mut remaining = val;
|
let mut has_pct = 0;
|
||||||
|
let mut pct = [b'%', 0, 0];
|
||||||
|
let mut idx = 0;
|
||||||
|
let mut cloned: Option<Vec<u8>> = None;
|
||||||
|
|
||||||
// early return indicates that no percent-encoded sequences exist and we can skip allocation
|
let len = val.len();
|
||||||
let (pre, decoded_char) = self.decode_next(&mut remaining)?;
|
|
||||||
|
|
||||||
// decoded output will always be shorter than the input
|
while idx < len {
|
||||||
let mut decoded = Vec::<u8>::with_capacity(val.len());
|
let ch = val[idx];
|
||||||
|
|
||||||
// push first segment and decoded char
|
if has_pct != 0 {
|
||||||
decoded.extend_from_slice(pre);
|
pct[has_pct] = val[idx];
|
||||||
decoded.push(decoded_char);
|
has_pct += 1;
|
||||||
|
|
||||||
// decode and push rest of segments and decoded chars
|
if has_pct == 3 {
|
||||||
while let Some((prev, ch)) = self.decode_next(&mut remaining) {
|
has_pct = 0;
|
||||||
// this ugly conditional achieves +50% perf in cases where this is a tight loop.
|
let buf = cloned.as_mut().unwrap();
|
||||||
if !prev.is_empty() {
|
|
||||||
decoded.extend_from_slice(prev);
|
if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) {
|
||||||
|
if ch < 128 {
|
||||||
|
if bit_at(&self.protected_table, ch) {
|
||||||
|
buf.extend_from_slice(&pct);
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if bit_at(&self.safe_table, ch) {
|
||||||
|
buf.push(ch);
|
||||||
|
idx += 1;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
buf.push(ch);
|
||||||
|
} else {
|
||||||
|
buf.extend_from_slice(&pct[..]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else if ch == b'%' {
|
||||||
|
has_pct = 1;
|
||||||
|
|
||||||
|
if cloned.is_none() {
|
||||||
|
let mut c = Vec::with_capacity(len);
|
||||||
|
c.extend_from_slice(&val[..idx]);
|
||||||
|
cloned = Some(c);
|
||||||
|
}
|
||||||
|
} else if let Some(ref mut cloned) = cloned {
|
||||||
|
cloned.push(ch)
|
||||||
}
|
}
|
||||||
decoded.push(ch);
|
|
||||||
|
idx += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
decoded.extend_from_slice(remaining);
|
cloned
|
||||||
|
|
||||||
Some(decoded)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
||||||
@@ -92,6 +135,24 @@ impl Quoter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer
|
||||||
|
/// representation from `0x0`–`0xF`.
|
||||||
|
///
|
||||||
|
/// - `0x30 ('0') => 0x0`
|
||||||
|
/// - `0x39 ('9') => 0x9`
|
||||||
|
/// - `0x41 ('a') => 0xA`
|
||||||
|
/// - `0x61 ('A') => 0xA`
|
||||||
|
/// - `0x46 ('f') => 0xF`
|
||||||
|
/// - `0x66 ('F') => 0xF`
|
||||||
|
fn from_ascii_hex(v: u8) -> Option<u8> {
|
||||||
|
match v {
|
||||||
|
b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30
|
||||||
|
b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41
|
||||||
|
b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61
|
||||||
|
_ => None,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Decode a ASCII hex-encoded pair to an integer.
|
/// Decode a ASCII hex-encoded pair to an integer.
|
||||||
///
|
///
|
||||||
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
||||||
@@ -99,52 +160,64 @@ impl Quoter {
|
|||||||
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
||||||
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
||||||
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
||||||
#[inline(always)]
|
|
||||||
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
||||||
let d_high = char::from(d1).to_digit(16)?;
|
let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?);
|
||||||
let d_low = char::from(d2).to_digit(16)?;
|
|
||||||
|
|
||||||
// left shift high nibble by 4 bits
|
// left shift high nibble by 4 bits
|
||||||
Some((d_high as u8) << 4 | (d_low as u8))
|
Some(d_high << 4 | d_low)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default, Clone)]
|
/// Sets bit in given bit-map to 1=true.
|
||||||
struct AsciiBitmap {
|
///
|
||||||
array: [u8; 16],
|
/// # Panics
|
||||||
|
/// Panics if `ch` index is out of bounds.
|
||||||
|
fn set_bit(array: &mut [u8], ch: u8) {
|
||||||
|
array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
||||||
}
|
}
|
||||||
|
|
||||||
impl AsciiBitmap {
|
/// Returns true if bit to true in given bit-map.
|
||||||
/// Sets bit in given bit-map to 1=true.
|
///
|
||||||
///
|
/// # Panics
|
||||||
/// # Panics
|
/// Panics if `ch` index is out of bounds.
|
||||||
/// Panics if `ch` index is out of bounds.
|
fn bit_at(array: &[u8], ch: u8) -> bool {
|
||||||
fn set_bit(&mut self, ch: u8) {
|
array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
||||||
self.array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Returns true if bit to true in given bit-map.
|
|
||||||
///
|
|
||||||
/// # Panics
|
|
||||||
/// Panics if `ch` index is out of bounds.
|
|
||||||
fn bit_at(&self, ch: u8) -> bool {
|
|
||||||
self.array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hex_encoding() {
|
||||||
|
let hex = b"0123456789abcdefABCDEF";
|
||||||
|
|
||||||
|
for i in 0..256 {
|
||||||
|
let c = i as u8;
|
||||||
|
if hex.contains(&c) {
|
||||||
|
assert!(from_ascii_hex(c).is_some())
|
||||||
|
} else {
|
||||||
|
assert!(from_ascii_hex(c).is_none())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let expected = [
|
||||||
|
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
|
||||||
|
];
|
||||||
|
for i in 0..hex.len() {
|
||||||
|
assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_quoter() {
|
fn custom_quoter() {
|
||||||
let q = Quoter::new(b"", b"+");
|
let q = Quoter::new(b"", b"+");
|
||||||
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
||||||
assert_eq!(q.requote(b"/a%2Bc"), None);
|
assert_eq!(q.requote(b"/a%2Bc").unwrap(), b"/a%2Bc");
|
||||||
|
|
||||||
let q = Quoter::new(b"%+", b"/");
|
let q = Quoter::new(b"%+", b"/");
|
||||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
||||||
assert_eq!(q.requote(b"/a%2fb"), None);
|
assert_eq!(q.requote(b"/a%2fb").unwrap(), b"/a%2fb");
|
||||||
assert_eq!(q.requote(b"/a%2Fb"), None);
|
assert_eq!(q.requote(b"/a%2Fb").unwrap(), b"/a%2Fb");
|
||||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
||||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||||
@@ -160,8 +233,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn invalid_sequences() {
|
fn invalid_sequences() {
|
||||||
let q = Quoter::new(b"%+", b"/");
|
let q = Quoter::new(b"%+", b"/");
|
||||||
assert_eq!(q.requote(b"/a%2x%2X%%"), None);
|
assert_eq!(q.requote(b"/a%2x%2X%%").unwrap(), b"/a%2x%2X");
|
||||||
assert_eq!(q.requote(b"/a%20%2X%%").unwrap(), b"/a %2X%%");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@@ -5,8 +5,8 @@ use std::{
|
|||||||
mem,
|
mem,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use firestorm::{profile_fn, profile_method, profile_section};
|
||||||
use regex::{escape, Regex, RegexSet};
|
use regex::{escape, Regex, RegexSet};
|
||||||
use tracing::error;
|
|
||||||
|
|
||||||
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
||||||
|
|
||||||
@@ -271,6 +271,7 @@ impl ResourceDef {
|
|||||||
/// assert!(!resource.is_match("/foo"));
|
/// assert!(!resource.is_match("/foo"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
||||||
|
profile_method!(new);
|
||||||
Self::construct(paths, false)
|
Self::construct(paths, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -298,6 +299,7 @@ impl ResourceDef {
|
|||||||
/// assert!(!resource.is_match("/foo"));
|
/// assert!(!resource.is_match("/foo"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
||||||
|
profile_method!(prefix);
|
||||||
ResourceDef::construct(paths, true)
|
ResourceDef::construct(paths, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -322,6 +324,7 @@ impl ResourceDef {
|
|||||||
/// assert!(!resource.is_match("user/123"));
|
/// assert!(!resource.is_match("user/123"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn root_prefix(path: &str) -> Self {
|
pub fn root_prefix(path: &str) -> Self {
|
||||||
|
profile_method!(root_prefix);
|
||||||
ResourceDef::prefix(insert_slash(path).into_owned())
|
ResourceDef::prefix(insert_slash(path).into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -545,6 +548,8 @@ impl ResourceDef {
|
|||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_match(&self, path: &str) -> bool {
|
pub fn is_match(&self, path: &str) -> bool {
|
||||||
|
profile_method!(is_match);
|
||||||
|
|
||||||
// this function could be expressed as:
|
// this function could be expressed as:
|
||||||
// `self.find_match(path).is_some()`
|
// `self.find_match(path).is_some()`
|
||||||
// but this skips some checks and uses potentially faster regex methods
|
// but this skips some checks and uses potentially faster regex methods
|
||||||
@@ -592,6 +597,8 @@ impl ResourceDef {
|
|||||||
/// assert_eq!(resource.find_match("/profile/1234"), Some(13));
|
/// assert_eq!(resource.find_match("/profile/1234"), Some(13));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn find_match(&self, path: &str) -> Option<usize> {
|
pub fn find_match(&self, path: &str) -> Option<usize> {
|
||||||
|
profile_method!(find_match);
|
||||||
|
|
||||||
match &self.pat_type {
|
match &self.pat_type {
|
||||||
PatternType::Static(pattern) => self.static_match(pattern, path),
|
PatternType::Static(pattern) => self.static_match(pattern, path),
|
||||||
|
|
||||||
@@ -626,6 +633,7 @@ impl ResourceDef {
|
|||||||
/// assert_eq!(path.unprocessed(), "");
|
/// assert_eq!(path.unprocessed(), "");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
|
pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
|
||||||
|
profile_method!(capture_match_info);
|
||||||
self.capture_match_info_fn(resource, |_| true)
|
self.capture_match_info_fn(resource, |_| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -649,7 +657,7 @@ impl ResourceDef {
|
|||||||
/// resource.capture_match_info_fn(
|
/// resource.capture_match_info_fn(
|
||||||
/// path,
|
/// path,
|
||||||
/// // when env var is not set, reject when path contains "admin"
|
/// // when env var is not set, reject when path contains "admin"
|
||||||
/// |path| !(!admin_allowed && path.as_str().contains("admin")),
|
/// |res| !(!admin_allowed && res.path().contains("admin")),
|
||||||
/// )
|
/// )
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@@ -671,35 +679,56 @@ impl ResourceDef {
|
|||||||
R: Resource,
|
R: Resource,
|
||||||
F: FnOnce(&R) -> bool,
|
F: FnOnce(&R) -> bool,
|
||||||
{
|
{
|
||||||
|
profile_method!(capture_match_info_fn);
|
||||||
|
|
||||||
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
|
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
|
||||||
let path = resource.resource_path();
|
let path = resource.resource_path();
|
||||||
let path_str = path.unprocessed();
|
let path_str = path.unprocessed();
|
||||||
|
|
||||||
let (matched_len, matched_vars) = match &self.pat_type {
|
let (matched_len, matched_vars) = match &self.pat_type {
|
||||||
PatternType::Static(pattern) => match self.static_match(pattern, path_str) {
|
PatternType::Static(pattern) => {
|
||||||
Some(len) => (len, None),
|
profile_section!(pattern_static_or_prefix);
|
||||||
None => return false,
|
|
||||||
},
|
match self.static_match(pattern, path_str) {
|
||||||
|
Some(len) => (len, None),
|
||||||
|
None => return false,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
PatternType::Dynamic(re, names) => {
|
PatternType::Dynamic(re, names) => {
|
||||||
let captures = match re.captures(path.unprocessed()) {
|
profile_section!(pattern_dynamic);
|
||||||
Some(captures) => captures,
|
|
||||||
_ => return false,
|
let captures = {
|
||||||
|
profile_section!(pattern_dynamic_regex_exec);
|
||||||
|
|
||||||
|
match re.captures(path.unprocessed()) {
|
||||||
|
Some(captures) => captures,
|
||||||
|
_ => return false,
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
for (no, name) in names.iter().enumerate() {
|
{
|
||||||
if let Some(m) = captures.name(name) {
|
profile_section!(pattern_dynamic_extract_captures);
|
||||||
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
|
||||||
} else {
|
for (no, name) in names.iter().enumerate() {
|
||||||
error!("Dynamic path match but not all segments found: {}", name);
|
if let Some(m) = captures.name(name) {
|
||||||
return false;
|
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
||||||
|
} else {
|
||||||
|
log::error!(
|
||||||
|
"Dynamic path match but not all segments found: {}",
|
||||||
|
name
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
|
|
||||||
(captures[1].len(), Some(names))
|
(captures[1].len(), Some(names))
|
||||||
}
|
}
|
||||||
|
|
||||||
PatternType::DynamicSet(re, params) => {
|
PatternType::DynamicSet(re, params) => {
|
||||||
|
profile_section!(pattern_dynamic_set);
|
||||||
|
|
||||||
let path = path.unprocessed();
|
let path = path.unprocessed();
|
||||||
let (pattern, names) = match re.matches(path).into_iter().next() {
|
let (pattern, names) = match re.matches(path).into_iter().next() {
|
||||||
Some(idx) => ¶ms[idx],
|
Some(idx) => ¶ms[idx],
|
||||||
@@ -715,7 +744,7 @@ impl ResourceDef {
|
|||||||
if let Some(m) = captures.name(name) {
|
if let Some(m) = captures.name(name) {
|
||||||
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
||||||
} else {
|
} else {
|
||||||
error!("Dynamic path match but not all segments found: {}", name);
|
log::error!("Dynamic path match but not all segments found: {}", name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -782,6 +811,7 @@ impl ResourceDef {
|
|||||||
I: IntoIterator,
|
I: IntoIterator,
|
||||||
I::Item: AsRef<str>,
|
I::Item: AsRef<str>,
|
||||||
{
|
{
|
||||||
|
profile_method!(resource_path_from_iter);
|
||||||
let mut iter = values.into_iter();
|
let mut iter = values.into_iter();
|
||||||
self.build_resource_path(path, |_| iter.next())
|
self.build_resource_path(path, |_| iter.next())
|
||||||
}
|
}
|
||||||
@@ -817,6 +847,7 @@ impl ResourceDef {
|
|||||||
V: AsRef<str>,
|
V: AsRef<str>,
|
||||||
S: BuildHasher,
|
S: BuildHasher,
|
||||||
{
|
{
|
||||||
|
profile_method!(resource_path_from_map);
|
||||||
self.build_resource_path(path, |name| values.get(name))
|
self.build_resource_path(path, |name| values.get(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -837,6 +868,8 @@ impl ResourceDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
||||||
|
profile_method!(construct);
|
||||||
|
|
||||||
let patterns = paths.patterns();
|
let patterns = paths.patterns();
|
||||||
let (pat_type, segments) = match &patterns {
|
let (pat_type, segments) = match &patterns {
|
||||||
Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
|
Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
|
||||||
@@ -895,6 +928,8 @@ impl ResourceDef {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if given patterns does not contain a dynamic segment.
|
/// Panics if given patterns does not contain a dynamic segment.
|
||||||
fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) {
|
fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) {
|
||||||
|
profile_method!(parse_param);
|
||||||
|
|
||||||
const DEFAULT_PATTERN: &str = "[^/]+";
|
const DEFAULT_PATTERN: &str = "[^/]+";
|
||||||
const DEFAULT_PATTERN_TAIL: &str = ".*";
|
const DEFAULT_PATTERN_TAIL: &str = ".*";
|
||||||
|
|
||||||
@@ -964,6 +999,8 @@ impl ResourceDef {
|
|||||||
is_prefix: bool,
|
is_prefix: bool,
|
||||||
force_dynamic: bool,
|
force_dynamic: bool,
|
||||||
) -> (PatternType, Vec<PatternSegment>) {
|
) -> (PatternType, Vec<PatternSegment>) {
|
||||||
|
profile_method!(parse);
|
||||||
|
|
||||||
if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') {
|
if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') {
|
||||||
// pattern is static
|
// pattern is static
|
||||||
return (
|
return (
|
||||||
@@ -1001,7 +1038,7 @@ impl ResourceDef {
|
|||||||
// tail segments in prefixes have no defined semantics
|
// tail segments in prefixes have no defined semantics
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
tracing::warn!(
|
log::warn!(
|
||||||
"Prefix resources should not have tail segments. \
|
"Prefix resources should not have tail segments. \
|
||||||
Use `ResourceDef::new` constructor. \
|
Use `ResourceDef::new` constructor. \
|
||||||
This may become a panic in the future."
|
This may become a panic in the future."
|
||||||
@@ -1016,7 +1053,7 @@ impl ResourceDef {
|
|||||||
// unnamed tail segment
|
// unnamed tail segment
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
tracing::warn!(
|
log::warn!(
|
||||||
"Tail segments must have names. \
|
"Tail segments must have names. \
|
||||||
Consider `.../{{tail}}*`. \
|
Consider `.../{{tail}}*`. \
|
||||||
This may become a panic in the future."
|
This may become a panic in the future."
|
||||||
@@ -1096,6 +1133,8 @@ impl From<String> for ResourceDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
|
pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
|
||||||
|
profile_fn!(insert_slash);
|
||||||
|
|
||||||
if !path.is_empty() && !path.starts_with('/') {
|
if !path.is_empty() && !path.starts_with('/') {
|
||||||
let mut new_path = String::with_capacity(path.len() + 1);
|
let mut new_path = String::with_capacity(path.len() + 1);
|
||||||
new_path.push('/');
|
new_path.push('/');
|
||||||
@@ -1503,31 +1542,31 @@ mod tests {
|
|||||||
fn build_path_list() {
|
fn build_path_list() {
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/test");
|
let resource = ResourceDef::new("/user/{item1}/test");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter()));
|
||||||
assert_eq!(s, "/user/user1/test");
|
assert_eq!(s, "/user/user1/test");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/{item2}/test");
|
let resource = ResourceDef::new("/user/{item1}/{item2}/test");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||||
assert_eq!(s, "/user/item/item2/test");
|
assert_eq!(s, "/user/item/item2/test");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/{item2}");
|
let resource = ResourceDef::new("/user/{item1}/{item2}");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||||
assert_eq!(s, "/user/item/item2");
|
assert_eq!(s, "/user/item/item2");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
let resource = ResourceDef::new("/user/{item1}/{item2}/");
|
let resource = ResourceDef::new("/user/{item1}/{item2}/");
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||||
assert_eq!(s, "/user/item/item2/");
|
assert_eq!(s, "/user/item/item2/");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
|
assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter()));
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["item", "item2"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut (&["item", "item2"]).iter()));
|
||||||
assert_eq!(s, "/user/item/item2/");
|
assert_eq!(s, "/user/item/item2/");
|
||||||
assert!(!resource.resource_path_from_iter(&mut s, &mut ["item"].iter()));
|
assert!(!resource.resource_path_from_iter(&mut s, &mut (&["item"]).iter()));
|
||||||
|
|
||||||
let mut s = String::new();
|
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, &mut vec!["item", "item2"].iter()));
|
||||||
@@ -1604,10 +1643,10 @@ mod tests {
|
|||||||
let resource = ResourceDef::new("/user/{item1}*");
|
let resource = ResourceDef::new("/user/{item1}*");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(!resource.resource_path_from_iter(&mut s, &mut [""; 0].iter()));
|
assert!(!resource.resource_path_from_iter(&mut s, &mut (&[""; 0]).iter()));
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
assert!(resource.resource_path_from_iter(&mut s, &mut ["user1"].iter()));
|
assert!(resource.resource_path_from_iter(&mut s, &mut (&["user1"]).iter()));
|
||||||
assert_eq!(s, "/user/user1");
|
assert_eq!(s, "/user/user1");
|
||||||
|
|
||||||
let mut s = String::new();
|
let mut s = String::new();
|
||||||
|
@@ -27,7 +27,7 @@ impl<'a> ResourcePath for &'a str {
|
|||||||
|
|
||||||
impl ResourcePath for bytestring::ByteString {
|
impl ResourcePath for bytestring::ByteString {
|
||||||
fn path(&self) -> &str {
|
fn path(&self) -> &str {
|
||||||
self
|
&*self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
|
use firestorm::profile_method;
|
||||||
|
|
||||||
use crate::{IntoPatterns, Resource, ResourceDef};
|
use crate::{IntoPatterns, Resource, ResourceDef};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||||
pub struct ResourceId(pub u16);
|
pub struct ResourceId(pub u16);
|
||||||
|
|
||||||
/// Resource router.
|
/// Resource router.
|
||||||
@@ -28,6 +30,7 @@ impl<T, U> Router<T, U> {
|
|||||||
where
|
where
|
||||||
R: Resource,
|
R: Resource,
|
||||||
{
|
{
|
||||||
|
profile_method!(recognize);
|
||||||
self.recognize_fn(resource, |_, _| true)
|
self.recognize_fn(resource, |_, _| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -36,6 +39,7 @@ impl<T, U> Router<T, U> {
|
|||||||
where
|
where
|
||||||
R: Resource,
|
R: Resource,
|
||||||
{
|
{
|
||||||
|
profile_method!(recognize_mut);
|
||||||
self.recognize_mut_fn(resource, |_, _| true)
|
self.recognize_mut_fn(resource, |_, _| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -51,6 +55,8 @@ impl<T, U> Router<T, U> {
|
|||||||
R: Resource,
|
R: Resource,
|
||||||
F: FnMut(&R, &U) -> bool,
|
F: FnMut(&R, &U) -> bool,
|
||||||
{
|
{
|
||||||
|
profile_method!(recognize_checked);
|
||||||
|
|
||||||
for (rdef, val, ctx) in self.routes.iter() {
|
for (rdef, val, ctx) in self.routes.iter() {
|
||||||
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
||||||
return Some((val, ResourceId(rdef.id())));
|
return Some((val, ResourceId(rdef.id())));
|
||||||
@@ -71,6 +77,8 @@ impl<T, U> Router<T, U> {
|
|||||||
R: Resource,
|
R: Resource,
|
||||||
F: FnMut(&R, &U) -> bool,
|
F: FnMut(&R, &U) -> bool,
|
||||||
{
|
{
|
||||||
|
profile_method!(recognize_mut_checked);
|
||||||
|
|
||||||
for (rdef, val, ctx) in self.routes.iter_mut() {
|
for (rdef, val, ctx) in self.routes.iter_mut() {
|
||||||
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
||||||
return Some((val, ResourceId(rdef.id())));
|
return Some((val, ResourceId(rdef.id())));
|
||||||
@@ -96,6 +104,7 @@ impl<T, U> RouterBuilder<T, U> {
|
|||||||
val: T,
|
val: T,
|
||||||
ctx: U,
|
ctx: U,
|
||||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
|
profile_method!(push);
|
||||||
self.routes.push((rdef, val, ctx));
|
self.routes.push((rdef, val, ctx));
|
||||||
self.routes
|
self.routes
|
||||||
.last_mut()
|
.last_mut()
|
||||||
@@ -122,6 +131,7 @@ where
|
|||||||
path: impl IntoPatterns,
|
path: impl IntoPatterns,
|
||||||
val: T,
|
val: T,
|
||||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
|
profile_method!(path);
|
||||||
self.push(ResourceDef::new(path), val, U::default())
|
self.push(ResourceDef::new(path), val, U::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -131,11 +141,13 @@ where
|
|||||||
prefix: impl IntoPatterns,
|
prefix: impl IntoPatterns,
|
||||||
val: T,
|
val: T,
|
||||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
|
profile_method!(prefix);
|
||||||
self.push(ResourceDef::prefix(prefix), val, U::default())
|
self.push(ResourceDef::prefix(prefix), val, U::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers resource for [`ResourceDef`].
|
/// Registers resource for [`ResourceDef`].
|
||||||
pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) {
|
pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
|
profile_method!(rdef);
|
||||||
self.push(rdef, val, U::default())
|
self.push(rdef, val, U::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,7 @@ use crate::ResourcePath;
|
|||||||
use crate::Quoter;
|
use crate::Quoter;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+");
|
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
@@ -1,11 +1,6 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
- Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
|
|
||||||
|
|
||||||
|
|
||||||
## 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 - 2022-02-16
|
## 0.1.0-beta.13 - 2022-02-16
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-test"
|
name = "actix-test"
|
||||||
version = "0.1.0"
|
version = "0.1.0-beta.13"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
@@ -29,20 +29,20 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3"
|
actix-http = "3.0.0"
|
||||||
actix-http-test = "3"
|
actix-http-test = "3.0.0-beta.13"
|
||||||
actix-rt = "2.1"
|
actix-rt = "2.1"
|
||||||
actix-service = "2"
|
actix-service = "2.0.0"
|
||||||
actix-utils = "3"
|
actix-utils = "3.0.0"
|
||||||
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
||||||
awc = { version = "3", default-features = false, features = ["cookies"] }
|
awc = { version = "3.0.0-beta.21", default-features = false, features = ["cookies"] }
|
||||||
|
|
||||||
futures-core = { version = "0.3.17", default-features = false, features = ["std"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||||
futures-util = { version = "0.3.17", default-features = false, features = [] }
|
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||||
tokio = { version = "1.18.4", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
@@ -321,7 +321,6 @@ where
|
|||||||
// all thread managed resources should be dropped at this point
|
// all thread managed resources should be dropped at this point
|
||||||
});
|
});
|
||||||
|
|
||||||
#[allow(clippy::let_underscore_future)]
|
|
||||||
let _ = thread_stop_tx.send(());
|
let _ = thread_stop_tx.send(());
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -568,7 +567,6 @@ impl Drop for TestServer {
|
|||||||
// without needing to await anything
|
// without needing to await anything
|
||||||
|
|
||||||
// signal server to stop
|
// signal server to stop
|
||||||
#[allow(clippy::let_underscore_future)]
|
|
||||||
let _ = self.server.stop(true);
|
let _ = self.server.stop(true);
|
||||||
|
|
||||||
// signal system to stop
|
// signal system to stop
|
||||||
|
@@ -1,16 +1,6 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2022-xx-xx
|
## Unreleased - 2021-xx-xx
|
||||||
|
|
||||||
|
|
||||||
## 4.2.0 - 2023-01-21
|
|
||||||
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
|
||||||
|
|
||||||
|
|
||||||
## 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 - 2022-02-25
|
## 4.0.0 - 2022-02-25
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web-actors"
|
name = "actix-web-actors"
|
||||||
version = "4.2.0"
|
version = "4.0.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix actors support for Actix Web"
|
description = "Actix actors support for Actix Web"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
@@ -14,25 +14,21 @@ name = "actix_web_actors"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = { version = ">=0.12, <0.14", default-features = false }
|
actix = { version = "0.12.0", default-features = false }
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3"
|
actix-http = "3.0.0"
|
||||||
actix-web = { version = "4", default-features = false }
|
actix-web = { version = "4.0.0", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
futures-core = { version = "0.3.17", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
tokio = { version = "1.18.4", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
tokio-util = { version = "0.7", features = ["codec"] }
|
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1"
|
actix-test = "0.1.0-beta.13"
|
||||||
awc = { version = "3", default-features = false }
|
awc = { version = "3.0.0-beta.21", default-features = false }
|
||||||
actix-web = { version = "4", features = ["macros"] }
|
|
||||||
|
|
||||||
mime = "0.3"
|
|
||||||
|
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
futures-util = { version = "0.3.17", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
@@ -3,11 +3,11 @@
|
|||||||
> Actix actors support for Actix Web.
|
> Actix actors support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://docs.rs/actix-web-actors/4.2.0)
|
[](https://docs.rs/actix-web-actors/4.0.0)
|
||||||

|
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-web-actors/4.2.0)
|
[](https://deps.rs/crate/actix-web-actors/4.0.0)
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@@ -14,58 +14,6 @@ use futures_core::Stream;
|
|||||||
use tokio::sync::oneshot::Sender;
|
use tokio::sync::oneshot::Sender;
|
||||||
|
|
||||||
/// Execution context for HTTP actors
|
/// Execution context for HTTP actors
|
||||||
///
|
|
||||||
/// # Example
|
|
||||||
///
|
|
||||||
/// A demonstration of [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) using actors:
|
|
||||||
///
|
|
||||||
/// ```no_run
|
|
||||||
/// use std::time::Duration;
|
|
||||||
///
|
|
||||||
/// use actix::{Actor, AsyncContext};
|
|
||||||
/// use actix_web::{get, http::header, App, HttpResponse, HttpServer};
|
|
||||||
/// use actix_web_actors::HttpContext;
|
|
||||||
/// use bytes::Bytes;
|
|
||||||
///
|
|
||||||
/// struct MyActor {
|
|
||||||
/// count: usize,
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// impl Actor for MyActor {
|
|
||||||
/// type Context = HttpContext<Self>;
|
|
||||||
///
|
|
||||||
/// fn started(&mut self, ctx: &mut Self::Context) {
|
|
||||||
/// ctx.run_later(Duration::from_millis(100), Self::write);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// impl MyActor {
|
|
||||||
/// fn write(&mut self, ctx: &mut HttpContext<Self>) {
|
|
||||||
/// self.count += 1;
|
|
||||||
/// if self.count > 3 {
|
|
||||||
/// ctx.write_eof()
|
|
||||||
/// } else {
|
|
||||||
/// ctx.write(Bytes::from(format!("event: count\ndata: {}\n\n", self.count)));
|
|
||||||
/// ctx.run_later(Duration::from_millis(100), Self::write);
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// #[get("/")]
|
|
||||||
/// async fn index() -> HttpResponse {
|
|
||||||
/// HttpResponse::Ok()
|
|
||||||
/// .insert_header(header::ContentType(mime::TEXT_EVENT_STREAM))
|
|
||||||
/// .streaming(HttpContext::create(MyActor { count: 0 }))
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// #[actix_web::main]
|
|
||||||
/// async fn main() -> std::io::Result<()> {
|
|
||||||
/// HttpServer::new(|| App::new().service(index))
|
|
||||||
/// .bind(("127.0.0.1", 8080))?
|
|
||||||
/// .run()
|
|
||||||
/// .await
|
|
||||||
/// }
|
|
||||||
/// ```
|
|
||||||
pub struct HttpContext<A>
|
pub struct HttpContext<A>
|
||||||
where
|
where
|
||||||
A: Actor<Context = HttpContext<A>>,
|
A: Actor<Context = HttpContext<A>>,
|
||||||
@@ -262,7 +210,7 @@ mod tests {
|
|||||||
type Context = HttpContext<Self>;
|
type Context = HttpContext<Self>;
|
||||||
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
ctx.run_later(Duration::from_millis(100), Self::write);
|
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -273,7 +221,7 @@ mod tests {
|
|||||||
ctx.write_eof()
|
ctx.write_eof()
|
||||||
} else {
|
} else {
|
||||||
ctx.write(Bytes::from(format!("LINE-{}", self.count)));
|
ctx.write(Bytes::from(format!("LINE-{}", self.count)));
|
||||||
ctx.run_later(Duration::from_millis(100), Self::write);
|
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,63 +1,7 @@
|
|||||||
//! Actix actors support for Actix Web.
|
//! Actix actors support for Actix Web.
|
||||||
//!
|
|
||||||
//! # Examples
|
|
||||||
//!
|
|
||||||
//! ```no_run
|
|
||||||
//! use actix::{Actor, StreamHandler};
|
|
||||||
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
|
||||||
//! use actix_web_actors::ws;
|
|
||||||
//!
|
|
||||||
//! /// Define Websocket actor
|
|
||||||
//! struct MyWs;
|
|
||||||
//!
|
|
||||||
//! impl Actor for MyWs {
|
|
||||||
//! type Context = ws::WebsocketContext<Self>;
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! /// Handler for ws::Message message
|
|
||||||
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
|
|
||||||
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
|
||||||
//! match msg {
|
|
||||||
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
|
||||||
//! Ok(ws::Message::Text(text)) => ctx.text(text),
|
|
||||||
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
|
|
||||||
//! _ => (),
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! #[get("/ws")]
|
|
||||||
//! async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
|
||||||
//! ws::start(MyWs, &req, stream)
|
|
||||||
//! }
|
|
||||||
//!
|
|
||||||
//! #[actix_web::main]
|
|
||||||
//! async fn main() -> std::io::Result<()> {
|
|
||||||
//! HttpServer::new(|| App::new().service(index))
|
|
||||||
//! .bind(("127.0.0.1", 8080))?
|
|
||||||
//! .run()
|
|
||||||
//! .await
|
|
||||||
//! }
|
|
||||||
//! ```
|
|
||||||
//!
|
|
||||||
//! # Documentation & Community Resources
|
|
||||||
//! In addition to this API documentation, several other resources are available:
|
|
||||||
//!
|
|
||||||
//! * [Website & User Guide](https://actix.rs/)
|
|
||||||
//! * [Documentation for `actix_web`](actix_web)
|
|
||||||
//! * [Examples Repository](https://github.com/actix/examples)
|
|
||||||
//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x)
|
|
||||||
//!
|
|
||||||
//! To get started navigating the API docs, you may consider looking at the following pages first:
|
|
||||||
//!
|
|
||||||
//! * [`ws`]: This module provides actor support for WebSockets.
|
|
||||||
//!
|
|
||||||
//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses.
|
|
||||||
//!
|
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
#![allow(clippy::uninlined_format_args)]
|
|
||||||
|
|
||||||
mod context;
|
mod context;
|
||||||
pub mod ws;
|
pub mod ws;
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user