Compare commits

..

No commits in common. "eb10b747511865fddff90ebde2c26460d5d7f911" and "98c99f3bc2e9f706287944f38c4c7c25495a74f8" have entirely different histories.

311 changed files with 4212 additions and 10030 deletions

View File

@ -1,10 +1,17 @@
[alias] [alias]
lint = "clippy --workspace --all-targets -- -Dclippy::todo" lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
lint-all = "clippy --workspace --all-features --all-targets -- -Dclippy::todo" lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
# lib checking # lib checking
ci-check-min = "hack --workspace check --no-default-features" ci-check-min = "hack --workspace check --no-default-features"
ci-check-default = "hack --workspace check" ci-check-default = "hack --workspace check"
ci-check-default-tests = "check --workspace --tests" ci-check-default-tests = "check --workspace --tests"
ci-check-all-feature-powerset="hack --workspace --feature-powerset --depth=4 --skip=__compress,experimental-io-uring check" ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,experimental-io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --depth=4 --skip=__compress check" ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
# testing
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
# compile docs as docs.rs would
# RUSTDOCFLAGS="--cfg=docsrs" cargo +nightly doc --no-deps --workspace

View File

@ -3,40 +3,34 @@ name: Bug Report
about: Create a bug report. about: Create a bug report.
--- ---
Your issue may already be reported! Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one. Your issue may already be reported!
Please search on the [Actix Web issue tracker](https://github.com/actix/actix-web/issues) before creating one.
## Expected Behavior ## Expected Behavior
<!--- If you're describing a bug, tell us what should happen --> <!--- If you're describing a bug, tell us what should happen -->
<!--- If you're suggesting a change/improvement, tell us how it should work --> <!--- If you're suggesting a change/improvement, tell us how it should work -->
## Current Behavior ## Current Behavior
<!--- If describing a bug, tell us what happens instead of the expected behavior --> <!--- If describing a bug, tell us what happens instead of the expected behavior -->
<!--- If suggesting a change/improvement, explain the difference from current behavior --> <!--- If suggesting a change/improvement, explain the difference from current behavior -->
## Possible Solution ## Possible Solution
<!--- Not obligatory, but suggest a fix/reason for the bug, --> <!--- Not obligatory, but suggest a fix/reason for the bug, -->
<!--- or ideas how to implement the addition or change --> <!--- or ideas how to implement the addition or change -->
## Steps to Reproduce (for bugs) ## Steps to Reproduce (for bugs)
<!--- Provide a link to a live example, or an unambiguous set of steps to --> <!--- Provide a link to a live example, or an unambiguous set of steps to -->
<!--- reproduce this bug. Include code to reproduce, if relevant --> <!--- reproduce this bug. Include code to reproduce, if relevant -->
1. 1.
2. 2.
3. 3.
4. 4.
## Context ## Context
<!--- How has this issue affected you? What are you trying to accomplish? --> <!--- How has this issue affected you? What are you trying to accomplish? -->
<!--- Providing context helps us come up with a solution that is most useful in the real world --> <!--- Providing context helps us come up with a solution that is most useful in the real world -->
## Your Environment ## Your Environment
<!--- Include as many relevant details about the environment you experienced the bug in --> <!--- Include as many relevant details about the environment you experienced the bug in -->
- Rust Version (I.e, output of `rustc -V`): - Rust Version (I.e, output of `rustc -V`):

View File

@ -2,14 +2,12 @@
<!-- Please fill out the following to get your PR reviewed quicker. --> <!-- Please fill out the following to get your PR reviewed quicker. -->
## PR Type ## PR Type
<!-- What kind of change does this PR make? --> <!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other --> <!-- Bug Fix / Feature / Refactor / Code Style / Other -->
PR_TYPE PR_TYPE
## PR Checklist
## PR Checklist
<!-- Check your PR fulfills the following items. --> <!-- Check your PR fulfills the following items. -->
<!-- For draft PRs check the boxes as you complete them. --> <!-- For draft PRs check the boxes as you complete them. -->
@ -19,10 +17,11 @@ PR_TYPE
- [ ] Format code with the latest stable rustfmt. - [ ] Format code with the latest stable rustfmt.
- [ ] (Team) Label with affected crates and semver status. - [ ] (Team) Label with affected crates and semver status.
## Overview
## Overview
<!-- Describe the current and new behavior. --> <!-- Describe the current and new behavior. -->
<!-- Emphasize any breaking changes. --> <!-- Emphasize any breaking changes. -->
<!-- If this PR fixes or closes an issue, reference it here. --> <!-- If this PR fixes or closes an issue, reference it here. -->
<!-- Closes #000 --> <!-- Closes #000 -->

View File

@ -1,10 +0,0 @@
version: 2
updates:
- package-ecosystem: cargo
directory: /
schedule:
interval: weekly
- package-ecosystem: github-actions
directory: /
schedule:
interval: weekly

View File

@ -2,27 +2,28 @@ name: Benchmark
on: on:
push: push:
branches: [master] branches:
- master
permissions: permissions:
contents: read contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
check_benchmark: check_benchmark:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Rust - name: Install Rust
run: | uses: actions-rs/toolchain@v1
rustup set profile minimal with:
rustup install nightly toolchain: nightly
rustup override set nightly profile: minimal
override: true
- name: Check benchmark - name: Check benchmark
run: cargo bench --bench=server -- --sample-size=15 uses: actions-rs/cargo@v1
with:
command: bench
args: --bench=server -- --sample-size=15

View File

@ -5,87 +5,132 @@ on:
branches: [master] branches: [master]
permissions: permissions:
contents: read contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
build_and_test_nightly: build_and_test_nightly:
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# prettier-ignore
target: target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
version: version:
- { name: nightly, version: nightly } - nightly
name: ${{ matrix.target.name }} / ${{ matrix.version.name }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env:
CI: 1
CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1
CARGO_UNSTABLE_SPARSE_REGISTRY: true
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install nasm
if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.1
# install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
# already installed on these Windows machines somewhere
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.triple == 'x86_64-pc-windows-msvc'
shell: bash run: vcpkg install openssl:x64-windows
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Install Rust (${{ matrix.version.name }}) - name: Install ${{ matrix.version }}
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rs/toolchain@v1
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install cargo-hack
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@cargo-hack
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean - name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: check minimal - name: check minimal
run: cargo ci-check-min uses: actions-rs/cargo@v1
with: { command: ci-check-min }
- name: check default - name: check default
run: cargo ci-check-default uses: actions-rs/cargo@v1
with: { command: ci-check-default }
- name: tests - name: tests
timeout-minutes: 60 timeout-minutes: 60
run: just test run: |
cargo test --lib --tests -p=actix-router --all-features
cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
- name: CI cache clean - name: Clear the cargo caches
run: cargo-ci-cache-clean run: |
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
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@v4 - uses: actions/checkout@v2
- name: Free Disk Space - uses: dtolnay/rust-toolchain@stable
run: ./scripts/free-disk-space.sh
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
- name: Install cargo-hack - name: Install cargo-hack
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@cargo-hack
with:
tool: cargo-hack - name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: check feature combinations - name: check feature combinations
run: cargo ci-check-all-feature-powerset run: cargo ci-check-all-feature-powerset
- name: check feature combinations - name: check feature combinations
run: cargo ci-check-all-feature-powerset-linux run: cargo ci-check-all-feature-powerset-linux
nextest:
name: nextest
runs-on: ubuntu-latest
env:
CI: 1
CARGO_INCREMENTAL: 0
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
- name: Install nextest
uses: taiki-e/install-action@nextest
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: Test with cargo-nextest
run: cargo nextest run

View File

@ -3,119 +3,135 @@ name: CI
on: on:
pull_request: pull_request:
types: [opened, synchronize, reopened] types: [opened, synchronize, reopened]
merge_group:
types: [checks_requested]
push: push:
branches: [master] branches: [master]
permissions: permissions:
contents: read contents: read # to fetch code (actions/checkout)
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
read_msrv:
name: Read MSRV
uses: actions-rust-lang/msrv/.github/workflows/msrv.yml@v0.1.0
build_and_test: build_and_test:
needs: read_msrv
strategy: strategy:
fail-fast: false fail-fast: false
matrix: matrix:
# prettier-ignore
target: target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
version: version:
- { name: msrv, version: "${{ needs.read_msrv.outputs.msrv }}" } - 1.59.0 # MSRV
- { name: stable, version: stable } - stable
name: ${{ matrix.target.name }} / ${{ matrix.version.name }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env:
CI: 1
CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install nasm
if: matrix.target.os == 'windows-latest'
uses: ilammy/setup-nasm@v1.5.1
# install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
# already installed on these Windows machines somewhere
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL - name: Install OpenSSL
if: matrix.target.os == 'windows-latest' if: matrix.target.triple == 'x86_64-pc-windows-msvc'
shell: bash run: vcpkg install openssl:x64-windows
run: |
set -e
choco install openssl --version=1.1.1.2100 -y --no-progress
echo 'OPENSSL_DIR=C:\Program Files\OpenSSL' >> $GITHUB_ENV
echo "RUSTFLAGS=-C target-feature=+crt-static" >> $GITHUB_ENV
- name: Setup mold linker - name: Install ${{ matrix.version }}
if: matrix.target.os == 'ubuntu-latest' uses: actions-rs/toolchain@v1
uses: rui314/setup-mold@v1
- name: Install Rust (${{ matrix.version.name }})
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: with:
toolchain: ${{ matrix.version.version }} toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install just, cargo-hack, cargo-nextest, cargo-ci-cache-clean - name: Install cargo-hack
uses: taiki-e/install-action@v2.34.0 uses: taiki-e/install-action@cargo-hack
with:
tool: just,cargo-hack,cargo-nextest,cargo-ci-cache-clean
- name: workaround MSRV issues - name: workaround MSRV issues
if: matrix.version.name == 'msrv' if: matrix.version != 'stable'
run: just downgrade-for-msrv run: |
cargo install cargo-edit --version=0.8.0
cargo add const-str@0.3 --dev -p=actix-web
cargo add const-str@0.3 --dev -p=awc
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: workaround MSRV issues
if: matrix.version != 'stable'
run: |
cargo update -p=zstd-sys --precise=2.0.1+zstd.1.5.2
- name: check minimal - name: check minimal
run: cargo ci-check-min uses: actions-rs/cargo@v1
with: { command: ci-check-min }
- name: check default - name: check default
run: cargo ci-check-default uses: actions-rs/cargo@v1
with: { command: ci-check-default }
- name: tests - name: tests
timeout-minutes: 60 timeout-minutes: 60
run: just test run: |
cargo test --lib --tests -p=actix-router --all-features
cargo test --lib --tests -p=actix-http --all-features
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
cargo test --lib --tests -p=actix-web-codegen --all-features
cargo test --lib --tests -p=awc --all-features
cargo test --lib --tests -p=actix-http-test --all-features
cargo test --lib --tests -p=actix-test --all-features
cargo test --lib --tests -p=actix-files
cargo test --lib --tests -p=actix-multipart --all-features
cargo test --lib --tests -p=actix-web-actors --all-features
- name: CI cache clean - name: Clear the cargo caches
run: cargo-ci-cache-clean run: |
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
cargo-cache
io-uring: io-uring:
name: io-uring tests name: io-uring tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Rust - uses: dtolnay/rust-toolchain@stable
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with: - name: Generate Cargo.lock
toolchain: nightly run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: tests (io-uring) - name: tests (io-uring)
timeout-minutes: 60 timeout-minutes: 60
run: > run: >
sudo bash -c "ulimit -Sl 512 && ulimit -Hl 512 && PATH=$PATH:/usr/share/rust/.cargo/bin && RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features" sudo bash -c "ulimit -Sl 512
&& ulimit -Hl 512
&& PATH=$PATH:/usr/share/rust/.cargo/bin
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
rustdoc: rustdoc:
name: doc tests name: doc tests
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Rust (nightly) - uses: dtolnay/rust-toolchain@nightly
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
toolchain: nightly
- name: Install just - name: Generate Cargo.lock
uses: taiki-e/install-action@v2.34.0 run: cargo generate-lockfile
with: - name: Cache Dependencies
tool: just uses: Swatinem/rust-cache@v1.3.0
- name: doc tests - name: doc tests
run: just test-docs run: cargo ci-doctest
timeout-minutes: 60

49
.github/workflows/clippy-fmt.yml vendored Normal file
View File

@ -0,0 +1,49 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly
with: { components: rustfmt }
- run: cargo fmt --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with: { components: clippy }
- name: Generate Cargo.lock
run: cargo generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Check with Clippy
uses: actions-rs/clippy-check@v1
with:
args: --workspace --tests --examples --all-features
token: ${{ secrets.GITHUB_TOKEN }}
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@stable
with: { components: rust-docs }
- name: Check for broken intra-doc links
uses: actions-rs/cargo@v1
env:
RUSTDOCFLAGS: "-D warnings"
with:
command: doc
args: --no-deps --all-features --workspace

View File

@ -1,39 +1,36 @@
# disabled because `cargo tarpaulin` currently segfaults
name: Coverage name: Coverage
on: on:
push: push:
branches: [master] branches: [master]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs: jobs:
# job currently (1st Feb 2022) segfaults
coverage: coverage:
name: coverage
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v2
- name: Install Rust - name: Install stable
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0 uses: actions-rs/toolchain@v1
with: with:
components: llvm-tools-preview toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Install just,cargo-llvm-cov - name: Generate Cargo.lock
uses: taiki-e/install-action@v2.34.0 uses: actions-rs/cargo@v1
with: with: { command: generate-lockfile }
tool: just,cargo-llvm-cov - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Generate code coverage - name: Generate coverage file
run: cargo llvm-cov --workspace --all-features --codecov --output-path codecov.json run: |
cargo install cargo-tarpaulin --vers "^0.13"
- name: Upload coverage to Codecov cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
uses: codecov/codecov-action@v4.4.1 - name: Upload to Codecov
with: uses: codecov/codecov-action@v1
files: codecov.json with: { file: cobertura.xml }
fail_ci_if_error: true
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}

View File

@ -1,93 +0,0 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
permissions:
contents: read
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
jobs:
fmt:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
toolchain: nightly
components: rustfmt
- name: Check with Rustfmt
run: cargo fmt --all -- --check
clippy:
permissions:
contents: read
checks: write # to add clippy checks to PR diffs
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
components: clippy
- name: Check with Clippy
uses: giraffate/clippy-action@v1.0.1
with:
reporter: github-pr-check
github_token: ${{ secrets.GITHUB_TOKEN }}
clippy_flags: >-
--workspace --all-features --tests --examples --bins --
-A unknown_lints -D clippy::todo -D clippy::dbg_macro
lint-docs:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Rust (nightly)
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
toolchain: nightly
components: rust-docs
- name: Check for broken intra-doc links
env:
RUSTDOCFLAGS: -D warnings
run: cargo +nightly doc --no-deps --workspace --all-features
public-api-diff:
runs-on: ubuntu-latest
steps:
- name: Checkout main branch
uses: actions/checkout@v4
with:
ref: ${{ github.base_ref }}
- name: Checkout PR branch
uses: actions/checkout@v4
- name: Install Rust
uses: actions-rust-lang/setup-rust-toolchain@v1.8.0
with:
toolchain: nightly-2024-06-07
- name: Install cargo-public-api
uses: taiki-e/install-action@v2.34.0
with:
tool: cargo-public-api
- name: Generate API diff
run: |
for f in $(find -mindepth 2 -maxdepth 2 -name Cargo.toml); do
cargo public-api --manifest-path "$f" --simplified diff ${{ github.event.pull_request.base.sha }}..${{ github.sha }}
done

32
.github/workflows/upload-doc.yml vendored Normal file
View File

@ -0,0 +1,32 @@
name: Upload Documentation
on:
push:
branches: [master]
permissions: {}
jobs:
build:
permissions:
contents: write # to push changes in repo (jamesives/github-pages-deploy-action)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: dtolnay/rust-toolchain@nightly
- name: Build Docs
run: cargo +nightly doc --no-deps --workspace --all-features
env:
RUSTDOCFLAGS: --cfg=docsrs
- name: Tweak HTML
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
- name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@v4.4.1
with:
folder: target/doc
single-commit: true

4
.gitignore vendored
View File

@ -19,7 +19,3 @@ guide/build/
# Configuration directory generated by VSCode # Configuration directory generated by VSCode
.vscode .vscode
# code coverage
/lcov.info
/codecov.json

3
.prettierrc.json Normal file
View File

@ -0,0 +1,3 @@
{
"proseWrap": "never"
}

View File

@ -1,5 +0,0 @@
overrides:
- files: "*.md"
options:
printWidth: 9999
proseWrap: never

View File

@ -1,3 +0,0 @@
group_imports = "StdExternalCrate"
imports_granularity = "Crate"
use_field_init_shorthand = true

View File

@ -5,7 +5,6 @@ members = [
"actix-http-test", "actix-http-test",
"actix-http", "actix-http",
"actix-multipart", "actix-multipart",
"actix-multipart-derive",
"actix-router", "actix-router",
"actix-test", "actix-test",
"actix-web-actors", "actix-web-actors",
@ -14,13 +13,6 @@ members = [
"awc", "awc",
] ]
[workspace.package]
homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web"
license = "MIT OR Apache-2.0"
edition = "2021"
rust-version = "1.72"
[profile.dev] [profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
debug = 0 debug = 0
@ -35,7 +27,6 @@ actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" } actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" } actix-http-test = { path = "actix-http-test" }
actix-multipart = { path = "actix-multipart" } actix-multipart = { path = "actix-multipart" }
actix-multipart-derive = { path = "actix-multipart-derive" }
actix-router = { path = "actix-router" } actix-router = { path = "actix-router" }
actix-test = { path = "actix-test" } actix-test = { path = "actix-test" }
actix-web = { path = "actix-web" } actix-web = { path = "actix-web" }

View File

@ -1,38 +1,23 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
## 0.6.6
- Update `tokio-uring` dependency to `0.4`.
- Minimum supported Rust version (MSRV) is now 1.72.
## 0.6.5
- Fix handling of special characters in filenames.
## 0.6.4
- Fix handling of newlines in filenames.
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 0.6.3
## 0.6.3 - 2023-01-21
- XHTML files now use `Content-Disposition: inline` instead of `attachment`. [#2903] - 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. - Minimum supported Rust version (MSRV) is now 1.59 due to transitive `time` dependency.
- Update `tokio-uring` dependency to `0.4`. - Update `tokio-uring` dependency to `0.4`.
[#2903]: https://github.com/actix/actix-web/pull/2903 [#2903]: https://github.com/actix/actix-web/pull/2903
## 0.6.2 ## 0.6.2 - 2022-07-23
- Allow partial range responses for video content to start streaming sooner. [#2817] - 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. - Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
[#2817]: https://github.com/actix/actix-web/pull/2817 [#2817]: https://github.com/actix/actix-web/pull/2817
## 0.6.1
## 0.6.1 - 2022-06-11
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021] - Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
- Update `tokio-uring` dependency to `0.3`. - Update `tokio-uring` dependency to `0.3`.
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645] - Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
@ -41,46 +26,46 @@
[#2021]: https://github.com/actix/actix-web/pull/2021 [#2021]: https://github.com/actix/actix-web/pull/2021
[#2645]: https://github.com/actix/actix-web/pull/2645 [#2645]: https://github.com/actix/actix-web/pull/2645
## 0.6.0
## 0.6.0 - 2022-02-25
- No significant changes since `0.6.0-beta.16`. - No significant changes since `0.6.0-beta.16`.
## 0.6.0-beta.16
## 0.6.0-beta.16 - 2022-01-31
- No significant changes since `0.6.0-beta.15`. - No significant changes since `0.6.0-beta.15`.
## 0.6.0-beta.15
## 0.6.0-beta.15 - 2022-01-21
- No significant changes since `0.6.0-beta.14`. - No significant changes since `0.6.0-beta.14`.
## 0.6.0-beta.14
## 0.6.0-beta.14 - 2022-01-14
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583] - The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
[#2583]: https://github.com/actix/actix-web/pull/2583 [#2583]: https://github.com/actix/actix-web/pull/2583
## 0.6.0-beta.13
## 0.6.0-beta.13 - 2022-01-04
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398] - The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398] - The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
[#2398]: https://github.com/actix/actix-web/pull/2398 [#2398]: https://github.com/actix/actix-web/pull/2398
## 0.6.0-beta.12
## 0.6.0-beta.12 - 2021-12-29
- No significant changes since `0.6.0-beta.11`. - No significant changes since `0.6.0-beta.11`.
## 0.6.0-beta.11
## 0.6.0-beta.11 - 2021-12-27
- No significant changes since `0.6.0-beta.10`. - No significant changes since `0.6.0-beta.10`.
## 0.6.0-beta.10
## 0.6.0-beta.10 - 2021-12-11
- No significant changes since `0.6.0-beta.9`. - No significant changes since `0.6.0-beta.9`.
## 0.6.0-beta.9
## 0.6.0-beta.9 - 2021-11-22
- Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408] - Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
- Add `NamedFile::open_async`. [#2408] - Add `NamedFile::open_async`. [#2408]
- Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453] - Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
@ -91,24 +76,24 @@
[#2408]: https://github.com/actix/actix-web/pull/2408 [#2408]: https://github.com/actix/actix-web/pull/2408
[#2453]: https://github.com/actix/actix-web/pull/2453 [#2453]: https://github.com/actix/actix-web/pull/2453
## 0.6.0-beta.8
## 0.6.0-beta.8 - 2021-10-20
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.
## 0.6.0-beta.7
## 0.6.0-beta.7 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51. - Minimum supported Rust version (MSRV) is now 1.51.
## 0.6.0-beta.6
## 0.6.0-beta.6 - 2021-06-26
- Added `Files::path_filter()`. [#2274] - Added `Files::path_filter()`. [#2274]
- `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] - `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]
[#2274]: https://github.com/actix/actix-web/pull/2274 [#2274]: https://github.com/actix/actix-web/pull/2274
[#2228]: https://github.com/actix/actix-web/pull/2228 [#2228]: https://github.com/actix/actix-web/pull/2228
## 0.6.0-beta.5
## 0.6.0-beta.5 - 2021-06-17
- `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135] - `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
- For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156] - For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
- `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225] - `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
@ -119,58 +104,58 @@
[#2225]: https://github.com/actix/actix-web/pull/2225 [#2225]: https://github.com/actix/actix-web/pull/2225
[#2257]: https://github.com/actix/actix-web/pull/2257 [#2257]: https://github.com/actix/actix-web/pull/2257
## 0.6.0-beta.4
## 0.6.0-beta.4 - 2021-04-02
- Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046] - Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
[#2046]: https://github.com/actix/actix-web/pull/2046 [#2046]: https://github.com/actix/actix-web/pull/2046
## 0.6.0-beta.3
## 0.6.0-beta.3 - 2021-03-09
- No notable changes. - No notable changes.
## 0.6.0-beta.2
## 0.6.0-beta.2 - 2021-02-10
- Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887] - Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
- Replace `v_htmlescape` with `askama_escape`. [#1953] - Replace `v_htmlescape` with `askama_escape`. [#1953]
[#1887]: https://github.com/actix/actix-web/pull/1887 [#1887]: https://github.com/actix/actix-web/pull/1887
[#1953]: https://github.com/actix/actix-web/pull/1953 [#1953]: https://github.com/actix/actix-web/pull/1953
## 0.6.0-beta.1
## 0.6.0-beta.1 - 2021-01-07
- `HttpRange::parse` now has its own error type. - `HttpRange::parse` now has its own error type.
- Update `bytes` to `1.0`. [#1813] - Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
## 0.5.0
## 0.5.0 - 2020-12-26
- Optionally support hidden files/directories. [#1811] - Optionally support hidden files/directories. [#1811]
[#1811]: https://github.com/actix/actix-web/pull/1811 [#1811]: https://github.com/actix/actix-web/pull/1811
## 0.4.1
## 0.4.1 - 2020-11-24
- Clarify order of parameters in `Files::new` and improve docs. - Clarify order of parameters in `Files::new` and improve docs.
## 0.4.0
## 0.4.0 - 2020-10-06
- Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714] - Add `Files::prefer_utf8` option that adds UTF-8 charset on certain response types. [#1714]
[#1714]: https://github.com/actix/actix-web/pull/1714 [#1714]: https://github.com/actix/actix-web/pull/1714
## 0.3.0
## 0.3.0 - 2020-09-11
- No significant changes from 0.3.0-beta.1. - No significant changes from 0.3.0-beta.1.
## 0.3.0-beta.1
## 0.3.0-beta.1 - 2020-07-15
- Update `v_htmlescape` to 0.10 - Update `v_htmlescape` to 0.10
- Update `actix-web` and `actix-http` dependencies to beta.1 - Update `actix-web` and `actix-http` dependencies to beta.1
## 0.3.0-alpha.1
## 0.3.0-alpha.1 - 2020-05-23
- Update `actix-web` and `actix-http` dependencies to alpha - Update `actix-web` and `actix-http` dependencies to alpha
- Fix some typos in the docs - Fix some typos in the docs
- Bump minimum supported Rust version to 1.40 - Bump minimum supported Rust version to 1.40
@ -178,73 +163,73 @@
[#1384]: https://github.com/actix/actix-web/pull/1384 [#1384]: https://github.com/actix/actix-web/pull/1384
## 0.2.1
## 0.2.1 - 2019-12-22
- Use the same format for file URLs regardless of platforms - Use the same format for file URLs regardless of platforms
## 0.2.0
## 0.2.0 - 2019-12-20
- Fix BodyEncoding trait import #1220 - Fix BodyEncoding trait import #1220
## 0.2.0-alpha.1
## 0.2.0-alpha.1 - 2019-12-07
- Migrate to `std::future` - Migrate to `std::future`
## 0.1.7
- Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151) ## 0.1.7 - 2019-11-06
- Add an additional `filename*` param in the `Content-Disposition` header of
## 0.1.6 `actix_files::NamedFile` to be more compatible. (#1151)
## 0.1.6 - 2019-10-14
- Add option to redirect to a slash-ended path `Files` #1132 - Add option to redirect to a slash-ended path `Files` #1132
## 0.1.5
## 0.1.5 - 2019-10-08
- Bump up `mime_guess` crate version to 2.0.1 - Bump up `mime_guess` crate version to 2.0.1
- Bump up `percent-encoding` crate version to 2.1 - Bump up `percent-encoding` crate version to 2.1
- Allow user defined request guards for `Files` #1113 - Allow user defined request guards for `Files` #1113
## 0.1.4
## 0.1.4 - 2019-07-20
- Allow to disable `Content-Disposition` header #686 - Allow to disable `Content-Disposition` header #686
## 0.1.3
## 0.1.3 - 2019-06-28
- Do not set `Content-Length` header, let actix-http set it #930 - Do not set `Content-Length` header, let actix-http set it #930
## 0.1.2
## 0.1.2 - 2019-06-13
- Content-Length is 0 for NamedFile HEAD request #914 - Content-Length is 0 for NamedFile HEAD request #914
- Fix ring dependency from actix-web default features for #741 - Fix ring dependency from actix-web default features for #741
## 0.1.1
## 0.1.1 - 2019-06-01
- Static files are incorrectly served as both chunked and with length #812 - Static files are incorrectly served as both chunked and with length #812
## 0.1.0
## 0.1.0 - 2019-05-25
- NamedFile last-modified check always fails due to nano-seconds in file modified date #820 - NamedFile last-modified check always fails due to nano-seconds in file modified date #820
## 0.1.0-beta.4
## 0.1.0-beta.4 - 2019-05-12
- Update actix-web to beta.4 - Update actix-web to beta.4
## 0.1.0-beta.1
## 0.1.0-beta.1 - 2019-04-20
- Update actix-web to beta.1 - Update actix-web to beta.1
## 0.1.0-alpha.6
## 0.1.0-alpha.6 - 2019-04-14
- Update actix-web to alpha6 - Update actix-web to alpha6
## 0.1.0-alpha.4
## 0.1.0-alpha.4 - 2019-04-08
- Update actix-web to alpha4 - Update actix-web to alpha4
## 0.1.0-alpha.2
## 0.1.0-alpha.2 - 2019-04-02
- Add default handler support - Add default handler support
## 0.1.0-alpha.1
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl - Initial impl

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.6" version = "0.6.3"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -11,7 +11,7 @@ homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web"
categories = ["asynchronous", "web-programming::http-server"] categories = ["asynchronous", "web-programming::http-server"]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2018"
[lib] [lib]
name = "actix_files" name = "actix_files"
@ -26,26 +26,25 @@ actix-service = "2"
actix-utils = "3" actix-utils = "3"
actix-web = { version = "4", default-features = false } actix-web = { version = "4", default-features = false }
bitflags = "2" 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.17", default-features = false, features = ["alloc"] }
http-range = "0.1.4" http-range = "0.1.4"
log = "0.4" log = "0.4"
mime = "0.3.9" 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.5" v_htmlescape= "0.15"
# experimental-io-uring # experimental-io-uring
[target.'cfg(target_os = "linux")'.dependencies] [target.'cfg(target_os = "linux")'.dependencies]
tokio-uring = { version = "0.5", optional = true, features = ["bytes"] } tokio-uring = { version = "0.4", optional = true, features = ["bytes"] }
actix-server = { version = "2.4", optional = true } # ensure matching tokio-uring versions actix-server = { version = "2.2", optional = true } # ensure matching tokio-uring versions
[dev-dependencies] [dev-dependencies]
actix-rt = "2.7" actix-rt = "2.7"
actix-test = "0.1" actix-test = "0.1"
actix-web = "4" actix-web = "4"
env_logger = "0.11"
tempfile = "3.2" tempfile = "3.2"

View File

@ -1,32 +1,18 @@
# `actix-files` # actix-files
<!-- prettier-ignore-start --> > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.6)](https://docs.rs/actix-files/0.6.6) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.3)](https://docs.rs/actix-files/0.6.3)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.6/status.svg)](https://deps.rs/crate/actix-files/0.6.6) [![dependency status](https://deps.rs/crate/actix-files/0.6.3/status.svg)](https://deps.rs/crate/actix-files/0.6.3)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> ## Documentation & Resources
<!-- cargo-rdme start --> - [API Documentation](https://docs.rs/actix-files)
- [Example Project](https://github.com/actix/examples/tree/master/basics/static-files)
Static file serving for Actix Web. - Minimum Supported Rust Version (MSRV): 1.54
Provides a non-blocking service for serving static files from disk.
## Examples
```rust
use actix_web::App;
use actix_files::Files;
let app = App::new()
.service(Files::new("/static", ".").prefer_utf8(true));
```
<!-- cargo-rdme end -->

View File

@ -1,33 +0,0 @@
use actix_files::Files;
use actix_web::{get, guard, middleware, App, HttpServer, Responder};
const EXAMPLES_DIR: &str = concat![env!("CARGO_MANIFEST_DIR"), "/examples"];
#[get("/")]
async fn index() -> impl Responder {
"Hello world!"
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
log::info!("starting HTTP server at http://localhost:8080");
HttpServer::new(|| {
App::new()
.service(index)
.service(
Files::new("/assets", EXAMPLES_DIR)
.show_files_listing()
.guard(guard::Header("show-listing", "?1")),
)
.service(Files::new("/assets", EXAMPLES_DIR))
.wrap(middleware::Compress::default())
.wrap(middleware::Logger::default())
})
.bind(("127.0.0.1", 8080))?
.workers(2)
.run()
.await
}

View File

@ -7,11 +7,12 @@ use std::{
}; };
use actix_web::{error::Error, web::Bytes}; use actix_web::{error::Error, web::Bytes};
#[cfg(feature = "experimental-io-uring")]
use bytes::BytesMut;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "experimental-io-uring")]
use bytes::BytesMut;
use super::named::File; use super::named::File;
pin_project! { pin_project! {

View File

@ -1,9 +1,4 @@
use std::{ use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
fmt::Write,
fs::DirEntry,
io,
path::{Path, PathBuf},
};
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use percent_encoding::{utf8_percent_encode, CONTROLS}; use percent_encoding::{utf8_percent_encode, CONTROLS};

View File

@ -4,45 +4,46 @@ 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(Debug, PartialEq, Eq, Display)]
pub enum FilesError { pub enum FilesError {
/// Path is not a directory. /// Path is not a directory
#[allow(dead_code)] #[allow(dead_code)]
#[display(fmt = "path is not a directory. Unable to serve static files")] #[display(fmt = "Path is not a directory. Unable to serve static files")]
IsNotDirectory, IsNotDirectory,
/// Cannot render directory. /// Cannot render directory
#[display(fmt = "unable to render directory without index file")] #[display(fmt = "Unable to render directory without index file")]
IsDirectory, IsDirectory,
} }
/// Return `NotFound` for `FilesError`
impl ResponseError for FilesError { impl ResponseError for FilesError {
/// Returns `404 Not Found`.
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::NOT_FOUND StatusCode::NOT_FOUND
} }
} }
#[allow(clippy::enum_variant_names)]
#[derive(Debug, PartialEq, Eq, Display)] #[derive(Debug, PartialEq, Eq, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum UriSegmentError { pub enum UriSegmentError {
/// Segment started with the wrapped invalid character. /// The segment started with the wrapped invalid character.
#[display(fmt = "segment started with invalid character: ('{_0}')")] #[display(fmt = "The segment started with the wrapped invalid character")]
BadStart(char), BadStart(char),
/// Segment contained the wrapped invalid character. /// The segment contained the wrapped invalid character.
#[display(fmt = "segment contained invalid character ('{_0}')")] #[display(fmt = "The segment contained the wrapped invalid character")]
BadChar(char), BadChar(char),
/// Segment ended with the wrapped invalid character. /// The segment ended with the wrapped invalid character.
#[display(fmt = "segment ended with invalid character: ('{_0}')")] #[display(fmt = "The segment ended with the wrapped invalid character")]
BadEnd(char), BadEnd(char),
/// Path is not a valid UTF-8 string after percent-decoding. /// The path is not a valid UTF-8 string after doing percent decoding.
#[display(fmt = "path is not a valid UTF-8 string after percent-decoding")] #[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
NotValidUtf8, NotValidUtf8,
} }
/// Return `BadRequest` for `UriSegmentError`
impl ResponseError for UriSegmentError { impl ResponseError for UriSegmentError {
/// Returns `400 Bad Request`.
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }

View File

@ -8,7 +8,8 @@ use std::{
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_web::{ use actix_web::{
dev::{ dev::{
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, ServiceResponse, AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
ServiceResponse,
}, },
error::Error, error::Error,
guard::Guard, guard::Guard,
@ -235,7 +236,7 @@ impl Files {
/// request starts being handled by the file service, it will not be able to back-out and try /// request starts being handled by the file service, it will not be able to back-out and try
/// the next service, you will simply get a 404 (or 405) error response. /// the next service, you will simply get a 404 (or 405) error response.
/// ///
/// To allow `POST` requests to retrieve files, see [`Files::method_guard()`]. /// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -300,8 +301,12 @@ impl Files {
pub fn default_handler<F, U>(mut self, f: F) -> Self pub fn default_handler<F, U>(mut self, f: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
U: ServiceFactory<ServiceRequest, Config = (), Response = ServiceResponse, Error = Error> U: ServiceFactory<
+ 'static, ServiceRequest,
Config = (),
Response = ServiceResponse,
Error = Error,
> + 'static,
{ {
// create and configure default resource // create and configure default resource
self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory( self.default = Rc::new(RefCell::new(Some(Rc::new(boxed::factory(
@ -417,14 +422,10 @@ mod tests {
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
let body = test::read_body(res).await; let body = test::read_body(res).await;
let body_str = std::str::from_utf8(&body).unwrap();
let actual_path = Path::new(&body_str);
let expected_path = Path::new("actix-files/tests");
assert!( assert!(
actual_path.ends_with(expected_path), body.ends_with(b"actix-files/tests/"),
"body {:?} does not end with {:?}", "body {:?} does not end with `actix-files/tests/`",
actual_path, body
expected_path
); );
} }
} }

View File

@ -13,11 +13,7 @@
#![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)]
#![doc(html_logo_url = "https://actix.rs/img/logo.png")] #![allow(clippy::uninlined_format_args)]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
use std::path::Path;
use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{ use actix_web::{
@ -26,6 +22,7 @@ use actix_web::{
http::header::DispositionType, http::header::DispositionType,
}; };
use mime_guess::from_ext; use mime_guess::from_ext;
use std::path::Path;
mod chunked; mod chunked;
mod directory; mod directory;
@ -37,15 +34,16 @@ mod path_buf;
mod range; mod range;
mod service; mod service;
pub use self::{ pub use self::chunked::ChunkedReadFile;
chunked::ChunkedReadFile, directory::Directory, files::Files, named::NamedFile, pub use self::directory::Directory;
range::HttpRange, service::FilesService, pub use self::files::Files;
}; pub use self::named::NamedFile;
use self::{ pub use self::range::HttpRange;
directory::{directory_listing, DirectoryRenderer}, pub use self::service::FilesService;
error::FilesError,
path_buf::PathBufWrap, use self::directory::{directory_listing, DirectoryRenderer};
}; use self::error::FilesError;
use self::path_buf::PathBufWrap;
type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>; type HttpService = BoxService<ServiceRequest, ServiceResponse, Error>;
type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>; type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
@ -65,7 +63,6 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::{
fmt::Write as _,
fs::{self}, fs::{self},
ops::Add, ops::Add,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
@ -75,7 +72,7 @@ mod tests {
dev::ServiceFactory, dev::ServiceFactory,
guard, guard,
http::{ http::{
header::{self, ContentDisposition, DispositionParam}, header::{self, ContentDisposition, DispositionParam, DispositionType},
Method, StatusCode, Method, StatusCode,
}, },
middleware::Compress, middleware::Compress,
@ -554,8 +551,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files_with_spaces() { async fn test_static_files_with_spaces() {
let srv = let srv = test::init_service(
test::init_service(App::new().service(Files::new("/", ".").index_file("Cargo.toml"))) App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
)
.await; .await;
let request = TestRequest::get() let request = TestRequest::get()
.uri("/tests/test%20space.binary") .uri("/tests/test%20space.binary")
@ -568,30 +566,6 @@ mod tests {
assert_eq!(bytes, data); assert_eq!(bytes, data);
} }
#[cfg(not(target_os = "windows"))]
#[actix_rt::test]
async fn test_static_files_with_special_characters() {
// Create the file we want to test against ad-hoc. We can't check it in as otherwise
// Windows can't even checkout this repository.
let temp_dir = tempfile::tempdir().unwrap();
let file_with_newlines = temp_dir.path().join("test\n\x0B\x0C\rnewline.text");
fs::write(&file_with_newlines, "Look at my newlines").unwrap();
let srv = test::init_service(
App::new().service(Files::new("/", temp_dir.path()).index_file("Cargo.toml")),
)
.await;
let request = TestRequest::get()
.uri("/test%0A%0B%0C%0Dnewline.text")
.to_request();
let response = test::call_service(&srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await;
let data = web::Bytes::from(fs::read(file_with_newlines).unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test] #[actix_rt::test]
async fn test_files_not_allowed() { async fn test_files_not_allowed() {
let srv = test::init_service(App::new().service(Files::new("/", "."))).await; let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
@ -690,7 +664,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files() { async fn test_static_files() {
let srv = let srv =
test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await; test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
.await;
let req = TestRequest::with_uri("/missing").to_request(); let req = TestRequest::with_uri("/missing").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
@ -703,7 +678,8 @@ mod tests {
assert_eq!(resp.status(), StatusCode::NOT_FOUND); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = let srv =
test::init_service(App::new().service(Files::new("/", ".").show_files_listing())).await; test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
.await;
let req = TestRequest::with_uri("/tests").to_request(); let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!( assert_eq!(
@ -864,21 +840,19 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_percent_encoding_2() { async fn test_percent_encoding_2() {
let temp_dir = tempfile::tempdir().unwrap(); let tmpdir = tempfile::tempdir().unwrap();
let filename = match cfg!(unix) { let filename = match cfg!(unix) {
true => "ض:?#[]{}<>()@!$&'`|*+,;= %20\n.test", true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test",
false => "ض#[]{}()@!$&'`+,;= %20.test", false => "ض#[]{}()@!$&'`+,;= %20.test",
}; };
let filename_encoded = filename let filename_encoded = filename
.as_bytes() .as_bytes()
.iter() .iter()
.fold(String::new(), |mut buf, c| { .map(|c| format!("%{:02X}", c))
write!(&mut buf, "%{:02X}", c).unwrap(); .collect::<String>();
buf std::fs::File::create(tmpdir.path().join(filename)).unwrap();
});
std::fs::File::create(temp_dir.path().join(filename)).unwrap();
let srv = test::init_service(App::new().service(Files::new("/", temp_dir.path()))).await; let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await;
let req = TestRequest::get() let req = TestRequest::get()
.uri(&format!("/{}", filename_encoded)) .uri(&format!("/{}", filename_encoded))

View File

@ -8,13 +8,13 @@ use std::{
use actix_web::{ use actix_web::{
body::{self, BoxBody, SizedStream}, body::{self, BoxBody, SizedStream},
dev::{ dev::{
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory, ServiceRequest, self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
ServiceResponse, ServiceRequest, ServiceResponse,
}, },
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, ContentEncoding, DispositionParam, DispositionType, self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
ExtendedValue, HeaderValue, DispositionType, ExtendedValue, HeaderValue,
}, },
StatusCode, StatusCode,
}, },
@ -24,11 +24,11 @@ use bitflags::bitflags;
use derive_more::{Deref, DerefMut}; use derive_more::{Deref, DerefMut};
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use mime::Mime; use mime::Mime;
use mime_guess::from_path;
use crate::{encoding::equiv_utf8_text, range::HttpRange}; use crate::{encoding::equiv_utf8_text, range::HttpRange};
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)]
pub(crate) struct Flags: u8 { pub(crate) struct Flags: u8 {
const ETAG = 0b0000_0001; const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010; const LAST_MD = 0b0000_0010;
@ -84,7 +84,6 @@ pub struct NamedFile {
#[cfg(not(feature = "experimental-io-uring"))] #[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File; pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")] #[cfg(feature = "experimental-io-uring")]
pub(crate) use tokio_uring::fs::File; pub(crate) use tokio_uring::fs::File;
@ -127,7 +126,7 @@ impl NamedFile {
} }
}; };
let ct = mime_guess::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::AUDIO | mime::VIDEO => DispositionType::Inline,
@ -139,13 +138,8 @@ impl NamedFile {
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
// replace special characters in filenames which could occur on some filesystems let mut parameters =
let filename_s = filename vec![DispositionParam::Filename(String::from(filename.as_ref()))];
.replace('\n', "%0A") // \n line break
.replace('\x0B', "%0B") // \v vertical tab
.replace('\x0C', "%0C") // \f form feed
.replace('\r', "%0D"); // \r carriage return
let mut parameters = vec![DispositionParam::Filename(filename_s)];
if !filename.is_ascii() { if !filename.is_ascii() {
parameters.push(DispositionParam::FilenameExt(ExtendedValue { parameters.push(DispositionParam::FilenameExt(ExtendedValue {

View File

@ -30,7 +30,7 @@ impl PathBufWrap {
let mut segment_count = path.matches('/').count() + 1; let mut segment_count = path.matches('/').count() + 1;
// we can decode the whole path here (instead of per-segment decoding) // we can decode the whole path here (instead of per-segment decoding)
// because we will reject `%2F` in paths using `segment_count`. // because we will reject `%2F` in paths using `segement_count`.
let path = percent_encoding::percent_decode_str(path) let path = percent_encoding::percent_decode_str(path)
.decode_utf8() .decode_utf8()
.map_err(|_| UriSegmentError::NotValidUtf8)?; .map_err(|_| UriSegmentError::NotValidUtf8)?;
@ -97,6 +97,8 @@ impl FromRequest for PathBufWrap {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::iter::FromIterator;
use super::*; use super::*;
#[test] #[test]

View File

@ -1,36 +1,4 @@
use std::fmt; use derive_more::{Display, Error};
use derive_more::Error;
/// Copy of `http_range::HttpRangeParseError`.
#[derive(Debug, Clone)]
enum HttpRangeParseError {
InvalidRange,
NoOverlap,
}
impl From<http_range::HttpRangeParseError> for HttpRangeParseError {
fn from(err: http_range::HttpRangeParseError) -> Self {
match err {
http_range::HttpRangeParseError::InvalidRange => Self::InvalidRange,
http_range::HttpRangeParseError::NoOverlap => Self::NoOverlap,
}
}
}
#[derive(Debug, Clone, Error)]
#[non_exhaustive]
pub struct ParseRangeErr(#[error(not(source))] HttpRangeParseError);
impl fmt::Display for ParseRangeErr {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("invalid Range header: ")?;
f.write_str(match self.0 {
HttpRangeParseError::InvalidRange => "invalid syntax",
HttpRangeParseError::NoOverlap => "range starts after end of content",
})
}
}
/// HTTP Range header representation. /// HTTP Range header representation.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -42,22 +10,26 @@ pub struct HttpRange {
pub length: u64, pub length: u64,
} }
#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "Parse HTTP Range failed")]
pub struct ParseRangeErr(#[error(not(source))] ());
impl HttpRange { impl HttpRange {
/// Parses Range HTTP header string as per RFC 2616. /// Parses Range HTTP header string as per RFC 2616.
/// ///
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file). /// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> { pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
let ranges = match http_range::HttpRange::parse(header, size) {
http_range::HttpRange::parse(header, size).map_err(|err| ParseRangeErr(err.into()))?; Ok(ranges) => Ok(ranges
Ok(ranges
.iter() .iter()
.map(|range| HttpRange { .map(|range| HttpRange {
start: range.start, start: range.start,
length: range.length, length: range.length,
}) })
.collect()) .collect()),
Err(_) => Err(ParseRangeErr(())),
}
} }
} }

View File

@ -62,7 +62,11 @@ impl FilesService {
} }
} }
fn serve_named_file(&self, req: ServiceRequest, mut named_file: NamedFile) -> ServiceResponse { fn serve_named_file(
&self,
req: ServiceRequest,
mut named_file: NamedFile,
) -> ServiceResponse {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = self.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_()); let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
@ -116,8 +120,10 @@ impl Service<ServiceRequest> for FilesService {
)); ));
} }
let path_on_disk = let path_on_disk = match PathBufWrap::parse_path(
match PathBufWrap::parse_path(req.match_info().unprocessed(), this.hidden_files) { req.match_info().unprocessed(),
this.hidden_files,
) {
Ok(item) => item, Ok(item) => item,
Err(err) => return Ok(req.error_response(err)), Err(err) => return Ok(req.error_response(err)),
}; };
@ -171,7 +177,8 @@ impl Service<ServiceRequest> for FilesService {
match NamedFile::open_async(&path).await { match NamedFile::open_async(&path).await {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = this.mime_override { if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_()); let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = this.file_flags; named_file.flags = this.file_flags;

View File

@ -24,7 +24,8 @@ async fn test_utf8_file_contents() {
// disable UTF-8 attribute // disable UTF-8 attribute
let srv = let srv =
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false))).await; test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request(); let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;

View File

@ -12,7 +12,9 @@ async fn test_guard_filter() {
let srv = test::init_service( let srv = test::init_service(
App::new() App::new()
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com"))) .service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
.service(Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com"))), .service(
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
),
) )
.await; .await;

View File

@ -9,7 +9,8 @@ use actix_web::{
async fn test_directory_traversal_prevention() { async fn test_directory_traversal_prevention() {
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req = TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request(); let req =
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND); assert_eq!(res.status(), StatusCode::NOT_FOUND);

View File

@ -1,19 +1,13 @@
# Changes # Changes
## Unreleased ## Unreleased - 2022-xx-xx
- Minimum supported Rust version (MSRV) is now 1.72.
## 3.2.0
- Minimum supported Rust version (MSRV) is now 1.68 due to transitive `time` dependency.
## 3.1.0
## 3.1.0 - 2023-01-21
- Minimum supported Rust version (MSRV) is now 1.59. - Minimum supported Rust version (MSRV) is now 1.59.
## 3.0.0
## 3.0.0 - 2022-07-24
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
- Added `TestServer::client_headers` method. [#2097] - Added `TestServer::client_headers` method. [#2097]
- Update `actix-server` dependency to `2`. - Update `actix-server` dependency to `2`.
@ -25,79 +19,78 @@
[#2097]: https://github.com/actix/actix-web/pull/2097 [#2097]: https://github.com/actix/actix-web/pull/2097
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
<details> <details>
<summary>3.0.0 Pre-Releases</summary> <summary>3.0.0 Pre-Releases</summary>
## 3.0.0-beta.13 ## 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`.
## 3.0.0-beta.12
## 3.0.0-beta.12 - 2022-01-31
- No significant changes since `3.0.0-beta.11`. - No significant changes since `3.0.0-beta.11`.
## 3.0.0-beta.11
## 3.0.0-beta.11 - 2022-01-04
- Minimum supported Rust version (MSRV) is now 1.54. - Minimum supported Rust version (MSRV) is now 1.54.
## 3.0.0-beta.10
## 3.0.0-beta.10 - 2021-12-27
- Update `actix-server` to `2.0.0-rc.2`. [#2550] - Update `actix-server` to `2.0.0-rc.2`. [#2550]
[#2550]: https://github.com/actix/actix-web/pull/2550 [#2550]: https://github.com/actix/actix-web/pull/2550
## 3.0.0-beta.9
## 3.0.0-beta.9 - 2021-12-11
- No significant changes since `3.0.0-beta.8`. - No significant changes since `3.0.0-beta.8`.
## 3.0.0-beta.8
## 3.0.0-beta.8 - 2021-11-30
- Update `actix-tls` to `3.0.0-rc.1`. [#2474] - Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474 [#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.7
## 3.0.0-beta.7 - 2021-11-22
- Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408] - Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408 [#2408]: https://github.com/actix/actix-web/pull/2408
## 3.0.0-beta.6
## 3.0.0-beta.6 - 2021-11-15
- `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442] - `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
- Update `actix-server` to `2.0.0-beta.9`. [#2442] - Update `actix-server` to `2.0.0-beta.9`. [#2442]
- Minimum supported Rust version (MSRV) is now 1.52. - Minimum supported Rust version (MSRV) is now 1.52.
[#2442]: https://github.com/actix/actix-web/pull/2442 [#2442]: https://github.com/actix/actix-web/pull/2442
## 3.0.0-beta.5
## 3.0.0-beta.5 - 2021-09-09
- Minimum supported Rust version (MSRV) is now 1.51. - Minimum supported Rust version (MSRV) is now 1.51.
## 3.0.0-beta.4
## 3.0.0-beta.4 - 2021-04-02
- Added `TestServer::client_headers` method. [#2097] - Added `TestServer::client_headers` method. [#2097]
[#2097]: https://github.com/actix/actix-web/pull/2097 [#2097]: https://github.com/actix/actix-web/pull/2097
## 3.0.0-beta.3
## 3.0.0-beta.3 - 2021-03-09
- No notable changes. - No notable changes.
## 3.0.0-beta.2
## 3.0.0-beta.2 - 2021-02-10
- No notable changes. - No notable changes.
## 3.0.0-beta.1
## 3.0.0-beta.1 - 2021-01-07
- Update `bytes` to `1.0`. [#1813] - Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813 [#1813]: https://github.com/actix/actix-web/pull/1813
</details> </details>
## 2.1.0 ## 2.1.0 - 2020-11-25
- Add ability to set address for `TestServer`. [#1645] - Add ability to set address for `TestServer`. [#1645]
- Upgrade `base64` to `0.13`. - Upgrade `base64` to `0.13`.
- Upgrade `serde_urlencoded` to `0.7`. [#1773] - Upgrade `serde_urlencoded` to `0.7`. [#1773]
@ -105,12 +98,12 @@
[#1773]: https://github.com/actix/actix-web/pull/1773 [#1773]: https://github.com/actix/actix-web/pull/1773
[#1645]: https://github.com/actix/actix-web/pull/1645 [#1645]: https://github.com/actix/actix-web/pull/1645
## 2.0.0
## 2.0.0 - 2020-09-11
- Update actix-codec and actix-utils dependencies. - Update actix-codec and actix-utils dependencies.
## 2.0.0-alpha.1
## 2.0.0-alpha.1 - 2020-05-23
- Update the `time` dependency to 0.2.7 - Update the `time` dependency to 0.2.7
- Update `actix-connect` dependency to 2.0.0-alpha.2 - Update `actix-connect` dependency to 2.0.0-alpha.2
- Make `test_server` `async` fn. - Make `test_server` `async` fn.
@ -119,57 +112,56 @@
- Update `base64` dependency to 0.12 - Update `base64` dependency to 0.12
- Update `env_logger` dependency to 0.7 - Update `env_logger` dependency to 0.7
## 1.0.0 ## 1.0.0 - 2019-12-13
- Replaced `TestServer::start()` with `test_server()` - Replaced `TestServer::start()` with `test_server()`
## 1.0.0-alpha.3
## 1.0.0-alpha.3 - 2019-12-07
- Migrate to `std::future` - Migrate to `std::future`
## 0.2.5
## 0.2.5 - 2019-09-17
- Update serde_urlencoded to "0.6.1" - Update serde_urlencoded to "0.6.1"
- Increase TestServerRuntime timeouts from 500ms to 3000ms - Increase TestServerRuntime timeouts from 500ms to 3000ms
- Do not override current `System` - Do not override current `System`
## 0.2.4
## 0.2.4 - 2019-07-18
- Update actix-server to 0.6 - Update actix-server to 0.6
## 0.2.3
## 0.2.3 - 2019-07-16
- Add `delete`, `options`, `patch` methods to `TestServerRunner` - Add `delete`, `options`, `patch` methods to `TestServerRunner`
## 0.2.2
## 0.2.2 - 2019-06-16
- Add .put() and .sput() methods - Add .put() and .sput() methods
## 0.2.1
## 0.2.1 - 2019-06-05
- Add license files - Add license files
## 0.2.0
## 0.2.0 - 2019-05-12
- Update awc and actix-http deps - Update awc and actix-http deps
## 0.1.1
## 0.1.1 - 2019-04-24
- Always make new connection for http client - Always make new connection for http client
## 0.1.0
## 0.1.0 - 2019-04-16
- No changes - No changes
## 0.1.0-alpha.3
## 0.1.0-alpha.3 - 2019-04-02
- Request functions accept path #743 - Request functions accept path #743
## 0.1.0-alpha.2
## 0.1.0-alpha.2 - 2019-03-29
- Added TestServerRuntime::load_body() method - Added TestServerRuntime::load_body() method
- Update actix-http and awc libraries - Update actix-http and awc libraries
## 0.1.0-alpha.1
## 0.1.0-alpha.1 - 2019-03-28
- Initial impl - Initial impl

View File

@ -1,11 +1,11 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.2.0" version = "3.1.0"
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"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web.git"
categories = [ categories = [
"network-programming", "network-programming",
"asynchronous", "asynchronous",
@ -13,7 +13,7 @@ categories = [
"web-programming::websocket", "web-programming::websocket",
] ]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2021" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
features = [] features = []
@ -39,15 +39,16 @@ awc = { version = "3", default-features = false }
bytes = "1" bytes = "1"
futures-core = { version = "0.3.17", default-features = false } futures-core = { version = "0.3.17", default-features = false }
http = "0.2.7" http = "0.2.5"
log = "0.4" log = "0.4"
socket2 = "0.5" socket2 = "0.4"
serde = "1" serde = "1.0"
serde_json = "1" serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
tls-openssl = { version = "0.10.55", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.24.2", features = ["sync"] } tokio = { version = "1.18.5", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4", default-features = false, features = ["cookies"] }
actix-http = "3" actix-http = "3"

View File

@ -1,16 +1,17 @@
# `actix-http-test` # actix-http-test
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.2.0)](https://docs.rs/actix-http-test/3.2.0) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.1.0)](https://docs.rs/actix-http-test/3.1.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.2.0/status.svg)](https://deps.rs/crate/actix-http-test/3.2.0) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.1.0/status.svg)](https://deps.rs/crate/actix-http-test/3.1.0)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http-test)
- Minimum Supported Rust Version (MSRV): 1.54

View File

@ -2,9 +2,9 @@
#![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")]
#![cfg_attr(docsrs, feature(doc_auto_cfg))]
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
@ -30,31 +30,27 @@ use tokio::sync::mpsc;
/// for HTTP applications. /// for HTTP applications.
/// ///
/// # Examples /// # Examples
/// /// ```no_run
/// ``` /// use actix_http::HttpService;
/// use actix_http::{HttpService, Response, Error, StatusCode};
/// use actix_http_test::test_server; /// use actix_http_test::test_server;
/// use actix_service::{fn_service, map_config, ServiceFactoryExt as _}; /// use actix_web::{web, App, HttpResponse, Error};
/// ///
/// #[actix_rt::test] /// async fn my_handler() -> Result<HttpResponse, Error> {
/// # async fn hidden_test() {} /// Ok(HttpResponse::Ok().into())
/// }
///
/// #[actix_web::test]
/// async fn test_example() { /// async fn test_example() {
/// let srv = test_server(|| { /// let mut srv = TestServer::start(||
/// HttpService::build() /// HttpService::new(
/// .h1(fn_service(|req| async move { /// App::new().service(web::resource("/").to(my_handler))
/// Ok::<_, Error>(Response::ok()) /// )
/// })) /// );
/// .tcp()
/// .map_err(|_| ())
/// })
/// .await;
/// ///
/// let req = srv.get("/"); /// let req = srv.get("/");
/// let response = req.send().await.unwrap(); /// let response = req.send().await.unwrap();
/// /// assert!(response.status().is_success());
/// assert_eq!(response.status(), StatusCode::OK);
/// } /// }
/// # actix_rt::System::new().block_on(test_example());
/// ``` /// ```
pub async fn test_server<F: ServerServiceFactory<TcpStream>>(factory: F) -> TestServer { pub async fn test_server<F: ServerServiceFactory<TcpStream>>(factory: F) -> TestServer {
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap(); let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();

File diff suppressed because it is too large Load Diff

View File

@ -1,38 +1,26 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.7.0" version = "3.3.0"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
] ]
description = "HTTP types and services for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web.git"
categories = [ categories = [
"network-programming", "network-programming",
"asynchronous", "asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket", "web-programming::websocket",
] ]
license.workspace = true license = "MIT OR Apache-2.0"
edition.workspace = true edition = "2018"
rust-version.workspace = true
[package.metadata.docs.rs] [package.metadata.docs.rs]
rustdoc-args = ["--cfg", "docsrs"] # features that docs.rs will build with
features = [ features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
"http2",
"ws",
"openssl",
"rustls-0_20",
"rustls-0_21",
"rustls-0_22",
"rustls-0_23",
"compress-brotli",
"compress-gzip",
"compress-zstd",
]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -55,27 +43,15 @@ ws = [
# TLS via OpenSSL # TLS via OpenSSL
openssl = ["actix-tls/accept", "actix-tls/openssl"] openssl = ["actix-tls/accept", "actix-tls/openssl"]
# TLS via Rustls v0.20 # TLS via Rustls
rustls = ["rustls-0_20"] rustls = ["actix-tls/accept", "actix-tls/rustls"]
# TLS via Rustls v0.20
rustls-0_20 = ["actix-tls/accept", "actix-tls/rustls-0_20"]
# TLS via Rustls v0.21
rustls-0_21 = ["actix-tls/accept", "actix-tls/rustls-0_21"]
# TLS via Rustls v0.22
rustls-0_22 = ["actix-tls/accept", "actix-tls/rustls-0_22"]
# TLS via Rustls v0.23
rustls-0_23 = ["actix-tls/accept", "actix-tls/rustls-0_23"]
# Compression codecs # Compression codecs
compress-brotli = ["__compress", "brotli"] compress-brotli = ["__compress", "brotli"]
compress-gzip = ["__compress", "flate2"] compress-gzip = ["__compress", "flate2"]
compress-zstd = ["__compress", "zstd"] compress-zstd = ["__compress", "zstd"]
# Internal (PRIVATE!) features used to aid testing and checking feature status. # Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime. # Don't rely on these whatsoever. They are semver-exempt and may disappear at anytime.
__compress = [] __compress = []
@ -85,80 +61,82 @@ actix-codec = "0.5"
actix-utils = "3" actix-utils = "3"
actix-rt = { version = "2.2", default-features = false } actix-rt = { version = "2.2", default-features = false }
ahash = "0.8" ahash = "0.7"
bitflags = "2" bitflags = "1.2"
bytes = "1" 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.17", default-features = false, features = ["alloc"] }
http = "0.2.7" 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"
mime = "0.3.4" 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.24.2", features = [] } tokio = { version = "1.18.5", features = [] }
tokio-util = { version = "0.7", features = ["io", "codec"] } tokio-util = { version = "0.7", features = ["io", "codec"] }
tracing = { version = "0.1.30", default-features = false, features = ["log"] } tracing = { version = "0.1.30", default-features = false, features = ["log"] }
# http2 # http2
h2 = { version = "0.3.26", 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.22", optional = true } base64 = { version = "0.21", optional = true }
rand = { version = "0.8", optional = true } rand = { version = "0.8", optional = true }
sha1 = { version = "0.10", optional = true } sha1 = { version = "0.10", optional = true }
# openssl/rustls # openssl/rustls
actix-tls = { version = "3.4", default-features = false, optional = true } actix-tls = { version = "3", default-features = false, optional = true }
# compress-* # compress-*
brotli = { version = "6", 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.13", optional = true } zstd = { version = "0.12", optional = true }
[dev-dependencies] [dev-dependencies]
actix-http-test = { version = "3", features = ["openssl"] } actix-http-test = { version = "3", features = ["openssl"] }
actix-server = "2" actix-server = "2"
actix-tls = { version = "3.4", features = ["openssl", "rustls-0_23-webpki-roots"] } actix-tls = { version = "3", features = ["openssl"] }
actix-web = "4" actix-web = "4"
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.5", features = ["html_reports"] } criterion = { version = "0.4", features = ["html_reports"] }
divan = "0.1.8" env_logger = "0.9"
env_logger = "0.11"
futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.17", default-features = false, features = ["alloc"] }
memchr = "2.4" memchr = "2.4"
once_cell = "1.9" once_cell = "1.9"
rcgen = "0.13" rcgen = "0.9"
regex = "1.3" regex = "1.3"
rustversion = "1" rustversion = "1"
rustls-pemfile = "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.55" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls_023 = { package = "rustls", version = "0.23" } tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.24.2", features = ["net", "rt", "macros"] } tokio = { version = "1.18.5", features = ["net", "rt", "macros"] }
[[example]] [[example]]
name = "ws" name = "ws"
required-features = ["ws", "rustls-0_23"] required-features = ["ws", "rustls"]
[[example]]
name = "tls_rustls"
required-features = ["http2", "rustls-0_23"]
[[bench]] [[bench]]
name = "response-body-compression" name = "write-camel-case"
harness = false harness = false
required-features = ["compress-brotli", "compress-gzip", "compress-zstd"]
[[bench]] [[bench]]
name = "date-formatting" name = "status-line"
harness = false
[[bench]]
name = "uninit-headers"
harness = false
[[bench]]
name = "quality-value"
harness = false harness = false

View File

@ -1,21 +1,22 @@
# `actix-http` # actix-http
> HTTP types and services for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
<!-- prettier-ignore-start -->
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.7.0)](https://docs.rs/actix-http/3.7.0) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.3.0)](https://docs.rs/actix-http/3.3.0)
![Version](https://img.shields.io/badge/rustc-1.72+-ab6000.svg) ![Version](https://img.shields.io/badge/rustc-1.59+-ab6000.svg)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.7.0/status.svg)](https://deps.rs/crate/actix-http/3.7.0) [![dependency status](https://deps.rs/crate/actix-http/3.3.0/status.svg)](https://deps.rs/crate/actix-http/3.3.0)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
<!-- prettier-ignore-end --> ## Documentation & Resources
## Examples - [API Documentation](https://docs.rs/actix-http)
- Minimum Supported Rust Version (MSRV): 1.54
## Example
```rust ```rust
use std::{env, io}; use std::{env, io};
@ -48,3 +49,18 @@ async fn main() -> io::Result<()> {
.await .await
} }
``` ```
## License
This project is licensed under either of
- Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or [http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
- MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
at your option.
## Code of Conduct
Contribution to the actix-http crate is organized under the terms of the
Contributor Covenant, the maintainer of actix-http, @fafhrd91, promises to
intervene to uphold that code of conduct.

View File

@ -1,20 +0,0 @@
use std::time::SystemTime;
use actix_http::header::HttpDate;
use divan::{black_box, AllocProfiler, Bencher};
#[global_allocator]
static ALLOC: AllocProfiler = AllocProfiler::system();
#[divan::bench]
fn date_formatting(b: Bencher<'_, '_>) {
let now = SystemTime::now();
b.bench(|| {
black_box(HttpDate::from(black_box(now)).to_string());
})
}
fn main() {
divan::main();
}

View File

@ -0,0 +1,97 @@
#![allow(clippy::uninlined_format_args)]
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
fn bench_quality_display_impls(c: &mut Criterion) {
let mut group = c.benchmark_group("quality value display impls");
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
b.iter(|| _new::Quality(i).to_string())
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| _naive::Quality(i).to_string())
});
}
group.finish();
}
criterion_group!(benches, bench_quality_display_impls);
criterion_main!(benches);
mod _new {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
// some number in the range 1999
x => {
f.write_str("0.")?;
// this implementation avoids string allocation otherwise required
// for `.trim_end_matches('0')`
if x < 10 {
f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return
itoa_fmt(f, x)
} else if x < 100 {
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa_fmt(f, x / 10)
} else {
itoa_fmt(f, x)
}
} else {
// x is in range 101999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa_fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa_fmt(f, x / 10)
} else {
itoa_fmt(f, x)
}
}
}
}
}
}
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
let mut buf = itoa::Buffer::new();
wr.write_str(buf.format(value))
}
}
mod _naive {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
x => {
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
}
}
}
}
}

View File

@ -1,88 +0,0 @@
use std::convert::Infallible;
use actix_http::{encoding::Encoder, ContentEncoding, Request, Response, StatusCode};
use actix_service::{fn_service, Service as _};
use criterion::{black_box, criterion_group, criterion_main, Criterion};
static BODY: &[u8] = include_bytes!("../Cargo.toml");
fn compression_responses(c: &mut Criterion) {
let mut group = c.benchmark_group("compression responses");
group.bench_function("identity", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Identity,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.bench_function("gzip", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Gzip,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.bench_function("br", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Brotli,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.bench_function("zstd", |b| {
let rt = actix_rt::Runtime::new().unwrap();
let identity_svc = fn_service(|_: Request| async move {
let mut res = Response::with_body(StatusCode::OK, ());
let body = black_box(Encoder::response(
ContentEncoding::Zstd,
res.head_mut(),
BODY,
));
Ok::<_, Infallible>(black_box(res.set_body(black_box(body))))
});
b.iter(|| {
rt.block_on(identity_svc.call(Request::new())).unwrap();
});
});
group.finish();
}
criterion_group!(benches, compression_responses);
criterion_main!(benches);

View File

@ -0,0 +1,214 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use bytes::BytesMut;
use http::Version;
const CODES: &[u16] = &[201, 303, 404, 515];
fn bench_write_status_line_11(c: &mut Criterion) {
let mut group = c.benchmark_group("write_status_line v1.1");
let version = Version::HTTP_11;
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_naive::write_status_line(version, i, &mut b);
})
});
}
group.finish();
}
fn bench_write_status_line_10(c: &mut Criterion) {
let mut group = c.benchmark_group("write_status_line v1.0");
let version = Version::HTTP_10;
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_naive::write_status_line(version, i, &mut b);
})
});
}
group.finish();
}
fn bench_write_status_line_09(c: &mut Criterion) {
let mut group = c.benchmark_group("write_status_line v0.9");
let version = Version::HTTP_09;
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_status_line(version, i, &mut b);
})
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_naive::write_status_line(version, i, &mut b);
})
});
}
group.finish();
}
criterion_group!(
benches,
bench_write_status_line_11,
bench_write_status_line_10,
bench_write_status_line_09
);
criterion_main!(benches);
mod _naive {
use bytes::{BufMut, BytesMut};
use http::Version;
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
_ => {
// other HTTP version handlers do not use this method
}
}
bytes.put_slice(n.to_string().as_bytes());
}
}
mod _new {
use bytes::{BufMut, BytesMut};
use http::Version;
const DIGITS_START: u8 = b'0';
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "),
_ => {
// other HTTP version handlers do not use this method
}
}
let d100 = (n / 100) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
bytes.put_u8(b' ');
}
}
mod _original {
use std::ptr;
use bytes::{BufMut, BytesMut};
use http::Version;
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
2021222324252627282930313233343536373839\
4041424344454647484950515253545556575859\
6061626364656667686970717273747576777879\
8081828384858687888990919293949596979899";
pub(crate) const STATUS_LINE_BUF_SIZE: usize = 13;
pub(crate) fn write_status_line(version: Version, mut n: u16, bytes: &mut BytesMut) {
let mut buf: [u8; STATUS_LINE_BUF_SIZE] = *b"HTTP/1.1 ";
match version {
Version::HTTP_2 => buf[5] = b'2',
Version::HTTP_10 => buf[7] = b'0',
Version::HTTP_09 => {
buf[5] = b'0';
buf[7] = b'9';
}
_ => {}
}
let mut curr: isize = 12;
let buf_ptr = buf.as_mut_ptr();
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
let four = n > 999;
// decode 2 more chars, if > 2 chars
let d1 = (n % 100) << 1;
n /= 100;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
}
// decode last 1 or 2 chars
if n < 10 {
curr -= 1;
unsafe {
*buf_ptr.offset(curr) = (n as u8) + b'0';
}
} else {
let d1 = n << 1;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1 as isize), buf_ptr.offset(curr), 2);
}
}
bytes.put_slice(&buf);
if four {
bytes.put_u8(b' ');
}
}
}

View File

@ -0,0 +1,135 @@
use criterion::{criterion_group, criterion_main, Criterion};
use bytes::BytesMut;
// A Miri run detects UB, seen on this playground:
// https://play.rust-lang.org/?version=stable&mode=debug&edition=2018&gist=f5d9aa166aa48df8dca05fce2b6c3915
fn bench_header_parsing(c: &mut Criterion) {
c.bench_function("Original (Unsound) [short]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ_SHORT);
_original::parse_headers(&mut buf);
})
});
c.bench_function("New (safe) [short]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ_SHORT);
_new::parse_headers(&mut buf);
})
});
c.bench_function("Original (Unsound) [realistic]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ);
_original::parse_headers(&mut buf);
})
});
c.bench_function("New (safe) [realistic]", |b| {
b.iter(|| {
let mut buf = BytesMut::from(REQ);
_new::parse_headers(&mut buf);
})
});
}
criterion_group!(benches, bench_header_parsing);
criterion_main!(benches);
const MAX_HEADERS: usize = 96;
const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
[httparse::EMPTY_HEADER; MAX_HEADERS];
#[derive(Clone, Copy)]
struct HeaderIndex {
name: (usize, usize),
value: (usize, usize),
}
const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
name: (0, 0),
value: (0, 0),
};
const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] = [EMPTY_HEADER_INDEX; MAX_HEADERS];
impl HeaderIndex {
fn record(bytes: &[u8], headers: &[httparse::Header<'_>], indices: &mut [HeaderIndex]) {
let bytes_ptr = bytes.as_ptr() as usize;
for (header, indices) in headers.iter().zip(indices.iter_mut()) {
let name_start = header.name.as_ptr() as usize - bytes_ptr;
let name_end = name_start + header.name.len();
indices.name = (name_start, name_end);
let value_start = header.value.as_ptr() as usize - bytes_ptr;
let value_end = value_start + header.value.len();
indices.value = (value_start, value_end);
}
}
}
// test cases taken from:
// https://github.com/seanmonstar/httparse/blob/master/benches/parse.rs
const REQ_SHORT: &[u8] = b"\
GET / HTTP/1.0\r\n\
Host: example.com\r\n\
Cookie: session=60; user_id=1\r\n\r\n";
const REQ: &[u8] = b"\
GET /wp-content/uploads/2010/03/hello-kitty-darth-vader-pink.jpg HTTP/1.1\r\n\
Host: www.kittyhell.com\r\n\
User-Agent: Mozilla/5.0 (Macintosh; U; Intel Mac OS X 10.6; ja-JP-mac; rv:1.9.2.3) Gecko/20100401 Firefox/3.6.3 Pathtraq/0.9\r\n\
Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8\r\n\
Accept-Language: ja,en-us;q=0.7,en;q=0.3\r\n\
Accept-Encoding: gzip,deflate\r\n\
Accept-Charset: Shift_JIS,utf-8;q=0.7,*;q=0.7\r\n\
Keep-Alive: 115\r\n\
Connection: keep-alive\r\n\
Cookie: wp_ozh_wsa_visits=2; wp_ozh_wsa_visit_lasttime=xxxxxxxxxx; __utma=xxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.xxxxxxxxxx.x; __utmz=xxxxxxxxx.xxxxxxxxxx.x.x.utmccn=(referral)|utmcsr=reader.livedoor.com|utmcct=/reader/|utmcmd=referral|padding=under256\r\n\r\n";
mod _new {
use super::*;
pub fn parse_headers(src: &mut BytesMut) -> usize {
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
let mut req = httparse::Request::new(&mut parsed);
match req.parse(src).unwrap() {
httparse::Status::Complete(_len) => {
HeaderIndex::record(src, req.headers, &mut headers);
req.headers.len()
}
_ => unreachable!(),
}
}
}
mod _original {
use super::*;
use std::mem::MaybeUninit;
pub fn parse_headers(src: &mut BytesMut) -> usize {
#![allow(invalid_value, clippy::uninit_assumed_init)]
let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
#[allow(invalid_value)]
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
let mut req = httparse::Request::new(&mut parsed);
match req.parse(src).unwrap() {
httparse::Status::Complete(_len) => {
HeaderIndex::record(src, req.headers, &mut headers);
req.headers.len()
}
_ => unreachable!(),
}
}
}

View File

@ -0,0 +1,93 @@
use criterion::{black_box, criterion_group, criterion_main, BenchmarkId, Criterion};
fn bench_write_camel_case(c: &mut Criterion) {
let mut group = c.benchmark_group("write_camel_case");
let names = ["connection", "Transfer-Encoding", "transfer-encoding"];
for &i in &names {
let bts = i.as_bytes();
group.bench_with_input(BenchmarkId::new("Original", i), bts, |b, bts| {
b.iter(|| {
let mut buf = black_box([0; 24]);
_original::write_camel_case(black_box(bts), &mut buf)
});
});
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
b.iter(|| {
let mut buf = black_box([0; 24]);
let len = black_box(bts.len());
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
});
});
}
group.finish();
}
criterion_group!(benches, bench_write_camel_case);
criterion_main!(benches);
mod _new {
pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
// first copy entire (potentially wrong) slice to output
let buffer = unsafe {
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
std::slice::from_raw_parts_mut(buf, len)
};
let mut iter = value.iter();
// first character should be uppercase
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[0] = c & 0b1101_1111;
}
// track 1 ahead of the current position since that's the location being assigned to
let mut index = 2;
// remaining characters after hyphens should also be uppercase
while let Some(&c) = iter.next() {
if c == b'-' {
// advance iter by one and uppercase if needed
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[index] = c & 0b1101_1111;
}
}
index += 1;
}
}
}
mod _original {
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
let mut index = 0;
let key = value;
let mut key_iter = key.iter();
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
} else {
return;
}
while let Some(c) = key_iter.next() {
buffer[index] = *c;
index += 1;
if *c == b'-' {
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
}
}
}
}
}

View File

@ -8,7 +8,7 @@
use std::{convert::Infallible, io}; use std::{convert::Infallible, io};
use actix_http::{body::BodyStream, HttpService, Request, Response, StatusCode}; use actix_http::{HttpService, Request, Response, StatusCode};
use actix_server::Server; use actix_server::Server;
#[tokio::main(flavor = "current_thread")] #[tokio::main(flavor = "current_thread")]
@ -19,12 +19,7 @@ async fn main() -> io::Result<()> {
.bind("h2c-detect", ("127.0.0.1", 8080), || { .bind("h2c-detect", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.finish(|_req: Request| async move { .finish(|_req: Request| async move {
Ok::<_, Infallible>(Response::build(StatusCode::OK).body(BodyStream::new( Ok::<_, Infallible>(Response::build(StatusCode::OK).body("Hello!"))
futures_util::stream::iter([
Ok::<_, String>("123".into()),
Err("wertyuikmnbvcxdfty6t".to_owned()),
]),
)))
}) })
.tcp_auto_h2c() .tcp_auto_h2c()
})? })?

View File

@ -23,7 +23,10 @@ async fn main() -> io::Result<()> {
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.conn_data::<u32>().unwrap().to_string();
res.insert_header(("x-forty-two", HeaderValue::from_str(&forty_two).unwrap())); res.insert_header((
"x-forty-two",
HeaderValue::from_str(&forty_two).unwrap(),
));
Ok::<_, Infallible>(res.body("Hello world!")) Ok::<_, Infallible>(res.body("Hello world!"))
}) })

View File

@ -1,76 +0,0 @@
//! Demonstrates TLS configuration (via Rustls) for HTTP/1.1 and HTTP/2 connections.
//!
//! Test using cURL:
//!
//! ```console
//! $ curl --insecure https://127.0.0.1:8443
//! Hello World!
//! Protocol: HTTP/2.0
//!
//! $ curl --insecure --http1.1 https://127.0.0.1:8443
//! Hello World!
//! Protocol: HTTP/1.1
//! ```
extern crate tls_rustls_023 as rustls;
use std::io;
use actix_http::{Error, HttpService, Request, Response};
use actix_utils::future::ok;
#[actix_rt::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
tracing::info!("starting HTTP server at https://127.0.0.1:8443");
actix_server::Server::build()
.bind("echo", ("127.0.0.1", 8443), || {
HttpService::build()
.finish(|req: Request| {
let body = format!(
"Hello World!\n\
Protocol: {:?}",
req.head().version
);
ok::<_, Error>(Response::ok().set_body(body))
})
.rustls_0_23(rustls_config())
})?
.run()
.await
}
fn rustls_config() -> rustls::ServerConfig {
let rcgen::CertifiedKey { cert, key_pair } =
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap();
let cert_file = cert.pem();
let key_file = key_pair.serialize_pem();
let cert_file = &mut io::BufReader::new(cert_file.as_bytes());
let key_file = &mut io::BufReader::new(key_file.as_bytes());
let cert_chain = rustls_pemfile::certs(cert_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut keys = rustls_pemfile::pkcs8_private_keys(key_file)
.collect::<Result<Vec<_>, _>>()
.unwrap();
let mut config = rustls::ServerConfig::builder()
.with_no_client_auth()
.with_single_cert(
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.unwrap();
const H1_ALPN: &[u8] = b"http/1.1";
const H2_ALPN: &[u8] = b"h2";
config.alpn_protocols.push(H2_ALPN.to_vec());
config.alpn_protocols.push(H1_ALPN.to_vec());
config
}

View File

@ -1,7 +1,7 @@
//! Sets up a WebSocket server over TCP and TLS. //! Sets up a WebSocket server over TCP and TLS.
//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames. //! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
extern crate tls_rustls_023 as rustls; extern crate tls_rustls as rustls;
use std::{ use std::{
io, io,
@ -28,9 +28,7 @@ async fn main() -> io::Result<()> {
HttpService::build().h1(handler).tcp() HttpService::build().h1(handler).tcp()
})? })?
.bind("tls", ("127.0.0.1", 8443), || { .bind("tls", ("127.0.0.1", 8443), || {
HttpService::build() HttpService::build().finish(handler).rustls(tls_config())
.finish(handler)
.rustls_0_23(tls_config())
})? })?
.run() .run()
.await .await
@ -85,27 +83,27 @@ impl Stream for Heartbeat {
fn tls_config() -> rustls::ServerConfig { fn tls_config() -> rustls::ServerConfig {
use std::io::BufReader; use std::io::BufReader;
use rustls::{Certificate, PrivateKey};
use rustls_pemfile::{certs, pkcs8_private_keys}; use rustls_pemfile::{certs, pkcs8_private_keys};
let rcgen::CertifiedKey { cert, key_pair } = let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap();
let cert_file = cert.pem(); let key_file = cert.serialize_private_key_pem();
let key_file = key_pair.serialize_pem();
let cert_file = &mut BufReader::new(cert_file.as_bytes()); let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).collect::<Result<Vec<_>, _>>().unwrap(); let cert_chain = certs(cert_file)
let mut keys = pkcs8_private_keys(key_file) .unwrap()
.collect::<Result<Vec<_>, _>>() .into_iter()
.unwrap(); .map(Certificate)
.collect();
let mut keys = pkcs8_private_keys(key_file).unwrap();
let mut config = rustls::ServerConfig::builder() let mut config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth() .with_no_client_auth()
.with_single_cert( .with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
cert_chain,
rustls::pki_types::PrivateKeyDer::Pkcs8(keys.remove(0)),
)
.unwrap(); .unwrap();
config.alpn_protocols.push(b"http/1.1".to_vec()); config.alpn_protocols.push(b"http/1.1".to_vec());

View File

@ -47,8 +47,9 @@ where
/// Attempts to pull out the next value of the underlying [`Stream`]. /// Attempts to pull out the next value of the underlying [`Stream`].
/// ///
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being ended on a /// Empty values are skipped to prevent [`BodyStream`]'s transmission being
/// zero-length chunk, but rather proceed until the underlying [`Stream`] ends. /// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,

View File

@ -77,8 +77,12 @@ impl MessageBody for BoxBody {
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match &mut self.0 { match &mut self.0 {
BoxBodyInner::None(body) => Pin::new(body).poll_next(cx).map_err(|err| match err {}), BoxBodyInner::None(body) => {
BoxBodyInner::Bytes(body) => Pin::new(body).poll_next(cx).map_err(|err| match err {}), Pin::new(body).poll_next(cx).map_err(|err| match err {})
}
BoxBodyInner::Bytes(body) => {
Pin::new(body).poll_next(cx).map_err(|err| match err {})
}
BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx), BoxBodyInner::Stream(body) => Pin::new(body).poll_next(cx),
} }
} }
@ -100,6 +104,7 @@ impl MessageBody for BoxBody {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use static_assertions::{assert_impl_all, assert_not_impl_any}; use static_assertions::{assert_impl_all, assert_not_impl_any};
use super::*; use super::*;

View File

@ -531,6 +531,7 @@ where
mod tests { 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 futures_util::stream; use futures_util::stream;
use super::*; use super::*;
@ -554,7 +555,6 @@ mod tests {
}; };
} }
#[allow(unused_allocation)] // triggered by `Box::new(()).size()`
#[actix_rt::test] #[actix_rt::test]
async fn boxing_equivalence() { async fn boxing_equivalence() {
assert_eq!(().size(), BodySize::Sized(0)); assert_eq!(().size(), BodySize::Sized(0));

View File

@ -14,14 +14,12 @@ mod size;
mod sized_stream; mod sized_stream;
mod utils; mod utils;
pub use self::body_stream::BodyStream;
pub use self::boxed::BoxBody;
pub use self::either::EitherBody;
pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr; pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::{ pub use self::none::None;
body_stream::BodyStream, pub use self::size::BodySize;
boxed::BoxBody, pub use self::sized_stream::SizedStream;
either::EitherBody, pub use self::utils::to_bytes;
message_body::MessageBody,
none::None,
size::BodySize,
sized_stream::SizedStream,
utils::{to_bytes, to_bytes_limited, BodyLimitExceeded},
};

View File

@ -3,196 +3,75 @@ use std::task::Poll;
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 derive_more::{Display, Error};
use futures_core::ready; use futures_core::ready;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
/// Collects all the bytes produced by `body`. /// Collects the body produced by a `MessageBody` implementation into `Bytes`.
/// ///
/// Any errors produced by the body stream are returned immediately. /// Any errors produced by the body stream are returned immediately.
/// ///
/// Consider using [`to_bytes_limited`] instead to protect against memory exhaustion.
///
/// # Examples /// # Examples
///
/// ``` /// ```
/// use actix_http::body::{self, to_bytes}; /// use actix_http::body::{self, to_bytes};
/// use bytes::Bytes; /// use bytes::Bytes;
/// ///
/// # actix_rt::System::new().block_on(async { /// # async fn test_to_bytes() {
/// let body = body::None::new(); /// let body = body::None::new();
/// let bytes = to_bytes(body).await.unwrap(); /// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty()); /// assert!(bytes.is_empty());
/// ///
/// let body = Bytes::from_static(b"123"); /// let body = Bytes::from_static(b"123");
/// let bytes = to_bytes(body).await.unwrap(); /// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, "123"); /// assert_eq!(bytes, b"123"[..]);
/// # }); /// # }
/// ``` /// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> { pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
to_bytes_limited(body, usize::MAX)
.await
.expect("body should never yield more than usize::MAX bytes")
}
/// Error type returned from [`to_bytes_limited`] when body produced exceeds limit.
#[derive(Debug, Display, Error)]
#[display(fmt = "limit exceeded while collecting body bytes")]
#[non_exhaustive]
pub struct BodyLimitExceeded;
/// Collects the bytes produced by `body`, up to `limit` bytes.
///
/// If a chunk read from `poll_next` causes the total number of bytes read to exceed `limit`, an
/// `Err(BodyLimitExceeded)` is returned.
///
/// Any errors produced by the body stream are returned immediately as `Ok(Err(B::Error))`.
///
/// # Examples
///
/// ```
/// use actix_http::body::{self, to_bytes_limited};
/// use bytes::Bytes;
///
/// # actix_rt::System::new().block_on(async {
/// let body = body::None::new();
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Bytes::from_static(b"123");
/// let bytes = to_bytes_limited(body, 10).await.unwrap().unwrap();
/// assert_eq!(bytes, "123");
///
/// let body = Bytes::from_static(b"123");
/// assert!(to_bytes_limited(body, 2).await.is_err());
/// # });
/// ```
pub async fn to_bytes_limited<B: MessageBody>(
body: B,
limit: usize,
) -> Result<Result<Bytes, B::Error>, BodyLimitExceeded> {
/// Sensible default (32kB) for initial, bounded allocation when collecting body bytes.
const INITIAL_ALLOC_BYTES: usize = 32 * 1024;
let cap = match body.size() { let cap = match body.size() {
BodySize::None | BodySize::Sized(0) => return Ok(Ok(Bytes::new())), BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) if size as usize > limit => return Err(BodyLimitExceeded), BodySize::Sized(size) => size as usize,
BodySize::Sized(size) => (size as usize).min(INITIAL_ALLOC_BYTES), // good enough first guess for chunk size
BodySize::Stream => INITIAL_ALLOC_BYTES, BodySize::Stream => 32_768,
}; };
let mut exceeded_limit = false;
let mut buf = BytesMut::with_capacity(cap); let mut buf = BytesMut::with_capacity(cap);
pin!(body); pin!(body);
match poll_fn(|cx| loop { poll_fn(|cx| loop {
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)) => { Some(Ok(bytes)) => buf.extend_from_slice(&bytes),
// if limit is exceeded...
if buf.len() + bytes.len() > limit {
// ...set flag to true and break out of poll_fn
exceeded_limit = true;
return Poll::Ready(Ok(()));
}
buf.extend_from_slice(&bytes)
}
None => return Poll::Ready(Ok(())), None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)), Some(Err(err)) => return Poll::Ready(Err(err)),
} }
}) })
.await .await?;
{
// propagate error returned from body poll
Err(err) => Ok(Err(err)),
// limit was exceeded while reading body Ok(buf.freeze())
Ok(()) if exceeded_limit => Err(BodyLimitExceeded),
// otherwise return body buffer
Ok(()) => Ok(Ok(buf.freeze())),
}
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod test {
use std::io;
use futures_util::{stream, StreamExt as _}; use futures_util::{stream, StreamExt as _};
use super::*; use super::*;
use crate::{ use crate::{body::BodyStream, Error};
body::{BodyStream, SizedStream},
Error,
};
#[actix_rt::test] #[actix_rt::test]
async fn to_bytes_complete() { async fn test_to_bytes() {
let bytes = to_bytes(()).await.unwrap(); let bytes = to_bytes(()).await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
let body = Bytes::from_static(b"123"); let body = Bytes::from_static(b"123");
let bytes = to_bytes(body).await.unwrap(); let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]); assert_eq!(bytes, b"123"[..]);
}
#[actix_rt::test]
async fn to_bytes_streams() {
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")]) let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
.map(Ok::<_, Error>); .map(Ok::<_, Error>);
let body = BodyStream::new(stream); let body = BodyStream::new(stream);
let bytes = to_bytes(body).await.unwrap(); let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123abc"[..]); assert_eq!(bytes, b"123abc"[..]);
} }
#[actix_rt::test]
async fn to_bytes_limited_complete() {
let bytes = to_bytes_limited((), 0).await.unwrap().unwrap();
assert!(bytes.is_empty());
let bytes = to_bytes_limited((), 1).await.unwrap().unwrap();
assert!(bytes.is_empty());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 0)
.await
.is_err());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 1)
.await
.is_err());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 2).await.is_ok());
assert!(to_bytes_limited(Bytes::from_static(b"12"), 3).await.is_ok());
}
#[actix_rt::test]
async fn to_bytes_limited_streams() {
// hinting a larger body fails
let body = SizedStream::new(8, stream::empty().map(Ok::<_, Error>));
assert!(to_bytes_limited(body, 3).await.is_err());
// hinting a smaller body is okay
let body = SizedStream::new(3, stream::empty().map(Ok::<_, Error>));
assert!(to_bytes_limited(body, 3).await.unwrap().unwrap().is_empty());
// hinting a smaller body then returning a larger one fails
let stream = stream::iter(vec![Bytes::from_static(b"1234")]).map(Ok::<_, Error>);
let body = SizedStream::new(3, stream);
assert!(to_bytes_limited(body, 3).await.is_err());
let stream = stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
.map(Ok::<_, Error>);
let body = BodyStream::new(stream);
assert!(to_bytes_limited(body, 3).await.is_err());
}
#[actix_rt::test]
async fn to_body_limit_error() {
let err_stream = stream::once(async { Err(io::Error::new(io::ErrorKind::Other, "")) });
let body = SizedStream::new(8, err_stream);
// not too big, but propagates error from body stream
assert!(to_bytes_limited(body, 10).await.unwrap().is_err());
}
} }

View File

@ -211,6 +211,7 @@ where
/// Finish service configuration and create a service for the HTTP/2 protocol. /// Finish service configuration and create a service for the 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>,

View File

@ -132,15 +132,15 @@ impl ServiceConfig {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*;
use crate::{date::DATE_VALUE_LENGTH, notify_on_drop};
use actix_rt::{ use actix_rt::{
task::yield_now, task::yield_now,
time::{sleep, sleep_until}, time::{sleep, sleep_until},
}; };
use memchr::memmem; use memchr::memmem;
use super::*;
use crate::{date::DATE_VALUE_LENGTH, notify_on_drop};
#[actix_rt::test] #[actix_rt::test]
async fn test_date_service_update() { async fn test_date_service_update() {
let settings = let settings =

View File

@ -28,7 +28,7 @@ impl Date {
fn update(&mut self) { fn update(&mut self) {
self.pos = 0; self.pos = 0;
write!(self, "{}", httpdate::HttpDate::from(SystemTime::now())).unwrap(); write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
} }
} }

View File

@ -9,9 +9,11 @@ use std::{
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream};
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream};
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
use zstd::stream::write::Decoder as ZstdDecoder; use zstd::stream::write::Decoder as ZstdDecoder;
@ -47,9 +49,9 @@ where
))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(ZlibDecoder::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
Writer::new(), ZlibDecoder::new(Writer::new()),
)))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new( ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(GzDecoder::new(
@ -191,7 +193,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@ -205,7 +207,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@ -218,7 +220,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
@ -231,7 +233,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
} }
} }
@ -250,7 +252,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@ -265,7 +267,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
@ -280,7 +282,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
@ -295,7 +297,7 @@ impl ContentDecoder {
Ok(None) Ok(None)
} }
} }
Err(err) => Err(err), Err(e) => Err(e),
}, },
} }
} }

View File

@ -11,10 +11,12 @@ use std::{
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use derive_more::Display; use derive_more::Display;
#[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready; use futures_core::ready;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "compress-gzip")]
use flate2::write::{GzEncoder, ZlibEncoder};
use tracing::trace; 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;
@ -50,21 +52,10 @@ impl<B: MessageBody> Encoder<B> {
} }
} }
fn empty() -> Self {
Encoder {
body: EncoderBody::Full { body: Bytes::new() },
encoder: None,
fut: None,
eof: true,
}
}
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self { pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
// no need to compress empty bodies // no need to compress an empty body
match body.size() { if matches!(body.size(), BodySize::None) {
BodySize::None => return Self::none(), return Self::none();
BodySize::Sized(0) => return Self::empty(),
_ => {}
} }
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING) let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)

View File

@ -7,12 +7,13 @@ use bytes::{Bytes, BytesMut};
mod decoder; mod decoder;
mod encoder; mod encoder;
pub use self::{decoder::Decoder, encoder::Encoder}; pub use self::decoder::Decoder;
pub use self::encoder::Encoder;
/// Special-purpose writer for streaming (de-)compression. /// Special-purpose writer for streaming (de-)compression.
/// ///
/// Pre-allocates 8KiB of capacity. /// Pre-allocates 8KiB of capacity.
struct Writer { pub(self) struct Writer {
buf: BytesMut, buf: BytesMut,
} }

View File

@ -3,11 +3,12 @@
use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error}; use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Error};
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
pub use http::{status::InvalidStatusCode, Error as HttpError};
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{body::BoxBody, Response}; use crate::{body::BoxBody, Response};
pub use http::Error as HttpError;
pub struct Error { pub struct Error {
inner: Box<ErrorInner>, inner: Box<ErrorInner>,
} }
@ -160,44 +161,44 @@ impl From<crate::ws::ProtocolError> for Error {
#[non_exhaustive] #[non_exhaustive]
pub enum ParseError { pub enum ParseError {
/// An invalid `Method`, such as `GE.T`. /// An invalid `Method`, such as `GE.T`.
#[display(fmt = "invalid method specified")] #[display(fmt = "Invalid Method specified")]
Method, Method,
/// An invalid `Uri`, such as `exam ple.domain`. /// An invalid `Uri`, such as `exam ple.domain`.
#[display(fmt = "URI error: {}", _0)] #[display(fmt = "Uri error: {}", _0)]
Uri(InvalidUri), Uri(InvalidUri),
/// An invalid `HttpVersion`, such as `HTP/1.1` /// An invalid `HttpVersion`, such as `HTP/1.1`
#[display(fmt = "invalid HTTP version specified")] #[display(fmt = "Invalid HTTP version specified")]
Version, Version,
/// An invalid `Header`. /// An invalid `Header`.
#[display(fmt = "invalid Header provided")] #[display(fmt = "Invalid Header provided")]
Header, Header,
/// A message head is too large to be reasonable. /// A message head is too large to be reasonable.
#[display(fmt = "message head is too large")] #[display(fmt = "Message head is too large")]
TooLarge, TooLarge,
/// A message reached EOF, but is not complete. /// A message reached EOF, but is not complete.
#[display(fmt = "message is incomplete")] #[display(fmt = "Message is incomplete")]
Incomplete, Incomplete,
/// An invalid `Status`, such as `1337 ELITE`. /// An invalid `Status`, such as `1337 ELITE`.
#[display(fmt = "invalid status provided")] #[display(fmt = "Invalid Status provided")]
Status, Status,
/// A timeout occurred waiting for an IO event. /// A timeout occurred waiting for an IO event.
#[allow(dead_code)] #[allow(dead_code)]
#[display(fmt = "timeout")] #[display(fmt = "Timeout")]
Timeout, Timeout,
/// An I/O error that occurred while trying to read or write to a network stream. /// An `io::Error` that occurred while trying to read or write to a network stream.
#[display(fmt = "I/O error: {}", _0)] #[display(fmt = "IO error: {}", _0)]
Io(io::Error), Io(io::Error),
/// Parsing a field as string failed. /// Parsing a field as string failed.
#[display(fmt = "UTF-8 error: {}", _0)] #[display(fmt = "UTF8 error: {}", _0)]
Utf8(Utf8Error), Utf8(Utf8Error),
} }
@ -256,19 +257,22 @@ impl From<ParseError> for Response<BoxBody> {
#[non_exhaustive] #[non_exhaustive]
pub enum PayloadError { pub enum PayloadError {
/// A payload reached EOF, but is not complete. /// A payload reached EOF, but is not complete.
#[display(fmt = "payload reached EOF before completing: {:?}", _0)] #[display(
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
_0
)]
Incomplete(Option<io::Error>), Incomplete(Option<io::Error>),
/// Content encoding stream corruption. /// Content encoding stream corruption.
#[display(fmt = "can not decode content-encoding")] #[display(fmt = "Can not decode content-encoding.")]
EncodingCorrupted, EncodingCorrupted,
/// Payload reached size limit. /// Payload reached size limit.
#[display(fmt = "payload reached size limit")] #[display(fmt = "Payload reached size limit.")]
Overflow, Overflow,
/// Payload length is unknown. /// Payload length is unknown.
#[display(fmt = "payload length is unknown")] #[display(fmt = "Payload length is unknown.")]
UnknownLength, UnknownLength,
/// HTTP/2 payload error. /// HTTP/2 payload error.
@ -290,6 +294,7 @@ 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),
} }
@ -326,44 +331,44 @@ impl From<PayloadError> for Error {
#[non_exhaustive] #[non_exhaustive]
pub enum DispatchError { pub enum DispatchError {
/// Service error. /// Service error.
#[display(fmt = "service error")] #[display(fmt = "Service Error")]
Service(Response<BoxBody>), Service(Response<BoxBody>),
/// Body streaming error. /// Body streaming error.
#[display(fmt = "body error: {}", _0)] #[display(fmt = "Body error: {}", _0)]
Body(Box<dyn StdError>), Body(Box<dyn StdError>),
/// Upgrade service error. /// Upgrade service error.
#[display(fmt = "upgrade error")]
Upgrade, Upgrade,
/// An `io::Error` that occurred while trying to read or write to a network stream. /// An `io::Error` that occurred while trying to read or write to a network stream.
#[display(fmt = "I/O error: {}", _0)] #[display(fmt = "IO error: {}", _0)]
Io(io::Error), Io(io::Error),
/// Request parse error. /// Request parse error.
#[display(fmt = "request parse error: {}", _0)] #[display(fmt = "Request parse error: {}", _0)]
Parse(ParseError), Parse(ParseError),
/// 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.
#[display(fmt = "request did not complete within the specified timeout")] #[display(fmt = "The first request did not complete within the specified timeout")]
SlowRequestTimeout, SlowRequestTimeout,
/// Disconnect timeout. Makes sense for TLS streams. /// Disconnect timeout. Makes sense for ssl streams.
#[display(fmt = "connection shutdown timeout")] #[display(fmt = "Connection shutdown timeout")]
DisconnectTimeout, DisconnectTimeout,
/// Handler dropped payload before reading EOF. /// Handler dropped payload before reading EOF.
#[display(fmt = "handler dropped payload before reading EOF")] #[display(fmt = "Handler dropped payload before reading EOF")]
HandlerDroppedPayload, HandlerDroppedPayload,
/// Internal error. /// Internal error.
#[display(fmt = "internal error")] #[display(fmt = "Internal error")]
InternalError, InternalError,
} }
@ -388,18 +393,20 @@ impl StdError for DispatchError {
#[cfg_attr(test, derive(PartialEq, Eq))] #[cfg_attr(test, derive(PartialEq, Eq))]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentTypeError { pub enum ContentTypeError {
/// Can not parse content type. /// Can not parse content type
#[display(fmt = "could not parse content type")] #[display(fmt = "Can not parse content type")]
ParseError, ParseError,
/// Unknown content encoding. /// Unknown content encoding
#[display(fmt = "unknown content encoding")] #[display(fmt = "Unknown content encoding")]
UnknownEncoding, UnknownEncoding,
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use http::Error as HttpError; use std::io;
use http::{Error as HttpError, StatusCode};
use super::*; use super::*;
@ -419,7 +426,7 @@ mod tests {
let err: Error = ParseError::Io(orig).into(); let err: Error = ParseError::Io(orig).into();
assert_eq!( assert_eq!(
format!("{}", err), format!("{}", err),
"error parsing HTTP message: I/O error: other" "error parsing HTTP message: IO error: other"
); );
} }
@ -446,7 +453,7 @@ mod tests {
let err = PayloadError::Incomplete(None); let err = PayloadError::Incomplete(None);
assert_eq!( assert_eq!(
err.to_string(), err.to_string(),
"payload reached EOF before completing: None" "A payload reached EOF, but is not complete. Inner error: None"
); );
} }
@ -466,7 +473,7 @@ mod tests {
match ParseError::from($from) { match ParseError::from($from) {
e @ $error => { e @ $error => {
let desc = format!("{}", e); let desc = format!("{}", e);
assert_eq!(desc, format!("I/O error: {}", $from)); assert_eq!(desc, format!("IO error: {}", $from));
} }
_ => unreachable!("{:?}", $from), _ => unreachable!("{:?}", $from),
} }

View File

@ -16,7 +16,6 @@ use crate::{
}; };
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)]
struct Flags: u8 { struct Flags: u8 {
const HEAD = 0b0000_0001; const HEAD = 0b0000_0001;
const KEEP_ALIVE_ENABLED = 0b0000_1000; const KEEP_ALIVE_ENABLED = 0b0000_1000;

View File

@ -9,10 +9,11 @@ use super::{
decoder::{self, PayloadDecoder, PayloadItem, PayloadType}, decoder::{self, PayloadDecoder, PayloadItem, PayloadType},
encoder, Message, MessageType, encoder, Message, MessageType,
}; };
use crate::{body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig}; use crate::{
body::BodySize, error::ParseError, ConnectionType, Request, Response, ServiceConfig,
};
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)]
struct Flags: u8 { struct Flags: u8 {
const HEAD = 0b0000_0001; const HEAD = 0b0000_0001;
const KEEP_ALIVE_ENABLED = 0b0000_0010; const KEEP_ALIVE_ENABLED = 0b0000_0010;
@ -198,6 +199,9 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::BytesMut;
use http::Method;
use super::*; use super::*;
use crate::HttpMessage as _; use crate::HttpMessage as _;

View File

@ -1,4 +1,4 @@
use std::{io, marker::PhantomData, mem::MaybeUninit, task::Poll}; use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll};
use actix_codec::Decoder; use actix_codec::Decoder;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@ -94,7 +94,9 @@ pub(crate) trait MessageType: Sized {
// SAFETY: httparse already checks header value is only visible ASCII bytes // SAFETY: httparse already checks header value is only visible ASCII bytes
// from_maybe_shared_unchecked contains debug assertions so they are omitted here // from_maybe_shared_unchecked contains debug assertions so they are omitted here
let value = unsafe { let value = unsafe {
HeaderValue::from_maybe_shared_unchecked(slice.slice(idx.value.0..idx.value.1)) HeaderValue::from_maybe_shared_unchecked(
slice.slice(idx.value.0..idx.value.1),
)
}; };
match name { match name {
@ -273,7 +275,8 @@ impl MessageType for Request {
let mut msg = Request::new(); let mut msg = Request::new();
// convert headers // convert headers
let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; let mut length =
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers // 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 // see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
@ -353,8 +356,8 @@ impl MessageType for ResponseHead {
Version::HTTP_10 Version::HTTP_10
}; };
let status = let status = StatusCode::from_u16(res.code.unwrap())
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())
@ -375,7 +378,8 @@ impl MessageType for ResponseHead {
msg.version = ver; msg.version = ver;
// convert headers // convert headers
let mut length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?; let mut length =
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed. // Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
// Protects against some request smuggling attacks. // Protects against some request smuggling attacks.
@ -532,7 +536,7 @@ impl Decoder for PayloadDecoder {
*state = match state.step(src, size, &mut buf) { *state = match state.step(src, size, &mut buf) {
Poll::Pending => return Ok(None), Poll::Pending => return Ok(None),
Poll::Ready(Ok(state)) => state, Poll::Ready(Ok(state)) => state,
Poll::Ready(Err(err)) => return Err(err), Poll::Ready(Err(e)) => return Err(e),
}; };
if *state == ChunkedState::End { if *state == ChunkedState::End {
@ -563,8 +567,15 @@ impl Decoder for PayloadDecoder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::{Bytes, BytesMut};
use http::{Method, Version};
use super::*; use super::*;
use crate::{header::SET_COOKIE, HttpMessage as _}; use crate::{
error::ParseError,
header::{HeaderName, SET_COOKIE},
HttpMessage as _,
};
impl PayloadType { impl PayloadType {
pub(crate) fn unwrap(self) -> PayloadDecoder { pub(crate) fn unwrap(self) -> PayloadDecoder {

View File

@ -19,13 +19,6 @@ use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder as _, Encoder as _}; use tokio_util::codec::{Decoder as _, Encoder as _};
use tracing::{error, trace}; use tracing::{error, trace};
use super::{
codec::Codec,
decoder::MAX_BUFFER_SIZE,
payload::{Payload, PayloadSender, PayloadStatus},
timer::TimerState,
Message, MessageType,
};
use crate::{ use crate::{
body::{BodySize, BoxBody, MessageBody}, body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
@ -34,12 +27,19 @@ use crate::{
Error, Extensions, OnConnectData, Request, Response, StatusCode, Error, Extensions, OnConnectData, Request, Response, StatusCode,
}; };
use super::{
codec::Codec,
decoder::MAX_BUFFER_SIZE,
payload::{Payload, PayloadSender, PayloadStatus},
timer::TimerState,
Message, MessageType,
};
const LW_BUFFER_SIZE: usize = 1024; const LW_BUFFER_SIZE: usize = 1024;
const HW_BUFFER_SIZE: usize = 1024 * 8; const HW_BUFFER_SIZE: usize = 1024 * 8;
const MAX_PIPELINED_MESSAGES: usize = 16; const MAX_PIPELINED_MESSAGES: usize = 16;
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)]
pub struct Flags: u8 { pub struct Flags: u8 {
/// Set when stream is read for first time. /// Set when stream is read for first time.
const STARTED = 0b0000_0001; const STARTED = 0b0000_0001;
@ -212,7 +212,9 @@ where
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self { match self {
Self::None => write!(f, "State::None"), Self::None => write!(f, "State::None"),
Self::ExpectCall { .. } => f.debug_struct("State::ExpectCall").finish_non_exhaustive(), Self::ExpectCall { .. } => {
f.debug_struct("State::ExpectCall").finish_non_exhaustive()
}
Self::ServiceCall { .. } => { Self::ServiceCall { .. } => {
f.debug_struct("State::ServiceCall").finish_non_exhaustive() f.debug_struct("State::ServiceCall").finish_non_exhaustive()
} }
@ -273,7 +275,9 @@ where
head_timer: TimerState::new(config.client_request_deadline().is_some()), head_timer: TimerState::new(config.client_request_deadline().is_some()),
ka_timer: TimerState::new(config.keep_alive().enabled()), ka_timer: TimerState::new(config.keep_alive().enabled()),
shutdown_timer: TimerState::new(config.client_disconnect_deadline().is_some()), shutdown_timer: TimerState::new(
config.client_disconnect_deadline().is_some(),
),
io: Some(io), io: Some(io),
read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE), read_buf: BytesMut::with_capacity(HW_BUFFER_SIZE),
@ -451,7 +455,9 @@ where
} }
// return with upgrade request and poll it exclusively // return with upgrade request and poll it exclusively
Some(DispatcherMessage::Upgrade(req)) => return Ok(PollResponse::Upgrade(req)), Some(DispatcherMessage::Upgrade(req)) => {
return Ok(PollResponse::Upgrade(req))
}
// all messages are dealt with // all messages are dealt with
None => { None => {
@ -512,10 +518,8 @@ where
} }
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {
let err = err.into();
tracing::error!("Response payload stream error: {err:?}");
this.flags.insert(Flags::FINISHED); this.flags.insert(Flags::FINISHED);
return Err(DispatchError::Body(err)); return Err(DispatchError::Body(err.into()));
} }
Poll::Pending => return Ok(PollResponse::DoNothing), Poll::Pending => return Ok(PollResponse::DoNothing),
@ -551,7 +555,6 @@ where
} }
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {
tracing::error!("Response payload stream error: {err:?}");
this.flags.insert(Flags::FINISHED); this.flags.insert(Flags::FINISHED);
return Err(DispatchError::Body( return Err(DispatchError::Body(
Error::new_body().with_cause(err).into(), Error::new_body().with_cause(err).into(),
@ -671,7 +674,9 @@ where
} }
_ => { _ => {
unreachable!("State must be set to ServiceCall or ExceptCall in handle_request") unreachable!(
"State must be set to ServiceCall or ExceptCall in handle_request"
)
} }
} }
} }
@ -680,7 +685,10 @@ where
/// Process one incoming request. /// Process one incoming request.
/// ///
/// Returns true if any meaningful work was done. /// Returns true if any meaningful work was done.
fn poll_request(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<bool, DispatchError> { fn poll_request(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<bool, DispatchError> {
let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES; let pipeline_queue_full = self.messages.len() >= MAX_PIPELINED_MESSAGES;
let can_not_read = !self.can_read(cx); let can_not_read = !self.can_read(cx);
@ -706,7 +714,7 @@ where
req.head_mut().peer_addr = *this.peer_addr; req.head_mut().peer_addr = *this.peer_addr;
req.conn_data.clone_from(this.conn_data); req.conn_data = this.conn_data.as_ref().map(Rc::clone);
match this.codec.message_type() { match this.codec.message_type() {
// request has no payload // request has no payload
@ -850,7 +858,10 @@ where
Ok(()) Ok(())
} }
fn poll_ka_timer(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), DispatchError> { fn poll_ka_timer(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<(), DispatchError> {
let this = self.as_mut().project(); let this = self.as_mut().project();
if let TimerState::Active { timer } = this.ka_timer { if let TimerState::Active { timer } = this.ka_timer {
debug_assert!( debug_assert!(
@ -915,7 +926,10 @@ where
} }
/// Poll head, keep-alive, and disconnect timer. /// Poll head, keep-alive, and disconnect timer.
fn poll_timers(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<(), DispatchError> { fn poll_timers(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<(), DispatchError> {
self.as_mut().poll_head_timer(cx)?; self.as_mut().poll_head_timer(cx)?;
self.as_mut().poll_ka_timer(cx)?; self.as_mut().poll_ka_timer(cx)?;
self.as_mut().poll_shutdown_timer(cx)?; self.as_mut().poll_shutdown_timer(cx)?;
@ -929,7 +943,10 @@ where
/// - `std::io::ErrorKind::ConnectionReset` after partial read; /// - `std::io::ErrorKind::ConnectionReset` after partial read;
/// - all data read done. /// - all data read done.
#[inline(always)] // TODO: bench this inline #[inline(always)] // TODO: bench this inline
fn read_available(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Result<bool, DispatchError> { fn read_available(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Result<bool, DispatchError> {
let this = self.project(); let this = self.project();
if this.flags.contains(Flags::READ_DISCONNECT) { if this.flags.contains(Flags::READ_DISCONNECT) {

View File

@ -1,12 +1,15 @@
use std::{future::Future, str, task::Poll, time::Duration}; use std::{future::Future, str, task::Poll, time::Duration};
use actix_codec::Framed;
use actix_rt::{pin, time::sleep}; use actix_rt::{pin, time::sleep};
use actix_service::{fn_service, Service}; use actix_service::fn_service;
use actix_utils::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use bytes::{Buf, Bytes, BytesMut}; use bytes::Bytes;
use futures_util::future::lazy; use futures_util::future::lazy;
use actix_codec::Framed;
use actix_service::Service;
use bytes::{Buf, BytesMut};
use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags}; use super::dispatcher::{Dispatcher, DispatcherState, DispatcherStateProj, Flags};
use crate::{ use crate::{
body::MessageBody, body::MessageBody,
@ -40,8 +43,8 @@ fn status_service(
fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status)))) fn_service(move |_req: Request| ready(Ok::<_, Error>(Response::new(status))))
} }
fn echo_path_service() -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> fn echo_path_service(
{ ) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error> {
fn_service(|req: Request| { fn_service(|req: Request| {
let path = req.path().as_bytes(); let path = req.path().as_bytes();
ready(Ok::<_, Error>( ready(Ok::<_, Error>(
@ -50,8 +53,8 @@ fn echo_path_service() -> impl Service<Request, Response = Response<impl Message
}) })
} }
fn drop_payload_service() -> impl Service<Request, Response = Response<&'static str>, Error = Error> fn drop_payload_service(
{ ) -> impl Service<Request, Response = Response<&'static str>, Error = Error> {
fn_service(|mut req: Request| async move { fn_service(|mut req: Request| async move {
let _ = req.take_payload(); let _ = req.take_payload();
Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped")) Ok::<_, Error>(Response::with_body(StatusCode::OK, "payload dropped"))
@ -929,6 +932,7 @@ fn http_msg(msg: impl AsRef<str>) -> BytesMut {
.as_ref() .as_ref()
.trim() .trim()
.split('\n') .split('\n')
.into_iter()
.map(|line| [line.trim_start(), "\r"].concat()) .map(|line| [line.trim_start(), "\r"].concat())
.collect::<Vec<_>>() .collect::<Vec<_>>()
.join("\n"); .join("\n");

View File

@ -17,16 +17,14 @@ mod timer;
mod upgrade; mod upgrade;
mod utils; mod utils;
pub use self::{ pub use self::client::{ClientCodec, ClientPayloadCodec};
client::{ClientCodec, ClientPayloadCodec}, pub use self::codec::Codec;
codec::Codec, pub use self::dispatcher::Dispatcher;
dispatcher::Dispatcher, pub use self::expect::ExpectHandler;
expect::ExpectHandler, pub use self::payload::Payload;
payload::Payload, pub use self::service::{H1Service, H1ServiceHandler};
service::{H1Service, H1ServiceHandler}, pub use self::upgrade::UpgradeHandler;
upgrade::UpgradeHandler, pub use self::utils::SendResponse;
utils::SendResponse,
};
#[derive(Debug)] #[derive(Debug)]
/// Codec message /// Codec message

View File

@ -117,7 +117,6 @@ impl PayloadSender {
} }
} }
#[allow(clippy::needless_pass_by_ref_mut)]
#[inline] #[inline]
pub fn need_read(&self, cx: &mut Context<'_>) -> PayloadStatus { pub fn need_read(&self, cx: &mut Context<'_>) -> PayloadStatus {
// we check need_read only if Payload (other side) is alive, // we check need_read only if Payload (other side) is alive,
@ -175,7 +174,7 @@ impl Inner {
/// Register future waiting data from payload. /// Register future waiting data from payload.
/// Waker would be used in `Inner::wake` /// Waker would be used in `Inner::wake`
fn register(&mut self, cx: &Context<'_>) { fn register(&mut self, cx: &mut Context<'_>) {
if self if self
.task .task
.as_ref() .as_ref()
@ -187,7 +186,7 @@ impl Inner {
// Register future feeding data to payload. // Register future feeding data to payload.
/// Waker would be used in `Inner::wake_io` /// Waker would be used in `Inner::wake_io`
fn register_io(&mut self, cx: &Context<'_>) { fn register_io(&mut self, cx: &mut Context<'_>) {
if self if self
.io_task .io_task
.as_ref() .as_ref()
@ -222,7 +221,7 @@ impl Inner {
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, PayloadError>>> { ) -> Poll<Option<Result<Bytes, PayloadError>>> {
if let Some(data) = self.items.pop_front() { if let Some(data) = self.items.pop_front() {
self.len -= data.len(); self.len -= data.len();

View File

@ -15,7 +15,6 @@ use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use tracing::error; use tracing::error;
use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
@ -24,6 +23,8 @@ use crate::{
ConnectCallback, OnConnectData, Request, Response, ConnectCallback, OnConnectData, Request, Response,
}; };
use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport /// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> { pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
srv: S, srv: S,
@ -81,8 +82,13 @@ where
/// Create simple tcp stream service /// Create simple tcp stream service
pub fn tcp( pub fn tcp(
self, self,
) -> impl ServiceFactory<TcpStream, Config = (), Response = (), Error = DispatchError, InitError = ()> ) -> impl ServiceFactory<
{ TcpStream,
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(|io: TcpStream| { fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr))) ready(Ok((io, peer_addr)))
@ -93,6 +99,8 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use super::*;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{ openssl::{
reexports::{Error as SslError, SslAcceptor}, reexports::{Error as SslError, SslAcceptor},
@ -101,8 +109,6 @@ mod openssl {
TlsError, TlsError,
}; };
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
@ -128,6 +134,7 @@ 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,
@ -152,13 +159,14 @@ mod openssl {
} }
} }
#[cfg(feature = "rustls-0_20")] #[cfg(feature = "rustls")]
mod rustls_0_20 { mod rustls {
use std::io; use std::io;
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream}, rustls::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError, TlsError,
}; };
@ -188,7 +196,8 @@ mod rustls_0_20 {
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create Rustls v0.20 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,
@ -213,189 +222,6 @@ mod rustls_0_20 {
} }
} }
#[cfg(feature = "rustls-0_21")]
mod rustls_0_21 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.21 based service.
pub fn rustls_021(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_22")]
mod rustls_0_22 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.22 based service.
pub fn rustls_0_22(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_23")]
mod rustls_0_23 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.23 based service.
pub fn rustls_0_23(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B, X, U> H1Service<T, S, B, X, U> impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,

View File

@ -4,7 +4,7 @@ use std::{
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
net, net,
pin::{pin, Pin}, pin::Pin,
rc::Rc, rc::Rc,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -20,6 +20,7 @@ use h2::{
Ping, PingPong, Ping, PingPong,
}; };
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},
@ -126,7 +127,7 @@ where
head.headers = parts.headers.into(); head.headers = parts.headers.into();
head.peer_addr = this.peer_addr; head.peer_addr = this.peer_addr;
req.conn_data.clone_from(&this.conn_data); req.conn_data = this.conn_data.as_ref().map(Rc::clone);
let fut = this.flow.service.call(req); let fut = this.flow.service.call(req);
let config = this.config.clone(); let config = this.config.clone();
@ -146,13 +147,11 @@ where
if let Err(err) = res { if let Err(err) = res {
match err { match err {
DispatchError::SendResponse(err) => { DispatchError::SendResponse(err) => {
tracing::trace!("Error sending response: {err:?}"); trace!("Error sending HTTP/2 response: {:?}", err)
}
DispatchError::SendData(err) => {
tracing::warn!("Send data error: {err:?}");
} }
DispatchError::SendData(err) => warn!("{:?}", err),
DispatchError::ResponseBody(err) => { DispatchError::ResponseBody(err) => {
tracing::error!("Response payload stream error: {err:?}"); error!("Response payload stream error: {:?}", err)
} }
} }
} }
@ -229,9 +228,9 @@ where
return Ok(()); return Ok(());
} }
let mut body = pin!(body);
// poll response body and send chunks to client // poll response body and send chunks to client
actix_rt::pin!(body);
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await { while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?; let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;

View File

@ -23,7 +23,8 @@ use crate::{
mod dispatcher; mod dispatcher;
mod service; mod service;
pub use self::{dispatcher::Dispatcher, service::H2Service}; pub use self::dispatcher::Dispatcher;
pub use self::service::H2Service;
/// HTTP/2 peer stream. /// HTTP/2 peer stream.
pub struct Payload { pub struct Payload {
@ -57,7 +58,10 @@ impl Stream for Payload {
} }
} }
pub(crate) fn handshake_with_timeout<T>(io: T, config: &ServiceConfig) -> HandshakeWithTimeout<T> pub(crate) fn handshake_with_timeout<T>(
io: T,
config: &ServiceConfig,
) -> HandshakeWithTimeout<T>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
{ {

View File

@ -16,7 +16,6 @@ use actix_utils::future::ready;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use tracing::{error, trace}; use tracing::{error, trace};
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
use crate::{ use crate::{
body::{BoxBody, MessageBody}, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
@ -25,6 +24,8 @@ use crate::{
ConnectCallback, OnConnectData, Request, Response, ConnectCallback, OnConnectData, Request, Response,
}; };
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
/// `ServiceFactory` implementation for HTTP/2 transport /// `ServiceFactory` implementation for HTTP/2 transport
pub struct H2Service<T, S, B> { pub struct H2Service<T, S, B> {
srv: S, srv: S,
@ -116,6 +117,7 @@ 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,
@ -140,8 +142,8 @@ mod openssl {
} }
} }
#[cfg(feature = "rustls-0_20")] #[cfg(feature = "rustls")]
mod rustls_0_20 { mod rustls {
use std::io; use std::io;
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
@ -162,7 +164,8 @@ mod rustls_0_20 {
B: MessageBody + 'static, B: MessageBody + 'static,
{ {
/// Create Rustls v0.20 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,
@ -191,159 +194,6 @@ mod rustls_0_20 {
} }
} }
#[cfg(feature = "rustls-0_21")]
mod rustls_0_21 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create Rustls v0.21 based service.
pub fn rustls_021(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
> {
let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_22")]
mod rustls_0_22 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create Rustls v0.22 based service.
pub fn rustls_0_22(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
> {
let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_23")]
mod rustls_0_23 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
/// Create Rustls v0.23 based service.
pub fn rustls_0_23(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
> {
let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
Acceptor::new(config)
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
(io, peer_addr)
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B> impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,

View File

@ -8,14 +8,12 @@ use http::header::HeaderName;
/// request. /// request.
/// ///
/// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics. /// See [RFC 9211](https://www.rfc-editor.org/rfc/rfc9211) for full semantics.
// TODO(breaking): replace with http's version
pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status"); pub const CACHE_STATUS: HeaderName = HeaderName::from_static("cache-status");
/// Response header field that allows origin servers to control the behavior of CDN caches /// 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. /// 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. /// See [RFC 9213](https://www.rfc-editor.org/rfc/rfc9213) for full semantics.
// TODO(breaking): replace with http's version
pub const CDN_CACHE_CONTROL: HeaderName = HeaderName::from_static("cdn-cache-control"); 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 /// Response header that prevents a document from loading any cross-origin resources that don't

View File

@ -1,5 +1,7 @@
//! [`TryIntoHeaderPair`] trait and implementations. //! [`TryIntoHeaderPair`] trait and implementations.
use std::convert::TryFrom as _;
use super::{ use super::{
Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue, Header, HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, TryIntoHeaderValue,
}; };

View File

@ -1,5 +1,7 @@
//! [`TryIntoHeaderValue`] trait and implementations. //! [`TryIntoHeaderValue`] trait and implementations.
use std::convert::TryFrom as _;
use bytes::Bytes; use bytes::Bytes;
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
use mime::Mime; use mime::Mime;

View File

@ -636,24 +636,10 @@ impl<'a> IntoIterator for &'a HeaderMap {
} }
} }
/// Convert a `http::HeaderMap` to our `HeaderMap`. /// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap { impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> Self { fn from(mut map: http::HeaderMap) -> HeaderMap {
Self::from_drain(map.drain()) HeaderMap::from_drain(map.drain())
}
}
/// Convert our `HeaderMap` to a `http::HeaderMap`.
impl From<HeaderMap> for http::HeaderMap {
fn from(map: HeaderMap) -> Self {
Self::from_iter(map)
}
}
/// Convert our `&HeaderMap` to a `http::HeaderMap`.
impl From<&HeaderMap> for http::HeaderMap {
fn from(map: &HeaderMap) -> Self {
map.to_owned().into()
} }
} }
@ -1134,7 +1120,9 @@ mod tests {
assert!(vals.next().is_none()); assert!(vals.next().is_none());
} }
fn owned_pair<'a>((name, val): (&'a HeaderName, &'a HeaderValue)) -> (HeaderName, HeaderValue) { fn owned_pair<'a>(
(name, val): (&'a HeaderName, &'a HeaderValue),
) -> (HeaderName, HeaderValue) {
(name.clone(), val.clone()) (name.clone(), val.clone())
} }
} }

View File

@ -3,30 +3,33 @@
// declaring new header consts will yield this error // declaring new header consts will yield this error
#![allow(clippy::declare_interior_mutable_const)] #![allow(clippy::declare_interior_mutable_const)]
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, list is explicit so that any updates to `common` module do not
// conflict with this set // 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_METHODS, ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN, ACCESS_CONTROL_EXPOSE_HEADERS,
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC, ACCESS_CONTROL_MAX_AGE, ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE,
AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING, ALLOW, ALT_SVC, AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION,
CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE, CONTENT_SECURITY_POLICY, CONTENT_ENCODING, CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE,
CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE, DNT, ETAG, EXPECT, EXPIRES, CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE, DATE,
FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH, IF_MODIFIED_SINCE,
IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED, LINK, LOCATION, MAX_FORWARDS,
PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, ORIGIN, PRAGMA, PROXY_AUTHENTICATE, PROXY_AUTHORIZATION, PUBLIC_KEY_PINS,
REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER, REFERRER_POLICY, REFRESH, RETRY_AFTER,
SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL, SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, SEC_WEBSOCKET_ACCEPT, SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
STRICT_TRANSPORT_SECURITY, TE, TRAILER, TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER,
USER_AGENT, VARY, VIA, WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA, WARNING,
X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS, X_XSS_PROTECTION, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL, X_FRAME_OPTIONS,
X_XSS_PROTECTION,
}; };
use percent_encoding::{AsciiSet, CONTROLS};
use crate::{error::ParseError, HttpMessage}; use crate::{error::ParseError, HttpMessage};
@ -40,22 +43,23 @@ mod utils;
pub use self::{ pub use self::{
as_name::AsHeaderName, as_name::AsHeaderName,
// re-export list is explicit so that any updates to `http` do not conflict with this set
common::{
CACHE_STATUS, CDN_CACHE_CONTROL, CROSS_ORIGIN_EMBEDDER_POLICY, CROSS_ORIGIN_OPENER_POLICY,
CROSS_ORIGIN_RESOURCE_POLICY, PERMISSIONS_POLICY, X_FORWARDED_FOR, X_FORWARDED_HOST,
X_FORWARDED_PROTO,
},
into_pair::TryIntoHeaderPair, into_pair::TryIntoHeaderPair,
into_value::TryIntoHeaderValue, into_value::TryIntoHeaderValue,
map::HeaderMap, map::HeaderMap,
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}, utils::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode},
}; };
// re-export list is explicit so that any updates to `http` do not conflict with this set
pub use self::common::{
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.
pub trait Header: TryIntoHeaderValue { pub trait Header: TryIntoHeaderValue {
/// Returns the name of the header field. /// Returns the name of the header field.

View File

@ -1,4 +1,4 @@
use std::str::FromStr; use std::{convert::TryFrom, str::FromStr};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use http::header::InvalidHeaderValue; use http::header::InvalidHeaderValue;

View File

@ -24,7 +24,8 @@ impl FromStr for HttpDate {
impl fmt::Display for HttpDate { impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
httpdate::HttpDate::from(self.0).fmt(f) let date_str = httpdate::fmt_http_date(self.0);
f.write_str(&date_str)
} }
} }
@ -36,7 +37,7 @@ impl TryIntoHeaderValue for HttpDate {
let mut wrt = MutWriter(&mut buf); let mut wrt = MutWriter(&mut buf);
// unwrap: date output is known to be well formed and of known length // unwrap: date output is known to be well formed and of known length
write!(wrt, "{}", self).unwrap(); write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap();
HeaderValue::from_maybe_shared(buf.split().freeze()) HeaderValue::from_maybe_shared(buf.split().freeze())
} }

View File

@ -1,7 +1,5 @@
//! Originally taken from `hyper::header::shared`. //! Originally taken from `hyper::header::shared`.
pub use language_tags::LanguageTag;
mod charset; mod charset;
mod content_encoding; mod content_encoding;
mod extended; mod extended;
@ -9,11 +7,10 @@ mod http_date;
mod quality; mod quality;
mod quality_item; mod quality_item;
pub use self::{ pub use self::charset::Charset;
charset::Charset, pub use self::content_encoding::ContentEncoding;
content_encoding::ContentEncoding, pub use self::extended::{parse_extended_value, ExtendedValue};
extended::{parse_extended_value, ExtendedValue}, pub use self::http_date::HttpDate;
http_date::HttpDate, pub use self::quality::{q, Quality};
quality::{q, Quality}, pub use self::quality_item::QualityItem;
quality_item::QualityItem, pub use language_tags::LanguageTag;
};

View File

@ -1,4 +1,7 @@
use std::fmt; use std::{
convert::{TryFrom, TryInto},
fmt,
};
use derive_more::{Display, Error}; use derive_more::{Display, Error};

View File

@ -1,7 +1,8 @@
use std::{cmp, fmt, str}; use std::{cmp, convert::TryFrom as _, fmt, str};
use crate::error::ParseError;
use super::Quality; use super::Quality;
use crate::error::ParseError;
/// Represents an item with a quality value as defined /// Represents an item with a quality value as defined
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1). /// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).

View File

@ -80,18 +80,18 @@ mod tests {
#[test] #[test]
fn comma_delimited_parsing() { fn comma_delimited_parsing() {
let headers = []; let headers = vec![];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap(); let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![0; 0]); assert_eq!(res, vec![0; 0]);
let headers = [ let headers = vec![
HeaderValue::from_static("1, 2"), HeaderValue::from_static("1, 2"),
HeaderValue::from_static("3,4"), HeaderValue::from_static("3,4"),
]; ];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap(); let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![1, 2, 3, 4]); assert_eq!(res, vec![1, 2, 3, 4]);
let headers = [ let headers = vec![
HeaderValue::from_static(""), HeaderValue::from_static(""),
HeaderValue::from_static(","), HeaderValue::from_static(","),
HeaderValue::from_static(" "), HeaderValue::from_static(" "),

View File

@ -61,7 +61,9 @@ pub trait HttpMessage: Sized {
fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> { fn encoding(&self) -> Result<&'static Encoding, ContentTypeError> {
if let Some(mime_type) = self.mime_type()? { if let Some(mime_type) = self.mime_type()? {
if let Some(charset) = mime_type.get_param("charset") { if let Some(charset) = mime_type.get_param("charset") {
if let Some(enc) = Encoding::for_label_no_replacement(charset.as_str().as_bytes()) { if let Some(enc) =
Encoding::for_label_no_replacement(charset.as_str().as_bytes())
{
Ok(enc) Ok(enc)
} else { } else {
Err(ContentTypeError::UnknownEncoding) Err(ContentTypeError::UnknownEncoding)
@ -144,7 +146,7 @@ mod tests {
.finish(); .finish();
assert_eq!(req.content_type(), "text/plain"); assert_eq!(req.content_type(), "text/plain");
let req = TestRequest::default() let req = TestRequest::default()
.insert_header(("content-type", "application/json; charset=utf-8")) .insert_header(("content-type", "application/json; charset=utf=8"))
.finish(); .finish();
assert_eq!(req.content_type(), "application/json"); assert_eq!(req.content_type(), "application/json");
let req = TestRequest::default().finish(); let req = TestRequest::default().finish();

View File

@ -1,15 +1,11 @@
//! HTTP types and services for the Actix ecosystem. //! HTTP primitives for the Actix ecosystem.
//! //!
//! ## Crate Features //! ## Crate Features
//!
//! | Feature | Functionality | //! | Feature | Functionality |
//! | ------------------- | ------------------------------------------- | //! | ------------------- | ------------------------------------------- |
//! | `http2` | HTTP/2 support via [h2]. | //! | `http2` | HTTP/2 support via [h2]. |
//! | `openssl` | TLS support via [OpenSSL]. | //! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls` | TLS support via [rustls] 0.20. | //! | `rustls` | TLS support via [rustls]. |
//! | `rustls-0_21` | TLS support via [rustls] 0.21. |
//! | `rustls-0_22` | TLS support via [rustls] 0.22. |
//! | `rustls-0_23` | TLS support via [rustls] 0.23. |
//! | `compress-brotli` | Payload compression support: Brotli. | //! | `compress-brotli` | Payload compression support: Brotli. |
//! | `compress-gzip` | Payload compression support: Deflate, Gzip. | //! | `compress-gzip` | Payload compression support: Deflate, Gzip. |
//! | `compress-zstd` | Payload compression support: Zstd. | //! | `compress-zstd` | Payload compression support: Zstd. |
@ -25,13 +21,15 @@
#![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_auto_cfg))] #![cfg_attr(docsrs, feature(doc_cfg))]
pub use http::{uri, uri::Uri, Method, StatusCode, Version}; pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version};
pub mod body; pub mod body;
mod builder; mod builder;
@ -43,6 +41,7 @@ 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;
@ -57,32 +56,26 @@ 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::config::ServiceConfig;
pub use self::error::Error;
pub use self::extensions::Extensions;
pub use self::header::ContentEncoding;
pub use self::http_message::HttpMessage;
pub use self::keep_alive::KeepAlive;
pub use self::message::ConnectionType;
pub use self::message::Message;
#[allow(deprecated)] #[allow(deprecated)]
pub use self::payload::PayloadStream; pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
#[cfg(any( pub use self::requests::{Request, RequestHead, RequestHeadType};
feature = "openssl", pub use self::responses::{Response, ResponseBuilder, ResponseHead};
feature = "rustls-0_20", pub use self::service::HttpService;
feature = "rustls-0_21", #[cfg(any(feature = "openssl", feature = "rustls"))]
feature = "rustls-0_22", #[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
feature = "rustls-0_23",
))]
pub use self::service::TlsAcceptorConfig; pub use self::service::TlsAcceptorConfig;
pub use self::{
builder::HttpServiceBuilder,
config::ServiceConfig,
error::Error,
extensions::Extensions,
header::ContentEncoding,
http_message::HttpMessage,
keep_alive::KeepAlive,
message::{ConnectionType, Message},
payload::{BoxedPayloadStream, Payload},
requests::{Request, RequestHead, RequestHeadType},
responses::{Response, ResponseBuilder, ResponseHead},
service::HttpService,
};
/// A major HTTP protocol version. /// A major HTTP protocol version.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]

View File

@ -16,7 +16,6 @@ pub enum ConnectionType {
} }
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)]
pub(crate) struct Flags: u8 { pub(crate) struct Flags: u8 {
const CLOSE = 0b0000_0001; const CLOSE = 0b0000_0001;
const KEEP_ALIVE = 0b0000_0010; const KEEP_ALIVE = 0b0000_0010;

View File

@ -5,7 +5,7 @@
use std::cell::RefCell; use std::cell::RefCell;
thread_local! { thread_local! {
static NOTIFY_DROPPED: RefCell<Option<bool>> = const { RefCell::new(None) }; static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
} }
/// Check if the spawned task is dropped. /// Check if the spawned task is dropped.

View File

@ -16,10 +16,7 @@ pub struct RequestHead {
pub uri: Uri, pub uri: Uri,
pub version: Version, pub version: Version,
pub headers: HeaderMap, pub headers: HeaderMap,
/// Will only be None when called in unit tests unless set manually.
pub peer_addr: Option<net::SocketAddr>, pub peer_addr: Option<net::SocketAddr>,
flags: Flags, flags: Flags,
} }

View File

@ -3,7 +3,5 @@
mod head; mod head;
mod request; mod request;
pub use self::{ pub use self::head::{RequestHead, RequestHeadType};
head::{RequestHead, RequestHeadType}, pub use self::request::Request;
request::Request,
};

View File

@ -10,7 +10,8 @@ use std::{
use http::{header, Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::{ use crate::{
header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload, RequestHead, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage, Message, Payload,
RequestHead,
}; };
/// An HTTP request. /// An HTTP request.
@ -173,7 +174,7 @@ impl<P> Request<P> {
/// Peer address is the directly connected peer's socket address. If a proxy is used in front of /// Peer address is the directly connected peer's socket address. If a proxy is used in front of
/// the Actix Web server, then it would be address of this proxy. /// the Actix Web server, then it would be address of this proxy.
/// ///
/// Will only return None when called in unit tests unless set manually. /// Will only return None when called in unit tests.
#[inline] #[inline]
pub fn peer_addr(&self) -> Option<net::SocketAddr> { pub fn peer_addr(&self) -> Option<net::SocketAddr> {
self.head().peer_addr self.head().peer_addr
@ -233,6 +234,7 @@ impl<P> fmt::Debug for Request<P> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use std::convert::TryFrom;
#[test] #[test]
fn test_basics() { fn test_basics() {

View File

@ -93,7 +93,7 @@ impl ResponseBuilder {
Ok((key, value)) => { Ok((key, value)) => {
parts.headers.insert(key, value); parts.headers.insert(key, value);
} }
Err(err) => self.err = Some(err.into()), Err(e) => self.err = Some(e.into()),
}; };
} }
@ -119,7 +119,7 @@ impl ResponseBuilder {
if let Some(parts) = self.inner() { if let Some(parts) = self.inner() {
match header.try_into_pair() { match header.try_into_pair() {
Ok((key, value)) => parts.headers.append(key, value), Ok((key, value)) => parts.headers.append(key, value),
Err(err) => self.err = Some(err.into()), Err(e) => self.err = Some(e.into()),
}; };
} }
@ -193,7 +193,7 @@ impl ResponseBuilder {
Ok(value) => { Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value); parts.headers.insert(header::CONTENT_TYPE, value);
} }
Err(err) => self.err = Some(err.into()), Err(e) => self.err = Some(e.into()),
}; };
} }
self self

View File

@ -5,5 +5,7 @@ mod head;
#[allow(clippy::module_inception)] #[allow(clippy::module_inception)]
mod response; mod response;
pub use self::builder::ResponseBuilder;
pub(crate) use self::head::BoxedResponseHead; pub(crate) use self::head::BoxedResponseHead;
pub use self::{builder::ResponseBuilder, head::ResponseHead, response::Response}; pub use self::head::ResponseHead;
pub use self::response::Response;

View File

@ -30,9 +30,9 @@ use crate::{
/// ///
/// # Automatic HTTP Version Selection /// # Automatic HTTP Version Selection
/// There are two ways to select the HTTP version of an incoming connection: /// 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 TLS (HTTPS); both versions /// - One is to rely on the ALPN information that is provided when using a TLS (HTTPS); both
/// are supported automatically when using either of the `.rustls()` or `.openssl()` finalizing /// versions are supported automatically when using either of the `.rustls()` or `.openssl()`
/// methods. /// finalizing methods.
/// - The other is to read the first few bytes of the TCP stream. This is the only viable approach /// - 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 /// 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. /// the `.tcp_auto_h2c()` finalizing method to enable this behavior.
@ -200,8 +200,13 @@ where
/// The resulting service only supports HTTP/1.x. /// The resulting service only supports HTTP/1.x.
pub fn tcp( pub fn tcp(
self, self,
) -> impl ServiceFactory<TcpStream, Config = (), Response = (), Error = DispatchError, InitError = ()> ) -> impl ServiceFactory<
{ TcpStream,
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(|io: TcpStream| async { fn_service(|io: TcpStream| async {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
Ok((io, Protocol::Http1, peer_addr)) Ok((io, Protocol::Http1, peer_addr))
@ -212,10 +217,16 @@ where
/// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2 /// Creates TCP stream service from HTTP service that automatically selects HTTP/1.x or HTTP/2
/// on plaintext connections. /// on plaintext connections.
#[cfg(feature = "http2")] #[cfg(feature = "http2")]
#[cfg_attr(docsrs, doc(cfg(feature = "http2")))]
pub fn tcp_auto_h2c( pub fn tcp_auto_h2c(
self, self,
) -> impl ServiceFactory<TcpStream, Config = (), Response = (), Error = DispatchError, InitError = ()> ) -> impl ServiceFactory<
{ TcpStream,
Config = (),
Response = (),
Error = DispatchError,
InitError = (),
> {
fn_service(move |io: TcpStream| async move { fn_service(move |io: TcpStream| async move {
// subset of HTTP/2 preface defined by RFC 9113 §3.4 // 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 // this subset was chosen to maximize likelihood that peeking only once will allow us to
@ -241,25 +252,14 @@ where
} }
/// Configuration options used when accepting TLS connection. /// Configuration options used when accepting TLS connection.
#[cfg(any( #[cfg(any(feature = "openssl", feature = "rustls"))]
feature = "openssl", #[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
feature = "rustls-0_20",
feature = "rustls-0_21",
feature = "rustls-0_22",
feature = "rustls-0_23",
))]
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct TlsAcceptorConfig { pub struct TlsAcceptorConfig {
pub(crate) handshake_timeout: Option<std::time::Duration>, pub(crate) handshake_timeout: Option<std::time::Duration>,
} }
#[cfg(any( #[cfg(any(feature = "openssl", feature = "rustls"))]
feature = "openssl",
feature = "rustls-0_20",
feature = "rustls-0_21",
feature = "rustls-0_22",
feature = "rustls-0_23",
))]
impl TlsAcceptorConfig { impl TlsAcceptorConfig {
/// Set TLS handshake timeout duration. /// Set TLS handshake timeout duration.
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self { pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
@ -309,6 +309,7 @@ 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,
@ -323,6 +324,7 @@ mod openssl {
} }
/// Create OpenSSL based service with custom TLS acceptor configuration. /// Create OpenSSL based service with custom TLS acceptor configuration.
#[cfg_attr(docsrs, doc(cfg(feature = "openssl")))]
pub fn openssl_with_config( pub fn openssl_with_config(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@ -364,13 +366,13 @@ mod openssl {
} }
} }
#[cfg(feature = "rustls-0_20")] #[cfg(feature = "rustls")]
mod rustls_0_20 { mod rustls {
use std::io; use std::io;
use actix_service::ServiceFactoryExt as _; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls_0_20::{reexports::ServerConfig, Acceptor, TlsStream}, rustls::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError, TlsError,
}; };
@ -401,7 +403,8 @@ mod rustls_0_20 {
U::Error: fmt::Display + Into<Response<BoxBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create Rustls v0.20 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,
@ -415,7 +418,8 @@ mod rustls_0_20 {
self.rustls_with_config(config, TlsAcceptorConfig::default()) self.rustls_with_config(config, TlsAcceptorConfig::default())
} }
/// Create Rustls v0.20 based service with custom TLS acceptor configuration. /// Create Rustls based service with custom TLS acceptor configuration.
#[cfg_attr(docsrs, doc(cfg(feature = "rustls")))]
pub fn rustls_with_config( pub fn rustls_with_config(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@ -460,294 +464,6 @@ mod rustls_0_20 {
} }
} }
#[cfg(feature = "rustls-0_21")]
mod rustls_0_21 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_21::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.21 based service.
pub fn rustls_021(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_021_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.21 based service with custom TLS acceptor configuration.
pub fn rustls_021_with_config(
self,
mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
} else {
Protocol::Http1
}
} else {
Protocol::Http1
};
let peer_addr = io.get_ref().0.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_22")]
mod rustls_0_22 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_22::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.22 based service.
pub fn rustls_0_22(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_0_22_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.22 based service with custom TLS acceptor configuration.
pub fn rustls_0_22_with_config(
self,
mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
} else {
Protocol::Http1
}
} else {
Protocol::Http1
};
let peer_addr = io.get_ref().0.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
}
}
#[cfg(feature = "rustls-0_23")]
mod rustls_0_23 {
use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls_0_23::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S::Future: 'static,
S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static,
X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (),
Response = (),
>,
U::Future: 'static,
U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug,
{
/// Create Rustls v0.23 based service.
pub fn rustls_0_23(
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
self.rustls_0_23_with_config(config, TlsAcceptorConfig::default())
}
/// Create Rustls v0.23 based service with custom TLS acceptor configuration.
pub fn rustls_0_23_with_config(
self,
mut config: ServerConfig,
tls_acceptor_config: TlsAcceptorConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
> {
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.alpn_protocols = protos;
let mut acceptor = Acceptor::new(config);
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
acceptor.set_handshake_timeout(handshake_timeout);
}
acceptor
.map_init_err(|_| {
unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
} else {
Protocol::Http1
}
} else {
Protocol::Http1
};
let peer_addr = io.get_ref().0.peer_addr().ok();
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)>
for HttpService<T, S, B, X, U> for HttpService<T, S, B, X, U>
where where
@ -853,7 +569,10 @@ where
} }
} }
pub(super) fn _poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Response<BoxBody>>> { pub(super) fn _poll_ready(
&self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Response<BoxBody>>> {
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
@ -912,7 +631,10 @@ where
}) })
} }
fn call(&self, (io, proto, peer_addr): (T, Protocol, Option<net::SocketAddr>)) -> Self::Future { fn call(
&self,
(io, proto, peer_addr): (T, Protocol, Option<net::SocketAddr>),
) -> Self::Future {
let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref()); let conn_data = OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
match proto { match proto {

View File

@ -74,7 +74,6 @@ pub struct Codec {
} }
bitflags! { bitflags! {
#[derive(Debug, Clone, Copy)]
struct Flags: u8 { struct Flags: u8 {
const SERVER = 0b0000_0001; const SERVER = 0b0000_0001;
const CONTINUATION = 0b0000_0010; const CONTINUATION = 0b0000_0010;
@ -296,7 +295,7 @@ impl Decoder for Codec {
} }
} }
Ok(None) => Ok(None), Ok(None) => Ok(None),
Err(err) => Err(err), Err(e) => Err(e),
} }
} }
} }

View File

@ -70,14 +70,15 @@ mod inner {
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_codec::Framed;
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 pin_project_lite::pin_project; use pin_project_lite::pin_project;
use tracing::debug;
use actix_codec::Framed;
use tokio::io::{AsyncRead, AsyncWrite}; use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::{Decoder, Encoder}; use tokio_util::codec::{Decoder, Encoder};
use tracing::debug;
use crate::{body::BoxBody, Response}; use crate::{body::BoxBody, Response};
@ -412,7 +413,9 @@ mod inner {
} }
State::Error(_) => { State::Error(_) => {
// flush write buffer // flush write buffer
if !this.framed.is_write_buf_empty() && this.framed.flush(cx).is_pending() { if !this.framed.is_write_buf_empty()
&& this.framed.flush(cx).is_pending()
{
return Poll::Pending; return Poll::Pending;
} }
Poll::Ready(Err(this.state.take_error())) Poll::Ready(Err(this.state.take_error()))

View File

@ -1,4 +1,4 @@
use std::cmp::min; use std::convert::TryFrom;
use bytes::{Buf, BufMut, BytesMut}; use bytes::{Buf, BufMut, BytesMut};
use tracing::debug; use tracing::debug;
@ -96,10 +96,6 @@ impl Parser {
// not enough data // not enough data
if src.len() < idx + length { if src.len() < idx + length {
let min_length = min(length, max_size);
if src.capacity() < idx + min_length {
src.reserve(idx + min_length - src.capacity());
}
return Ok(None); return Ok(None);
} }
@ -178,14 +174,14 @@ impl Parser {
}; };
if payload_len < 126 { if payload_len < 126 {
dst.reserve(p_len + 2); dst.reserve(p_len + 2 + if mask { 4 } else { 0 });
dst.put_slice(&[one, two | payload_len as u8]); dst.put_slice(&[one, two | payload_len as u8]);
} else if payload_len <= 65_535 { } else if payload_len <= 65_535 {
dst.reserve(p_len + 4); dst.reserve(p_len + 4 + if mask { 4 } else { 0 });
dst.put_slice(&[one, two | 126]); dst.put_slice(&[one, two | 126]);
dst.put_u16(payload_len as u16); dst.put_u16(payload_len as u16);
} else { } else {
dst.reserve(p_len + 10); dst.reserve(p_len + 10 + if mask { 4 } else { 0 });
dst.put_slice(&[one, two | 127]); dst.put_slice(&[one, two | 127]);
dst.put_u64(payload_len as u64); dst.put_u64(payload_len as u64);
}; };
@ -221,9 +217,8 @@ impl Parser {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes;
use super::*; use super::*;
use bytes::Bytes;
struct F { struct F {
finished: bool, finished: bool,

View File

@ -50,7 +50,7 @@ mod tests {
#[test] #[test]
fn test_apply_mask() { fn test_apply_mask() {
let mask = [0x6d, 0xb6, 0xb2, 0x80]; let mask = [0x6d, 0xb6, 0xb2, 0x80];
let unmasked = [ let unmasked = vec![
0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9, 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0x74, 0xf9,
0x12, 0x03, 0x12, 0x03,
]; ];

View File

@ -8,7 +8,8 @@ use std::io;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::{body::BoxBody, header::HeaderValue, RequestHead, Response, ResponseBuilder}; use crate::body::BoxBody;
use crate::{header::HeaderValue, RequestHead, Response, ResponseBuilder};
mod codec; mod codec;
mod dispatcher; mod dispatcher;
@ -16,50 +17,48 @@ mod frame;
mod mask; mod mask;
mod proto; mod proto;
pub use self::{ pub use self::codec::{Codec, Frame, Item, Message};
codec::{Codec, Frame, Item, Message}, pub use self::dispatcher::Dispatcher;
dispatcher::Dispatcher, pub use self::frame::Parser;
frame::Parser, pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode};
proto::{hash_key, CloseCode, CloseReason, OpCode},
};
/// WebSocket protocol errors. /// WebSocket protocol errors.
#[derive(Debug, Display, Error, From)] #[derive(Debug, Display, Error, From)]
pub enum ProtocolError { pub enum ProtocolError {
/// Received an unmasked frame from client. /// Received an unmasked frame from client.
#[display(fmt = "received an unmasked frame from client")] #[display(fmt = "Received an unmasked frame from client.")]
UnmaskedFrame, UnmaskedFrame,
/// Received a masked frame from server. /// Received a masked frame from server.
#[display(fmt = "received a masked frame from server")] #[display(fmt = "Received a masked frame from server.")]
MaskedFrame, MaskedFrame,
/// Encountered invalid opcode. /// Encountered invalid opcode.
#[display(fmt = "invalid opcode ({})", _0)] #[display(fmt = "Invalid opcode: {}.", _0)]
InvalidOpcode(#[error(not(source))] u8), InvalidOpcode(#[error(not(source))] u8),
/// Invalid control frame length /// Invalid control frame length
#[display(fmt = "invalid control frame length ({})", _0)] #[display(fmt = "Invalid control frame length: {}.", _0)]
InvalidLength(#[error(not(source))] usize), InvalidLength(#[error(not(source))] usize),
/// Bad opcode. /// Bad opcode.
#[display(fmt = "bad opcode")] #[display(fmt = "Bad opcode.")]
BadOpCode, BadOpCode,
/// A payload reached size limit. /// A payload reached size limit.
#[display(fmt = "payload reached size limit")] #[display(fmt = "A payload reached size limit.")]
Overflow, Overflow,
/// Continuation has not started. /// Continuation is not started.
#[display(fmt = "continuation has not started")] #[display(fmt = "Continuation is not started.")]
ContinuationNotStarted, ContinuationNotStarted,
/// Received new continuation but it is already started. /// Received new continuation but it is already started.
#[display(fmt = "received new continuation but it has already started")] #[display(fmt = "Received new continuation but it is already started.")]
ContinuationStarted, ContinuationStarted,
/// Unknown continuation fragment. /// Unknown continuation fragment.
#[display(fmt = "unknown continuation fragment: {}", _0)] #[display(fmt = "Unknown continuation fragment: {}.", _0)]
ContinuationFragment(#[error(not(source))] OpCode), ContinuationFragment(#[error(not(source))] OpCode),
/// I/O error. /// I/O error.
@ -71,27 +70,27 @@ pub enum ProtocolError {
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display, Error)] #[derive(Debug, Clone, Copy, PartialEq, Eq, 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.")]
GetMethodRequired, GetMethodRequired,
/// Upgrade header if not set to WebSocket. /// Upgrade header if not set to WebSocket.
#[display(fmt = "WebSocket upgrade is expected")] #[display(fmt = "WebSocket upgrade is expected.")]
NoWebsocketUpgrade, NoWebsocketUpgrade,
/// Connection header is not set to upgrade. /// Connection header is not set to upgrade.
#[display(fmt = "connection upgrade is expected")] #[display(fmt = "Connection upgrade is expected.")]
NoConnectionUpgrade, NoConnectionUpgrade,
/// WebSocket version header is not set. /// WebSocket version header is not set.
#[display(fmt = "WebSocket version header is required")] #[display(fmt = "WebSocket version header is required.")]
NoVersionHeader, NoVersionHeader,
/// Unsupported WebSocket version. /// Unsupported WebSocket version.
#[display(fmt = "unsupported WebSocket version")] #[display(fmt = "Unsupported WebSocket version.")]
UnsupportedVersion, UnsupportedVersion,
/// WebSocket key is not set or wrong. /// WebSocket key is not set or wrong.
#[display(fmt = "unknown WebSocket key")] #[display(fmt = "Unknown websocket key.")]
BadWebsocketKey, BadWebsocketKey,
} }
@ -220,8 +219,10 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{header, Method};
use super::*; use super::*;
use crate::{header, test::TestRequest}; use crate::test::TestRequest;
#[test] #[test]
fn test_handshake() { fn test_handshake() {

View File

@ -1,4 +1,7 @@
use std::fmt; use std::{
convert::{From, Into},
fmt,
};
use base64::prelude::*; use base64::prelude::*;
use tracing::error; use tracing::error;

View File

@ -1,4 +1,5 @@
#![cfg(feature = "openssl")] #![cfg(feature = "openssl")]
#![allow(clippy::uninlined_format_args)]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
@ -42,11 +43,9 @@ where
} }
fn tls_config() -> SslAcceptor { fn tls_config() -> SslAcceptor {
let rcgen::CertifiedKey { cert, key_pair } = let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
rcgen::generate_simple_self_signed(["localhost".to_owned()]).unwrap(); let cert_file = cert.serialize_pem().unwrap();
let cert_file = cert.pem(); let key_file = cert.serialize_private_key_pem();
let key_file = key_pair.serialize_pem();
let cert = X509::from_pem(cert_file.as_bytes()).unwrap(); let cert = X509::from_pem(cert_file.as_bytes()).unwrap();
let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap(); let key = PKey::private_key_from_pem(key_file.as_bytes()).unwrap();
@ -322,7 +321,8 @@ async fn h2_body_length() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| async { .h2(|_| async {
let body = once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) }); let body =
once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_ref())) });
Ok::<_, Infallible>( Ok::<_, Infallible>(
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),

Some files were not shown because too many files have changed in this diff Show More