mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-04 01:51:30 +02:00
Compare commits
90 Commits
codegen-v4
...
files-v0.6
Author | SHA1 | Date | |
---|---|---|---|
3e25742a41 | |||
20f4cfe6b5 | |||
6408291ab0 | |||
8d260e599f | |||
14bcf72ec1 | |||
6485434a33 | |||
16c7c16463 | |||
9b0fdca6e9 | |||
8759d79b03 | |||
c0d5d7bdb5 | |||
40eab1f091 | |||
75517cce82 | |||
9b51624b27 | |||
8e2ae8cd40 | |||
9a2f8450e0 | |||
23ef51609e | |||
f7d629a61a | |||
e0845d9ad9 | |||
2f79daec16 | |||
f3f41a0cc7 | |||
987067698b | |||
b62f1b4ef7 | |||
df5257c373 | |||
226ea696ce | |||
e524fc86ea | |||
7e990e423f | |||
8f9a12ed5d | |||
c6eba2da9b | |||
06c7945801 | |||
0dba6310c6 | |||
f7d7d92984 | |||
3d6ea7fe9b | |||
8dbf7da89f | |||
de92b3be2e | |||
5d0e8138ee | |||
6b7196225e | |||
265fa0d050 | |||
062127a210 | |||
3926416580 | |||
43671ae4aa | |||
264a703d94 | |||
498fb954b3 | |||
2253eae2bb | |||
8e76a1c775 | |||
dce57a79c9 | |||
6a5b370206 | |||
b1c85ba85b | |||
9aab911600 | |||
017e40f733 | |||
45592b37b6 | |||
8abcb94512 | |||
f2cacc4c9d | |||
56b9c0d08e | |||
de9e41484a | |||
2fed978597 | |||
40048a5811 | |||
e942d3e3b1 | |||
09cffc093c | |||
c58f287044 | |||
7b27493e4c | |||
478b33b8a3 | |||
592b40f914 | |||
fe5279c77a | |||
80d222aa78 | |||
a03a2a0076 | |||
745e738955 | |||
1fd90f0b10 | |||
a35804b89f | |||
5611b98c0d | |||
dce9438518 | |||
be986d96b3 | |||
8ddb24b49b | |||
87f627cd5d | |||
03456b8a33 | |||
8c2fad3164 | |||
62fbd225bc | |||
0fa4d999d9 | |||
da4c849f62 | |||
49cd303c3b | |||
955c3ac0c4 | |||
56e5c19b85 | |||
3f03af1c59 | |||
25c0673278 | |||
e7a05f9892 | |||
2f13e5f675 | |||
9f964751f6 | |||
fcca515387 | |||
075932d823 | |||
cb379c0e0c | |||
d4a5d450de |
77
.github/workflows/ci-post-merge.yml
vendored
77
.github/workflows/ci-post-merge.yml
vendored
@ -23,6 +23,7 @@ jobs:
|
|||||||
CI: 1
|
CI: 1
|
||||||
CARGO_INCREMENTAL: 0
|
CARGO_INCREMENTAL: 0
|
||||||
VCPKGRS_DYNAMIC: 1
|
VCPKGRS_DYNAMIC: 1
|
||||||
|
CARGO_UNSTABLE_SPARSE_REGISTRY: true
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
@ -44,18 +45,15 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: generate-lockfile }
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-hack
|
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-check-min }
|
with: { command: ci-check-min }
|
||||||
@ -78,23 +76,19 @@ jobs:
|
|||||||
cargo test --lib --tests -p=actix-multipart --all-features
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
- name: tests (io-uring)
|
|
||||||
if: matrix.target.os == 'ubuntu-latest'
|
|
||||||
timeout-minutes: 60
|
|
||||||
run: >
|
|
||||||
sudo bash -c "ulimit -Sl 512
|
|
||||||
&& ulimit -Hl 512
|
|
||||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
|
||||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
- name: Clear the cargo caches
|
||||||
run: |
|
run: |
|
||||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
||||||
cargo-cache
|
cargo-cache
|
||||||
|
|
||||||
ci_feature_powerset_check:
|
ci_feature_powerset_check:
|
||||||
name: Verify Feature Combinations
|
name: Verify Feature Combinations
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: 1
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
@ -105,18 +99,15 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: generate-lockfile }
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-hack
|
|
||||||
|
|
||||||
- name: check feature combinations
|
- name: check feature combinations
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-check-all-feature-powerset }
|
with: { command: ci-check-all-feature-powerset }
|
||||||
@ -125,37 +116,14 @@ jobs:
|
|||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-check-all-feature-powerset-linux }
|
with: { command: ci-check-all-feature-powerset-linux }
|
||||||
|
|
||||||
# job currently (1st Feb 2022) segfaults
|
|
||||||
# coverage:
|
|
||||||
# name: coverage
|
|
||||||
# runs-on: ubuntu-latest
|
|
||||||
# steps:
|
|
||||||
# - uses: actions/checkout@v2
|
|
||||||
|
|
||||||
# - name: Install stable
|
|
||||||
# uses: actions-rs/toolchain@v1
|
|
||||||
# with:
|
|
||||||
# toolchain: stable-x86_64-unknown-linux-gnu
|
|
||||||
# profile: minimal
|
|
||||||
# override: true
|
|
||||||
|
|
||||||
# - name: Generate Cargo.lock
|
|
||||||
# uses: actions-rs/cargo@v1
|
|
||||||
# with: { command: generate-lockfile }
|
|
||||||
# - name: Cache Dependencies
|
|
||||||
# uses: Swatinem/rust-cache@v1.2.0
|
|
||||||
|
|
||||||
# - name: Generate coverage file
|
|
||||||
# run: |
|
|
||||||
# cargo install cargo-tarpaulin --vers "^0.13"
|
|
||||||
# cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
|
||||||
# - name: Upload to Codecov
|
|
||||||
# uses: codecov/codecov-action@v1
|
|
||||||
# with: { file: cobertura.xml }
|
|
||||||
|
|
||||||
nextest:
|
nextest:
|
||||||
name: nextest
|
name: nextest
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
env:
|
||||||
|
CI: 1
|
||||||
|
CARGO_INCREMENTAL: 0
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v2
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
@ -166,18 +134,15 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install nextest
|
||||||
|
uses: taiki-e/install-action@nextest
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: generate-lockfile }
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.3.0
|
uses: Swatinem/rust-cache@v1.3.0
|
||||||
|
|
||||||
- name: Install cargo-nextest
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-nextest
|
|
||||||
|
|
||||||
- name: Test with cargo-nextest
|
- name: Test with cargo-nextest
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with:
|
with:
|
||||||
|
50
.github/workflows/ci.yml
vendored
50
.github/workflows/ci.yml
vendored
@ -16,7 +16,7 @@ jobs:
|
|||||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||||
version:
|
version:
|
||||||
- 1.54.0 # MSRV
|
- 1.57.0 # MSRV
|
||||||
- stable
|
- stable
|
||||||
|
|
||||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||||
@ -47,18 +47,22 @@ jobs:
|
|||||||
profile: minimal
|
profile: minimal
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
|
- name: Install cargo-hack
|
||||||
|
uses: taiki-e/install-action@cargo-hack
|
||||||
|
|
||||||
|
- name: workaround MSRV issues
|
||||||
|
if: matrix.version != 'stable'
|
||||||
|
run: |
|
||||||
|
cargo install cargo-edit --version=0.8.0
|
||||||
|
cargo add const-str@0.3 --dev -p=actix-web
|
||||||
|
cargo add const-str@0.3 --dev -p=awc
|
||||||
|
|
||||||
- name: Generate Cargo.lock
|
- name: Generate Cargo.lock
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: generate-lockfile }
|
with: { command: generate-lockfile }
|
||||||
- name: Cache Dependencies
|
- name: Cache Dependencies
|
||||||
uses: Swatinem/rust-cache@v1.2.0
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
- name: Install cargo-hack
|
|
||||||
uses: actions-rs/cargo@v1
|
|
||||||
with:
|
|
||||||
command: install
|
|
||||||
args: cargo-hack
|
|
||||||
|
|
||||||
- name: check minimal
|
- name: check minimal
|
||||||
uses: actions-rs/cargo@v1
|
uses: actions-rs/cargo@v1
|
||||||
with: { command: ci-check-min }
|
with: { command: ci-check-min }
|
||||||
@ -81,19 +85,37 @@ jobs:
|
|||||||
cargo test --lib --tests -p=actix-multipart --all-features
|
cargo test --lib --tests -p=actix-multipart --all-features
|
||||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||||
|
|
||||||
|
- name: Clear the cargo caches
|
||||||
|
run: |
|
||||||
|
cargo install cargo-cache --version 0.8.2 --no-default-features --features ci-autoclean
|
||||||
|
cargo-cache
|
||||||
|
|
||||||
|
io-uring:
|
||||||
|
name: io-uring tests
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install Rust
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.3.0
|
||||||
|
|
||||||
- name: tests (io-uring)
|
- name: tests (io-uring)
|
||||||
if: matrix.target.os == 'ubuntu-latest'
|
|
||||||
timeout-minutes: 60
|
timeout-minutes: 60
|
||||||
run: >
|
run: >
|
||||||
sudo bash -c "ulimit -Sl 512
|
sudo bash -c "ulimit -Sl 512
|
||||||
&& ulimit -Hl 512
|
&& ulimit -Hl 512
|
||||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
&& RUSTUP_TOOLCHAIN=stable cargo test --lib --tests -p=actix-files --all-features"
|
||||||
|
|
||||||
- name: Clear the cargo caches
|
|
||||||
run: |
|
|
||||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
|
||||||
cargo-cache
|
|
||||||
|
|
||||||
rustdoc:
|
rustdoc:
|
||||||
name: doc tests
|
name: doc tests
|
||||||
|
36
.github/workflows/coverage.yml
vendored
Normal file
36
.github/workflows/coverage.yml
vendored
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
# disabled because `cargo tarpaulin` currently segfaults
|
||||||
|
|
||||||
|
name: Coverage
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [master]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
# job currently (1st Feb 2022) segfaults
|
||||||
|
coverage:
|
||||||
|
name: coverage
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
|
||||||
|
- name: Install stable
|
||||||
|
uses: actions-rs/toolchain@v1
|
||||||
|
with:
|
||||||
|
toolchain: stable-x86_64-unknown-linux-gnu
|
||||||
|
profile: minimal
|
||||||
|
override: true
|
||||||
|
|
||||||
|
- name: Generate Cargo.lock
|
||||||
|
uses: actions-rs/cargo@v1
|
||||||
|
with: { command: generate-lockfile }
|
||||||
|
- name: Cache Dependencies
|
||||||
|
uses: Swatinem/rust-cache@v1.2.0
|
||||||
|
|
||||||
|
- name: Generate coverage file
|
||||||
|
run: |
|
||||||
|
cargo install cargo-tarpaulin --vers "^0.13"
|
||||||
|
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||||
|
- name: Upload to Codecov
|
||||||
|
uses: codecov/codecov-action@v1
|
||||||
|
with: { file: cobertura.xml }
|
@ -1,6 +1,27 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.2 - 2022-07-23
|
||||||
|
- Allow partial range responses for video content to start streaming sooner. [#2817]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2817]: https://github.com/actix/actix-web/pull/2817
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.1 - 2022-06-11
|
||||||
|
- Add `NamedFile::{modified, metadata, content_type, content_disposition, encoding}()` getters. [#2021]
|
||||||
|
- Update `tokio-uring` dependency to `0.3`.
|
||||||
|
- Audio files now use `Content-Disposition: inline` instead of `attachment`. [#2645]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
||||||
|
|
||||||
|
[#2021]: https://github.com/actix/actix-web/pull/2021
|
||||||
|
[#2645]: https://github.com/actix/actix-web/pull/2645
|
||||||
|
|
||||||
|
|
||||||
|
## 0.6.0 - 2022-02-25
|
||||||
|
- No significant changes since `0.6.0-beta.16`.
|
||||||
|
|
||||||
|
|
||||||
## 0.6.0-beta.16 - 2022-01-31
|
## 0.6.0-beta.16 - 2022-01-31
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-files"
|
name = "actix-files"
|
||||||
version = "0.6.0-beta.16"
|
version = "0.6.2"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"fakeshadow <24548779@qq.com>",
|
"fakeshadow <24548779@qq.com>",
|
||||||
@ -22,10 +22,10 @@ path = "src/lib.rs"
|
|||||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
actix-service = "2"
|
actix-service = "2"
|
||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
|
|
||||||
askama_escape = "0.10"
|
askama_escape = "0.10"
|
||||||
bitflags = "1"
|
bitflags = "1"
|
||||||
@ -39,10 +39,13 @@ 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"
|
||||||
|
|
||||||
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
# experimental-io-uring
|
||||||
|
[target.'cfg(target_os = "linux")'.dependencies]
|
||||||
|
tokio-uring = { version = "0.3", optional = true, features = ["bytes"] }
|
||||||
|
actix-server = { version = "2.1", optional = true } # ensure matching tokio-uring versions
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.7"
|
||||||
actix-test = "0.1.0-beta.13"
|
actix-test = "0.1.0-beta.13"
|
||||||
actix-web = "4.0.0"
|
actix-web = "4"
|
||||||
tempfile = "3.2"
|
tempfile = "3.2"
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> Static file serving for Actix Web
|
> Static file serving for Actix Web
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://docs.rs/actix-files/0.6.0-beta.16)
|
[](https://docs.rs/actix-files/0.6.2)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.16)
|
[](https://deps.rs/crate/actix-files/0.6.2)
|
||||||
[](https://crates.io/crates/actix-files)
|
[](https://crates.io/crates/actix-files)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@ -2,7 +2,7 @@ use actix_web::{http::StatusCode, ResponseError};
|
|||||||
use derive_more::Display;
|
use derive_more::Display;
|
||||||
|
|
||||||
/// Errors which can occur when serving static files.
|
/// Errors which can occur when serving static files.
|
||||||
#[derive(Display, Debug, PartialEq)]
|
#[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)]
|
||||||
@ -22,7 +22,7 @@ impl ResponseError for FilesError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[allow(clippy::enum_variant_names)]
|
#[allow(clippy::enum_variant_names)]
|
||||||
#[derive(Display, Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq, Display)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum UriSegmentError {
|
pub enum UriSegmentError {
|
||||||
/// The segment started with the wrapped invalid character.
|
/// The segment started with the wrapped invalid character.
|
||||||
|
@ -364,20 +364,43 @@ mod tests {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(deprecated)]
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_named_file_status_code_text() {
|
async fn status_code_customize_same_output() {
|
||||||
let mut file = NamedFile::open_async("Cargo.toml")
|
let file1 = NamedFile::open_async("Cargo.toml")
|
||||||
.await
|
.await
|
||||||
.unwrap()
|
.unwrap()
|
||||||
.set_status_code(StatusCode::NOT_FOUND);
|
.set_status_code(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let file2 = NamedFile::open_async("Cargo.toml")
|
||||||
|
.await
|
||||||
|
.unwrap()
|
||||||
|
.customize()
|
||||||
|
.with_status(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
|
let req = TestRequest::default().to_http_request();
|
||||||
|
let res1 = file1.respond_to(&req);
|
||||||
|
let res2 = file2.respond_to(&req);
|
||||||
|
|
||||||
|
assert_eq!(res1.status(), StatusCode::NOT_FOUND);
|
||||||
|
assert_eq!(res2.status(), StatusCode::NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn test_named_file_status_code_text() {
|
||||||
|
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||||
|
|
||||||
{
|
{
|
||||||
file.file();
|
file.file();
|
||||||
let _f: &File = &file;
|
let _f: &File = &file;
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
let _f: &mut File = &mut file;
|
let _f: &mut File = &mut file;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let file = file.customize().with_status(StatusCode::NOT_FOUND);
|
||||||
|
|
||||||
let req = TestRequest::default().to_http_request();
|
let req = TestRequest::default().to_http_request();
|
||||||
let resp = file.respond_to(&req);
|
let resp = file.respond_to(&req);
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
|
@ -23,6 +23,7 @@ use actix_web::{
|
|||||||
use bitflags::bitflags;
|
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_guess::from_path;
|
use mime_guess::from_path;
|
||||||
|
|
||||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||||
@ -76,8 +77,8 @@ pub struct NamedFile {
|
|||||||
pub(crate) md: Metadata,
|
pub(crate) md: Metadata,
|
||||||
pub(crate) flags: Flags,
|
pub(crate) flags: Flags,
|
||||||
pub(crate) status_code: StatusCode,
|
pub(crate) status_code: StatusCode,
|
||||||
pub(crate) content_type: mime::Mime,
|
pub(crate) content_type: Mime,
|
||||||
pub(crate) content_disposition: header::ContentDisposition,
|
pub(crate) content_disposition: ContentDisposition,
|
||||||
pub(crate) encoding: Option<ContentEncoding>,
|
pub(crate) encoding: Option<ContentEncoding>,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -128,7 +129,7 @@ impl NamedFile {
|
|||||||
let ct = from_path(&path).first_or_octet_stream();
|
let ct = from_path(&path).first_or_octet_stream();
|
||||||
|
|
||||||
let disposition = match ct.type_() {
|
let disposition = match ct.type_() {
|
||||||
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
|
mime::IMAGE | mime::TEXT | mime::AUDIO | mime::VIDEO => DispositionType::Inline,
|
||||||
mime::APPLICATION => match ct.subtype() {
|
mime::APPLICATION => match ct.subtype() {
|
||||||
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
|
||||||
name if name == "wasm" => DispositionType::Inline,
|
name if name == "wasm" => DispositionType::Inline,
|
||||||
@ -209,11 +210,10 @@ impl NamedFile {
|
|||||||
Self::from_file(file, path)
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[allow(rustdoc::broken_intra_doc_links)]
|
|
||||||
/// Attempts to open a file asynchronously in read-only mode.
|
/// Attempts to open a file asynchronously in read-only mode.
|
||||||
///
|
///
|
||||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
/// When the `experimental-io-uring` crate feature is enabled, this will be async. Otherwise, it
|
||||||
/// Otherwise, it will be just like [`open`][Self::open].
|
/// will behave just like `open`.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
@ -238,13 +238,13 @@ impl NamedFile {
|
|||||||
Self::from_file(file, path)
|
Self::from_file(file, path)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns reference to the underlying `File` object.
|
/// Returns reference to the underlying file object.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn file(&self) -> &File {
|
pub fn file(&self) -> &File {
|
||||||
&self.file
|
&self.file
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Retrieve the path of this file.
|
/// Returns the filesystem path to this file.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
@ -262,16 +262,53 @@ impl NamedFile {
|
|||||||
self.path.as_path()
|
self.path.as_path()
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set response **Status Code**
|
/// Returns the time the file was last modified.
|
||||||
|
///
|
||||||
|
/// Returns `None` only on unsupported platforms; see [`std::fs::Metadata::modified()`].
|
||||||
|
/// Therefore, it is usually safe to unwrap this.
|
||||||
|
#[inline]
|
||||||
|
pub fn modified(&self) -> Option<SystemTime> {
|
||||||
|
self.modified
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the filesystem metadata associated with this file.
|
||||||
|
#[inline]
|
||||||
|
pub fn metadata(&self) -> &Metadata {
|
||||||
|
&self.md
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `Content-Type` header that will be used when serving this file.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_type(&self) -> &Mime {
|
||||||
|
&self.content_type
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `Content-Disposition` that will be used when serving this file.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_disposition(&self) -> &ContentDisposition {
|
||||||
|
&self.content_disposition
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the `Content-Encoding` that will be used when serving this file.
|
||||||
|
///
|
||||||
|
/// A return value of `None` indicates that the content is not already using a compressed
|
||||||
|
/// representation and may be subject to compression downstream.
|
||||||
|
#[inline]
|
||||||
|
pub fn content_encoding(&self) -> Option<ContentEncoding> {
|
||||||
|
self.encoding
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Set response status code.
|
||||||
|
#[deprecated(since = "0.7.0", note = "Prefer `Responder::customize()`.")]
|
||||||
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
pub fn set_status_code(mut self, status: StatusCode) -> Self {
|
||||||
self.status_code = status;
|
self.status_code = status;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
|
/// Sets the `Content-Type` header that will be used when serving this file. By default the
|
||||||
/// from the filename extension.
|
/// `Content-Type` is inferred from the filename extension.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
pub fn set_content_type(mut self, mime_type: Mime) -> Self {
|
||||||
self.content_type = mime_type;
|
self.content_type = mime_type;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
@ -284,15 +321,15 @@ impl NamedFile {
|
|||||||
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||||
/// (using `to_string_lossy`).
|
/// (using `to_string_lossy`).
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
pub fn set_content_disposition(mut self, cd: ContentDisposition) -> Self {
|
||||||
self.content_disposition = cd;
|
self.content_disposition = cd;
|
||||||
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
self.flags.insert(Flags::CONTENT_DISPOSITION);
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Disable `Content-Disposition` header.
|
/// Disables `Content-Disposition` header.
|
||||||
///
|
///
|
||||||
/// By default Content-Disposition` header is enabled.
|
/// By default, the `Content-Disposition` header is sent.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn disable_content_disposition(mut self) -> Self {
|
pub fn disable_content_disposition(mut self) -> Self {
|
||||||
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
self.flags.remove(Flags::CONTENT_DISPOSITION);
|
||||||
@ -491,11 +528,26 @@ impl NamedFile {
|
|||||||
length = ranges[0].length;
|
length = ranges[0].length;
|
||||||
offset = ranges[0].start;
|
offset = ranges[0].start;
|
||||||
|
|
||||||
// don't allow compression middleware to modify partial content
|
// When a Content-Encoding header is present in a 206 partial content response
|
||||||
res.insert_header((
|
// for video content, it prevents browser video players from starting playback
|
||||||
header::CONTENT_ENCODING,
|
// before loading the whole video and also prevents seeking.
|
||||||
HeaderValue::from_static("identity"),
|
//
|
||||||
));
|
// See: https://github.com/actix/actix-web/issues/2815
|
||||||
|
//
|
||||||
|
// The assumption of this fix is that the video player knows to not send an
|
||||||
|
// Accept-Encoding header for this request and that downstream middleware will
|
||||||
|
// not attempt compression for requests without it.
|
||||||
|
//
|
||||||
|
// TODO: Solve question around what to do if self.encoding is set and partial
|
||||||
|
// range is requested. Reject request? Ignoring self.encoding seems wrong, too.
|
||||||
|
// In practice, it should not come up.
|
||||||
|
if req.headers().contains_key(&header::ACCEPT_ENCODING) {
|
||||||
|
// don't allow compression middleware to modify partial content
|
||||||
|
res.insert_header((
|
||||||
|
header::CONTENT_ENCODING,
|
||||||
|
HeaderValue::from_static("identity"),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
header::CONTENT_RANGE,
|
header::CONTENT_RANGE,
|
||||||
|
@ -1,11 +1,11 @@
|
|||||||
use actix_files::Files;
|
use actix_files::{Files, NamedFile};
|
||||||
use actix_web::{
|
use actix_web::{
|
||||||
http::{
|
http::{
|
||||||
header::{self, HeaderValue},
|
header::{self, HeaderValue},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
test::{self, TestRequest},
|
test::{self, TestRequest},
|
||||||
App,
|
web, App,
|
||||||
};
|
};
|
||||||
|
|
||||||
#[actix_web::test]
|
#[actix_web::test]
|
||||||
@ -36,3 +36,31 @@ async fn test_utf8_file_contents() {
|
|||||||
Some(&HeaderValue::from_static("text/plain")),
|
Some(&HeaderValue::from_static("text/plain")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_web::test]
|
||||||
|
async fn partial_range_response_encoding() {
|
||||||
|
let srv = test::init_service(App::new().default_service(web::to(|| async {
|
||||||
|
NamedFile::open_async("./tests/test.binary").await.unwrap()
|
||||||
|
})))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// range request without accept-encoding returns no content-encoding header
|
||||||
|
let req = TestRequest::with_uri("/")
|
||||||
|
.append_header((header::RANGE, "bytes=10-20"))
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
|
||||||
|
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
||||||
|
|
||||||
|
// range request with accept-encoding returns a content-encoding header
|
||||||
|
let req = TestRequest::with_uri("/")
|
||||||
|
.append_header((header::RANGE, "bytes=10-20"))
|
||||||
|
.append_header((header::ACCEPT_ENCODING, "identity"))
|
||||||
|
.to_request();
|
||||||
|
let res = test::call_service(&srv, req).await;
|
||||||
|
assert_eq!(res.status(), StatusCode::PARTIAL_CONTENT);
|
||||||
|
assert_eq!(
|
||||||
|
res.headers().get(header::CONTENT_ENCODING).unwrap(),
|
||||||
|
"identity"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 3.0.0-beta.13 - 2022-02-16
|
## 3.0.0-beta.13 - 2022-02-16
|
||||||
|
@ -29,13 +29,13 @@ default = []
|
|||||||
openssl = ["tls-openssl", "awc/openssl"]
|
openssl = ["tls-openssl", "awc/openssl"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-service = "2.0.0"
|
actix-service = "2"
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-tls = "3"
|
actix-tls = "3"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
awc = { version = "3.0.0-beta.21", default-features = false }
|
awc = { version = "3", default-features = false }
|
||||||
|
|
||||||
base64 = "0.13"
|
base64 = "0.13"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
@ -51,5 +51,5 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
|||||||
tokio = { version = "1.8.4", features = ["sync"] }
|
tokio = { version = "1.8.4", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
|
@ -4,7 +4,7 @@
|
|||||||
|
|
||||||
[](https://crates.io/crates/actix-http-test)
|
[](https://crates.io/crates/actix-http-test)
|
||||||
[](https://docs.rs/actix-http-test/3.0.0-beta.13)
|
[](https://docs.rs/actix-http-test/3.0.0-beta.13)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|

|
||||||

|

|
||||||
<br>
|
<br>
|
||||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.13)
|
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.13)
|
||||||
|
@ -1,8 +1,63 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
|
||||||
|
|
||||||
|
## 3.2.1 - 2022-07-02
|
||||||
|
### Fixed
|
||||||
|
- Fix parsing ambiguity in Transfer-Encoding and Content-Length headers for HTTP/1.0 requests. [#2794]
|
||||||
|
|
||||||
|
[#2794]: https://github.com/actix/actix-web/pull/2794
|
||||||
|
|
||||||
|
|
||||||
|
## 3.2.0 - 2022-06-30
|
||||||
|
### Changed
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Websocket parser no longer throws endless overflow errors after receiving an oversized frame. [#2790]
|
||||||
|
- Retain previously set Vary headers when using compression encoder. [#2798]
|
||||||
|
|
||||||
|
[#2790]: https://github.com/actix/actix-web/pull/2790
|
||||||
|
[#2798]: https://github.com/actix/actix-web/pull/2798
|
||||||
|
|
||||||
|
|
||||||
|
## 3.1.0 - 2022-06-11
|
||||||
|
### Changed
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Revert broken fix in [#2624] that caused erroneous 500 error responses. Temporarily re-introduces [#2357] bug. [#2779]
|
||||||
|
|
||||||
|
[#2357]: https://github.com/actix/actix-web/issues/2357
|
||||||
|
[#2624]: https://github.com/actix/actix-web/issues/2624
|
||||||
|
[#2779]: https://github.com/actix/actix-web/issues/2779
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.4 - 2022-03-09
|
||||||
|
### Fixed
|
||||||
|
- Document on docs.rs with `ws` feature enabled.
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.3 - 2022-03-08
|
||||||
|
### Fixed
|
||||||
|
- Allow spaces between header name and colon when parsing responses. [#2684]
|
||||||
|
|
||||||
|
[#2684]: https://github.com/actix/actix-web/issues/2684
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.2 - 2022-03-05
|
||||||
|
### Fixed
|
||||||
|
- Fix encoding camel-case header names with more than one hyphen. [#2683]
|
||||||
|
|
||||||
|
[#2683]: https://github.com/actix/actix-web/issues/2683
|
||||||
|
|
||||||
|
|
||||||
|
## 3.0.1 - 2022-03-04
|
||||||
|
- Fix panic in H1 dispatcher when pipelining is used with keep-alive. [#2678]
|
||||||
|
|
||||||
|
[#2678]: https://github.com/actix/actix-web/issues/2678
|
||||||
|
|
||||||
## 3.0.0 - 2022-02-25
|
## 3.0.0 - 2022-02-25
|
||||||
### Dependencies
|
### Dependencies
|
||||||
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
- Updated `actix-*` to Tokio v1-based versions. [#1813]
|
||||||
@ -745,10 +800,10 @@
|
|||||||
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
- Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
|
||||||
due to deprecate of resolver actor. [#1813]
|
due to deprecate of resolver actor. [#1813]
|
||||||
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
- Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
|
||||||
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
due to the removal of this type from `tokio-openssl` crate. openssl handshake
|
||||||
error would return as `ConnectError::SslError`. [#1813]
|
error would return as `ConnectError::SslError`. [#1813]
|
||||||
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
- Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
|
||||||
Due to this change `actix_threadpool::BlockingError` type is moved into
|
Due to this change `actix_threadpool::BlockingError` type is moved into
|
||||||
`actix_http::error` module. [#1878]
|
`actix_http::error` module. [#1878]
|
||||||
|
|
||||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-http"
|
name = "actix-http"
|
||||||
version = "3.0.0"
|
version = "3.2.1"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
@ -20,7 +20,7 @@ edition = "2018"
|
|||||||
|
|
||||||
[package.metadata.docs.rs]
|
[package.metadata.docs.rs]
|
||||||
# features that docs.rs will build with
|
# features that docs.rs will build with
|
||||||
features = ["http2", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
features = ["http2", "ws", "openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd"]
|
||||||
|
|
||||||
[lib]
|
[lib]
|
||||||
name = "actix_http"
|
name = "actix_http"
|
||||||
@ -37,7 +37,7 @@ ws = [
|
|||||||
"local-channel",
|
"local-channel",
|
||||||
"base64",
|
"base64",
|
||||||
"rand",
|
"rand",
|
||||||
"sha-1",
|
"sha1",
|
||||||
]
|
]
|
||||||
|
|
||||||
# TLS via OpenSSL
|
# TLS via OpenSSL
|
||||||
@ -73,11 +73,11 @@ httparse = "1.5.1"
|
|||||||
httpdate = "1.0.1"
|
httpdate = "1.0.1"
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
log = "0.4"
|
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
percent-encoding = "2.1"
|
percent-encoding = "2.1"
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
smallvec = "1.6.1"
|
smallvec = "1.6.1"
|
||||||
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
# http2
|
# http2
|
||||||
h2 = { version = "0.3.9", optional = true }
|
h2 = { version = "0.3.9", optional = true }
|
||||||
@ -86,7 +86,7 @@ h2 = { version = "0.3.9", optional = true }
|
|||||||
local-channel = { version = "0.1", optional = true }
|
local-channel = { version = "0.1", optional = true }
|
||||||
base64 = { version = "0.13", optional = true }
|
base64 = { version = "0.13", optional = true }
|
||||||
rand = { version = "0.8", optional = true }
|
rand = { version = "0.8", optional = true }
|
||||||
sha-1 = { version = "0.10", optional = true }
|
sha1 = { version = "0.10", optional = true }
|
||||||
|
|
||||||
# openssl/rustls
|
# openssl/rustls
|
||||||
actix-tls = { version = "3", default-features = false, optional = true }
|
actix-tls = { version = "3", default-features = false, optional = true }
|
||||||
@ -94,13 +94,13 @@ actix-tls = { version = "3", default-features = false, optional = true }
|
|||||||
# compress-*
|
# compress-*
|
||||||
brotli = { version = "3.3.3", optional = true }
|
brotli = { version = "3.3.3", optional = true }
|
||||||
flate2 = { version = "1.0.13", optional = true }
|
flate2 = { version = "1.0.13", optional = true }
|
||||||
zstd = { version = "0.10", optional = true }
|
zstd = { version = "0.11", optional = true }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
actix-http-test = { version = "3.0.0-beta.13", features = ["openssl"] }
|
||||||
actix-server = "2"
|
actix-server = "2"
|
||||||
actix-tls = { version = "3", features = ["openssl"] }
|
actix-tls = { version = "3", features = ["openssl"] }
|
||||||
actix-web = "4.0.0"
|
actix-web = "4"
|
||||||
|
|
||||||
async-stream = "0.3"
|
async-stream = "0.3"
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
@ -108,9 +108,10 @@ env_logger = "0.9"
|
|||||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||||
memchr = "2.4"
|
memchr = "2.4"
|
||||||
once_cell = "1.9"
|
once_cell = "1.9"
|
||||||
rcgen = "0.8"
|
rcgen = "0.9"
|
||||||
regex = "1.3"
|
regex = "1.3"
|
||||||
rustls-pemfile = "0.2"
|
rustversion = "1"
|
||||||
|
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"
|
||||||
@ -120,7 +121,7 @@ tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
|||||||
|
|
||||||
[[example]]
|
[[example]]
|
||||||
name = "ws"
|
name = "ws"
|
||||||
required-features = ["rustls"]
|
required-features = ["ws", "rustls"]
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "write-camel-case"
|
name = "write-camel-case"
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> HTTP primitives for the Actix ecosystem.
|
> HTTP primitives for the Actix ecosystem.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://docs.rs/actix-http/3.0.0)
|
[](https://docs.rs/actix-http/3.2.1)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-http/3.0.0)
|
[](https://deps.rs/crate/actix-http/3.2.1)
|
||||||
[](https://crates.io/crates/actix-http)
|
[](https://crates.io/crates/actix-http)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
@ -25,7 +25,7 @@ use actix_http::{HttpService, Response};
|
|||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use futures_util::future;
|
use futures_util::future;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
use log::info;
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
|
@ -114,11 +114,12 @@ mod _original {
|
|||||||
use std::mem::MaybeUninit;
|
use std::mem::MaybeUninit;
|
||||||
|
|
||||||
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
pub fn parse_headers(src: &mut BytesMut) -> usize {
|
||||||
#![allow(clippy::uninit_assumed_init)]
|
#![allow(invalid_value, clippy::uninit_assumed_init)]
|
||||||
|
|
||||||
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
let mut headers: [HeaderIndex; MAX_HEADERS] =
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
#[allow(invalid_value)]
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] =
|
||||||
unsafe { MaybeUninit::uninit().assume_init() };
|
unsafe { MaybeUninit::uninit().assume_init() };
|
||||||
|
|
||||||
|
@ -18,7 +18,8 @@ async fn main() -> std::io::Result<()> {
|
|||||||
HttpService::build()
|
HttpService::build()
|
||||||
// pass the app to service builder
|
// pass the app to service builder
|
||||||
// map_config is used to map App's configuration to ServiceBuilder
|
// map_config is used to map App's configuration to ServiceBuilder
|
||||||
.finish(map_config(app, |_| AppConfig::default()))
|
// h1 will configure server to only use HTTP/1.1
|
||||||
|
.h1(map_config(app, |_| AppConfig::default()))
|
||||||
.tcp()
|
.tcp()
|
||||||
})?
|
})?
|
||||||
.run()
|
.run()
|
||||||
|
@ -5,6 +5,7 @@ use actix_server::Server;
|
|||||||
use bytes::BytesMut;
|
use bytes::BytesMut;
|
||||||
use futures_util::StreamExt as _;
|
use futures_util::StreamExt as _;
|
||||||
use http::header::HeaderValue;
|
use http::header::HeaderValue;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@ -22,7 +23,7 @@ async fn main() -> io::Result<()> {
|
|||||||
body.extend_from_slice(&item?);
|
body.extend_from_slice(&item?);
|
||||||
}
|
}
|
||||||
|
|
||||||
log::info!("request body: {:?}", body);
|
info!("request body: {:?}", body);
|
||||||
|
|
||||||
let res = Response::build(StatusCode::OK)
|
let res = Response::build(StatusCode::OK)
|
||||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
use std::{convert::Infallible, io, time::Duration};
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{header::HeaderValue, HttpService, Request, Response, StatusCode};
|
||||||
header::HeaderValue, HttpMessage, HttpService, Request, Response, StatusCode,
|
|
||||||
};
|
|
||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@ -18,12 +17,12 @@ async fn main() -> io::Result<()> {
|
|||||||
ext.insert(42u32);
|
ext.insert(42u32);
|
||||||
})
|
})
|
||||||
.finish(|req: Request| async move {
|
.finish(|req: Request| async move {
|
||||||
log::info!("{:?}", req);
|
info!("{:?}", req);
|
||||||
|
|
||||||
let mut res = Response::build(StatusCode::OK);
|
let mut res = Response::build(StatusCode::OK);
|
||||||
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
res.insert_header(("x-head", HeaderValue::from_static("dummy value!")));
|
||||||
|
|
||||||
let forty_two = req.extensions().get::<u32>().unwrap().to_string();
|
let forty_two = req.conn_data::<u32>().unwrap().to_string();
|
||||||
res.insert_header((
|
res.insert_header((
|
||||||
"x-forty-two",
|
"x-forty-two",
|
||||||
HeaderValue::from_str(&forty_two).unwrap(),
|
HeaderValue::from_str(&forty_two).unwrap(),
|
||||||
|
@ -12,6 +12,7 @@ use actix_http::{body::BodyStream, HttpService, Response};
|
|||||||
use actix_server::Server;
|
use actix_server::Server;
|
||||||
use async_stream::stream;
|
use async_stream::stream;
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
|
use tracing::info;
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@ -21,7 +22,7 @@ async fn main() -> io::Result<()> {
|
|||||||
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
.bind("streaming-error", ("127.0.0.1", 8080), || {
|
||||||
HttpService::build()
|
HttpService::build()
|
||||||
.finish(|req| async move {
|
.finish(|req| async move {
|
||||||
log::info!("{:?}", req);
|
info!("{:?}", req);
|
||||||
let res = Response::ok();
|
let res = Response::ok();
|
||||||
|
|
||||||
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
|
||||||
|
@ -17,6 +17,7 @@ use actix_server::Server;
|
|||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
use futures_core::{ready, Stream};
|
use futures_core::{ready, Stream};
|
||||||
|
use tracing::{info, trace};
|
||||||
|
|
||||||
#[actix_rt::main]
|
#[actix_rt::main]
|
||||||
async fn main() -> io::Result<()> {
|
async fn main() -> io::Result<()> {
|
||||||
@ -34,13 +35,13 @@ async fn main() -> io::Result<()> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
|
||||||
log::info!("handshaking");
|
info!("handshaking");
|
||||||
let mut res = ws::handshake(req.head())?;
|
let mut res = ws::handshake(req.head())?;
|
||||||
|
|
||||||
// handshake will always fail under HTTP/2
|
// handshake will always fail under HTTP/2
|
||||||
|
|
||||||
log::info!("responding");
|
info!("responding");
|
||||||
Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
|
res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Heartbeat {
|
struct Heartbeat {
|
||||||
@ -61,7 +62,7 @@ impl Stream for Heartbeat {
|
|||||||
type Item = Result<Bytes, Error>;
|
type Item = Result<Bytes, Error>;
|
||||||
|
|
||||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||||
log::trace!("poll");
|
trace!("poll");
|
||||||
|
|
||||||
ready!(self.as_mut().interval.poll_tick(cx));
|
ready!(self.as_mut().interval.poll_tick(cx));
|
||||||
|
|
||||||
|
@ -481,6 +481,7 @@ mod tests {
|
|||||||
assert_poll_next_none!(pl);
|
assert_poll_next_none!(pl);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::let_unit_value)]
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unit() {
|
async fn test_unit() {
|
||||||
let pl = ();
|
let pl = ();
|
||||||
|
@ -17,6 +17,7 @@ use pin_project_lite::pin_project;
|
|||||||
#[cfg(feature = "compress-gzip")]
|
#[cfg(feature = "compress-gzip")]
|
||||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||||
|
|
||||||
|
use tracing::trace;
|
||||||
#[cfg(feature = "compress-zstd")]
|
#[cfg(feature = "compress-zstd")]
|
||||||
use zstd::stream::write::Encoder as ZstdEncoder;
|
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||||
|
|
||||||
@ -256,7 +257,7 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
|||||||
head.headers_mut()
|
head.headers_mut()
|
||||||
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
||||||
head.headers_mut()
|
head.headers_mut()
|
||||||
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
|
.append(header::VARY, HeaderValue::from_static("accept-encoding"));
|
||||||
|
|
||||||
head.no_chunking(false);
|
head.no_chunking(false);
|
||||||
}
|
}
|
||||||
@ -356,7 +357,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding br encoding: {}", err);
|
trace!("Error decoding br encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -365,7 +366,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding gzip encoding: {}", err);
|
trace!("Error decoding gzip encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -374,7 +375,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding deflate encoding: {}", err);
|
trace!("Error decoding deflate encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -383,7 +384,7 @@ impl ContentEncoder {
|
|||||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||||
Ok(_) => Ok(()),
|
Ok(_) => Ok(()),
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("Error decoding ztsd encoding: {}", err);
|
trace!("Error decoding ztsd encoding: {}", err);
|
||||||
Err(err)
|
Err(err)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -388,7 +388,7 @@ impl StdError for DispatchError {
|
|||||||
|
|
||||||
/// A set of error that can occur during parsing content type.
|
/// A set of error that can occur during parsing content type.
|
||||||
#[derive(Debug, Display, Error)]
|
#[derive(Debug, Display, Error)]
|
||||||
#[cfg_attr(test, derive(PartialEq))]
|
#[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
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::{io, task::Poll};
|
use std::{io, task::Poll};
|
||||||
|
|
||||||
use bytes::{Buf as _, Bytes, BytesMut};
|
use bytes::{Buf as _, Bytes, BytesMut};
|
||||||
|
use tracing::{debug, trace};
|
||||||
|
|
||||||
macro_rules! byte (
|
macro_rules! byte (
|
||||||
($rdr:ident) => ({
|
($rdr:ident) => ({
|
||||||
@ -14,7 +15,7 @@ macro_rules! byte (
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Clone)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub(super) enum ChunkedState {
|
pub(super) enum ChunkedState {
|
||||||
Size,
|
Size,
|
||||||
SizeLws,
|
SizeLws,
|
||||||
@ -76,7 +77,7 @@ impl ChunkedState {
|
|||||||
Poll::Ready(Ok(ChunkedState::Size))
|
Poll::Ready(Ok(ChunkedState::Size))
|
||||||
}
|
}
|
||||||
None => {
|
None => {
|
||||||
log::debug!("chunk size would overflow u64");
|
debug!("chunk size would overflow u64");
|
||||||
Poll::Ready(Err(io::Error::new(
|
Poll::Ready(Err(io::Error::new(
|
||||||
io::ErrorKind::InvalidInput,
|
io::ErrorKind::InvalidInput,
|
||||||
"Invalid chunk size line: Size is too big",
|
"Invalid chunk size line: Size is too big",
|
||||||
@ -124,7 +125,7 @@ impl ChunkedState {
|
|||||||
rem: &mut u64,
|
rem: &mut u64,
|
||||||
buf: &mut Option<Bytes>,
|
buf: &mut Option<Bytes>,
|
||||||
) -> Poll<Result<ChunkedState, io::Error>> {
|
) -> Poll<Result<ChunkedState, io::Error>> {
|
||||||
log::trace!("Chunked read, remaining={:?}", rem);
|
trace!("Chunked read, remaining={:?}", rem);
|
||||||
|
|
||||||
let len = rdr.len() as u64;
|
let len = rdr.len() as u64;
|
||||||
if len == 0 {
|
if len == 0 {
|
||||||
|
@ -6,7 +6,7 @@ use http::{
|
|||||||
header::{self, HeaderName, HeaderValue},
|
header::{self, HeaderName, HeaderValue},
|
||||||
Method, StatusCode, Uri, Version,
|
Method, StatusCode, Uri, Version,
|
||||||
};
|
};
|
||||||
use log::{debug, error, trace};
|
use tracing::{debug, error, trace};
|
||||||
|
|
||||||
use super::chunked::ChunkedState;
|
use super::chunked::ChunkedState;
|
||||||
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
use crate::{error::ParseError, header::HeaderMap, ConnectionType, Request, ResponseHead};
|
||||||
@ -46,6 +46,23 @@ pub(crate) enum PayloadLength {
|
|||||||
None,
|
None,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl PayloadLength {
|
||||||
|
/// Returns true if variant is `None`.
|
||||||
|
fn is_none(&self) -> bool {
|
||||||
|
matches!(self, Self::None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if variant is represents zero-length (not none) payload.
|
||||||
|
fn is_zero(&self) -> bool {
|
||||||
|
matches!(
|
||||||
|
self,
|
||||||
|
PayloadLength::Payload(PayloadType::Payload(PayloadDecoder {
|
||||||
|
kind: Kind::Length(0)
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) trait MessageType: Sized {
|
pub(crate) trait MessageType: Sized {
|
||||||
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
fn set_connection_type(&mut self, conn_type: Option<ConnectionType>);
|
||||||
|
|
||||||
@ -59,6 +76,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
&mut self,
|
&mut self,
|
||||||
slice: &Bytes,
|
slice: &Bytes,
|
||||||
raw_headers: &[HeaderIndex],
|
raw_headers: &[HeaderIndex],
|
||||||
|
version: Version,
|
||||||
) -> Result<PayloadLength, ParseError> {
|
) -> Result<PayloadLength, ParseError> {
|
||||||
let mut ka = None;
|
let mut ka = None;
|
||||||
let mut has_upgrade_websocket = false;
|
let mut has_upgrade_websocket = false;
|
||||||
@ -87,21 +105,23 @@ pub(crate) trait MessageType: Sized {
|
|||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
header::CONTENT_LENGTH => match value.to_str() {
|
header::CONTENT_LENGTH => match value.to_str().map(str::trim) {
|
||||||
Ok(s) if s.trim().starts_with('+') => {
|
Ok(val) if val.starts_with('+') => {
|
||||||
debug!("illegal Content-Length: {:?}", s);
|
debug!("illegal Content-Length: {:?}", val);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
Ok(s) => {
|
|
||||||
if let Ok(len) = s.parse::<u64>() {
|
Ok(val) => {
|
||||||
if len != 0 {
|
if let Ok(len) = val.parse::<u64>() {
|
||||||
content_length = Some(len);
|
// accept 0 lengths here and remove them in `decode` after all
|
||||||
}
|
// headers have been processed to prevent request smuggling issues
|
||||||
|
content_length = Some(len);
|
||||||
} else {
|
} else {
|
||||||
debug!("illegal Content-Length: {:?}", s);
|
debug!("illegal Content-Length: {:?}", val);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
debug!("illegal Content-Length: {:?}", value);
|
debug!("illegal Content-Length: {:?}", value);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
@ -114,22 +134,23 @@ pub(crate) trait MessageType: Sized {
|
|||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
|
|
||||||
header::TRANSFER_ENCODING => {
|
header::TRANSFER_ENCODING if version == Version::HTTP_11 => {
|
||||||
seen_te = true;
|
seen_te = true;
|
||||||
|
|
||||||
if let Ok(s) = value.to_str().map(str::trim) {
|
if let Ok(val) = value.to_str().map(str::trim) {
|
||||||
if s.eq_ignore_ascii_case("chunked") {
|
if val.eq_ignore_ascii_case("chunked") {
|
||||||
chunked = true;
|
chunked = true;
|
||||||
} else if s.eq_ignore_ascii_case("identity") {
|
} else if val.eq_ignore_ascii_case("identity") {
|
||||||
// allow silently since multiple TE headers are already checked
|
// allow silently since multiple TE headers are already checked
|
||||||
} else {
|
} else {
|
||||||
debug!("illegal Transfer-Encoding: {:?}", s);
|
debug!("illegal Transfer-Encoding: {:?}", val);
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
return Err(ParseError::Header);
|
return Err(ParseError::Header);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// connection keep-alive state
|
// connection keep-alive state
|
||||||
header::CONNECTION => {
|
header::CONNECTION => {
|
||||||
ka = if let Ok(conn) = value.to_str().map(str::trim) {
|
ka = if let Ok(conn) = value.to_str().map(str::trim) {
|
||||||
@ -146,6 +167,7 @@ pub(crate) trait MessageType: Sized {
|
|||||||
None
|
None
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
header::UPGRADE => {
|
header::UPGRADE => {
|
||||||
if let Ok(val) = value.to_str().map(str::trim) {
|
if let Ok(val) = value.to_str().map(str::trim) {
|
||||||
if val.eq_ignore_ascii_case("websocket") {
|
if val.eq_ignore_ascii_case("websocket") {
|
||||||
@ -153,19 +175,23 @@ pub(crate) trait MessageType: Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
header::EXPECT => {
|
header::EXPECT => {
|
||||||
let bytes = value.as_bytes();
|
let bytes = value.as_bytes();
|
||||||
if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
|
if bytes.len() >= 4 && &bytes[0..4] == b"100-" {
|
||||||
expect = true;
|
expect = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ => {}
|
_ => {}
|
||||||
}
|
}
|
||||||
|
|
||||||
headers.append(name, value);
|
headers.append(name, value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
self.set_connection_type(ka);
|
self.set_connection_type(ka);
|
||||||
|
|
||||||
if expect {
|
if expect {
|
||||||
self.set_expect()
|
self.set_expect()
|
||||||
}
|
}
|
||||||
@ -249,7 +275,22 @@ impl MessageType for Request {
|
|||||||
let mut msg = Request::new();
|
let mut msg = Request::new();
|
||||||
|
|
||||||
// convert headers
|
// convert headers
|
||||||
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
|
let mut length =
|
||||||
|
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
|
||||||
|
|
||||||
|
// disallow HTTP/1.0 POST requests that do not contain a Content-Length headers
|
||||||
|
// see https://datatracker.ietf.org/doc/html/rfc1945#section-7.2.2
|
||||||
|
if ver == Version::HTTP_10 && method == Method::POST && length.is_none() {
|
||||||
|
debug!("no Content-Length specified for HTTP/1.0 POST request");
|
||||||
|
return Err(ParseError::Header);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
|
||||||
|
// Protects against some request smuggling attacks.
|
||||||
|
// See https://github.com/actix/actix-web/issues/2767.
|
||||||
|
if length.is_zero() {
|
||||||
|
length = PayloadLength::None;
|
||||||
|
}
|
||||||
|
|
||||||
// payload decoder
|
// payload decoder
|
||||||
let decoder = match length {
|
let decoder = match length {
|
||||||
@ -293,22 +334,35 @@ impl MessageType for ResponseHead {
|
|||||||
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
|
||||||
|
|
||||||
let (len, ver, status, h_len) = {
|
let (len, ver, status, h_len) = {
|
||||||
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY;
|
// SAFETY:
|
||||||
|
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is safe because the
|
||||||
|
// type we are claiming to have initialized here is a bunch of `MaybeUninit`s, which
|
||||||
|
// do not require initialization.
|
||||||
|
let mut parsed = unsafe {
|
||||||
|
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
|
||||||
|
.assume_init()
|
||||||
|
};
|
||||||
|
|
||||||
let mut res = httparse::Response::new(&mut parsed);
|
let mut res = httparse::Response::new(&mut []);
|
||||||
match res.parse(src)? {
|
|
||||||
|
let mut config = httparse::ParserConfig::default();
|
||||||
|
config.allow_spaces_after_header_name_in_responses(true);
|
||||||
|
|
||||||
|
match config.parse_response_with_uninit_headers(&mut res, src, &mut parsed)? {
|
||||||
httparse::Status::Complete(len) => {
|
httparse::Status::Complete(len) => {
|
||||||
let version = if res.version.unwrap() == 1 {
|
let version = if res.version.unwrap() == 1 {
|
||||||
Version::HTTP_11
|
Version::HTTP_11
|
||||||
} else {
|
} else {
|
||||||
Version::HTTP_10
|
Version::HTTP_10
|
||||||
};
|
};
|
||||||
|
|
||||||
let status = StatusCode::from_u16(res.code.unwrap())
|
let status = StatusCode::from_u16(res.code.unwrap())
|
||||||
.map_err(|_| ParseError::Status)?;
|
.map_err(|_| ParseError::Status)?;
|
||||||
HeaderIndex::record(src, res.headers, &mut headers);
|
HeaderIndex::record(src, res.headers, &mut headers);
|
||||||
|
|
||||||
(len, version, status, res.headers.len())
|
(len, version, status, res.headers.len())
|
||||||
}
|
}
|
||||||
|
|
||||||
httparse::Status::Partial => {
|
httparse::Status::Partial => {
|
||||||
return if src.len() >= MAX_BUFFER_SIZE {
|
return if src.len() >= MAX_BUFFER_SIZE {
|
||||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||||
@ -324,7 +378,15 @@ impl MessageType for ResponseHead {
|
|||||||
msg.version = ver;
|
msg.version = ver;
|
||||||
|
|
||||||
// convert headers
|
// convert headers
|
||||||
let length = msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len])?;
|
let mut length =
|
||||||
|
msg.set_headers(&src.split_to(len).freeze(), &headers[..h_len], ver)?;
|
||||||
|
|
||||||
|
// Remove CL value if 0 now that all headers and HTTP/1.0 special cases are processed.
|
||||||
|
// Protects against some request smuggling attacks.
|
||||||
|
// See https://github.com/actix/actix-web/issues/2767.
|
||||||
|
if length.is_zero() {
|
||||||
|
length = PayloadLength::None;
|
||||||
|
}
|
||||||
|
|
||||||
// message payload
|
// message payload
|
||||||
let decoder = if let PayloadLength::Payload(pl) = length {
|
let decoder = if let PayloadLength::Payload(pl) = length {
|
||||||
@ -360,9 +422,6 @@ pub(crate) const EMPTY_HEADER_INDEX: HeaderIndex = HeaderIndex {
|
|||||||
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
pub(crate) const EMPTY_HEADER_INDEX_ARRAY: [HeaderIndex; MAX_HEADERS] =
|
||||||
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
[EMPTY_HEADER_INDEX; MAX_HEADERS];
|
||||||
|
|
||||||
pub(crate) const EMPTY_HEADER_ARRAY: [httparse::Header<'static>; MAX_HEADERS] =
|
|
||||||
[httparse::EMPTY_HEADER; MAX_HEADERS];
|
|
||||||
|
|
||||||
impl HeaderIndex {
|
impl HeaderIndex {
|
||||||
pub(crate) fn record(
|
pub(crate) fn record(
|
||||||
bytes: &[u8],
|
bytes: &[u8],
|
||||||
@ -381,7 +440,7 @@ impl HeaderIndex {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
/// Chunk type yielded while decoding a payload.
|
/// Chunk type yielded while decoding a payload.
|
||||||
pub enum PayloadItem {
|
pub enum PayloadItem {
|
||||||
Chunk(Bytes),
|
Chunk(Bytes),
|
||||||
@ -391,7 +450,7 @@ pub enum PayloadItem {
|
|||||||
/// Decoder that can handle different payload types.
|
/// Decoder that can handle different payload types.
|
||||||
///
|
///
|
||||||
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
/// If a message body does not use `Transfer-Encoding`, it should include a `Content-Length`.
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub struct PayloadDecoder {
|
pub struct PayloadDecoder {
|
||||||
kind: Kind,
|
kind: Kind,
|
||||||
}
|
}
|
||||||
@ -417,7 +476,7 @@ impl PayloadDecoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, PartialEq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
enum Kind {
|
enum Kind {
|
||||||
/// A reader used when a `Content-Length` header is passed with a positive integer.
|
/// A reader used when a `Content-Length` header is passed with a positive integer.
|
||||||
Length(u64),
|
Length(u64),
|
||||||
@ -596,14 +655,100 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_post() {
|
fn parse_h09_reject() {
|
||||||
let mut buf = BytesMut::from("POST /test2 HTTP/1.0\r\n\r\n");
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test1 HTTP/0.9\r\n\
|
||||||
|
\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
reader.decode(&mut buf).unwrap_err();
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"POST /test2 HTTP/0.9\r\n\
|
||||||
|
Content-Length: 3\r\n\
|
||||||
|
\r\n
|
||||||
|
abc",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
reader.decode(&mut buf).unwrap_err();
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_h10_get() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test1 HTTP/1.0\r\n\
|
||||||
|
\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(req.version(), Version::HTTP_10);
|
||||||
|
assert_eq!(*req.method(), Method::GET);
|
||||||
|
assert_eq!(req.path(), "/test1");
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test2 HTTP/1.0\r\n\
|
||||||
|
Content-Length: 0\r\n\
|
||||||
|
\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(req.version(), Version::HTTP_10);
|
||||||
|
assert_eq!(*req.method(), Method::GET);
|
||||||
|
assert_eq!(req.path(), "/test2");
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET /test3 HTTP/1.0\r\n\
|
||||||
|
Content-Length: 3\r\n\
|
||||||
|
\r\n
|
||||||
|
abc",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(req.version(), Version::HTTP_10);
|
||||||
|
assert_eq!(*req.method(), Method::GET);
|
||||||
|
assert_eq!(req.path(), "/test3");
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn parse_h10_post() {
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"POST /test1 HTTP/1.0\r\n\
|
||||||
|
Content-Length: 3\r\n\
|
||||||
|
\r\n\
|
||||||
|
abc",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert_eq!(req.version(), Version::HTTP_10);
|
||||||
|
assert_eq!(*req.method(), Method::POST);
|
||||||
|
assert_eq!(req.path(), "/test1");
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"POST /test2 HTTP/1.0\r\n\
|
||||||
|
Content-Length: 0\r\n\
|
||||||
|
\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
let mut reader = MessageDecoder::<Request>::default();
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
assert_eq!(req.version(), Version::HTTP_10);
|
assert_eq!(req.version(), Version::HTTP_10);
|
||||||
assert_eq!(*req.method(), Method::POST);
|
assert_eq!(*req.method(), Method::POST);
|
||||||
assert_eq!(req.path(), "/test2");
|
assert_eq!(req.path(), "/test2");
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"POST /test3 HTTP/1.0\r\n\
|
||||||
|
\r\n",
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
|
let err = reader.decode(&mut buf).unwrap_err();
|
||||||
|
assert!(err.to_string().contains("Header"))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -699,121 +844,98 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_default_1_0() {
|
fn test_conn_default_1_0() {
|
||||||
let mut buf = BytesMut::from("GET /test HTTP/1.0\r\n\r\n");
|
let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.0\r\n\r\n"));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_default_1_1() {
|
fn test_conn_default_1_1() {
|
||||||
let mut buf = BytesMut::from("GET /test HTTP/1.1\r\n\r\n");
|
let req = parse_ready!(&mut BytesMut::from("GET /test HTTP/1.1\r\n\r\n"));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_close() {
|
fn test_conn_close() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: close\r\n\r\n",
|
connection: close\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: Close\r\n\r\n",
|
connection: Close\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_close_1_0() {
|
fn test_conn_close_1_0() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: close\r\n\r\n",
|
connection: close\r\n\r\n",
|
||||||
);
|
));
|
||||||
|
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_keep_alive_1_0() {
|
fn test_conn_keep_alive_1_0() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: keep-alive\r\n\r\n",
|
connection: keep-alive\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: Keep-Alive\r\n\r\n",
|
connection: Keep-Alive\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_keep_alive_1_1() {
|
fn test_conn_keep_alive_1_1() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: keep-alive\r\n\r\n",
|
connection: keep-alive\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_other_1_0() {
|
fn test_conn_other_1_0() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.0\r\n\
|
"GET /test HTTP/1.0\r\n\
|
||||||
connection: other\r\n\r\n",
|
connection: other\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
assert_eq!(req.head().connection_type(), ConnectionType::Close);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_other_1_1() {
|
fn test_conn_other_1_1() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
connection: other\r\n\r\n",
|
connection: other\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
assert_eq!(req.head().connection_type(), ConnectionType::KeepAlive);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_upgrade() {
|
fn test_conn_upgrade() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
upgrade: websockets\r\n\
|
upgrade: websockets\r\n\
|
||||||
connection: upgrade\r\n\r\n",
|
connection: upgrade\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||||
|
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
upgrade: Websockets\r\n\
|
upgrade: Websockets\r\n\
|
||||||
connection: Upgrade\r\n\r\n",
|
connection: Upgrade\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
assert_eq!(req.head().connection_type(), ConnectionType::Upgrade);
|
||||||
@ -821,59 +943,62 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_conn_upgrade_connect_method() {
|
fn test_conn_upgrade_connect_method() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"CONNECT /test HTTP/1.1\r\n\
|
"CONNECT /test HTTP/1.1\r\n\
|
||||||
content-type: text/plain\r\n\r\n",
|
content-type: text/plain\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert!(req.upgrade());
|
assert!(req.upgrade());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_headers_content_length_err_1() {
|
fn test_headers_bad_content_length() {
|
||||||
let mut buf = BytesMut::from(
|
// string CL
|
||||||
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
content-length: line\r\n\r\n",
|
content-length: line\r\n\r\n",
|
||||||
);
|
));
|
||||||
|
|
||||||
expect_parse_err!(&mut buf)
|
// negative CL
|
||||||
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
|
"GET /test HTTP/1.1\r\n\
|
||||||
|
content-length: -1\r\n\r\n",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_headers_content_length_err_2() {
|
fn octal_ish_cl_parsed_as_decimal() {
|
||||||
let mut buf = BytesMut::from(
|
let mut buf = BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"POST /test HTTP/1.1\r\n\
|
||||||
content-length: -1\r\n\r\n",
|
content-length: 011\r\n\r\n",
|
||||||
);
|
);
|
||||||
|
let mut reader = MessageDecoder::<Request>::default();
|
||||||
expect_parse_err!(&mut buf);
|
let (_req, pl) = reader.decode(&mut buf).unwrap().unwrap();
|
||||||
|
assert!(matches!(
|
||||||
|
pl,
|
||||||
|
PayloadType::Payload(pl) if pl == PayloadDecoder::length(11)
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_header() {
|
fn test_invalid_header() {
|
||||||
let mut buf = BytesMut::from(
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
test line\r\n\r\n",
|
test line\r\n\r\n",
|
||||||
);
|
));
|
||||||
|
|
||||||
expect_parse_err!(&mut buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_invalid_name() {
|
fn test_invalid_name() {
|
||||||
let mut buf = BytesMut::from(
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
test[]: line\r\n\r\n",
|
test[]: line\r\n\r\n",
|
||||||
);
|
));
|
||||||
|
|
||||||
expect_parse_err!(&mut buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_bad_status_line() {
|
fn test_http_request_bad_status_line() {
|
||||||
let mut buf = BytesMut::from("getpath \r\n\r\n");
|
expect_parse_err!(&mut BytesMut::from("getpath \r\n\r\n"));
|
||||||
expect_parse_err!(&mut buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -913,11 +1038,10 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_utf8() {
|
fn test_http_request_parser_utf8() {
|
||||||
let mut buf = BytesMut::from(
|
let req = parse_ready!(&mut BytesMut::from(
|
||||||
"GET /test HTTP/1.1\r\n\
|
"GET /test HTTP/1.1\r\n\
|
||||||
x-test: тест\r\n\r\n",
|
x-test: тест\r\n\r\n",
|
||||||
);
|
));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
req.headers().get("x-test").unwrap().as_bytes(),
|
req.headers().get("x-test").unwrap().as_bytes(),
|
||||||
@ -927,24 +1051,18 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_two_slashes() {
|
fn test_http_request_parser_two_slashes() {
|
||||||
let mut buf = BytesMut::from("GET //path HTTP/1.1\r\n\r\n");
|
let req = parse_ready!(&mut BytesMut::from("GET //path HTTP/1.1\r\n\r\n"));
|
||||||
let req = parse_ready!(&mut buf);
|
|
||||||
|
|
||||||
assert_eq!(req.path(), "//path");
|
assert_eq!(req.path(), "//path");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_bad_method() {
|
fn test_http_request_parser_bad_method() {
|
||||||
let mut buf = BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n");
|
expect_parse_err!(&mut BytesMut::from("!12%()+=~$ /get HTTP/1.1\r\n\r\n"));
|
||||||
|
|
||||||
expect_parse_err!(&mut buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_http_request_parser_bad_version() {
|
fn test_http_request_parser_bad_version() {
|
||||||
let mut buf = BytesMut::from("GET //get HT/11\r\n\r\n");
|
expect_parse_err!(&mut BytesMut::from("GET //get HT/11\r\n\r\n"));
|
||||||
|
|
||||||
expect_parse_err!(&mut buf);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
@ -961,29 +1079,66 @@ mod tests {
|
|||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hrs_multiple_content_length() {
|
fn hrs_multiple_content_length() {
|
||||||
let mut buf = BytesMut::from(
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
"GET / HTTP/1.1\r\n\
|
"GET / HTTP/1.1\r\n\
|
||||||
Host: example.com\r\n\
|
Host: example.com\r\n\
|
||||||
Content-Length: 4\r\n\
|
Content-Length: 4\r\n\
|
||||||
Content-Length: 2\r\n\
|
Content-Length: 2\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
abcd",
|
abcd",
|
||||||
);
|
));
|
||||||
|
|
||||||
expect_parse_err!(&mut buf);
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
|
"GET / HTTP/1.1\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Content-Length: 0\r\n\
|
||||||
|
Content-Length: 2\r\n\
|
||||||
|
\r\n\
|
||||||
|
ab",
|
||||||
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn hrs_content_length_plus() {
|
fn hrs_content_length_plus() {
|
||||||
let mut buf = BytesMut::from(
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
"GET / HTTP/1.1\r\n\
|
"GET / HTTP/1.1\r\n\
|
||||||
Host: example.com\r\n\
|
Host: example.com\r\n\
|
||||||
Content-Length: +3\r\n\
|
Content-Length: +3\r\n\
|
||||||
\r\n\
|
\r\n\
|
||||||
000",
|
000",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_te_http10() {
|
||||||
|
// in HTTP/1.0 transfer encoding is ignored and must therefore contain a CL header
|
||||||
|
|
||||||
|
expect_parse_err!(&mut BytesMut::from(
|
||||||
|
"POST / HTTP/1.0\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
3\r\n\
|
||||||
|
aaa\r\n\
|
||||||
|
0\r\n\
|
||||||
|
",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn hrs_cl_and_te_http10() {
|
||||||
|
// in HTTP/1.0 transfer encoding is simply ignored so it's fine to have both
|
||||||
|
|
||||||
|
let mut buf = BytesMut::from(
|
||||||
|
"GET / HTTP/1.0\r\n\
|
||||||
|
Host: example.com\r\n\
|
||||||
|
Content-Length: 3\r\n\
|
||||||
|
Transfer-Encoding: chunked\r\n\
|
||||||
|
\r\n\
|
||||||
|
000",
|
||||||
);
|
);
|
||||||
|
|
||||||
expect_parse_err!(&mut buf);
|
parse_ready!(&mut buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -15,13 +15,14 @@ use bitflags::bitflags;
|
|||||||
use bytes::{Buf, BytesMut};
|
use bytes::{Buf, BytesMut};
|
||||||
use futures_core::ready;
|
use futures_core::ready;
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::{error, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BodySize, BoxBody, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
config::ServiceConfig,
|
config::ServiceConfig,
|
||||||
error::{DispatchError, ParseError, PayloadError},
|
error::{DispatchError, ParseError, PayloadError},
|
||||||
service::HttpFlow,
|
service::HttpFlow,
|
||||||
ConnectionType, Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
Error, Extensions, OnConnectData, Request, Response, StatusCode,
|
||||||
};
|
};
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
@ -336,7 +337,7 @@ where
|
|||||||
while written < len {
|
while written < len {
|
||||||
match io.as_mut().poll_write(cx, &write_buf[written..])? {
|
match io.as_mut().poll_write(cx, &write_buf[written..])? {
|
||||||
Poll::Ready(0) => {
|
Poll::Ready(0) => {
|
||||||
log::error!("write zero; closing");
|
error!("write zero; closing");
|
||||||
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
|
return Poll::Ready(Err(io::Error::new(io::ErrorKind::WriteZero, "")));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -375,8 +376,6 @@ where
|
|||||||
DispatchError::Io(err)
|
DispatchError::Io(err)
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
|
||||||
|
|
||||||
Ok(size)
|
Ok(size)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -459,7 +458,12 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
// all messages are dealt with
|
// all messages are dealt with
|
||||||
None => return Ok(PollResponse::DoNothing),
|
None => {
|
||||||
|
// start keep-alive if last request allowed it
|
||||||
|
this.flags.set(Flags::KEEP_ALIVE, this.codec.keep_alive());
|
||||||
|
|
||||||
|
return Ok(PollResponse::DoNothing);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
StateProj::ServiceCall { fut } => {
|
StateProj::ServiceCall { fut } => {
|
||||||
@ -565,7 +569,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
StateProj::ExpectCall { fut } => {
|
StateProj::ExpectCall { fut } => {
|
||||||
log::trace!(" calling expect service");
|
trace!(" calling expect service");
|
||||||
|
|
||||||
match fut.poll(cx) {
|
match fut.poll(cx) {
|
||||||
// expect resolved. write continue to buffer and set InnerDispatcher state
|
// expect resolved. write continue to buffer and set InnerDispatcher state
|
||||||
@ -687,76 +691,15 @@ where
|
|||||||
let can_not_read = !self.can_read(cx);
|
let can_not_read = !self.can_read(cx);
|
||||||
|
|
||||||
// limit amount of non-processed requests
|
// limit amount of non-processed requests
|
||||||
if pipeline_queue_full {
|
if pipeline_queue_full || can_not_read {
|
||||||
return Ok(false);
|
return Ok(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
let mut this = self.as_mut().project();
|
let mut this = self.as_mut().project();
|
||||||
|
|
||||||
if can_not_read {
|
|
||||||
log::debug!("cannot read request payload");
|
|
||||||
|
|
||||||
if let Some(sender) = &this.payload {
|
|
||||||
// ...maybe handler does not want to read any more payload...
|
|
||||||
if let PayloadStatus::Dropped = sender.need_read(cx) {
|
|
||||||
log::debug!("handler dropped payload early; attempt to clean connection");
|
|
||||||
// ...in which case poll request payload a few times
|
|
||||||
loop {
|
|
||||||
match this.codec.decode(this.read_buf)? {
|
|
||||||
Some(msg) => {
|
|
||||||
match msg {
|
|
||||||
// payload decoded did not yield EOF yet
|
|
||||||
Message::Chunk(Some(_)) => {
|
|
||||||
// if non-clean connection, next loop iter will detect empty
|
|
||||||
// read buffer and close connection
|
|
||||||
}
|
|
||||||
|
|
||||||
// connection is in clean state for next request
|
|
||||||
Message::Chunk(None) => {
|
|
||||||
log::debug!("connection successfully cleaned");
|
|
||||||
|
|
||||||
// reset dispatcher state
|
|
||||||
let _ = this.payload.take();
|
|
||||||
this.state.set(State::None);
|
|
||||||
|
|
||||||
// break out of payload decode loop
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Either whole payload is read and loop is broken or more data
|
|
||||||
// was expected in which case connection is closed. In both
|
|
||||||
// situations dispatcher cannot get here.
|
|
||||||
Message::Item(_) => {
|
|
||||||
unreachable!("dispatcher is in payload receive state")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// not enough info to decide if connection is going to be clean or not
|
|
||||||
None => {
|
|
||||||
log::error!(
|
|
||||||
"handler did not read whole payload and dispatcher could not \
|
|
||||||
drain read buf; return 500 and close connection"
|
|
||||||
);
|
|
||||||
|
|
||||||
this.flags.insert(Flags::SHUTDOWN);
|
|
||||||
let mut res = Response::internal_server_error().drop_body();
|
|
||||||
res.head_mut().set_connection_type(ConnectionType::Close);
|
|
||||||
this.messages.push_back(DispatcherMessage::Error(res));
|
|
||||||
*this.error = Some(DispatchError::HandlerDroppedPayload);
|
|
||||||
return Ok(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// can_not_read and no request payload
|
|
||||||
return Ok(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let mut updated = false;
|
let mut updated = false;
|
||||||
|
|
||||||
|
// decode from read buf as many full requests as possible
|
||||||
loop {
|
loop {
|
||||||
match this.codec.decode(this.read_buf) {
|
match this.codec.decode(this.read_buf) {
|
||||||
Ok(Some(msg)) => {
|
Ok(Some(msg)) => {
|
||||||
@ -809,7 +752,7 @@ where
|
|||||||
if let Some(ref mut payload) = this.payload {
|
if let Some(ref mut payload) = this.payload {
|
||||||
payload.feed_data(chunk);
|
payload.feed_data(chunk);
|
||||||
} else {
|
} else {
|
||||||
log::error!("Internal server error: unexpected payload chunk");
|
error!("Internal server error: unexpected payload chunk");
|
||||||
this.flags.insert(Flags::READ_DISCONNECT);
|
this.flags.insert(Flags::READ_DISCONNECT);
|
||||||
this.messages.push_back(DispatcherMessage::Error(
|
this.messages.push_back(DispatcherMessage::Error(
|
||||||
Response::internal_server_error().drop_body(),
|
Response::internal_server_error().drop_body(),
|
||||||
@ -823,7 +766,7 @@ where
|
|||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.feed_eof();
|
payload.feed_eof();
|
||||||
} else {
|
} else {
|
||||||
log::error!("Internal server error: unexpected eof");
|
error!("Internal server error: unexpected eof");
|
||||||
this.flags.insert(Flags::READ_DISCONNECT);
|
this.flags.insert(Flags::READ_DISCONNECT);
|
||||||
this.messages.push_back(DispatcherMessage::Error(
|
this.messages.push_back(DispatcherMessage::Error(
|
||||||
Response::internal_server_error().drop_body(),
|
Response::internal_server_error().drop_body(),
|
||||||
@ -840,7 +783,7 @@ where
|
|||||||
Ok(None) => break,
|
Ok(None) => break,
|
||||||
|
|
||||||
Err(ParseError::Io(err)) => {
|
Err(ParseError::Io(err)) => {
|
||||||
log::trace!("I/O error: {}", &err);
|
trace!("I/O error: {}", &err);
|
||||||
self.as_mut().client_disconnected();
|
self.as_mut().client_disconnected();
|
||||||
this = self.as_mut().project();
|
this = self.as_mut().project();
|
||||||
*this.error = Some(DispatchError::Io(err));
|
*this.error = Some(DispatchError::Io(err));
|
||||||
@ -848,7 +791,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(ParseError::TooLarge) => {
|
Err(ParseError::TooLarge) => {
|
||||||
log::trace!("request head was too big; returning 431 response");
|
trace!("request head was too big; returning 431 response");
|
||||||
|
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.set_error(PayloadError::Overflow);
|
payload.set_error(PayloadError::Overflow);
|
||||||
@ -868,7 +811,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("parse error {}", &err);
|
trace!("parse error {}", &err);
|
||||||
|
|
||||||
if let Some(mut payload) = this.payload.take() {
|
if let Some(mut payload) = this.payload.take() {
|
||||||
payload.set_error(PayloadError::EncodingCorrupted);
|
payload.set_error(PayloadError::EncodingCorrupted);
|
||||||
@ -899,10 +842,7 @@ where
|
|||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
// timeout on first request (slow request) return 408
|
// timeout on first request (slow request) return 408
|
||||||
|
|
||||||
log::trace!(
|
trace!("timed out on slow request; replying with 408 and closing connection");
|
||||||
"timed out on slow request; \
|
|
||||||
replying with 408 and closing connection"
|
|
||||||
);
|
|
||||||
|
|
||||||
let _ = self.as_mut().send_error_response(
|
let _ = self.as_mut().send_error_response(
|
||||||
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
|
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
|
||||||
@ -945,7 +885,7 @@ where
|
|||||||
// keep-alive timer has timed out
|
// keep-alive timer has timed out
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
// no tasks at hand
|
// no tasks at hand
|
||||||
log::trace!("timer timed out; closing connection");
|
trace!("timer timed out; closing connection");
|
||||||
this.flags.insert(Flags::SHUTDOWN);
|
this.flags.insert(Flags::SHUTDOWN);
|
||||||
|
|
||||||
if let Some(deadline) = this.config.client_disconnect_deadline() {
|
if let Some(deadline) = this.config.client_disconnect_deadline() {
|
||||||
@ -975,7 +915,7 @@ where
|
|||||||
|
|
||||||
// timed-out during shutdown; drop connection
|
// timed-out during shutdown; drop connection
|
||||||
if timer.as_mut().poll(cx).is_ready() {
|
if timer.as_mut().poll(cx).is_ready() {
|
||||||
log::trace!("timed-out during shutdown");
|
trace!("timed-out during shutdown");
|
||||||
return Err(DispatchError::DisconnectTimeout);
|
return Err(DispatchError::DisconnectTimeout);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1134,12 +1074,12 @@ where
|
|||||||
|
|
||||||
match this.inner.project() {
|
match this.inner.project() {
|
||||||
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
DispatcherStateProj::Upgrade { fut: upgrade } => upgrade.poll(cx).map_err(|err| {
|
||||||
log::error!("Upgrade handler error: {}", err);
|
error!("Upgrade handler error: {}", err);
|
||||||
DispatchError::Upgrade
|
DispatchError::Upgrade
|
||||||
}),
|
}),
|
||||||
|
|
||||||
DispatcherStateProj::Normal { mut inner } => {
|
DispatcherStateProj::Normal { mut inner } => {
|
||||||
log::trace!("start flags: {:?}", &inner.flags);
|
trace!("start flags: {:?}", &inner.flags);
|
||||||
|
|
||||||
trace_timer_states(
|
trace_timer_states(
|
||||||
"start",
|
"start",
|
||||||
@ -1246,7 +1186,7 @@ where
|
|||||||
|
|
||||||
// client is gone
|
// client is gone
|
||||||
if inner.flags.contains(Flags::WRITE_DISCONNECT) {
|
if inner.flags.contains(Flags::WRITE_DISCONNECT) {
|
||||||
log::trace!("client is gone; disconnecting");
|
trace!("client is gone; disconnecting");
|
||||||
return Poll::Ready(Ok(()));
|
return Poll::Ready(Ok(()));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1255,14 +1195,14 @@ where
|
|||||||
|
|
||||||
// read half is closed; we do not process any responses
|
// read half is closed; we do not process any responses
|
||||||
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
if inner_p.flags.contains(Flags::READ_DISCONNECT) && state_is_none {
|
||||||
log::trace!("read half closed; start shutdown");
|
trace!("read half closed; start shutdown");
|
||||||
inner_p.flags.insert(Flags::SHUTDOWN);
|
inner_p.flags.insert(Flags::SHUTDOWN);
|
||||||
}
|
}
|
||||||
|
|
||||||
// keep-alive and stream errors
|
// keep-alive and stream errors
|
||||||
if state_is_none && inner_p.write_buf.is_empty() {
|
if state_is_none && inner_p.write_buf.is_empty() {
|
||||||
if let Some(err) = inner_p.error.take() {
|
if let Some(err) = inner_p.error.take() {
|
||||||
log::error!("stream error: {}", &err);
|
error!("stream error: {}", &err);
|
||||||
return Poll::Ready(Err(err));
|
return Poll::Ready(Err(err));
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1291,7 +1231,7 @@ where
|
|||||||
Poll::Pending
|
Poll::Pending
|
||||||
};
|
};
|
||||||
|
|
||||||
log::trace!("end flags: {:?}", &inner.flags);
|
trace!("end flags: {:?}", &inner.flags);
|
||||||
|
|
||||||
poll
|
poll
|
||||||
}
|
}
|
||||||
@ -1306,17 +1246,17 @@ fn trace_timer_states(
|
|||||||
ka_timer: &TimerState,
|
ka_timer: &TimerState,
|
||||||
shutdown_timer: &TimerState,
|
shutdown_timer: &TimerState,
|
||||||
) {
|
) {
|
||||||
log::trace!("{} timers:", label);
|
trace!("{} timers:", label);
|
||||||
|
|
||||||
if head_timer.is_enabled() {
|
if head_timer.is_enabled() {
|
||||||
log::trace!(" head {}", &head_timer);
|
trace!(" head {}", &head_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ka_timer.is_enabled() {
|
if ka_timer.is_enabled() {
|
||||||
log::trace!(" keep-alive {}", &ka_timer);
|
trace!(" keep-alive {}", &ka_timer);
|
||||||
}
|
}
|
||||||
|
|
||||||
if shutdown_timer.is_enabled() {
|
if shutdown_timer.is_enabled() {
|
||||||
log::trace!(" shutdown {}", &shutdown_timer);
|
trace!(" shutdown {}", &shutdown_timer);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -783,6 +783,9 @@ async fn upgrade_handling() {
|
|||||||
.await;
|
.await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// fix in #2624 reverted temporarily
|
||||||
|
// complete fix tracked in #2745
|
||||||
|
#[ignore]
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn handler_drop_payload() {
|
async fn handler_drop_payload() {
|
||||||
let _ = env_logger::try_init();
|
let _ = env_logger::try_init();
|
||||||
|
@ -517,6 +517,7 @@ unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
|
|||||||
if let Some(c @ b'a'..=b'z') = iter.next() {
|
if let Some(c @ b'a'..=b'z') = iter.next() {
|
||||||
buffer[index] = c & 0b1101_1111;
|
buffer[index] = c & 0b1101_1111;
|
||||||
}
|
}
|
||||||
|
index += 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
index += 1;
|
index += 1;
|
||||||
@ -528,7 +529,7 @@ mod tests {
|
|||||||
use std::rc::Rc;
|
use std::rc::Rc;
|
||||||
|
|
||||||
use bytes::Bytes;
|
use bytes::Bytes;
|
||||||
use http::header::AUTHORIZATION;
|
use http::header::{AUTHORIZATION, UPGRADE_INSECURE_REQUESTS};
|
||||||
|
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::{
|
use crate::{
|
||||||
@ -559,6 +560,9 @@ mod tests {
|
|||||||
head.headers
|
head.headers
|
||||||
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
|
||||||
|
|
||||||
|
head.headers
|
||||||
|
.insert(UPGRADE_INSECURE_REQUESTS, HeaderValue::from_static("1"));
|
||||||
|
|
||||||
let mut head = RequestHeadType::Owned(head);
|
let mut head = RequestHeadType::Owned(head);
|
||||||
|
|
||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
@ -574,6 +578,7 @@ mod tests {
|
|||||||
assert!(data.contains("Connection: close\r\n"));
|
assert!(data.contains("Connection: close\r\n"));
|
||||||
assert!(data.contains("Content-Type: plain/text\r\n"));
|
assert!(data.contains("Content-Type: plain/text\r\n"));
|
||||||
assert!(data.contains("Date: date\r\n"));
|
assert!(data.contains("Date: date\r\n"));
|
||||||
|
assert!(data.contains("Upgrade-Insecure-Requests: 1\r\n"));
|
||||||
|
|
||||||
let _ = head.encode_headers(
|
let _ = head.encode_headers(
|
||||||
&mut bytes,
|
&mut bytes,
|
||||||
|
@ -16,7 +16,7 @@ use crate::error::PayloadError;
|
|||||||
/// max buffer size 32k
|
/// max buffer size 32k
|
||||||
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
pub(crate) const MAX_BUFFER_SIZE: usize = 32_768;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum PayloadStatus {
|
pub enum PayloadStatus {
|
||||||
Read,
|
Read,
|
||||||
Pause,
|
Pause,
|
||||||
@ -263,7 +263,8 @@ mod tests {
|
|||||||
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
assert_not_impl_any!(Payload: Send, Sync, UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
assert_impl_all!(Inner: Unpin, Send, Sync);
|
assert_impl_all!(Inner: Unpin, Send, Sync);
|
||||||
assert_not_impl_any!(Inner: UnwindSafe, RefUnwindSafe);
|
// assertion not stable wrt rustc versions yet
|
||||||
|
// assert_impl_all!(Inner: UnwindSafe, RefUnwindSafe);
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_unread_data() {
|
async fn test_unread_data() {
|
||||||
|
@ -13,6 +13,7 @@ use actix_service::{
|
|||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
@ -305,13 +306,13 @@ where
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let expect = expect
|
let expect = expect
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade
|
let upgrade = upgrade
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@ -319,7 +320,7 @@ where
|
|||||||
|
|
||||||
let service = service
|
let service = service
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
Ok(H1ServiceHandler::new(
|
Ok(H1ServiceHandler::new(
|
||||||
cfg,
|
cfg,
|
||||||
@ -357,7 +358,7 @@ where
|
|||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|err| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
log::error!("HTTP/1 service readiness error: {:?}", err);
|
error!("HTTP/1 service readiness error: {:?}", err);
|
||||||
DispatchError::Service(err)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
use std::{fmt, future::Future, pin::Pin, task::Context};
|
use std::{fmt, future::Future, pin::Pin, task::Context};
|
||||||
|
|
||||||
use actix_rt::time::{Instant, Sleep};
|
use actix_rt::time::{Instant, Sleep};
|
||||||
|
use tracing::trace;
|
||||||
|
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub(super) enum TimerState {
|
pub(super) enum TimerState {
|
||||||
@ -24,7 +25,7 @@ impl TimerState {
|
|||||||
|
|
||||||
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
pub(super) fn set(&mut self, timer: Sleep, line: u32) {
|
||||||
if matches!(self, Self::Disabled) {
|
if matches!(self, Self::Disabled) {
|
||||||
log::trace!("setting disabled timer from line {}", line);
|
trace!("setting disabled timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = Self::Active {
|
*self = Self::Active {
|
||||||
@ -39,11 +40,11 @@ impl TimerState {
|
|||||||
|
|
||||||
pub(super) fn clear(&mut self, line: u32) {
|
pub(super) fn clear(&mut self, line: u32) {
|
||||||
if matches!(self, Self::Disabled) {
|
if matches!(self, Self::Disabled) {
|
||||||
log::trace!("trying to clear a disabled timer from line {}", line);
|
trace!("trying to clear a disabled timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
if matches!(self, Self::Inactive) {
|
if matches!(self, Self::Inactive) {
|
||||||
log::trace!("trying to clear an inactive timer from line {}", line);
|
trace!("trying to clear an inactive timer from line {}", line);
|
||||||
}
|
}
|
||||||
|
|
||||||
*self = Self::Inactive;
|
*self = Self::Inactive;
|
||||||
|
@ -19,8 +19,8 @@ use h2::{
|
|||||||
server::{Connection, SendResponse},
|
server::{Connection, SendResponse},
|
||||||
Ping, PingPong,
|
Ping, PingPong,
|
||||||
};
|
};
|
||||||
use log::{error, trace};
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::{error, trace, warn};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BodySize, BoxBody, MessageBody},
|
body::{BodySize, BoxBody, MessageBody},
|
||||||
@ -143,7 +143,7 @@ where
|
|||||||
DispatchError::SendResponse(err) => {
|
DispatchError::SendResponse(err) => {
|
||||||
trace!("Error sending HTTP/2 response: {:?}", err)
|
trace!("Error sending HTTP/2 response: {:?}", err)
|
||||||
}
|
}
|
||||||
DispatchError::SendData(err) => log::warn!("{:?}", err),
|
DispatchError::SendData(err) => warn!("{:?}", err),
|
||||||
DispatchError::ResponseBody(err) => {
|
DispatchError::ResponseBody(err) => {
|
||||||
error!("Response payload stream error: {:?}", err)
|
error!("Response payload stream error: {:?}", err)
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ use actix_service::{
|
|||||||
};
|
};
|
||||||
use actix_utils::future::ready;
|
use actix_utils::future::ready;
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use log::error;
|
use tracing::{error, trace};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
@ -355,7 +355,7 @@ where
|
|||||||
}
|
}
|
||||||
|
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("H2 handshake error: {}", err);
|
trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -12,7 +12,7 @@ use crate::header::{Charset, HTTP_VALUE};
|
|||||||
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||||
///
|
///
|
||||||
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
||||||
#[derive(Clone, Debug, PartialEq)]
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||||
pub struct ExtendedValue {
|
pub struct ExtendedValue {
|
||||||
/// The character set that is used to encode the `value` to a string.
|
/// The character set that is used to encode the `value` to a string.
|
||||||
pub charset: Charset,
|
pub charset: Charset,
|
||||||
|
@ -147,7 +147,7 @@ mod tests {
|
|||||||
|
|
||||||
// copy of encoding from actix-web headers
|
// copy of encoding from actix-web headers
|
||||||
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
||||||
#[derive(Clone, PartialEq, Debug)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
pub enum Encoding {
|
pub enum Encoding {
|
||||||
Chunked,
|
Chunked,
|
||||||
Brotli,
|
Brotli,
|
||||||
|
@ -25,6 +25,7 @@
|
|||||||
)]
|
)]
|
||||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||||
|
#![cfg_attr(docsrs, feature(doc_cfg))]
|
||||||
|
|
||||||
pub use ::http::{uri, uri::Uri};
|
pub use ::http::{uri, uri::Uri};
|
||||||
pub use ::http::{Method, StatusCode, Version};
|
pub use ::http::{Method, StatusCode, Version};
|
||||||
@ -69,6 +70,8 @@ pub use self::payload::{BoxedPayloadStream, Payload, PayloadStream};
|
|||||||
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
pub use self::requests::{Request, RequestHead, RequestHeadType};
|
||||||
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
pub use self::responses::{Response, ResponseBuilder, ResponseHead};
|
||||||
pub use self::service::HttpService;
|
pub use self::service::HttpService;
|
||||||
|
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||||
|
pub use self::service::TlsAcceptorConfig;
|
||||||
|
|
||||||
/// A major HTTP protocol version.
|
/// A major HTTP protocol version.
|
||||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
@ -3,7 +3,7 @@ use std::{cell::RefCell, ops, rc::Rc};
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
|
|
||||||
/// Represents various types of connection
|
/// Represents various types of connection
|
||||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
pub enum ConnectionType {
|
pub enum ConnectionType {
|
||||||
/// Close connection after response.
|
/// Close connection after response.
|
||||||
Close,
|
Close,
|
||||||
|
@ -13,7 +13,8 @@ use crate::error::PayloadError;
|
|||||||
/// A boxed payload stream.
|
/// A boxed payload stream.
|
||||||
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
pub type BoxedPayloadStream = Pin<Box<dyn Stream<Item = Result<Bytes, PayloadError>>>>;
|
||||||
|
|
||||||
#[deprecated(since = "4.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
#[doc(hidden)]
|
||||||
|
#[deprecated(since = "3.0.0", note = "Renamed to `BoxedPayloadStream`.")]
|
||||||
pub type PayloadStream = BoxedPayloadStream;
|
pub type PayloadStream = BoxedPayloadStream;
|
||||||
|
|
||||||
#[cfg(not(feature = "http2"))]
|
#[cfg(not(feature = "http2"))]
|
||||||
|
@ -144,7 +144,7 @@ impl ResponseBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set connection type to Upgrade
|
/// Set connection type to `Upgrade`.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
|
||||||
where
|
where
|
||||||
@ -161,7 +161,7 @@ impl ResponseBuilder {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Force close connection, even if it is marked as keep-alive
|
/// Force-close connection, even if it is marked as keep-alive.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn force_close(&mut self) -> &mut Self {
|
pub fn force_close(&mut self) -> &mut Self {
|
||||||
if let Some(parts) = self.inner() {
|
if let Some(parts) = self.inner() {
|
||||||
|
@ -237,7 +237,7 @@ mod tests {
|
|||||||
.await;
|
.await;
|
||||||
|
|
||||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||||
let _ = stream
|
stream
|
||||||
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut data = vec![];
|
let mut data = vec![];
|
||||||
@ -251,7 +251,7 @@ mod tests {
|
|||||||
assert!(memmem::find(&data, b"content-length").is_none());
|
assert!(memmem::find(&data, b"content-length").is_none());
|
||||||
|
|
||||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||||
let _ = stream
|
stream
|
||||||
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n")
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut data = vec![];
|
let mut data = vec![];
|
||||||
|
@ -15,6 +15,7 @@ use actix_service::{
|
|||||||
};
|
};
|
||||||
use futures_core::{future::LocalBoxFuture, ready};
|
use futures_core::{future::LocalBoxFuture, ready};
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
body::{BoxBody, MessageBody},
|
body::{BoxBody, MessageBody},
|
||||||
@ -180,6 +181,25 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Configuration options used when accepting TLS connection.
|
||||||
|
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||||
|
#[cfg_attr(docsrs, doc(cfg(any(feature = "openssl", feature = "rustls"))))]
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
pub struct TlsAcceptorConfig {
|
||||||
|
pub(crate) handshake_timeout: Option<std::time::Duration>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||||
|
impl TlsAcceptorConfig {
|
||||||
|
/// Set TLS handshake timeout duration.
|
||||||
|
pub fn handshake_timeout(self, dur: std::time::Duration) -> Self {
|
||||||
|
Self {
|
||||||
|
handshake_timeout: Some(dur),
|
||||||
|
// ..self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
mod openssl {
|
mod openssl {
|
||||||
use actix_service::ServiceFactoryExt as _;
|
use actix_service::ServiceFactoryExt as _;
|
||||||
@ -229,7 +249,28 @@ mod openssl {
|
|||||||
Error = TlsError<SslError, DispatchError>,
|
Error = TlsError<SslError, DispatchError>,
|
||||||
InitError = (),
|
InitError = (),
|
||||||
> {
|
> {
|
||||||
Acceptor::new(acceptor)
|
self.openssl_with_config(acceptor, TlsAcceptorConfig::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create OpenSSL based service with custom TLS acceptor configuration.
|
||||||
|
pub fn openssl_with_config(
|
||||||
|
self,
|
||||||
|
acceptor: SslAcceptor,
|
||||||
|
tls_acceptor_config: TlsAcceptorConfig,
|
||||||
|
) -> impl ServiceFactory<
|
||||||
|
TcpStream,
|
||||||
|
Config = (),
|
||||||
|
Response = (),
|
||||||
|
Error = TlsError<SslError, DispatchError>,
|
||||||
|
InitError = (),
|
||||||
|
> {
|
||||||
|
let mut acceptor = Acceptor::new(acceptor);
|
||||||
|
|
||||||
|
if let Some(handshake_timeout) = tls_acceptor_config.handshake_timeout {
|
||||||
|
acceptor.set_handshake_timeout(handshake_timeout);
|
||||||
|
}
|
||||||
|
|
||||||
|
acceptor
|
||||||
.map_init_err(|_| {
|
.map_init_err(|_| {
|
||||||
unreachable!("TLS acceptor service factory does not error on init")
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
})
|
})
|
||||||
@ -292,8 +333,23 @@ mod rustls {
|
|||||||
{
|
{
|
||||||
/// Create Rustls based service.
|
/// Create Rustls based service.
|
||||||
pub fn rustls(
|
pub fn rustls(
|
||||||
|
self,
|
||||||
|
config: ServerConfig,
|
||||||
|
) -> impl ServiceFactory<
|
||||||
|
TcpStream,
|
||||||
|
Config = (),
|
||||||
|
Response = (),
|
||||||
|
Error = TlsError<io::Error, DispatchError>,
|
||||||
|
InitError = (),
|
||||||
|
> {
|
||||||
|
self.rustls_with_config(config, TlsAcceptorConfig::default())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create Rustls based service with custom TLS acceptor configuration.
|
||||||
|
pub fn rustls_with_config(
|
||||||
self,
|
self,
|
||||||
mut config: ServerConfig,
|
mut config: ServerConfig,
|
||||||
|
tls_acceptor_config: TlsAcceptorConfig,
|
||||||
) -> impl ServiceFactory<
|
) -> impl ServiceFactory<
|
||||||
TcpStream,
|
TcpStream,
|
||||||
Config = (),
|
Config = (),
|
||||||
@ -305,7 +361,13 @@ mod rustls {
|
|||||||
protos.extend_from_slice(&config.alpn_protocols);
|
protos.extend_from_slice(&config.alpn_protocols);
|
||||||
config.alpn_protocols = protos;
|
config.alpn_protocols = protos;
|
||||||
|
|
||||||
Acceptor::new(config)
|
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(|_| {
|
.map_init_err(|_| {
|
||||||
unreachable!("TLS acceptor service factory does not error on init")
|
unreachable!("TLS acceptor service factory does not error on init")
|
||||||
})
|
})
|
||||||
@ -369,13 +431,13 @@ where
|
|||||||
Box::pin(async move {
|
Box::pin(async move {
|
||||||
let expect = expect
|
let expect = expect
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
|
.map_err(|e| error!("Init http expect service error: {:?}", e))?;
|
||||||
|
|
||||||
let upgrade = match upgrade {
|
let upgrade = match upgrade {
|
||||||
Some(upgrade) => {
|
Some(upgrade) => {
|
||||||
let upgrade = upgrade
|
let upgrade = upgrade
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http upgrade service error: {:?}", e))?;
|
.map_err(|e| error!("Init http upgrade service error: {:?}", e))?;
|
||||||
Some(upgrade)
|
Some(upgrade)
|
||||||
}
|
}
|
||||||
None => None,
|
None => None,
|
||||||
@ -383,7 +445,7 @@ where
|
|||||||
|
|
||||||
let service = service
|
let service = service
|
||||||
.await
|
.await
|
||||||
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
|
.map_err(|e| error!("Init http service error: {:?}", e))?;
|
||||||
|
|
||||||
Ok(HttpServiceHandler::new(
|
Ok(HttpServiceHandler::new(
|
||||||
cfg,
|
cfg,
|
||||||
@ -490,7 +552,7 @@ where
|
|||||||
|
|
||||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||||
self._poll_ready(cx).map_err(|err| {
|
self._poll_ready(cx).map_err(|err| {
|
||||||
log::error!("HTTP service readiness error: {:?}", err);
|
error!("HTTP service readiness error: {:?}", err);
|
||||||
DispatchError::Service(err)
|
DispatchError::Service(err)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -666,7 +728,7 @@ where
|
|||||||
self.poll(cx)
|
self.poll(cx)
|
||||||
}
|
}
|
||||||
Err(err) => {
|
Err(err) => {
|
||||||
log::trace!("H2 handshake error: {}", err);
|
tracing::trace!("H2 handshake error: {}", err);
|
||||||
Poll::Ready(Err(err))
|
Poll::Ready(Err(err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -19,29 +19,7 @@ use crate::{
|
|||||||
Request,
|
Request,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// Test `Request` builder
|
/// Test `Request` builder.
|
||||||
///
|
|
||||||
/// ```ignore
|
|
||||||
/// # use http::{header, StatusCode};
|
|
||||||
/// # use actix_web::*;
|
|
||||||
/// use actix_web::test::TestRequest;
|
|
||||||
///
|
|
||||||
/// fn index(req: &HttpRequest) -> Response {
|
|
||||||
/// if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
|
||||||
/// Response::Ok().into()
|
|
||||||
/// } else {
|
|
||||||
/// Response::BadRequest().into()
|
|
||||||
/// }
|
|
||||||
/// }
|
|
||||||
///
|
|
||||||
/// let resp = TestRequest::default().insert_header("content-type", "text/plain")
|
|
||||||
/// .run(&index)
|
|
||||||
/// .unwrap();
|
|
||||||
/// assert_eq!(resp.status(), StatusCode::OK);
|
|
||||||
///
|
|
||||||
/// let resp = TestRequest::default().run(&index).unwrap();
|
|
||||||
/// assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
|
||||||
/// ```
|
|
||||||
pub struct TestRequest(Option<Inner>);
|
pub struct TestRequest(Option<Inner>);
|
||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
|
@ -2,6 +2,7 @@ use actix_codec::{Decoder, Encoder};
|
|||||||
use bitflags::bitflags;
|
use bitflags::bitflags;
|
||||||
use bytes::{Bytes, BytesMut};
|
use bytes::{Bytes, BytesMut};
|
||||||
use bytestring::ByteString;
|
use bytestring::ByteString;
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
frame::Parser,
|
frame::Parser,
|
||||||
@ -10,7 +11,7 @@ use super::{
|
|||||||
};
|
};
|
||||||
|
|
||||||
/// A WebSocket message.
|
/// A WebSocket message.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Message {
|
pub enum Message {
|
||||||
/// Text message.
|
/// Text message.
|
||||||
Text(ByteString),
|
Text(ByteString),
|
||||||
@ -35,7 +36,7 @@ pub enum Message {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A WebSocket frame.
|
/// A WebSocket frame.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Frame {
|
pub enum Frame {
|
||||||
/// Text frame. Note that the codec does not validate UTF-8 encoding.
|
/// Text frame. Note that the codec does not validate UTF-8 encoding.
|
||||||
Text(Bytes),
|
Text(Bytes),
|
||||||
@ -57,7 +58,7 @@ pub enum Frame {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// A WebSocket continuation item.
|
/// A WebSocket continuation item.
|
||||||
#[derive(Debug, PartialEq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum Item {
|
pub enum Item {
|
||||||
FirstText(Bytes),
|
FirstText(Bytes),
|
||||||
FirstBinary(Bytes),
|
FirstBinary(Bytes),
|
||||||
@ -253,7 +254,7 @@ impl Decoder for Codec {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
_ => {
|
_ => {
|
||||||
log::error!("Unfinished fragment {:?}", opcode);
|
error!("Unfinished fragment {:?}", opcode);
|
||||||
Err(ProtocolError::ContinuationFragment(opcode))
|
Err(ProtocolError::ContinuationFragment(opcode))
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -73,8 +73,8 @@ mod inner {
|
|||||||
use actix_service::{IntoService, Service};
|
use actix_service::{IntoService, Service};
|
||||||
use futures_core::stream::Stream;
|
use futures_core::stream::Stream;
|
||||||
use local_channel::mpsc;
|
use local_channel::mpsc;
|
||||||
use log::debug;
|
|
||||||
use pin_project_lite::pin_project;
|
use pin_project_lite::pin_project;
|
||||||
|
use tracing::debug;
|
||||||
|
|
||||||
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
||||||
|
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
use std::convert::TryFrom;
|
use std::convert::TryFrom;
|
||||||
|
|
||||||
use bytes::{Buf, BufMut, BytesMut};
|
use bytes::{Buf, BufMut, BytesMut};
|
||||||
use log::debug;
|
use tracing::debug;
|
||||||
|
|
||||||
use super::{
|
use super::{
|
||||||
mask::apply_mask,
|
mask::apply_mask,
|
||||||
@ -17,7 +17,6 @@ impl Parser {
|
|||||||
fn parse_metadata(
|
fn parse_metadata(
|
||||||
src: &[u8],
|
src: &[u8],
|
||||||
server: bool,
|
server: bool,
|
||||||
max_size: usize,
|
|
||||||
) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
|
) -> Result<Option<(usize, bool, OpCode, usize, Option<[u8; 4]>)>, ProtocolError> {
|
||||||
let chunk_len = src.len();
|
let chunk_len = src.len();
|
||||||
|
|
||||||
@ -60,20 +59,12 @@ impl Parser {
|
|||||||
return Ok(None);
|
return Ok(None);
|
||||||
}
|
}
|
||||||
let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
|
let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
|
||||||
if len > max_size as u64 {
|
|
||||||
return Err(ProtocolError::Overflow);
|
|
||||||
}
|
|
||||||
idx += 8;
|
idx += 8;
|
||||||
len as usize
|
len as usize
|
||||||
} else {
|
} else {
|
||||||
len as usize
|
len as usize
|
||||||
};
|
};
|
||||||
|
|
||||||
// check for max allowed size
|
|
||||||
if length > max_size {
|
|
||||||
return Err(ProtocolError::Overflow);
|
|
||||||
}
|
|
||||||
|
|
||||||
let mask = if server {
|
let mask = if server {
|
||||||
if chunk_len < idx + 4 {
|
if chunk_len < idx + 4 {
|
||||||
return Ok(None);
|
return Ok(None);
|
||||||
@ -98,11 +89,10 @@ impl Parser {
|
|||||||
max_size: usize,
|
max_size: usize,
|
||||||
) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
|
) -> Result<Option<(bool, OpCode, Option<BytesMut>)>, ProtocolError> {
|
||||||
// try to parse ws frame metadata
|
// try to parse ws frame metadata
|
||||||
let (idx, finished, opcode, length, mask) =
|
let (idx, finished, opcode, length, mask) = match Parser::parse_metadata(src, server)? {
|
||||||
match Parser::parse_metadata(src, server, max_size)? {
|
None => return Ok(None),
|
||||||
None => return Ok(None),
|
Some(res) => res,
|
||||||
Some(res) => res,
|
};
|
||||||
};
|
|
||||||
|
|
||||||
// not enough data
|
// not enough data
|
||||||
if src.len() < idx + length {
|
if src.len() < idx + length {
|
||||||
@ -112,6 +102,13 @@ impl Parser {
|
|||||||
// remove prefix
|
// remove prefix
|
||||||
src.advance(idx);
|
src.advance(idx);
|
||||||
|
|
||||||
|
// check for max allowed size
|
||||||
|
if length > max_size {
|
||||||
|
// drop the payload
|
||||||
|
src.advance(length);
|
||||||
|
return Err(ProtocolError::Overflow);
|
||||||
|
}
|
||||||
|
|
||||||
// no need for body
|
// no need for body
|
||||||
if length == 0 {
|
if length == 0 {
|
||||||
return Ok(Some((finished, opcode, None)));
|
return Ok(Some((finished, opcode, None)));
|
||||||
@ -339,6 +336,30 @@ mod tests {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_parse_frame_max_size_recoverability() {
|
||||||
|
let mut buf = BytesMut::new();
|
||||||
|
// The first text frame with length == 2, payload doesn't matter.
|
||||||
|
buf.extend(&[0b0000_0001u8, 0b0000_0010u8, 0b0000_0000u8, 0b0000_0000u8]);
|
||||||
|
// Next binary frame with length == 2 and payload == `[0x1111_1111u8, 0x1111_1111u8]`.
|
||||||
|
buf.extend(&[0b0000_0010u8, 0b0000_0010u8, 0b1111_1111u8, 0b1111_1111u8]);
|
||||||
|
|
||||||
|
assert_eq!(buf.len(), 8);
|
||||||
|
assert!(matches!(
|
||||||
|
Parser::parse(&mut buf, false, 1),
|
||||||
|
Err(ProtocolError::Overflow)
|
||||||
|
));
|
||||||
|
assert_eq!(buf.len(), 4);
|
||||||
|
let frame = extract(Parser::parse(&mut buf, false, 2));
|
||||||
|
assert!(!frame.finished);
|
||||||
|
assert_eq!(frame.opcode, OpCode::Binary);
|
||||||
|
assert_eq!(
|
||||||
|
frame.payload,
|
||||||
|
Bytes::from(vec![0b1111_1111u8, 0b1111_1111u8])
|
||||||
|
);
|
||||||
|
assert_eq!(buf.len(), 0);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_ping_frame() {
|
fn test_ping_frame() {
|
||||||
let mut buf = BytesMut::new();
|
let mut buf = BytesMut::new();
|
||||||
|
@ -67,7 +67,7 @@ pub enum ProtocolError {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// WebSocket handshake errors
|
/// WebSocket handshake errors
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, 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.")]
|
||||||
|
@ -3,6 +3,8 @@ use std::{
|
|||||||
fmt,
|
fmt,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
/// Operation codes defined in [RFC 6455 §11.8].
|
/// Operation codes defined in [RFC 6455 §11.8].
|
||||||
///
|
///
|
||||||
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
||||||
@ -58,7 +60,7 @@ impl From<OpCode> for u8 {
|
|||||||
Ping => 9,
|
Ping => 9,
|
||||||
Pong => 10,
|
Pong => 10,
|
||||||
Bad => {
|
Bad => {
|
||||||
log::error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
error!("Attempted to convert invalid opcode to u8. This is a bug.");
|
||||||
8 // if this somehow happens, a close frame will help us tear down quickly
|
8 // if this somehow happens, a close frame will help us tear down quickly
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,13 +2,13 @@
|
|||||||
|
|
||||||
extern crate tls_openssl as openssl;
|
extern crate tls_openssl as openssl;
|
||||||
|
|
||||||
use std::{convert::Infallible, io};
|
use std::{convert::Infallible, io, time::Duration};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{BodyStream, BoxBody, SizedStream},
|
body::{BodyStream, BoxBody, SizedStream},
|
||||||
error::PayloadError,
|
error::PayloadError,
|
||||||
header::{self, HeaderValue},
|
header::{self, HeaderValue},
|
||||||
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
use actix_service::{fn_service, ServiceFactoryExt};
|
use actix_service::{fn_service, ServiceFactoryExt};
|
||||||
@ -89,7 +89,10 @@ async fn h2_1() -> io::Result<()> {
|
|||||||
assert_eq!(req.version(), Version::HTTP_2);
|
assert_eq!(req.version(), Version::HTTP_2);
|
||||||
ok::<_, Error>(Response::ok())
|
ok::<_, Error>(Response::ok())
|
||||||
})
|
})
|
||||||
.openssl(tls_config())
|
.openssl_with_config(
|
||||||
|
tls_config(),
|
||||||
|
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
|
||||||
|
)
|
||||||
.map_err(|_| ())
|
.map_err(|_| ())
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
@ -8,13 +8,14 @@ use std::{
|
|||||||
net::{SocketAddr, TcpStream as StdTcpStream},
|
net::{SocketAddr, TcpStream as StdTcpStream},
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
task::Poll,
|
task::Poll,
|
||||||
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
use actix_http::{
|
use actix_http::{
|
||||||
body::{BodyStream, BoxBody, SizedStream},
|
body::{BodyStream, BoxBody, SizedStream},
|
||||||
error::PayloadError,
|
error::PayloadError,
|
||||||
header::{self, HeaderName, HeaderValue},
|
header::{self, HeaderName, HeaderValue},
|
||||||
Error, HttpService, Method, Request, Response, StatusCode, Version,
|
Error, HttpService, Method, Request, Response, StatusCode, TlsAcceptorConfig, Version,
|
||||||
};
|
};
|
||||||
use actix_http_test::test_server;
|
use actix_http_test::test_server;
|
||||||
use actix_rt::pin;
|
use actix_rt::pin;
|
||||||
@ -160,7 +161,10 @@ async fn h2_1() -> io::Result<()> {
|
|||||||
assert_eq!(req.version(), Version::HTTP_2);
|
assert_eq!(req.version(), Version::HTTP_2);
|
||||||
ok::<_, Error>(Response::ok())
|
ok::<_, Error>(Response::ok())
|
||||||
})
|
})
|
||||||
.rustls(tls_config())
|
.rustls_with_config(
|
||||||
|
tls_config(),
|
||||||
|
TlsAcceptorConfig::default().handshake_timeout(Duration::from_secs(5)),
|
||||||
|
)
|
||||||
})
|
})
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
@ -212,6 +216,7 @@ async fn h2_content_length() {
|
|||||||
let value = HeaderValue::from_static("0");
|
let value = HeaderValue::from_static("0");
|
||||||
|
|
||||||
{
|
{
|
||||||
|
#[allow(clippy::single_element_loop)]
|
||||||
for &i in &[0] {
|
for &i in &[0] {
|
||||||
let req = srv
|
let req = srv
|
||||||
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
||||||
@ -226,6 +231,7 @@ async fn h2_content_length() {
|
|||||||
// assert_eq!(response.headers().get(&header), None);
|
// assert_eq!(response.headers().get(&header), None);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::single_element_loop)]
|
||||||
for &i in &[1] {
|
for &i in &[1] {
|
||||||
let req = srv
|
let req = srv
|
||||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||||
|
@ -1,6 +1,11 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
|
## 0.4.0 - 2022-02-25
|
||||||
|
- No significant changes since `0.4.0-beta.13`.
|
||||||
|
|
||||||
|
|
||||||
## 0.4.0-beta.13 - 2022-01-31
|
## 0.4.0-beta.13 - 2022-01-31
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-multipart"
|
name = "actix-multipart"
|
||||||
version = "0.4.0-beta.13"
|
version = "0.4.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Multipart form support for Actix Web"
|
description = "Multipart form support for Actix Web"
|
||||||
keywords = ["http", "web", "framework", "async", "futures"]
|
keywords = ["http", "web", "framework", "async", "futures"]
|
||||||
@ -14,8 +14,8 @@ name = "actix_multipart"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
derive_more = "0.99.5"
|
derive_more = "0.99.5"
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> Multipart form support for Actix Web.
|
> Multipart form support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://docs.rs/actix-multipart/0.4.0-beta.13)
|
[](https://docs.rs/actix-multipart/0.4.0)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.13)
|
[](https://deps.rs/crate/actix-multipart/0.4.0)
|
||||||
[](https://crates.io/crates/actix-multipart)
|
[](https://crates.io/crates/actix-multipart)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 0.5.0 - 2022-02-22
|
## 0.5.0 - 2022-02-22
|
||||||
|
@ -21,18 +21,21 @@ default = ["http"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
bytestring = ">=0.1.5, <2"
|
bytestring = ">=0.1.5, <2"
|
||||||
firestorm = "0.5"
|
http = { version = "0.2.5", optional = true }
|
||||||
http = { version = "0.2.3", optional = true }
|
|
||||||
log = "0.4"
|
|
||||||
regex = "1.5"
|
regex = "1.5"
|
||||||
serde = "1"
|
serde = "1"
|
||||||
|
tracing = { version = "0.1.30", default-features = false, features = ["log"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
firestorm = { version = "0.5", features = ["enable_system_time"] }
|
|
||||||
http = "0.2.5"
|
http = "0.2.5"
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
|
percent-encoding = "2.1"
|
||||||
|
|
||||||
[[bench]]
|
[[bench]]
|
||||||
name = "router"
|
name = "router"
|
||||||
harness = false
|
harness = false
|
||||||
|
|
||||||
|
[[bench]]
|
||||||
|
name = "quoter"
|
||||||
|
harness = false
|
||||||
|
52
actix-router/benches/quoter.rs
Normal file
52
actix-router/benches/quoter.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use criterion::{black_box, criterion_group, criterion_main, Criterion};
|
||||||
|
|
||||||
|
use std::borrow::Cow;
|
||||||
|
|
||||||
|
fn compare_quoters(c: &mut Criterion) {
|
||||||
|
let mut group = c.benchmark_group("Compare Quoters");
|
||||||
|
|
||||||
|
let quoter = actix_router::Quoter::new(b"", b"");
|
||||||
|
let path_quoted = (0..=0x7f)
|
||||||
|
.map(|c| format!("%{:02X}", c))
|
||||||
|
.collect::<String>();
|
||||||
|
let path_unquoted = ('\u{00}'..='\u{7f}').collect::<String>();
|
||||||
|
|
||||||
|
group.bench_function("quoter_unquoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
black_box(quoter.requote(path_unquoted.as_bytes()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("percent_encode_unquoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
let decode = percent_encoding::percent_decode(path_unquoted.as_bytes());
|
||||||
|
black_box(Into::<Cow<'_, [u8]>>::into(decode));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("quoter_quoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
black_box(quoter.requote(path_quoted.as_bytes()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.bench_function("percent_encode_quoted", |b| {
|
||||||
|
b.iter(|| {
|
||||||
|
for _ in 0..10 {
|
||||||
|
let decode = percent_encoding::percent_decode(path_quoted.as_bytes());
|
||||||
|
black_box(Into::<Cow<'_, [u8]>>::into(decode));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
group.finish();
|
||||||
|
}
|
||||||
|
|
||||||
|
criterion_group!(benches, compare_quoters);
|
||||||
|
criterion_main!(benches);
|
@ -1,169 +0,0 @@
|
|||||||
macro_rules! register {
|
|
||||||
(brackets) => {{
|
|
||||||
register!(finish => "{p1}", "{p2}", "{p3}", "{p4}")
|
|
||||||
}};
|
|
||||||
(finish => $p1:literal, $p2:literal, $p3:literal, $p4:literal) => {{
|
|
||||||
let arr = [
|
|
||||||
concat!("/authorizations"),
|
|
||||||
concat!("/authorizations/", $p1),
|
|
||||||
concat!("/applications/", $p1, "/tokens/", $p2),
|
|
||||||
concat!("/events"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/events"),
|
|
||||||
concat!("/networks/", $p1, "/", $p2, "/events"),
|
|
||||||
concat!("/orgs/", $p1, "/events"),
|
|
||||||
concat!("/users/", $p1, "/received_events"),
|
|
||||||
concat!("/users/", $p1, "/received_events/public"),
|
|
||||||
concat!("/users/", $p1, "/events"),
|
|
||||||
concat!("/users/", $p1, "/events/public"),
|
|
||||||
concat!("/users/", $p1, "/events/orgs/", $p2),
|
|
||||||
concat!("/feeds"),
|
|
||||||
concat!("/notifications"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/notifications"),
|
|
||||||
concat!("/notifications/threads/", $p1),
|
|
||||||
concat!("/notifications/threads/", $p1, "/subscription"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/stargazers"),
|
|
||||||
concat!("/users/", $p1, "/starred"),
|
|
||||||
concat!("/user/starred"),
|
|
||||||
concat!("/user/starred/", $p1, "/", $p2),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/subscribers"),
|
|
||||||
concat!("/users/", $p1, "/subscriptions"),
|
|
||||||
concat!("/user/subscriptions"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/subscription"),
|
|
||||||
concat!("/user/subscriptions/", $p1, "/", $p2),
|
|
||||||
concat!("/users/", $p1, "/gists"),
|
|
||||||
concat!("/gists"),
|
|
||||||
concat!("/gists/", $p1),
|
|
||||||
concat!("/gists/", $p1, "/star"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/git/blobs/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/git/commits/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/git/refs"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/git/tags/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/git/trees/", $p3),
|
|
||||||
concat!("/issues"),
|
|
||||||
concat!("/user/issues"),
|
|
||||||
concat!("/orgs/", $p1, "/issues"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/issues"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/assignees"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/assignees/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/comments"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/events"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/labels"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/labels/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/issues/", $p3, "/labels"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3, "/labels"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/milestones/"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/milestones/", $p3),
|
|
||||||
concat!("/emojis"),
|
|
||||||
concat!("/gitignore/templates"),
|
|
||||||
concat!("/gitignore/templates/", $p1),
|
|
||||||
concat!("/meta"),
|
|
||||||
concat!("/rate_limit"),
|
|
||||||
concat!("/users/", $p1, "/orgs"),
|
|
||||||
concat!("/user/orgs"),
|
|
||||||
concat!("/orgs/", $p1),
|
|
||||||
concat!("/orgs/", $p1, "/members"),
|
|
||||||
concat!("/orgs/", $p1, "/members", $p2),
|
|
||||||
concat!("/orgs/", $p1, "/public_members"),
|
|
||||||
concat!("/orgs/", $p1, "/public_members/", $p2),
|
|
||||||
concat!("/orgs/", $p1, "/teams"),
|
|
||||||
concat!("/teams/", $p1),
|
|
||||||
concat!("/teams/", $p1, "/members"),
|
|
||||||
concat!("/teams/", $p1, "/members", $p2),
|
|
||||||
concat!("/teams/", $p1, "/repos"),
|
|
||||||
concat!("/teams/", $p1, "/repos/", $p2, "/", $p3),
|
|
||||||
concat!("/user/teams"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/pulls"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/commits"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/files"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/merge"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/pulls/", $p3, "/comments"),
|
|
||||||
concat!("/user/repos"),
|
|
||||||
concat!("/users/", $p1, "/repos"),
|
|
||||||
concat!("/orgs/", $p1, "/repos"),
|
|
||||||
concat!("/repositories"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/contributors"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/languages"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/teams"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/tags"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/branches"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/branches/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/collaborators"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/collaborators/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/comments"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/commits/", $p3, "/comments"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/commits"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/commits/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/readme"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/keys"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/keys", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/downloads"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/downloads", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/forks"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/hooks"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/hooks", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/releases"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/releases/", $p3),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/releases/", $p3, "/assets"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/stats/contributors"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/stats/commit_activity"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/stats/code_frequency"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/stats/participation"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/stats/punch_card"),
|
|
||||||
concat!("/repos/", $p1, "/", $p2, "/statuses/", $p3),
|
|
||||||
concat!("/search/repositories"),
|
|
||||||
concat!("/search/code"),
|
|
||||||
concat!("/search/issues"),
|
|
||||||
concat!("/search/users"),
|
|
||||||
concat!("/legacy/issues/search/", $p1, "/", $p2, "/", $p3, "/", $p4),
|
|
||||||
concat!("/legacy/repos/search/", $p1),
|
|
||||||
concat!("/legacy/user/search/", $p1),
|
|
||||||
concat!("/legacy/user/email/", $p1),
|
|
||||||
concat!("/users/", $p1),
|
|
||||||
concat!("/user"),
|
|
||||||
concat!("/users"),
|
|
||||||
concat!("/user/emails"),
|
|
||||||
concat!("/users/", $p1, "/followers"),
|
|
||||||
concat!("/user/followers"),
|
|
||||||
concat!("/users/", $p1, "/following"),
|
|
||||||
concat!("/user/following"),
|
|
||||||
concat!("/user/following/", $p1),
|
|
||||||
concat!("/users/", $p1, "/following", $p2),
|
|
||||||
concat!("/users/", $p1, "/keys"),
|
|
||||||
concat!("/user/keys"),
|
|
||||||
concat!("/user/keys/", $p1),
|
|
||||||
];
|
|
||||||
|
|
||||||
arr.to_vec()
|
|
||||||
}};
|
|
||||||
}
|
|
||||||
|
|
||||||
static PATHS: [&str; 5] = [
|
|
||||||
"/authorizations",
|
|
||||||
"/user/repos",
|
|
||||||
"/repos/rust-lang/rust/stargazers",
|
|
||||||
"/orgs/rust-lang/public_members/nikomatsakis",
|
|
||||||
"/repos/rust-lang/rust/releases/1.51.0",
|
|
||||||
];
|
|
||||||
|
|
||||||
fn main() {
|
|
||||||
let mut router = actix_router::Router::<bool>::build();
|
|
||||||
|
|
||||||
for route in register!(brackets) {
|
|
||||||
router.path(route, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
let actix = router.finish();
|
|
||||||
|
|
||||||
if firestorm::enabled() {
|
|
||||||
firestorm::bench("target", || {
|
|
||||||
for &route in &PATHS {
|
|
||||||
let mut path = actix_router::Path::new(route);
|
|
||||||
actix.recognize(&mut path).unwrap();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
.unwrap();
|
|
||||||
}
|
|
||||||
}
|
|
@ -7,7 +7,7 @@ use crate::path::{Path, PathIter};
|
|||||||
use crate::{Quoter, ResourcePath};
|
use crate::{Quoter, ResourcePath};
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b"");
|
static FULL_QUOTER: Quoter = Quoter::new(b"", b"");
|
||||||
}
|
}
|
||||||
|
|
||||||
macro_rules! unsupported_type {
|
macro_rules! unsupported_type {
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
use std::ops::{DerefMut, Index};
|
use std::ops::{DerefMut, Index};
|
||||||
|
|
||||||
use firestorm::profile_method;
|
|
||||||
use serde::de;
|
use serde::de;
|
||||||
|
|
||||||
use crate::{de::PathDeserializer, Resource, ResourcePath};
|
use crate::{de::PathDeserializer, Resource, ResourcePath};
|
||||||
@ -52,7 +51,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Returns full path as a string.
|
/// Returns full path as a string.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn as_str(&self) -> &str {
|
pub fn as_str(&self) -> &str {
|
||||||
profile_method!(as_str);
|
|
||||||
self.path.path()
|
self.path.path()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -61,7 +59,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Returns empty string if no more is to be processed.
|
/// Returns empty string if no more is to be processed.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn unprocessed(&self) -> &str {
|
pub fn unprocessed(&self) -> &str {
|
||||||
profile_method!(unprocessed);
|
|
||||||
// clamp skip to path length
|
// clamp skip to path length
|
||||||
let skip = (self.skip as usize).min(self.as_str().len());
|
let skip = (self.skip as usize).min(self.as_str().len());
|
||||||
&self.path.path()[skip..]
|
&self.path.path()[skip..]
|
||||||
@ -72,8 +69,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
|
#[deprecated(since = "0.6.0", note = "Use `.as_str()` or `.unprocessed()`.")]
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn path(&self) -> &str {
|
pub fn path(&self) -> &str {
|
||||||
profile_method!(path);
|
|
||||||
|
|
||||||
let skip = self.skip as usize;
|
let skip = self.skip as usize;
|
||||||
let path = self.path.path();
|
let path = self.path.path();
|
||||||
if skip <= path.len() {
|
if skip <= path.len() {
|
||||||
@ -86,8 +81,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Set new path.
|
/// Set new path.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn set(&mut self, path: T) {
|
pub fn set(&mut self, path: T) {
|
||||||
profile_method!(set);
|
|
||||||
|
|
||||||
self.skip = 0;
|
self.skip = 0;
|
||||||
self.path = path;
|
self.path = path;
|
||||||
self.segments.clear();
|
self.segments.clear();
|
||||||
@ -96,8 +89,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Reset state.
|
/// Reset state.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn reset(&mut self) {
|
pub fn reset(&mut self) {
|
||||||
profile_method!(reset);
|
|
||||||
|
|
||||||
self.skip = 0;
|
self.skip = 0;
|
||||||
self.segments.clear();
|
self.segments.clear();
|
||||||
}
|
}
|
||||||
@ -105,13 +96,10 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
/// Skip first `n` chars in path.
|
/// Skip first `n` chars in path.
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn skip(&mut self, n: u16) {
|
pub fn skip(&mut self, n: u16) {
|
||||||
profile_method!(skip);
|
|
||||||
self.skip += n;
|
self.skip += n;
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn add(&mut self, name: impl Into<Cow<'static, str>>, value: PathItem) {
|
pub(crate) fn add(&mut self, name: impl Into<Cow<'static, str>>, value: PathItem) {
|
||||||
profile_method!(add);
|
|
||||||
|
|
||||||
match value {
|
match value {
|
||||||
PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))),
|
PathItem::Static(s) => self.segments.push((name.into(), PathItem::Static(s))),
|
||||||
PathItem::Segment(begin, end) => self.segments.push((
|
PathItem::Segment(begin, end) => self.segments.push((
|
||||||
@ -127,8 +115,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
name: impl Into<Cow<'static, str>>,
|
name: impl Into<Cow<'static, str>>,
|
||||||
value: impl Into<Cow<'static, str>>,
|
value: impl Into<Cow<'static, str>>,
|
||||||
) {
|
) {
|
||||||
profile_method!(add_static);
|
|
||||||
|
|
||||||
self.segments
|
self.segments
|
||||||
.push((name.into(), PathItem::Static(value.into())));
|
.push((name.into(), PathItem::Static(value.into())));
|
||||||
}
|
}
|
||||||
@ -147,8 +133,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
|
|
||||||
/// Get matched parameter by name without type conversion
|
/// Get matched parameter by name without type conversion
|
||||||
pub fn get(&self, name: &str) -> Option<&str> {
|
pub fn get(&self, name: &str) -> Option<&str> {
|
||||||
profile_method!(get);
|
|
||||||
|
|
||||||
for (seg_name, val) in self.segments.iter() {
|
for (seg_name, val) in self.segments.iter() {
|
||||||
if name == seg_name {
|
if name == seg_name {
|
||||||
return match val {
|
return match val {
|
||||||
@ -167,8 +151,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
///
|
///
|
||||||
/// If keyed parameter is not available empty string is used as default value.
|
/// If keyed parameter is not available empty string is used as default value.
|
||||||
pub fn query(&self, key: &str) -> &str {
|
pub fn query(&self, key: &str) -> &str {
|
||||||
profile_method!(query);
|
|
||||||
|
|
||||||
if let Some(s) = self.get(key) {
|
if let Some(s) = self.get(key) {
|
||||||
s
|
s
|
||||||
} else {
|
} else {
|
||||||
@ -186,7 +168,6 @@ impl<T: ResourcePath> Path<T> {
|
|||||||
|
|
||||||
/// Try to deserialize matching parameters to a specified type `U`
|
/// Try to deserialize matching parameters to a specified type `U`
|
||||||
pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result<U, de::value::Error> {
|
pub fn load<'de, U: serde::Deserialize<'de>>(&'de self) -> Result<U, de::value::Error> {
|
||||||
profile_method!(load);
|
|
||||||
de::Deserialize::deserialize(PathDeserializer::new(self))
|
de::Deserialize::deserialize(PathDeserializer::new(self))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,132 +1,89 @@
|
|||||||
#[allow(dead_code)]
|
/// Partial percent-decoding.
|
||||||
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
///
|
||||||
|
/// Performs percent-decoding on a slice but can selectively skip decoding certain sequences.
|
||||||
#[allow(dead_code)]
|
///
|
||||||
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
/// # Examples
|
||||||
|
/// ```
|
||||||
#[allow(dead_code)]
|
/// # use actix_router::Quoter;
|
||||||
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
/// // + is set as a protected character and will not be decoded...
|
||||||
|
/// let q = Quoter::new(&[], b"+");
|
||||||
#[allow(dead_code)]
|
///
|
||||||
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
/// // ...but the other encoded characters (like the hyphen below) will.
|
||||||
|
/// assert_eq!(q.requote(b"/a%2Db%2Bc").unwrap(), b"/a-b%2Bc");
|
||||||
#[allow(dead_code)]
|
/// ```
|
||||||
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
|
||||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
||||||
1234567890
|
|
||||||
-._~";
|
|
||||||
|
|
||||||
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
|
||||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
|
||||||
1234567890
|
|
||||||
-._~
|
|
||||||
!$'()*,";
|
|
||||||
|
|
||||||
const QS: &[u8] = b"+&=;b";
|
|
||||||
|
|
||||||
/// A quoter
|
|
||||||
pub struct Quoter {
|
pub struct Quoter {
|
||||||
/// Simple bit-map of safe values in the 0-127 ASCII range.
|
|
||||||
safe_table: [u8; 16],
|
|
||||||
|
|
||||||
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
/// Simple bit-map of protected values in the 0-127 ASCII range.
|
||||||
protected_table: [u8; 16],
|
protected_table: AsciiBitmap,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Quoter {
|
impl Quoter {
|
||||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
/// Constructs a new `Quoter` instance given a set of protected ASCII bytes.
|
||||||
let mut quoter = Quoter {
|
///
|
||||||
safe_table: [0; 16],
|
/// The first argument is ignored but is kept for backward compatibility.
|
||||||
protected_table: [0; 16],
|
///
|
||||||
};
|
/// # Panics
|
||||||
|
/// Panics if any of the `protected` bytes are not in the 0-127 ASCII range.
|
||||||
// prepare safe table
|
pub fn new(_: &[u8], protected: &[u8]) -> Quoter {
|
||||||
for ch in 0..128 {
|
let mut protected_table = AsciiBitmap::default();
|
||||||
if ALLOWED.contains(&ch) {
|
|
||||||
set_bit(&mut quoter.safe_table, ch);
|
|
||||||
}
|
|
||||||
|
|
||||||
if QS.contains(&ch) {
|
|
||||||
set_bit(&mut quoter.safe_table, ch);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
for &ch in safe {
|
|
||||||
set_bit(&mut quoter.safe_table, ch)
|
|
||||||
}
|
|
||||||
|
|
||||||
// prepare protected table
|
// prepare protected table
|
||||||
for &ch in protected {
|
for &ch in protected {
|
||||||
set_bit(&mut quoter.safe_table, ch);
|
protected_table.set_bit(ch);
|
||||||
set_bit(&mut quoter.protected_table, ch);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
quoter
|
Quoter { protected_table }
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Decodes safe percent-encoded sequences from `val`.
|
/// Decodes the next escape sequence, if any, and advances `val`.
|
||||||
///
|
#[inline(always)]
|
||||||
/// Returns `None` when no modification to the original byte string was required.
|
fn decode_next<'a>(&self, val: &mut &'a [u8]) -> Option<(&'a [u8], u8)> {
|
||||||
///
|
for i in 0..val.len() {
|
||||||
/// Non-ASCII bytes are accepted as valid input.
|
if let (prev, [b'%', p1, p2, rem @ ..]) = val.split_at(i) {
|
||||||
///
|
if let Some(ch) = hex_pair_to_char(*p1, *p2)
|
||||||
/// Behavior for invalid/incomplete percent-encoding sequences is unspecified and may include
|
// ignore protected ascii bytes
|
||||||
/// removing the invalid sequence from the output or passing it as-is.
|
.filter(|&ch| !(ch < 128 && self.protected_table.bit_at(ch)))
|
||||||
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
{
|
||||||
let mut has_pct = 0;
|
*val = rem;
|
||||||
let mut pct = [b'%', 0, 0];
|
return Some((prev, ch));
|
||||||
let mut idx = 0;
|
|
||||||
let mut cloned: Option<Vec<u8>> = None;
|
|
||||||
|
|
||||||
let len = val.len();
|
|
||||||
|
|
||||||
while idx < len {
|
|
||||||
let ch = val[idx];
|
|
||||||
|
|
||||||
if has_pct != 0 {
|
|
||||||
pct[has_pct] = val[idx];
|
|
||||||
has_pct += 1;
|
|
||||||
|
|
||||||
if has_pct == 3 {
|
|
||||||
has_pct = 0;
|
|
||||||
let buf = cloned.as_mut().unwrap();
|
|
||||||
|
|
||||||
if let Some(ch) = hex_pair_to_char(pct[1], pct[2]) {
|
|
||||||
if ch < 128 {
|
|
||||||
if bit_at(&self.protected_table, ch) {
|
|
||||||
buf.extend_from_slice(&pct);
|
|
||||||
idx += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if bit_at(&self.safe_table, ch) {
|
|
||||||
buf.push(ch);
|
|
||||||
idx += 1;
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
buf.push(ch);
|
|
||||||
} else {
|
|
||||||
buf.extend_from_slice(&pct[..]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
} else if ch == b'%' {
|
|
||||||
has_pct = 1;
|
|
||||||
|
|
||||||
if cloned.is_none() {
|
|
||||||
let mut c = Vec::with_capacity(len);
|
|
||||||
c.extend_from_slice(&val[..idx]);
|
|
||||||
cloned = Some(c);
|
|
||||||
}
|
|
||||||
} else if let Some(ref mut cloned) = cloned {
|
|
||||||
cloned.push(ch)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
idx += 1;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cloned
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Partially percent-decodes the given bytes.
|
||||||
|
///
|
||||||
|
/// Escape sequences of the protected set are *not* decoded.
|
||||||
|
///
|
||||||
|
/// Returns `None` when no modification to the original bytes was required.
|
||||||
|
///
|
||||||
|
/// Invalid/incomplete percent-encoding sequences are passed unmodified.
|
||||||
|
pub fn requote(&self, val: &[u8]) -> Option<Vec<u8>> {
|
||||||
|
let mut remaining = val;
|
||||||
|
|
||||||
|
// early return indicates that no percent-encoded sequences exist and we can skip allocation
|
||||||
|
let (pre, decoded_char) = self.decode_next(&mut remaining)?;
|
||||||
|
|
||||||
|
// decoded output will always be shorter than the input
|
||||||
|
let mut decoded = Vec::<u8>::with_capacity(val.len());
|
||||||
|
|
||||||
|
// push first segment and decoded char
|
||||||
|
decoded.extend_from_slice(pre);
|
||||||
|
decoded.push(decoded_char);
|
||||||
|
|
||||||
|
// decode and push rest of segments and decoded chars
|
||||||
|
while let Some((prev, ch)) = self.decode_next(&mut remaining) {
|
||||||
|
// this ugly conditional achieves +50% perf in cases where this is a tight loop.
|
||||||
|
if !prev.is_empty() {
|
||||||
|
decoded.extend_from_slice(prev);
|
||||||
|
}
|
||||||
|
decoded.push(ch);
|
||||||
|
}
|
||||||
|
|
||||||
|
decoded.extend_from_slice(remaining);
|
||||||
|
|
||||||
|
Some(decoded)
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
pub(crate) fn requote_str_lossy(&self, val: &str) -> Option<String> {
|
||||||
@ -135,24 +92,6 @@ impl Quoter {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Converts an ASCII character in the hex-encoded set (`0-9`, `A-F`, `a-f`) to its integer
|
|
||||||
/// representation from `0x0`–`0xF`.
|
|
||||||
///
|
|
||||||
/// - `0x30 ('0') => 0x0`
|
|
||||||
/// - `0x39 ('9') => 0x9`
|
|
||||||
/// - `0x41 ('a') => 0xA`
|
|
||||||
/// - `0x61 ('A') => 0xA`
|
|
||||||
/// - `0x46 ('f') => 0xF`
|
|
||||||
/// - `0x66 ('F') => 0xF`
|
|
||||||
fn from_ascii_hex(v: u8) -> Option<u8> {
|
|
||||||
match v {
|
|
||||||
b'0'..=b'9' => Some(v - 0x30), // ord('0') == 0x30
|
|
||||||
b'A'..=b'F' => Some(v - 0x41 + 10), // ord('A') == 0x41
|
|
||||||
b'a'..=b'f' => Some(v - 0x61 + 10), // ord('a') == 0x61
|
|
||||||
_ => None,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Decode a ASCII hex-encoded pair to an integer.
|
/// Decode a ASCII hex-encoded pair to an integer.
|
||||||
///
|
///
|
||||||
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
||||||
@ -160,64 +99,52 @@ fn from_ascii_hex(v: u8) -> Option<u8> {
|
|||||||
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
||||||
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
||||||
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
||||||
|
#[inline(always)]
|
||||||
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
||||||
let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?);
|
let d_high = char::from(d1).to_digit(16)?;
|
||||||
|
let d_low = char::from(d2).to_digit(16)?;
|
||||||
|
|
||||||
// left shift high nibble by 4 bits
|
// left shift high nibble by 4 bits
|
||||||
Some(d_high << 4 | d_low)
|
Some((d_high as u8) << 4 | (d_low as u8))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sets bit in given bit-map to 1=true.
|
#[derive(Debug, Default, Clone)]
|
||||||
///
|
struct AsciiBitmap {
|
||||||
/// # Panics
|
array: [u8; 16],
|
||||||
/// Panics if `ch` index is out of bounds.
|
|
||||||
fn set_bit(array: &mut [u8], ch: u8) {
|
|
||||||
array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if bit to true in given bit-map.
|
impl AsciiBitmap {
|
||||||
///
|
/// Sets bit in given bit-map to 1=true.
|
||||||
/// # Panics
|
///
|
||||||
/// Panics if `ch` index is out of bounds.
|
/// # Panics
|
||||||
fn bit_at(array: &[u8], ch: u8) -> bool {
|
/// Panics if `ch` index is out of bounds.
|
||||||
array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
fn set_bit(&mut self, ch: u8) {
|
||||||
|
self.array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if bit to true in given bit-map.
|
||||||
|
///
|
||||||
|
/// # Panics
|
||||||
|
/// Panics if `ch` index is out of bounds.
|
||||||
|
fn bit_at(&self, ch: u8) -> bool {
|
||||||
|
self.array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn hex_encoding() {
|
|
||||||
let hex = b"0123456789abcdefABCDEF";
|
|
||||||
|
|
||||||
for i in 0..256 {
|
|
||||||
let c = i as u8;
|
|
||||||
if hex.contains(&c) {
|
|
||||||
assert!(from_ascii_hex(c).is_some())
|
|
||||||
} else {
|
|
||||||
assert!(from_ascii_hex(c).is_none())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
let expected = [
|
|
||||||
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 10, 11, 12, 13, 14, 15,
|
|
||||||
];
|
|
||||||
for i in 0..hex.len() {
|
|
||||||
assert_eq!(from_ascii_hex(hex[i]).unwrap(), expected[i]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn custom_quoter() {
|
fn custom_quoter() {
|
||||||
let q = Quoter::new(b"", b"+");
|
let q = Quoter::new(b"", b"+");
|
||||||
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
assert_eq!(q.requote(b"/a%25c").unwrap(), b"/a%c");
|
||||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), b"/a%2Bc");
|
assert_eq!(q.requote(b"/a%2Bc"), None);
|
||||||
|
|
||||||
let q = Quoter::new(b"%+", b"/");
|
let q = Quoter::new(b"%+", b"/");
|
||||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), b"/a%b+c");
|
||||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), b"/a%2fb");
|
assert_eq!(q.requote(b"/a%2fb"), None);
|
||||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), b"/a%2Fb");
|
assert_eq!(q.requote(b"/a%2Fb"), None);
|
||||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
assert_eq!(q.requote(b"/a%0Ab").unwrap(), b"/a\nb");
|
||||||
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
assert_eq!(q.requote(b"/a%FE\xffb").unwrap(), b"/a\xfe\xffb");
|
||||||
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
assert_eq!(q.requote(b"/a\xfe\xffb"), None);
|
||||||
@ -233,7 +160,8 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn invalid_sequences() {
|
fn invalid_sequences() {
|
||||||
let q = Quoter::new(b"%+", b"/");
|
let q = Quoter::new(b"%+", b"/");
|
||||||
assert_eq!(q.requote(b"/a%2x%2X%%").unwrap(), b"/a%2x%2X");
|
assert_eq!(q.requote(b"/a%2x%2X%%"), None);
|
||||||
|
assert_eq!(q.requote(b"/a%20%2X%%").unwrap(), b"/a %2X%%");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
@ -5,8 +5,8 @@ use std::{
|
|||||||
mem,
|
mem,
|
||||||
};
|
};
|
||||||
|
|
||||||
use firestorm::{profile_fn, profile_method, profile_section};
|
|
||||||
use regex::{escape, Regex, RegexSet};
|
use regex::{escape, Regex, RegexSet};
|
||||||
|
use tracing::error;
|
||||||
|
|
||||||
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
use crate::{path::PathItem, IntoPatterns, Patterns, Resource, ResourcePath};
|
||||||
|
|
||||||
@ -271,7 +271,6 @@ impl ResourceDef {
|
|||||||
/// assert!(!resource.is_match("/foo"));
|
/// assert!(!resource.is_match("/foo"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
pub fn new<T: IntoPatterns>(paths: T) -> Self {
|
||||||
profile_method!(new);
|
|
||||||
Self::construct(paths, false)
|
Self::construct(paths, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -299,7 +298,6 @@ impl ResourceDef {
|
|||||||
/// assert!(!resource.is_match("/foo"));
|
/// assert!(!resource.is_match("/foo"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
|
||||||
profile_method!(prefix);
|
|
||||||
ResourceDef::construct(paths, true)
|
ResourceDef::construct(paths, true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -324,7 +322,6 @@ impl ResourceDef {
|
|||||||
/// assert!(!resource.is_match("user/123"));
|
/// assert!(!resource.is_match("user/123"));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn root_prefix(path: &str) -> Self {
|
pub fn root_prefix(path: &str) -> Self {
|
||||||
profile_method!(root_prefix);
|
|
||||||
ResourceDef::prefix(insert_slash(path).into_owned())
|
ResourceDef::prefix(insert_slash(path).into_owned())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -548,8 +545,6 @@ impl ResourceDef {
|
|||||||
/// ```
|
/// ```
|
||||||
#[inline]
|
#[inline]
|
||||||
pub fn is_match(&self, path: &str) -> bool {
|
pub fn is_match(&self, path: &str) -> bool {
|
||||||
profile_method!(is_match);
|
|
||||||
|
|
||||||
// this function could be expressed as:
|
// this function could be expressed as:
|
||||||
// `self.find_match(path).is_some()`
|
// `self.find_match(path).is_some()`
|
||||||
// but this skips some checks and uses potentially faster regex methods
|
// but this skips some checks and uses potentially faster regex methods
|
||||||
@ -597,8 +592,6 @@ impl ResourceDef {
|
|||||||
/// assert_eq!(resource.find_match("/profile/1234"), Some(13));
|
/// assert_eq!(resource.find_match("/profile/1234"), Some(13));
|
||||||
/// ```
|
/// ```
|
||||||
pub fn find_match(&self, path: &str) -> Option<usize> {
|
pub fn find_match(&self, path: &str) -> Option<usize> {
|
||||||
profile_method!(find_match);
|
|
||||||
|
|
||||||
match &self.pat_type {
|
match &self.pat_type {
|
||||||
PatternType::Static(pattern) => self.static_match(pattern, path),
|
PatternType::Static(pattern) => self.static_match(pattern, path),
|
||||||
|
|
||||||
@ -633,7 +626,6 @@ impl ResourceDef {
|
|||||||
/// assert_eq!(path.unprocessed(), "");
|
/// assert_eq!(path.unprocessed(), "");
|
||||||
/// ```
|
/// ```
|
||||||
pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
|
pub fn capture_match_info<R: Resource>(&self, resource: &mut R) -> bool {
|
||||||
profile_method!(capture_match_info);
|
|
||||||
self.capture_match_info_fn(resource, |_| true)
|
self.capture_match_info_fn(resource, |_| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -657,7 +649,7 @@ impl ResourceDef {
|
|||||||
/// resource.capture_match_info_fn(
|
/// resource.capture_match_info_fn(
|
||||||
/// path,
|
/// path,
|
||||||
/// // when env var is not set, reject when path contains "admin"
|
/// // when env var is not set, reject when path contains "admin"
|
||||||
/// |res| !(!admin_allowed && res.path().contains("admin")),
|
/// |path| !(!admin_allowed && path.as_str().contains("admin")),
|
||||||
/// )
|
/// )
|
||||||
/// }
|
/// }
|
||||||
///
|
///
|
||||||
@ -679,56 +671,35 @@ impl ResourceDef {
|
|||||||
R: Resource,
|
R: Resource,
|
||||||
F: FnOnce(&R) -> bool,
|
F: FnOnce(&R) -> bool,
|
||||||
{
|
{
|
||||||
profile_method!(capture_match_info_fn);
|
|
||||||
|
|
||||||
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
|
let mut segments = <[PathItem; MAX_DYNAMIC_SEGMENTS]>::default();
|
||||||
let path = resource.resource_path();
|
let path = resource.resource_path();
|
||||||
let path_str = path.unprocessed();
|
let path_str = path.unprocessed();
|
||||||
|
|
||||||
let (matched_len, matched_vars) = match &self.pat_type {
|
let (matched_len, matched_vars) = match &self.pat_type {
|
||||||
PatternType::Static(pattern) => {
|
PatternType::Static(pattern) => match self.static_match(pattern, path_str) {
|
||||||
profile_section!(pattern_static_or_prefix);
|
Some(len) => (len, None),
|
||||||
|
None => return false,
|
||||||
match self.static_match(pattern, path_str) {
|
},
|
||||||
Some(len) => (len, None),
|
|
||||||
None => return false,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
PatternType::Dynamic(re, names) => {
|
PatternType::Dynamic(re, names) => {
|
||||||
profile_section!(pattern_dynamic);
|
let captures = match re.captures(path.unprocessed()) {
|
||||||
|
Some(captures) => captures,
|
||||||
let captures = {
|
_ => return false,
|
||||||
profile_section!(pattern_dynamic_regex_exec);
|
|
||||||
|
|
||||||
match re.captures(path.unprocessed()) {
|
|
||||||
Some(captures) => captures,
|
|
||||||
_ => return false,
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
{
|
for (no, name) in names.iter().enumerate() {
|
||||||
profile_section!(pattern_dynamic_extract_captures);
|
if let Some(m) = captures.name(name) {
|
||||||
|
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
||||||
for (no, name) in names.iter().enumerate() {
|
} else {
|
||||||
if let Some(m) = captures.name(name) {
|
error!("Dynamic path match but not all segments found: {}", name);
|
||||||
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
return false;
|
||||||
} else {
|
|
||||||
log::error!(
|
|
||||||
"Dynamic path match but not all segments found: {}",
|
|
||||||
name
|
|
||||||
);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
}
|
||||||
|
|
||||||
(captures[1].len(), Some(names))
|
(captures[1].len(), Some(names))
|
||||||
}
|
}
|
||||||
|
|
||||||
PatternType::DynamicSet(re, params) => {
|
PatternType::DynamicSet(re, params) => {
|
||||||
profile_section!(pattern_dynamic_set);
|
|
||||||
|
|
||||||
let path = path.unprocessed();
|
let path = path.unprocessed();
|
||||||
let (pattern, names) = match re.matches(path).into_iter().next() {
|
let (pattern, names) = match re.matches(path).into_iter().next() {
|
||||||
Some(idx) => ¶ms[idx],
|
Some(idx) => ¶ms[idx],
|
||||||
@ -744,7 +715,7 @@ impl ResourceDef {
|
|||||||
if let Some(m) = captures.name(name) {
|
if let Some(m) = captures.name(name) {
|
||||||
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
|
||||||
} else {
|
} else {
|
||||||
log::error!("Dynamic path match but not all segments found: {}", name);
|
error!("Dynamic path match but not all segments found: {}", name);
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -811,7 +782,6 @@ impl ResourceDef {
|
|||||||
I: IntoIterator,
|
I: IntoIterator,
|
||||||
I::Item: AsRef<str>,
|
I::Item: AsRef<str>,
|
||||||
{
|
{
|
||||||
profile_method!(resource_path_from_iter);
|
|
||||||
let mut iter = values.into_iter();
|
let mut iter = values.into_iter();
|
||||||
self.build_resource_path(path, |_| iter.next())
|
self.build_resource_path(path, |_| iter.next())
|
||||||
}
|
}
|
||||||
@ -847,7 +817,6 @@ impl ResourceDef {
|
|||||||
V: AsRef<str>,
|
V: AsRef<str>,
|
||||||
S: BuildHasher,
|
S: BuildHasher,
|
||||||
{
|
{
|
||||||
profile_method!(resource_path_from_map);
|
|
||||||
self.build_resource_path(path, |name| values.get(name))
|
self.build_resource_path(path, |name| values.get(name))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -868,8 +837,6 @@ impl ResourceDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
fn construct<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
|
||||||
profile_method!(construct);
|
|
||||||
|
|
||||||
let patterns = paths.patterns();
|
let patterns = paths.patterns();
|
||||||
let (pat_type, segments) = match &patterns {
|
let (pat_type, segments) = match &patterns {
|
||||||
Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
|
Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
|
||||||
@ -928,8 +895,6 @@ impl ResourceDef {
|
|||||||
/// # Panics
|
/// # Panics
|
||||||
/// Panics if given patterns does not contain a dynamic segment.
|
/// Panics if given patterns does not contain a dynamic segment.
|
||||||
fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) {
|
fn parse_param(pattern: &str) -> (PatternSegment, String, &str, bool) {
|
||||||
profile_method!(parse_param);
|
|
||||||
|
|
||||||
const DEFAULT_PATTERN: &str = "[^/]+";
|
const DEFAULT_PATTERN: &str = "[^/]+";
|
||||||
const DEFAULT_PATTERN_TAIL: &str = ".*";
|
const DEFAULT_PATTERN_TAIL: &str = ".*";
|
||||||
|
|
||||||
@ -999,8 +964,6 @@ impl ResourceDef {
|
|||||||
is_prefix: bool,
|
is_prefix: bool,
|
||||||
force_dynamic: bool,
|
force_dynamic: bool,
|
||||||
) -> (PatternType, Vec<PatternSegment>) {
|
) -> (PatternType, Vec<PatternSegment>) {
|
||||||
profile_method!(parse);
|
|
||||||
|
|
||||||
if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') {
|
if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') {
|
||||||
// pattern is static
|
// pattern is static
|
||||||
return (
|
return (
|
||||||
@ -1038,7 +1001,7 @@ impl ResourceDef {
|
|||||||
// tail segments in prefixes have no defined semantics
|
// tail segments in prefixes have no defined semantics
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Prefix resources should not have tail segments. \
|
"Prefix resources should not have tail segments. \
|
||||||
Use `ResourceDef::new` constructor. \
|
Use `ResourceDef::new` constructor. \
|
||||||
This may become a panic in the future."
|
This may become a panic in the future."
|
||||||
@ -1053,7 +1016,7 @@ impl ResourceDef {
|
|||||||
// unnamed tail segment
|
// unnamed tail segment
|
||||||
|
|
||||||
#[cfg(not(test))]
|
#[cfg(not(test))]
|
||||||
log::warn!(
|
tracing::warn!(
|
||||||
"Tail segments must have names. \
|
"Tail segments must have names. \
|
||||||
Consider `.../{{tail}}*`. \
|
Consider `.../{{tail}}*`. \
|
||||||
This may become a panic in the future."
|
This may become a panic in the future."
|
||||||
@ -1133,8 +1096,6 @@ impl From<String> for ResourceDef {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
|
pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
|
||||||
profile_fn!(insert_slash);
|
|
||||||
|
|
||||||
if !path.is_empty() && !path.starts_with('/') {
|
if !path.is_empty() && !path.starts_with('/') {
|
||||||
let mut new_path = String::with_capacity(path.len() + 1);
|
let mut new_path = String::with_capacity(path.len() + 1);
|
||||||
new_path.push('/');
|
new_path.push('/');
|
||||||
|
@ -27,7 +27,7 @@ impl<'a> ResourcePath for &'a str {
|
|||||||
|
|
||||||
impl ResourcePath for bytestring::ByteString {
|
impl ResourcePath for bytestring::ByteString {
|
||||||
fn path(&self) -> &str {
|
fn path(&self) -> &str {
|
||||||
&*self
|
self
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -1,8 +1,6 @@
|
|||||||
use firestorm::profile_method;
|
|
||||||
|
|
||||||
use crate::{IntoPatterns, Resource, ResourceDef};
|
use crate::{IntoPatterns, Resource, ResourceDef};
|
||||||
|
|
||||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||||
pub struct ResourceId(pub u16);
|
pub struct ResourceId(pub u16);
|
||||||
|
|
||||||
/// Resource router.
|
/// Resource router.
|
||||||
@ -30,7 +28,6 @@ impl<T, U> Router<T, U> {
|
|||||||
where
|
where
|
||||||
R: Resource,
|
R: Resource,
|
||||||
{
|
{
|
||||||
profile_method!(recognize);
|
|
||||||
self.recognize_fn(resource, |_, _| true)
|
self.recognize_fn(resource, |_, _| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -39,7 +36,6 @@ impl<T, U> Router<T, U> {
|
|||||||
where
|
where
|
||||||
R: Resource,
|
R: Resource,
|
||||||
{
|
{
|
||||||
profile_method!(recognize_mut);
|
|
||||||
self.recognize_mut_fn(resource, |_, _| true)
|
self.recognize_mut_fn(resource, |_, _| true)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -55,8 +51,6 @@ impl<T, U> Router<T, U> {
|
|||||||
R: Resource,
|
R: Resource,
|
||||||
F: FnMut(&R, &U) -> bool,
|
F: FnMut(&R, &U) -> bool,
|
||||||
{
|
{
|
||||||
profile_method!(recognize_checked);
|
|
||||||
|
|
||||||
for (rdef, val, ctx) in self.routes.iter() {
|
for (rdef, val, ctx) in self.routes.iter() {
|
||||||
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
||||||
return Some((val, ResourceId(rdef.id())));
|
return Some((val, ResourceId(rdef.id())));
|
||||||
@ -77,8 +71,6 @@ impl<T, U> Router<T, U> {
|
|||||||
R: Resource,
|
R: Resource,
|
||||||
F: FnMut(&R, &U) -> bool,
|
F: FnMut(&R, &U) -> bool,
|
||||||
{
|
{
|
||||||
profile_method!(recognize_mut_checked);
|
|
||||||
|
|
||||||
for (rdef, val, ctx) in self.routes.iter_mut() {
|
for (rdef, val, ctx) in self.routes.iter_mut() {
|
||||||
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
if rdef.capture_match_info_fn(resource, |res| check(res, ctx)) {
|
||||||
return Some((val, ResourceId(rdef.id())));
|
return Some((val, ResourceId(rdef.id())));
|
||||||
@ -104,7 +96,6 @@ impl<T, U> RouterBuilder<T, U> {
|
|||||||
val: T,
|
val: T,
|
||||||
ctx: U,
|
ctx: U,
|
||||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
profile_method!(push);
|
|
||||||
self.routes.push((rdef, val, ctx));
|
self.routes.push((rdef, val, ctx));
|
||||||
self.routes
|
self.routes
|
||||||
.last_mut()
|
.last_mut()
|
||||||
@ -131,7 +122,6 @@ where
|
|||||||
path: impl IntoPatterns,
|
path: impl IntoPatterns,
|
||||||
val: T,
|
val: T,
|
||||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
profile_method!(path);
|
|
||||||
self.push(ResourceDef::new(path), val, U::default())
|
self.push(ResourceDef::new(path), val, U::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -141,13 +131,11 @@ where
|
|||||||
prefix: impl IntoPatterns,
|
prefix: impl IntoPatterns,
|
||||||
val: T,
|
val: T,
|
||||||
) -> (&mut ResourceDef, &mut T, &mut U) {
|
) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
profile_method!(prefix);
|
|
||||||
self.push(ResourceDef::prefix(prefix), val, U::default())
|
self.push(ResourceDef::prefix(prefix), val, U::default())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Registers resource for [`ResourceDef`].
|
/// Registers resource for [`ResourceDef`].
|
||||||
pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) {
|
pub fn rdef(&mut self, rdef: ResourceDef, val: T) -> (&mut ResourceDef, &mut T, &mut U) {
|
||||||
profile_method!(rdef);
|
|
||||||
self.push(rdef, val, U::default())
|
self.push(rdef, val, U::default())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -3,7 +3,7 @@ use crate::ResourcePath;
|
|||||||
use crate::Quoter;
|
use crate::Quoter;
|
||||||
|
|
||||||
thread_local! {
|
thread_local! {
|
||||||
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
|
static DEFAULT_QUOTER: Quoter = Quoter::new(b"", b"%/+");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Default)]
|
#[derive(Debug, Clone, Default)]
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
## 0.1.0-beta.13 - 2022-02-16
|
## 0.1.0-beta.13 - 2022-02-16
|
||||||
|
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
actix-http-test = "3.0.0-beta.13"
|
actix-http-test = "3.0.0-beta.13"
|
||||||
actix-rt = "2.1"
|
actix-rt = "2.1"
|
||||||
actix-service = "2.0.0"
|
actix-service = "2"
|
||||||
actix-utils = "3.0.0"
|
actix-utils = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false, features = ["cookies"] }
|
actix-web = { version = "4", default-features = false, features = ["cookies"] }
|
||||||
awc = { version = "3.0.0-beta.21", default-features = false, features = ["cookies"] }
|
awc = { version = "3", default-features = false, features = ["cookies"] }
|
||||||
|
|
||||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
|
||||||
|
## 4.1.0 - 2022-03-02
|
||||||
|
- Add support for `actix` version `0.13`. [#2675]
|
||||||
|
|
||||||
|
[#2675]: https://github.com/actix/actix-web/pull/2675
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.0 - 2022-02-25
|
||||||
|
- No significant changes since `4.0.0-beta.12`.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0-beta.12 - 2022-02-16
|
## 4.0.0-beta.12 - 2022-02-16
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web-actors"
|
name = "actix-web-actors"
|
||||||
version = "4.0.0-beta.12"
|
version = "4.1.0"
|
||||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||||
description = "Actix actors support for Actix Web"
|
description = "Actix actors support for Actix Web"
|
||||||
keywords = ["actix", "http", "web", "framework", "async"]
|
keywords = ["actix", "http", "web", "framework", "async"]
|
||||||
@ -14,21 +14,24 @@ name = "actix_web_actors"
|
|||||||
path = "src/lib.rs"
|
path = "src/lib.rs"
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix = { version = "0.12.0", default-features = false }
|
actix = { version = ">=0.12, <0.14", default-features = false }
|
||||||
actix-codec = "0.5"
|
actix-codec = "0.5"
|
||||||
actix-http = "3.0.0"
|
actix-http = "3"
|
||||||
actix-web = { version = "4.0.0", default-features = false }
|
actix-web = { version = "4", default-features = false }
|
||||||
|
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
bytestring = "1"
|
bytestring = "1"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
pin-project-lite = "0.2"
|
pin-project-lite = "0.2"
|
||||||
tokio = { version = "1.8.4", features = ["sync"] }
|
tokio = { version = "1.13.1", features = ["sync"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-rt = "2.2"
|
actix-rt = "2.2"
|
||||||
actix-test = "0.1.0-beta.13"
|
actix-test = "0.1.0-beta.13"
|
||||||
awc = { version = "3.0.0-beta.21", default-features = false }
|
awc = { version = "3", default-features = false }
|
||||||
|
actix-web = { version = "4", features = ["macros"] }
|
||||||
|
|
||||||
|
mime = "0.3"
|
||||||
|
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> Actix actors support for Actix Web.
|
> Actix actors support for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://docs.rs/actix-web-actors/4.0.0-beta.12)
|
[](https://docs.rs/actix-web-actors/4.1.0)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.12)
|
[](https://deps.rs/crate/actix-web-actors/4.1.0)
|
||||||
[](https://crates.io/crates/actix-web-actors)
|
[](https://crates.io/crates/actix-web-actors)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@ -14,6 +14,58 @@ use futures_core::Stream;
|
|||||||
use tokio::sync::oneshot::Sender;
|
use tokio::sync::oneshot::Sender;
|
||||||
|
|
||||||
/// Execution context for HTTP actors
|
/// Execution context for HTTP actors
|
||||||
|
///
|
||||||
|
/// # Example
|
||||||
|
///
|
||||||
|
/// A demonstration of [server-sent events](https://developer.mozilla.org/docs/Web/API/Server-sent_events) using actors:
|
||||||
|
///
|
||||||
|
/// ```no_run
|
||||||
|
/// use std::time::Duration;
|
||||||
|
///
|
||||||
|
/// use actix::{Actor, AsyncContext};
|
||||||
|
/// use actix_web::{get, http::header, App, HttpResponse, HttpServer};
|
||||||
|
/// use actix_web_actors::HttpContext;
|
||||||
|
/// use bytes::Bytes;
|
||||||
|
///
|
||||||
|
/// struct MyActor {
|
||||||
|
/// count: usize,
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl Actor for MyActor {
|
||||||
|
/// type Context = HttpContext<Self>;
|
||||||
|
///
|
||||||
|
/// fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
|
/// ctx.run_later(Duration::from_millis(100), Self::write);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// impl MyActor {
|
||||||
|
/// fn write(&mut self, ctx: &mut HttpContext<Self>) {
|
||||||
|
/// self.count += 1;
|
||||||
|
/// if self.count > 3 {
|
||||||
|
/// ctx.write_eof()
|
||||||
|
/// } else {
|
||||||
|
/// ctx.write(Bytes::from(format!("event: count\ndata: {}\n\n", self.count)));
|
||||||
|
/// ctx.run_later(Duration::from_millis(100), Self::write);
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[get("/")]
|
||||||
|
/// async fn index() -> HttpResponse {
|
||||||
|
/// HttpResponse::Ok()
|
||||||
|
/// .insert_header(header::ContentType(mime::TEXT_EVENT_STREAM))
|
||||||
|
/// .streaming(HttpContext::create(MyActor { count: 0 }))
|
||||||
|
/// }
|
||||||
|
///
|
||||||
|
/// #[actix_web::main]
|
||||||
|
/// async fn main() -> std::io::Result<()> {
|
||||||
|
/// HttpServer::new(|| App::new().service(index))
|
||||||
|
/// .bind(("127.0.0.1", 8080))?
|
||||||
|
/// .run()
|
||||||
|
/// .await
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
pub struct HttpContext<A>
|
pub struct HttpContext<A>
|
||||||
where
|
where
|
||||||
A: Actor<Context = HttpContext<A>>,
|
A: Actor<Context = HttpContext<A>>,
|
||||||
@ -210,7 +262,7 @@ mod tests {
|
|||||||
type Context = HttpContext<Self>;
|
type Context = HttpContext<Self>;
|
||||||
|
|
||||||
fn started(&mut self, ctx: &mut Self::Context) {
|
fn started(&mut self, ctx: &mut Self::Context) {
|
||||||
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx));
|
ctx.run_later(Duration::from_millis(100), Self::write);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -221,7 +273,7 @@ mod tests {
|
|||||||
ctx.write_eof()
|
ctx.write_eof()
|
||||||
} else {
|
} else {
|
||||||
ctx.write(Bytes::from(format!("LINE-{}", self.count)));
|
ctx.write(Bytes::from(format!("LINE-{}", self.count)));
|
||||||
ctx.run_later(Duration::from_millis(100), |slf, ctx| slf.write(ctx));
|
ctx.run_later(Duration::from_millis(100), Self::write);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -1,4 +1,59 @@
|
|||||||
//! Actix actors support for Actix Web.
|
//! Actix actors support for Actix Web.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use actix::{Actor, StreamHandler};
|
||||||
|
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
//! use actix_web_actors::ws;
|
||||||
|
//!
|
||||||
|
//! /// Define Websocket actor
|
||||||
|
//! struct MyWs;
|
||||||
|
//!
|
||||||
|
//! impl Actor for MyWs {
|
||||||
|
//! type Context = ws::WebsocketContext<Self>;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! /// Handler for ws::Message message
|
||||||
|
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
|
||||||
|
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||||
|
//! match msg {
|
||||||
|
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||||
|
//! Ok(ws::Message::Text(text)) => ctx.text(text),
|
||||||
|
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
|
||||||
|
//! _ => (),
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[get("/ws")]
|
||||||
|
//! async fn index(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
|
//! ws::start(MyWs, &req, stream)
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[actix_web::main]
|
||||||
|
//! async fn main() -> std::io::Result<()> {
|
||||||
|
//! HttpServer::new(|| App::new().service(index))
|
||||||
|
//! .bind(("127.0.0.1", 8080))?
|
||||||
|
//! .run()
|
||||||
|
//! .await
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! # Documentation & Community Resources
|
||||||
|
//! In addition to this API documentation, several other resources are available:
|
||||||
|
//!
|
||||||
|
//! * [Website & User Guide](https://actix.rs/)
|
||||||
|
//! * [Documentation for `actix_web`](actix_web)
|
||||||
|
//! * [Examples Repository](https://github.com/actix/examples)
|
||||||
|
//! * [Community Chat on Discord](https://discord.gg/NWpN5mmg3x)
|
||||||
|
//!
|
||||||
|
//! To get started navigating the API docs, you may consider looking at the following pages first:
|
||||||
|
//!
|
||||||
|
//! * [`ws`]: This module provides actor support for WebSockets.
|
||||||
|
//!
|
||||||
|
//! * [`HttpContext`]: This struct provides actor support for streaming HTTP responses.
|
||||||
|
//!
|
||||||
|
|
||||||
#![deny(rust_2018_idioms, nonstandard_style)]
|
#![deny(rust_2018_idioms, nonstandard_style)]
|
||||||
#![warn(future_incompatible)]
|
#![warn(future_incompatible)]
|
||||||
|
@ -1,4 +1,60 @@
|
|||||||
//! Websocket integration.
|
//! Websocket integration.
|
||||||
|
//!
|
||||||
|
//! # Examples
|
||||||
|
//!
|
||||||
|
//! ```no_run
|
||||||
|
//! use actix::{Actor, StreamHandler};
|
||||||
|
//! use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||||
|
//! use actix_web_actors::ws;
|
||||||
|
//!
|
||||||
|
//! /// Define Websocket actor
|
||||||
|
//! struct MyWs;
|
||||||
|
//!
|
||||||
|
//! impl Actor for MyWs {
|
||||||
|
//! type Context = ws::WebsocketContext<Self>;
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! /// Handler for ws::Message message
|
||||||
|
//! impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
|
||||||
|
//! fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||||
|
//! match msg {
|
||||||
|
//! Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||||
|
//! Ok(ws::Message::Text(text)) => ctx.text(text),
|
||||||
|
//! Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
|
||||||
|
//! _ => (),
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[get("/ws")]
|
||||||
|
//! async fn websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
|
//! ws::start(MyWs, &req, stream)
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
|
||||||
|
//!
|
||||||
|
//! #[get("/custom-ws")]
|
||||||
|
//! async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
|
//! // Create a Websocket session with a specific max frame size, and protocols.
|
||||||
|
//! ws::WsResponseBuilder::new(MyWs, &req, stream)
|
||||||
|
//! .frame_size(MAX_FRAME_SIZE)
|
||||||
|
//! .protocols(&["A", "B"])
|
||||||
|
//! .start()
|
||||||
|
//! }
|
||||||
|
//!
|
||||||
|
//! #[actix_web::main]
|
||||||
|
//! async fn main() -> std::io::Result<()> {
|
||||||
|
//! HttpServer::new(|| {
|
||||||
|
//! App::new()
|
||||||
|
//! .service(websocket)
|
||||||
|
//! .service(custom_websocket)
|
||||||
|
//! })
|
||||||
|
//! .bind(("127.0.0.1", 8080))?
|
||||||
|
//! .run()
|
||||||
|
//! .await
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
collections::VecDeque,
|
collections::VecDeque,
|
||||||
@ -41,20 +97,51 @@ use tokio::sync::oneshot;
|
|||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
///
|
///
|
||||||
/// Create a Websocket session response with default configuration.
|
/// ```no_run
|
||||||
/// ```ignore
|
/// # use actix::{Actor, StreamHandler};
|
||||||
/// WsResponseBuilder::new(WsActor, &req, stream).start()
|
/// # use actix_web::{get, web, App, Error, HttpRequest, HttpResponse, HttpServer};
|
||||||
/// ```
|
/// # use actix_web_actors::ws;
|
||||||
|
/// #
|
||||||
|
/// # struct MyWs;
|
||||||
|
/// #
|
||||||
|
/// # impl Actor for MyWs {
|
||||||
|
/// # type Context = ws::WebsocketContext<Self>;
|
||||||
|
/// # }
|
||||||
|
/// #
|
||||||
|
/// # /// Handler for ws::Message message
|
||||||
|
/// # impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for MyWs {
|
||||||
|
/// # fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {}
|
||||||
|
/// # }
|
||||||
|
/// #
|
||||||
|
/// #[get("/ws")]
|
||||||
|
/// async fn websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
|
/// ws::WsResponseBuilder::new(MyWs, &req, stream).start()
|
||||||
|
/// }
|
||||||
///
|
///
|
||||||
/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols.
|
|
||||||
/// ```ignore
|
|
||||||
/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
|
/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
|
||||||
///
|
///
|
||||||
/// ws::WsResponseBuilder::new(WsActor, &req, stream)
|
/// #[get("/custom-ws")]
|
||||||
/// .codec(Codec::new())
|
/// async fn custom_websocket(req: HttpRequest, stream: web::Payload) -> Result<HttpResponse, Error> {
|
||||||
/// .protocols(&["A", "B"])
|
/// // Create a Websocket session with a specific max frame size, codec, and protocols.
|
||||||
/// .frame_size(MAX_FRAME_SIZE)
|
/// ws::WsResponseBuilder::new(MyWs, &req, stream)
|
||||||
/// .start()
|
/// .codec(actix_http::ws::Codec::new())
|
||||||
|
/// // This will overwrite the codec's max frame-size
|
||||||
|
/// .frame_size(MAX_FRAME_SIZE)
|
||||||
|
/// .protocols(&["A", "B"])
|
||||||
|
/// .start()
|
||||||
|
/// }
|
||||||
|
/// #
|
||||||
|
/// # #[actix_web::main]
|
||||||
|
/// # async fn main() -> std::io::Result<()> {
|
||||||
|
/// # HttpServer::new(|| {
|
||||||
|
/// # App::new()
|
||||||
|
/// # .service(websocket)
|
||||||
|
/// # .service(custom_websocket)
|
||||||
|
/// # })
|
||||||
|
/// # .bind(("127.0.0.1", 8080))?
|
||||||
|
/// # .run()
|
||||||
|
/// # .await
|
||||||
|
/// # }
|
||||||
/// ```
|
/// ```
|
||||||
pub struct WsResponseBuilder<'a, A, T>
|
pub struct WsResponseBuilder<'a, A, T>
|
||||||
where
|
where
|
||||||
|
@ -1,6 +1,17 @@
|
|||||||
# Changes
|
# Changes
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2718]: https://github.com/actix/actix-web/pull/2718
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.1 - 2022-06-11
|
||||||
|
- Fix support for guard paths in route handler macros. [#2771]
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
||||||
|
|
||||||
|
[#2771]: https://github.com/actix/actix-web/pull/2771
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0 - 2022-02-24
|
## 4.0.0 - 2022-02-24
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web-codegen"
|
name = "actix-web-codegen"
|
||||||
version = "4.0.0"
|
version = "4.0.1"
|
||||||
description = "Routing and runtime macros for Actix Web"
|
description = "Routing and runtime macros for Actix Web"
|
||||||
homepage = "https://actix.rs"
|
homepage = "https://actix.rs"
|
||||||
repository = "https://github.com/actix/actix-web.git"
|
repository = "https://github.com/actix/actix-web.git"
|
||||||
@ -18,7 +18,7 @@ proc-macro = true
|
|||||||
actix-router = "0.5.0"
|
actix-router = "0.5.0"
|
||||||
proc-macro2 = "1"
|
proc-macro2 = "1"
|
||||||
quote = "1"
|
quote = "1"
|
||||||
syn = { version = "1", features = ["full", "parsing"] }
|
syn = { version = "1", features = ["full", "extra-traits"] }
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-macros = "0.2.3"
|
actix-macros = "0.2.3"
|
||||||
|
@ -3,11 +3,11 @@
|
|||||||
> Routing and runtime macros for Actix Web.
|
> Routing and runtime macros for Actix Web.
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web-codegen)
|
[](https://crates.io/crates/actix-web-codegen)
|
||||||
[](https://docs.rs/actix-web-codegen/4.0.0)
|
[](https://docs.rs/actix-web-codegen/4.0.1)
|
||||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|

|
||||||

|

|
||||||
<br />
|
<br />
|
||||||
[](https://deps.rs/crate/actix-web-codegen/4.0.0)
|
[](https://deps.rs/crate/actix-web-codegen/4.0.1)
|
||||||
[](https://crates.io/crates/actix-web-codegen)
|
[](https://crates.io/crates/actix-web-codegen)
|
||||||
[](https://discord.gg/NWpN5mmg3x)
|
[](https://discord.gg/NWpN5mmg3x)
|
||||||
|
|
||||||
|
@ -46,9 +46,20 @@
|
|||||||
//! ```
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! # Multiple Path Handlers
|
//! # Multiple Path Handlers
|
||||||
//! There are no macros to generate multi-path handlers. Let us know in [this issue].
|
//! Acts as a wrapper for multiple single method handler macros. It takes no arguments and
|
||||||
|
//! delegates those to the macros for the individual methods. See [macro@routes] macro docs.
|
||||||
//!
|
//!
|
||||||
//! [this issue]: https://github.com/actix/actix-web/issues/1709
|
//! ```
|
||||||
|
//! # use actix_web::HttpResponse;
|
||||||
|
//! # use actix_web_codegen::routes;
|
||||||
|
//! #[routes]
|
||||||
|
//! #[get("/test")]
|
||||||
|
//! #[get("/test2")]
|
||||||
|
//! #[delete("/test")]
|
||||||
|
//! async fn example() -> HttpResponse {
|
||||||
|
//! HttpResponse::Ok().finish()
|
||||||
|
//! }
|
||||||
|
//! ```
|
||||||
//!
|
//!
|
||||||
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
|
//! [actix-web attributes docs]: https://docs.rs/actix-web/latest/actix_web/#attributes
|
||||||
//! [GET]: macro@get
|
//! [GET]: macro@get
|
||||||
@ -104,6 +115,39 @@ pub fn route(args: TokenStream, input: TokenStream) -> TokenStream {
|
|||||||
route::with_method(None, args, input)
|
route::with_method(None, args, input)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Creates resource handler, allowing multiple HTTP methods and paths.
|
||||||
|
///
|
||||||
|
/// # Syntax
|
||||||
|
/// ```plain
|
||||||
|
/// #[routes]
|
||||||
|
/// #[<method>("path", ...)]
|
||||||
|
/// #[<method>("path", ...)]
|
||||||
|
/// ...
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// # Attributes
|
||||||
|
/// The `routes` macro itself has no parameters, but allows specifying the attribute macros for
|
||||||
|
/// the multiple paths and/or methods, e.g. [`GET`](macro@get) and [`POST`](macro@post).
|
||||||
|
///
|
||||||
|
/// These helper attributes take the same parameters as the [single method handlers](crate#single-method-handler).
|
||||||
|
///
|
||||||
|
/// # Examples
|
||||||
|
/// ```
|
||||||
|
/// # use actix_web::HttpResponse;
|
||||||
|
/// # use actix_web_codegen::routes;
|
||||||
|
/// #[routes]
|
||||||
|
/// #[get("/test")]
|
||||||
|
/// #[get("/test2")]
|
||||||
|
/// #[delete("/test")]
|
||||||
|
/// async fn example() -> HttpResponse {
|
||||||
|
/// HttpResponse::Ok().finish()
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[proc_macro_attribute]
|
||||||
|
pub fn routes(_: TokenStream, input: TokenStream) -> TokenStream {
|
||||||
|
route::with_methods(input)
|
||||||
|
}
|
||||||
|
|
||||||
macro_rules! method_macro {
|
macro_rules! method_macro {
|
||||||
($variant:ident, $method:ident) => {
|
($variant:ident, $method:ident) => {
|
||||||
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]
|
#[doc = concat!("Creates route handler with `actix_web::guard::", stringify!($variant), "`.")]
|
||||||
|
@ -3,24 +3,12 @@ use std::{collections::HashSet, convert::TryFrom};
|
|||||||
use actix_router::ResourceDef;
|
use actix_router::ResourceDef;
|
||||||
use proc_macro::TokenStream;
|
use proc_macro::TokenStream;
|
||||||
use proc_macro2::{Span, TokenStream as TokenStream2};
|
use proc_macro2::{Span, TokenStream as TokenStream2};
|
||||||
use quote::{format_ident, quote, ToTokens, TokenStreamExt};
|
use quote::{quote, ToTokens, TokenStreamExt};
|
||||||
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, NestedMeta};
|
use syn::{parse_macro_input, AttributeArgs, Ident, LitStr, Meta, NestedMeta, Path};
|
||||||
|
|
||||||
enum ResourceType {
|
|
||||||
Async,
|
|
||||||
Sync,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl ToTokens for ResourceType {
|
|
||||||
fn to_tokens(&self, stream: &mut TokenStream2) {
|
|
||||||
let ident = format_ident!("to");
|
|
||||||
stream.append(ident);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
macro_rules! method_type {
|
macro_rules! method_type {
|
||||||
(
|
(
|
||||||
$($variant:ident, $upper:ident,)+
|
$($variant:ident, $upper:ident, $lower:ident,)+
|
||||||
) => {
|
) => {
|
||||||
#[derive(Debug, PartialEq, Eq, Hash)]
|
#[derive(Debug, PartialEq, Eq, Hash)]
|
||||||
pub enum MethodType {
|
pub enum MethodType {
|
||||||
@ -42,20 +30,27 @@ macro_rules! method_type {
|
|||||||
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
|
_ => Err(format!("Unexpected HTTP method: `{}`", method)),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn from_path(method: &Path) -> Result<Self, ()> {
|
||||||
|
match () {
|
||||||
|
$(_ if method.is_ident(stringify!($lower)) => Ok(Self::$variant),)+
|
||||||
|
_ => Err(()),
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
method_type! {
|
method_type! {
|
||||||
Get, GET,
|
Get, GET, get,
|
||||||
Post, POST,
|
Post, POST, post,
|
||||||
Put, PUT,
|
Put, PUT, put,
|
||||||
Delete, DELETE,
|
Delete, DELETE, delete,
|
||||||
Head, HEAD,
|
Head, HEAD, head,
|
||||||
Connect, CONNECT,
|
Connect, CONNECT, connect,
|
||||||
Options, OPTIONS,
|
Options, OPTIONS, options,
|
||||||
Trace, TRACE,
|
Trace, TRACE, trace,
|
||||||
Patch, PATCH,
|
Patch, PATCH, patch,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ToTokens for MethodType {
|
impl ToTokens for MethodType {
|
||||||
@ -77,7 +72,7 @@ impl TryFrom<&syn::LitStr> for MethodType {
|
|||||||
struct Args {
|
struct Args {
|
||||||
path: syn::LitStr,
|
path: syn::LitStr,
|
||||||
resource_name: Option<syn::LitStr>,
|
resource_name: Option<syn::LitStr>,
|
||||||
guards: Vec<Ident>,
|
guards: Vec<Path>,
|
||||||
wrappers: Vec<syn::Type>,
|
wrappers: Vec<syn::Type>,
|
||||||
methods: HashSet<MethodType>,
|
methods: HashSet<MethodType>,
|
||||||
}
|
}
|
||||||
@ -90,6 +85,18 @@ impl Args {
|
|||||||
let mut wrappers = Vec::new();
|
let mut wrappers = Vec::new();
|
||||||
let mut methods = HashSet::new();
|
let mut methods = HashSet::new();
|
||||||
|
|
||||||
|
if args.is_empty() {
|
||||||
|
return Err(syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
format!(
|
||||||
|
r#"invalid service definition, expected #[{}("<path>")]"#,
|
||||||
|
method
|
||||||
|
.map_or("route", |it| it.as_str())
|
||||||
|
.to_ascii_lowercase()
|
||||||
|
),
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
let is_route_macro = method.is_none();
|
let is_route_macro = method.is_none();
|
||||||
if let Some(method) = method {
|
if let Some(method) = method {
|
||||||
methods.insert(method);
|
methods.insert(method);
|
||||||
@ -121,7 +128,7 @@ impl Args {
|
|||||||
}
|
}
|
||||||
} else if nv.path.is_ident("guard") {
|
} else if nv.path.is_ident("guard") {
|
||||||
if let syn::Lit::Str(lit) = nv.lit {
|
if let syn::Lit::Str(lit) = nv.lit {
|
||||||
guards.push(Ident::new(&lit.value(), Span::call_site()));
|
guards.push(lit.parse::<Path>()?);
|
||||||
} else {
|
} else {
|
||||||
return Err(syn::Error::new_spanned(
|
return Err(syn::Error::new_spanned(
|
||||||
nv.lit,
|
nv.lit,
|
||||||
@ -183,55 +190,27 @@ impl Args {
|
|||||||
}
|
}
|
||||||
|
|
||||||
pub struct Route {
|
pub struct Route {
|
||||||
|
/// Name of the handler function being annotated.
|
||||||
name: syn::Ident,
|
name: syn::Ident,
|
||||||
args: Args,
|
|
||||||
|
/// Args passed to routing macro.
|
||||||
|
///
|
||||||
|
/// When using `#[routes]`, this will contain args for each specific routing macro.
|
||||||
|
args: Vec<Args>,
|
||||||
|
|
||||||
|
/// AST of the handler function being annotated.
|
||||||
ast: syn::ItemFn,
|
ast: syn::ItemFn,
|
||||||
resource_type: ResourceType,
|
|
||||||
|
|
||||||
/// The doc comment attributes to copy to generated struct, if any.
|
/// The doc comment attributes to copy to generated struct, if any.
|
||||||
doc_attributes: Vec<syn::Attribute>,
|
doc_attributes: Vec<syn::Attribute>,
|
||||||
}
|
}
|
||||||
|
|
||||||
fn guess_resource_type(typ: &syn::Type) -> ResourceType {
|
|
||||||
let mut guess = ResourceType::Sync;
|
|
||||||
|
|
||||||
if let syn::Type::ImplTrait(typ) = typ {
|
|
||||||
for bound in typ.bounds.iter() {
|
|
||||||
if let syn::TypeParamBound::Trait(bound) = bound {
|
|
||||||
for bound in bound.path.segments.iter() {
|
|
||||||
if bound.ident == "Future" {
|
|
||||||
guess = ResourceType::Async;
|
|
||||||
break;
|
|
||||||
} else if bound.ident == "Responder" {
|
|
||||||
guess = ResourceType::Sync;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
guess
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Route {
|
impl Route {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
args: AttributeArgs,
|
args: AttributeArgs,
|
||||||
ast: syn::ItemFn,
|
ast: syn::ItemFn,
|
||||||
method: Option<MethodType>,
|
method: Option<MethodType>,
|
||||||
) -> syn::Result<Self> {
|
) -> syn::Result<Self> {
|
||||||
if args.is_empty() {
|
|
||||||
return Err(syn::Error::new(
|
|
||||||
Span::call_site(),
|
|
||||||
format!(
|
|
||||||
r#"invalid service definition, expected #[{}("<some path>")]"#,
|
|
||||||
method
|
|
||||||
.map_or("route", |it| it.as_str())
|
|
||||||
.to_ascii_lowercase()
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let name = ast.sig.ident.clone();
|
let name = ast.sig.ident.clone();
|
||||||
|
|
||||||
// Try and pull out the doc comments so that we can reapply them to the generated struct.
|
// Try and pull out the doc comments so that we can reapply them to the generated struct.
|
||||||
@ -244,6 +223,7 @@ impl Route {
|
|||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let args = Args::new(args, method)?;
|
let args = Args::new(args, method)?;
|
||||||
|
|
||||||
if args.methods.is_empty() {
|
if args.methods.is_empty() {
|
||||||
return Err(syn::Error::new(
|
return Err(syn::Error::new(
|
||||||
Span::call_site(),
|
Span::call_site(),
|
||||||
@ -251,25 +231,44 @@ impl Route {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
let resource_type = if ast.sig.asyncness.is_some() {
|
if matches!(ast.sig.output, syn::ReturnType::Default) {
|
||||||
ResourceType::Async
|
return Err(syn::Error::new_spanned(
|
||||||
} else {
|
ast,
|
||||||
match ast.sig.output {
|
"Function has no return type. Cannot be used as handler",
|
||||||
syn::ReturnType::Default => {
|
));
|
||||||
return Err(syn::Error::new_spanned(
|
}
|
||||||
ast,
|
|
||||||
"Function has no return type. Cannot be used as handler",
|
Ok(Self {
|
||||||
));
|
name,
|
||||||
}
|
args: vec![args],
|
||||||
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
|
ast,
|
||||||
}
|
doc_attributes,
|
||||||
};
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn multiple(args: Vec<Args>, ast: syn::ItemFn) -> syn::Result<Self> {
|
||||||
|
let name = ast.sig.ident.clone();
|
||||||
|
|
||||||
|
// Try and pull out the doc comments so that we can reapply them to the generated struct.
|
||||||
|
// Note that multi line doc comments are converted to multiple doc attributes.
|
||||||
|
let doc_attributes = ast
|
||||||
|
.attrs
|
||||||
|
.iter()
|
||||||
|
.filter(|attr| attr.path.is_ident("doc"))
|
||||||
|
.cloned()
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
if matches!(ast.sig.output, syn::ReturnType::Default) {
|
||||||
|
return Err(syn::Error::new_spanned(
|
||||||
|
ast,
|
||||||
|
"Function has no return type. Cannot be used as handler",
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
Ok(Self {
|
Ok(Self {
|
||||||
name,
|
name,
|
||||||
args,
|
args,
|
||||||
ast,
|
ast,
|
||||||
resource_type,
|
|
||||||
doc_attributes,
|
doc_attributes,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@ -280,38 +279,57 @@ impl ToTokens for Route {
|
|||||||
let Self {
|
let Self {
|
||||||
name,
|
name,
|
||||||
ast,
|
ast,
|
||||||
args:
|
args,
|
||||||
Args {
|
doc_attributes,
|
||||||
|
} = self;
|
||||||
|
|
||||||
|
let registrations: TokenStream2 = args
|
||||||
|
.iter()
|
||||||
|
.map(|args| {
|
||||||
|
let Args {
|
||||||
path,
|
path,
|
||||||
resource_name,
|
resource_name,
|
||||||
guards,
|
guards,
|
||||||
wrappers,
|
wrappers,
|
||||||
methods,
|
methods,
|
||||||
},
|
} = args;
|
||||||
resource_type,
|
|
||||||
doc_attributes,
|
let resource_name = resource_name
|
||||||
} = self;
|
.as_ref()
|
||||||
let resource_name = resource_name
|
.map_or_else(|| name.to_string(), LitStr::value);
|
||||||
.as_ref()
|
|
||||||
.map_or_else(|| name.to_string(), LitStr::value);
|
let method_guards = {
|
||||||
let method_guards = {
|
let mut others = methods.iter();
|
||||||
let mut others = methods.iter();
|
|
||||||
// unwrapping since length is checked to be at least one
|
// unwrapping since length is checked to be at least one
|
||||||
let first = others.next().unwrap();
|
let first = others.next().unwrap();
|
||||||
|
|
||||||
|
if methods.len() > 1 {
|
||||||
|
quote! {
|
||||||
|
.guard(
|
||||||
|
::actix_web::guard::Any(::actix_web::guard::#first())
|
||||||
|
#(.or(::actix_web::guard::#others()))*
|
||||||
|
)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
quote! {
|
||||||
|
.guard(::actix_web::guard::#first())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
if methods.len() > 1 {
|
|
||||||
quote! {
|
quote! {
|
||||||
.guard(
|
let __resource = ::actix_web::Resource::new(#path)
|
||||||
::actix_web::guard::Any(::actix_web::guard::#first())
|
.name(#resource_name)
|
||||||
#(.or(::actix_web::guard::#others()))*
|
#method_guards
|
||||||
)
|
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
||||||
|
#(.wrap(#wrappers))*
|
||||||
|
.to(#name);
|
||||||
|
|
||||||
|
::actix_web::dev::HttpServiceFactory::register(__resource, __config);
|
||||||
}
|
}
|
||||||
} else {
|
})
|
||||||
quote! {
|
.collect();
|
||||||
.guard(::actix_web::guard::#first())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
let stream = quote! {
|
let stream = quote! {
|
||||||
#(#doc_attributes)*
|
#(#doc_attributes)*
|
||||||
@ -321,14 +339,7 @@ impl ToTokens for Route {
|
|||||||
impl ::actix_web::dev::HttpServiceFactory for #name {
|
impl ::actix_web::dev::HttpServiceFactory for #name {
|
||||||
fn register(self, __config: &mut actix_web::dev::AppService) {
|
fn register(self, __config: &mut actix_web::dev::AppService) {
|
||||||
#ast
|
#ast
|
||||||
let __resource = ::actix_web::Resource::new(#path)
|
#registrations
|
||||||
.name(#resource_name)
|
|
||||||
#method_guards
|
|
||||||
#(.guard(::actix_web::guard::fn_guard(#guards)))*
|
|
||||||
#(.wrap(#wrappers))*
|
|
||||||
.#resource_type(#name);
|
|
||||||
|
|
||||||
::actix_web::dev::HttpServiceFactory::register(__resource, __config)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -357,6 +368,57 @@ pub(crate) fn with_method(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn with_methods(input: TokenStream) -> TokenStream {
|
||||||
|
let mut ast = match syn::parse::<syn::ItemFn>(input.clone()) {
|
||||||
|
Ok(ast) => ast,
|
||||||
|
// on parse error, make IDEs happy; see fn docs
|
||||||
|
Err(err) => return input_and_compile_error(input, err),
|
||||||
|
};
|
||||||
|
|
||||||
|
let (methods, others) = ast
|
||||||
|
.attrs
|
||||||
|
.into_iter()
|
||||||
|
.map(|attr| match MethodType::from_path(&attr.path) {
|
||||||
|
Ok(method) => Ok((method, attr)),
|
||||||
|
Err(_) => Err(attr),
|
||||||
|
})
|
||||||
|
.partition::<Vec<_>, _>(Result::is_ok);
|
||||||
|
|
||||||
|
ast.attrs = others.into_iter().map(Result::unwrap_err).collect();
|
||||||
|
|
||||||
|
let methods =
|
||||||
|
match methods
|
||||||
|
.into_iter()
|
||||||
|
.map(Result::unwrap)
|
||||||
|
.map(|(method, attr)| {
|
||||||
|
attr.parse_meta().and_then(|args| {
|
||||||
|
if let Meta::List(args) = args {
|
||||||
|
Args::new(args.nested.into_iter().collect(), Some(method))
|
||||||
|
} else {
|
||||||
|
Err(syn::Error::new_spanned(attr, "Invalid input for macro"))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect::<Result<Vec<_>, _>>()
|
||||||
|
{
|
||||||
|
Ok(methods) if methods.is_empty() => return input_and_compile_error(
|
||||||
|
input,
|
||||||
|
syn::Error::new(
|
||||||
|
Span::call_site(),
|
||||||
|
"The #[routes] macro requires at least one `#[<method>(..)]` attribute.",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Ok(methods) => methods,
|
||||||
|
Err(err) => return input_and_compile_error(input, err),
|
||||||
|
};
|
||||||
|
|
||||||
|
match Route::multiple(methods, ast) {
|
||||||
|
Ok(route) => route.into_token_stream().into(),
|
||||||
|
// on macro related error, make IDEs happy; see fn docs
|
||||||
|
Err(err) => input_and_compile_error(input, err),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Converts the error to a token stream and appends it to the original input.
|
/// Converts the error to a token stream and appends it to the original input.
|
||||||
///
|
///
|
||||||
/// Returning the original input in addition to the error is good for IDEs which can gracefully
|
/// Returning the original input in addition to the error is good for IDEs which can gracefully
|
||||||
|
@ -8,9 +8,11 @@ use actix_web::{
|
|||||||
header::{HeaderName, HeaderValue},
|
header::{HeaderName, HeaderValue},
|
||||||
StatusCode,
|
StatusCode,
|
||||||
},
|
},
|
||||||
web, App, Error, HttpResponse, Responder,
|
web, App, Error, HttpRequest, HttpResponse, Responder,
|
||||||
|
};
|
||||||
|
use actix_web_codegen::{
|
||||||
|
connect, delete, get, head, options, patch, post, put, route, routes, trace,
|
||||||
};
|
};
|
||||||
use actix_web_codegen::{connect, delete, get, head, options, patch, post, put, route, trace};
|
|
||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
|
|
||||||
// Make sure that we can name function as 'config'
|
// Make sure that we can name function as 'config'
|
||||||
@ -89,13 +91,61 @@ async fn route_test() -> impl Responder {
|
|||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[get("/routes/test")]
|
||||||
|
#[get("/routes/test2")]
|
||||||
|
#[post("/routes/test")]
|
||||||
|
async fn routes_test() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// routes overlap with the more specific route first, therefore accessible
|
||||||
|
#[routes]
|
||||||
|
#[get("/routes/overlap/test")]
|
||||||
|
#[get("/routes/overlap/{foo}")]
|
||||||
|
async fn routes_overlapping_test(req: HttpRequest) -> impl Responder {
|
||||||
|
// foo is only populated when route is not /routes/overlap/test
|
||||||
|
match req.match_info().get("foo") {
|
||||||
|
None => assert!(req.uri() == "/routes/overlap/test"),
|
||||||
|
Some(_) => assert!(req.uri() != "/routes/overlap/test"),
|
||||||
|
}
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
|
// routes overlap with the more specific route last, therefore inaccessible
|
||||||
|
#[routes]
|
||||||
|
#[get("/routes/overlap2/{foo}")]
|
||||||
|
#[get("/routes/overlap2/test")]
|
||||||
|
async fn routes_overlapping_inaccessible_test(req: HttpRequest) -> impl Responder {
|
||||||
|
// foo is always populated even when path is /routes/overlap2/test
|
||||||
|
assert!(req.match_info().get("foo").is_some());
|
||||||
|
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
#[get("/custom_resource_name", name = "custom")]
|
#[get("/custom_resource_name", name = "custom")]
|
||||||
async fn custom_resource_name_test<'a>(req: actix_web::HttpRequest) -> impl Responder {
|
async fn custom_resource_name_test<'a>(req: HttpRequest) -> impl Responder {
|
||||||
assert!(req.url_for_static("custom").is_ok());
|
assert!(req.url_for_static("custom").is_ok());
|
||||||
assert!(req.url_for_static("custom_resource_name_test").is_err());
|
assert!(req.url_for_static("custom_resource_name_test").is_err());
|
||||||
HttpResponse::Ok()
|
HttpResponse::Ok()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
mod guard_module {
|
||||||
|
use actix_web::{guard::GuardContext, http::header};
|
||||||
|
|
||||||
|
pub fn guard(ctx: &GuardContext) -> bool {
|
||||||
|
ctx.header::<header::Accept>()
|
||||||
|
.map(|h| h.preference() == "image/*")
|
||||||
|
.unwrap_or(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/test/guard", guard = "guard_module::guard")]
|
||||||
|
async fn guard_test() -> impl Responder {
|
||||||
|
HttpResponse::Ok()
|
||||||
|
}
|
||||||
|
|
||||||
pub struct ChangeStatusCode;
|
pub struct ChangeStatusCode;
|
||||||
|
|
||||||
impl<S, B> Transform<S, ServiceRequest> for ChangeStatusCode
|
impl<S, B> Transform<S, ServiceRequest> for ChangeStatusCode
|
||||||
@ -186,7 +236,11 @@ async fn test_body() {
|
|||||||
.service(patch_test)
|
.service(patch_test)
|
||||||
.service(test_handler)
|
.service(test_handler)
|
||||||
.service(route_test)
|
.service(route_test)
|
||||||
|
.service(routes_overlapping_test)
|
||||||
|
.service(routes_overlapping_inaccessible_test)
|
||||||
|
.service(routes_test)
|
||||||
.service(custom_resource_name_test)
|
.service(custom_resource_name_test)
|
||||||
|
.service(guard_test)
|
||||||
});
|
});
|
||||||
let request = srv.request(http::Method::GET, srv.url("/test"));
|
let request = srv.request(http::Method::GET, srv.url("/test"));
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
@ -242,9 +296,47 @@ async fn test_body() {
|
|||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(!response.status().is_success());
|
assert!(!response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/test2"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::POST, srv.url("/routes/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/not-set"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_client_error());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap/bar"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/test"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.request(http::Method::GET, srv.url("/routes/overlap2/bar"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
let request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
|
let request = srv.request(http::Method::GET, srv.url("/custom_resource_name"));
|
||||||
let response = request.send().await.unwrap();
|
let response = request.send().await.unwrap();
|
||||||
assert!(response.status().is_success());
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv
|
||||||
|
.request(http::Method::GET, srv.url("/test/guard"))
|
||||||
|
.insert_header(("Accept", "image/*"));
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#[rustversion::stable(1.54)] // MSRV
|
#[rustversion::stable(1.57)] // MSRV
|
||||||
#[test]
|
#[test]
|
||||||
fn compile_macros() {
|
fn compile_macros() {
|
||||||
let t = trybuild::TestCases::new();
|
let t = trybuild::TestCases::new();
|
||||||
@ -12,6 +12,10 @@ fn compile_macros() {
|
|||||||
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
|
||||||
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
|
||||||
|
|
||||||
|
t.pass("tests/trybuild/routes-ok.rs");
|
||||||
|
t.compile_fail("tests/trybuild/routes-missing-method-fail.rs");
|
||||||
|
t.compile_fail("tests/trybuild/routes-missing-args-fail.rs");
|
||||||
|
|
||||||
t.pass("tests/trybuild/docstring-ok.rs");
|
t.pass("tests/trybuild/docstring-ok.rs");
|
||||||
|
|
||||||
t.pass("tests/trybuild/test-runtime.rs");
|
t.pass("tests/trybuild/test-runtime.rs");
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
error: HTTP method defined more than once: `GET`
|
error: HTTP method defined more than once: `GET`
|
||||||
--> $DIR/route-duplicate-method-fail.rs:3:35
|
--> tests/trybuild/route-duplicate-method-fail.rs:3:35
|
||||||
|
|
|
|
||||||
3 | #[route("/", method="GET", method="GET")]
|
3 | #[route("/", method="GET", method="GET")]
|
||||||
| ^^^^^
|
| ^^^^^
|
||||||
|
|
||||||
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||||
--> $DIR/route-duplicate-method-fail.rs:12:55
|
--> tests/trybuild/route-duplicate-method-fail.rs:12:55
|
||||||
|
|
|
|
||||||
12 | let srv = actix_test::start(|| App::new().service(index));
|
12 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
||||||
|
@ -10,4 +10,6 @@ error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpSer
|
|||||||
--> tests/trybuild/route-missing-method-fail.rs:12:55
|
--> tests/trybuild/route-missing-method-fail.rs:12:55
|
||||||
|
|
|
|
||||||
12 | let srv = actix_test::start(|| App::new().service(index));
|
12 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
||||||
|
@ -1,11 +1,13 @@
|
|||||||
error: Unexpected HTTP method: `UNEXPECTED`
|
error: Unexpected HTTP method: `UNEXPECTED`
|
||||||
--> $DIR/route-unexpected-method-fail.rs:3:21
|
--> tests/trybuild/route-unexpected-method-fail.rs:3:21
|
||||||
|
|
|
|
||||||
3 | #[route("/", method="UNEXPECTED")]
|
3 | #[route("/", method="UNEXPECTED")]
|
||||||
| ^^^^^^^^^^^^
|
| ^^^^^^^^^^^^
|
||||||
|
|
||||||
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||||
--> $DIR/route-unexpected-method-fail.rs:12:55
|
--> tests/trybuild/route-unexpected-method-fail.rs:12:55
|
||||||
|
|
|
|
||||||
12 | let srv = actix_test::start(|| App::new().service(index));
|
12 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
||||||
|
14
actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs
Normal file
14
actix-web-codegen/tests/trybuild/routes-missing-args-fail.rs
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
use actix_web_codegen::*;
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[get]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
}
|
@ -0,0 +1,21 @@
|
|||||||
|
error: invalid service definition, expected #[get("<path>")]
|
||||||
|
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
||||||
|
|
|
||||||
|
4 | #[get]
|
||||||
|
| ^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `get` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
||||||
|
error: Invalid input for macro
|
||||||
|
--> tests/trybuild/routes-missing-args-fail.rs:4:1
|
||||||
|
|
|
||||||
|
4 | #[get]
|
||||||
|
| ^^^^^^
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||||
|
--> tests/trybuild/routes-missing-args-fail.rs:13:55
|
||||||
|
|
|
||||||
|
13 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
@ -0,0 +1,13 @@
|
|||||||
|
use actix_web_codegen::*;
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
}
|
@ -0,0 +1,15 @@
|
|||||||
|
error: The #[routes] macro requires at least one `#[<method>(..)]` attribute.
|
||||||
|
--> tests/trybuild/routes-missing-method-fail.rs:3:1
|
||||||
|
|
|
||||||
|
3 | #[routes]
|
||||||
|
| ^^^^^^^^^
|
||||||
|
|
|
||||||
|
= note: this error originates in the attribute macro `routes` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||||
|
|
||||||
|
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||||
|
--> tests/trybuild/routes-missing-method-fail.rs:12:55
|
||||||
|
|
|
||||||
|
12 | let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
| ------- ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||||
|
| |
|
||||||
|
| required by a bound introduced by this call
|
23
actix-web-codegen/tests/trybuild/routes-ok.rs
Normal file
23
actix-web-codegen/tests/trybuild/routes-ok.rs
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
use actix_web_codegen::*;
|
||||||
|
|
||||||
|
#[routes]
|
||||||
|
#[get("/")]
|
||||||
|
#[post("/")]
|
||||||
|
async fn index() -> String {
|
||||||
|
"Hello World!".to_owned()
|
||||||
|
}
|
||||||
|
|
||||||
|
#[actix_web::main]
|
||||||
|
async fn main() {
|
||||||
|
use actix_web::App;
|
||||||
|
|
||||||
|
let srv = actix_test::start(|| App::new().service(index));
|
||||||
|
|
||||||
|
let request = srv.get("/");
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
|
||||||
|
let request = srv.post("/");
|
||||||
|
let response = request.send().await.unwrap();
|
||||||
|
assert!(response.status().is_success());
|
||||||
|
}
|
@ -1,6 +1,42 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
## Unreleased - 2021-xx-xx
|
## Unreleased - 2022-xx-xx
|
||||||
|
### Added
|
||||||
|
- Add `#[routes]` macro to support multiple paths for one handler. [#2718]
|
||||||
|
- Add `ServiceRequest::{parts, request}()` getter methods. [#2786]
|
||||||
|
- Add configuration options for TLS handshake timeout via `HttpServer::{rustls, openssl}_with_config` methods. [#2752]
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.57 due to transitive `time` dependency.
|
||||||
|
|
||||||
|
[#2718]: https://github.com/actix/actix-web/pull/2718
|
||||||
|
[#2752]: https://github.com/actix/actix-web/pull/2752
|
||||||
|
[#2786]: https://github.com/actix/actix-web/pull/2786
|
||||||
|
|
||||||
|
|
||||||
|
## 4.1.0 - 2022-06-11
|
||||||
|
### Added
|
||||||
|
- Add `ServiceRequest::extract()` to make it easier to use extractors when writing middlewares. [#2647]
|
||||||
|
- Add `Route::wrap()` to allow individual routes to use middleware. [#2725]
|
||||||
|
- Add `ServiceConfig::default_service()`. [#2338] [#2743]
|
||||||
|
- Implement `ResponseError` for `std::convert::Infallible`
|
||||||
|
|
||||||
|
### Changed
|
||||||
|
- Minimum supported Rust version (MSRV) is now 1.56 due to transitive `hashbrown` dependency.
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Clear connection-level data on `HttpRequest` drop. [#2742]
|
||||||
|
|
||||||
|
[#2338]: https://github.com/actix/actix-web/pull/2338
|
||||||
|
[#2647]: https://github.com/actix/actix-web/pull/2647
|
||||||
|
[#2725]: https://github.com/actix/actix-web/pull/2725
|
||||||
|
[#2742]: https://github.com/actix/actix-web/pull/2742
|
||||||
|
[#2743]: https://github.com/actix/actix-web/pull/2743
|
||||||
|
|
||||||
|
|
||||||
|
## 4.0.1 - 2022-02-25
|
||||||
|
### Fixed
|
||||||
|
- Use stable version in readme example.
|
||||||
|
|
||||||
|
|
||||||
## 4.0.0 - 2022-02-25
|
## 4.0.0 - 2022-02-25
|
||||||
@ -10,7 +46,7 @@
|
|||||||
- Updated `cookie` to `0.16`. [#2555]
|
- Updated `cookie` to `0.16`. [#2555]
|
||||||
- Updated `language-tags` to `0.3`.
|
- Updated `language-tags` to `0.3`.
|
||||||
- Updated `rand` to `0.8`.
|
- Updated `rand` to `0.8`.
|
||||||
- Updated `rustls` to `0.20.0`. [#2414]
|
- Updated `rustls` to `0.20`. [#2414]
|
||||||
- Updated `tokio` to `1`.
|
- Updated `tokio` to `1`.
|
||||||
|
|
||||||
### Added
|
### Added
|
||||||
@ -711,7 +747,7 @@
|
|||||||
### Removed
|
### Removed
|
||||||
- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now
|
- Public modules `middleware::{normalize, err_handlers}`. All necessary middleware types are now
|
||||||
exposed directly by the `middleware` module.
|
exposed directly by the `middleware` module.
|
||||||
- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
|
- Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
|
||||||
from `actix_web::error` module. [#1878]
|
from `actix_web::error` module. [#1878]
|
||||||
|
|
||||||
[#1812]: https://github.com/actix/actix-web/pull/1812
|
[#1812]: https://github.com/actix/actix-web/pull/1812
|
||||||
@ -813,7 +849,7 @@
|
|||||||
|
|
||||||
## 3.0.0-beta.4 - 2020-09-09
|
## 3.0.0-beta.4 - 2020-09-09
|
||||||
### Added
|
### Added
|
||||||
- `middleware::NormalizePath` now has configurable behavior for either always having a trailing
|
- `middleware::NormalizePath` now has configurable behavior for either always having a trailing
|
||||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||||
|
|
||||||
### Changed
|
### Changed
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "actix-web"
|
name = "actix-web"
|
||||||
version = "4.0.0"
|
version = "4.1.0"
|
||||||
authors = [
|
authors = [
|
||||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||||
"Rob Ede <robjtede@icloud.com>",
|
"Rob Ede <robjtede@icloud.com>",
|
||||||
@ -59,7 +59,7 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
|||||||
# Don't rely on these whatsoever. They may disappear at anytime.
|
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||||
__compress = []
|
__compress = []
|
||||||
|
|
||||||
# io-uring feature only avaiable for Linux OSes.
|
# io-uring feature only available for Linux OSes.
|
||||||
experimental-io-uring = ["actix-server/io-uring"]
|
experimental-io-uring = ["actix-server/io-uring"]
|
||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
@ -71,9 +71,9 @@ actix-service = "2"
|
|||||||
actix-utils = "3"
|
actix-utils = "3"
|
||||||
actix-tls = { version = "3", default-features = false, optional = true }
|
actix-tls = { version = "3", default-features = false, optional = true }
|
||||||
|
|
||||||
actix-http = { version = "3.0.0", features = ["http2", "ws"] }
|
actix-http = { version = "3", features = ["http2", "ws"] }
|
||||||
actix-router = "0.5.0"
|
actix-router = "0.5"
|
||||||
actix-web-codegen = { version = "4.0.0", optional = true }
|
actix-web-codegen = { version = "4", optional = true }
|
||||||
|
|
||||||
ahash = "0.7"
|
ahash = "0.7"
|
||||||
bytes = "1"
|
bytes = "1"
|
||||||
@ -84,13 +84,14 @@ derive_more = "0.99.5"
|
|||||||
encoding_rs = "0.8"
|
encoding_rs = "0.8"
|
||||||
futures-core = { version = "0.3.7", default-features = false }
|
futures-core = { version = "0.3.7", default-features = false }
|
||||||
futures-util = { version = "0.3.7", default-features = false }
|
futures-util = { version = "0.3.7", default-features = false }
|
||||||
|
http = "0.2.8"
|
||||||
itoa = "1"
|
itoa = "1"
|
||||||
language-tags = "0.3"
|
language-tags = "0.3"
|
||||||
once_cell = "1.5"
|
|
||||||
log = "0.4"
|
log = "0.4"
|
||||||
mime = "0.3"
|
mime = "0.3"
|
||||||
|
once_cell = "1.5"
|
||||||
pin-project-lite = "0.2.7"
|
pin-project-lite = "0.2.7"
|
||||||
regex = "1.4"
|
regex = "1.5.5"
|
||||||
serde = "1.0"
|
serde = "1.0"
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
serde_urlencoded = "0.7"
|
serde_urlencoded = "0.7"
|
||||||
@ -100,25 +101,25 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
|||||||
url = "2.1"
|
url = "2.1"
|
||||||
|
|
||||||
[dev-dependencies]
|
[dev-dependencies]
|
||||||
actix-files = "0.6.0-beta.16"
|
actix-files = "0.6"
|
||||||
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
|
actix-test = { version = "0.1.0-beta.13", features = ["openssl", "rustls"] }
|
||||||
awc = { version = "3.0.0-beta.21", features = ["openssl"] }
|
awc = { version = "3", features = ["openssl"] }
|
||||||
|
|
||||||
brotli = "3.3.3"
|
brotli = "3.3.3"
|
||||||
const-str = "0.3"
|
const-str = "0.4"
|
||||||
criterion = { version = "0.3", features = ["html_reports"] }
|
criterion = { version = "0.3", features = ["html_reports"] }
|
||||||
env_logger = "0.9"
|
env_logger = "0.9"
|
||||||
flate2 = "1.0.13"
|
flate2 = "1.0.13"
|
||||||
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||||
rand = "0.8"
|
rand = "0.8"
|
||||||
rcgen = "0.8"
|
rcgen = "0.9"
|
||||||
rustls-pemfile = "0.2"
|
rustls-pemfile = "1"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
static_assertions = "1"
|
static_assertions = "1"
|
||||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||||
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
tokio = { version = "1.13.1", features = ["rt-multi-thread", "macros"] }
|
||||||
zstd = "0.10"
|
zstd = "0.11"
|
||||||
|
|
||||||
[[test]]
|
[[test]]
|
||||||
name = "test_server"
|
name = "test_server"
|
||||||
|
@ -3,7 +3,7 @@
|
|||||||
This guide walks you through the process of migrating from v3.x.y to v4.x.y.
|
This guide walks you through the process of migrating from v3.x.y to v4.x.y.
|
||||||
If you are migrating to v4.x.y from an older version of Actix Web (v2.x.y or earlier), check out the other historical migration notes in this folder.
|
If you are migrating to v4.x.y from an older version of Actix Web (v2.x.y or earlier), check out the other historical migration notes in this folder.
|
||||||
|
|
||||||
This document is not designed to be exhaustive—it focuses on the most significant changes coming in v4. You can find an exhaustive changelog in the changelogs for [`actix-web`](./CHANGES.md#400---2022-02-25) and [`actix-http`](../actix-http/CHANGES.md#300---2022-02-25), complete of PR links. If you think that some of the changes that we omitted deserve to be called out in this document, please open an issue or submit a PR.
|
This document is not designed to be exhaustive—it focuses on the most significant changes in v4. You can find an exhaustive changelog in the changelogs for [`actix-web`](./CHANGES.md#400---2022-02-25) and [`actix-http`](../actix-http/CHANGES.md#300---2022-02-25), complete with PR links. If you think there are any changes that deserve to be called out in this document, please open an issue or pull request.
|
||||||
|
|
||||||
Headings marked with :warning: are **breaking behavioral changes**. They will probably not surface as compile-time errors though automated tests _might_ detect their effects on your app.
|
Headings marked with :warning: are **breaking behavioral changes**. They will probably not surface as compile-time errors though automated tests _might_ detect their effects on your app.
|
||||||
|
|
||||||
@ -29,8 +29,9 @@ Headings marked with :warning: are **breaking behavioral changes**. They will pr
|
|||||||
- [Server Must Be Polled :warning:](#server-must-be-polled-warning)
|
- [Server Must Be Polled :warning:](#server-must-be-polled-warning)
|
||||||
- [Guards API](#guards-api)
|
- [Guards API](#guards-api)
|
||||||
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
|
- [Returning `HttpResponse` synchronously](#returning-httpresponse-synchronously)
|
||||||
- [`#[actix_web::main]` and `#[tokio::main]`](#actixwebmain-and-tokiomain)
|
- [`#[actix_web::main]` and `#[tokio::main]`](#actix_webmain-and-tokiomain)
|
||||||
- [`web::block`](#webblock)
|
- [`web::block`](#webblock)
|
||||||
|
-
|
||||||
|
|
||||||
## MSRV
|
## MSRV
|
||||||
|
|
||||||
@ -111,6 +112,8 @@ The inner field for `web::Path` is now private. It was causing ambiguity when tr
|
|||||||
+ let (foo, bar) = params.into_inner();
|
+ let (foo, bar) = params.into_inner();
|
||||||
```
|
```
|
||||||
|
|
||||||
|
An alternative [path param type with public field but no `Deref` impl is available in `actix-web-lab`](https://docs.rs/actix-web-lab/0.12.0/actix_web_lab/extract/struct.Path.html).
|
||||||
|
|
||||||
## Rustls Crate Upgrade
|
## Rustls Crate Upgrade
|
||||||
|
|
||||||
Actix Web now depends on version 0.20 of `rustls`. As a result, the server config builder has changed. [See the updated example project.](https://github.com/actix/examples/tree/master/https-tls/rustls/)
|
Actix Web now depends on version 0.20 of `rustls`. As a result, the server config builder has changed. [See the updated example project.](https://github.com/actix/examples/tree/master/https-tls/rustls/)
|
||||||
@ -481,3 +484,24 @@ The `web::block` helper has changed return type from roughly `async fn(fn() -> R
|
|||||||
- let n: u32 = web::block(|| Ok(123)).await?;
|
- let n: u32 = web::block(|| Ok(123)).await?;
|
||||||
+ let n: u32 = web::block(|| Ok(123)).await??;
|
+ let n: u32 = web::block(|| Ok(123)).await??;
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## `HttpResponse` as a `ResponseError`
|
||||||
|
|
||||||
|
The implementation of `ResponseError` for `HttpResponse` has been removed.
|
||||||
|
|
||||||
|
It was common in v3 to use `HttpResponse` as an error type in fallible handlers. The problem is that `HttpResponse` contains no knowledge or reference to the source error. Being able to guarantee that an "error" response actually contains an error reference makes middleware and other parts of Actix Web more effective.
|
||||||
|
|
||||||
|
The error response builders in the `error` module were available in v3 but are now the best method for simple error responses without requiring you to implement the trait on your own custom error types. These builders can receive simple strings and third party errors that can not implement the `ResponseError` trait.
|
||||||
|
|
||||||
|
A few common patterns are affected by this change:
|
||||||
|
|
||||||
|
```diff
|
||||||
|
- Err(HttpResponse::InternalServerError().finish())
|
||||||
|
+ Err(error::ErrorInternalServerError("reason"))
|
||||||
|
|
||||||
|
- Err(HttpResponse::InternalServerError().body(third_party_error.to_string()))
|
||||||
|
+ Err(error::ErrorInternalServerError(err))
|
||||||
|
|
||||||
|
- .map_err(|err| HttpResponse::InternalServerError().finish())?
|
||||||
|
+ .map_err(error::ErrorInternalServerError)?
|
||||||
|
```
|
||||||
|
@ -6,10 +6,10 @@
|
|||||||
<p>
|
<p>
|
||||||
|
|
||||||
[](https://crates.io/crates/actix-web)
|
[](https://crates.io/crates/actix-web)
|
||||||
[](https://docs.rs/actix-web/4.0.0)
|
[](https://docs.rs/actix-web/4.1.0)
|
||||||

|

|
||||||

|

|
||||||
[](https://deps.rs/crate/actix-web/4.0.0)
|
[](https://deps.rs/crate/actix-web/4.1.0)
|
||||||
<br />
|
<br />
|
||||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||||
[](https://codecov.io/gh/actix/actix-web)
|
[](https://codecov.io/gh/actix/actix-web)
|
||||||
@ -33,7 +33,7 @@
|
|||||||
- SSL support using OpenSSL or Rustls
|
- SSL support using OpenSSL or Rustls
|
||||||
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||||
- Integrates with the [`awc` HTTP client](https://docs.rs/awc/)
|
- Integrates with the [`awc` HTTP client](https://docs.rs/awc/)
|
||||||
- Runs on stable Rust 1.54+
|
- Runs on stable Rust 1.57+
|
||||||
|
|
||||||
## Documentation
|
## Documentation
|
||||||
|
|
||||||
@ -48,7 +48,7 @@ Dependencies:
|
|||||||
|
|
||||||
```toml
|
```toml
|
||||||
[dependencies]
|
[dependencies]
|
||||||
actix-web = "4.0.0"
|
actix-web = "4"
|
||||||
```
|
```
|
||||||
|
|
||||||
Code:
|
Code:
|
||||||
@ -56,18 +56,19 @@ Code:
|
|||||||
```rust
|
```rust
|
||||||
use actix_web::{get, web, App, HttpServer, Responder};
|
use actix_web::{get, web, App, HttpServer, Responder};
|
||||||
|
|
||||||
#[get("/{id}/{name}/index.html")]
|
#[get("/hello/{name}")]
|
||||||
async fn index(params: web::Path<(u32, String)>) -> impl Responder {
|
async fn greet(name: web::Path<String>) -> impl Responder {
|
||||||
let (id, name) = params.into_inner();
|
format!("Hello {name}!")
|
||||||
format!("Hello {}! id:{}", name, id)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[actix_web::main] // or #[tokio::main]
|
#[actix_web::main] // or #[tokio::main]
|
||||||
async fn main() -> std::io::Result<()> {
|
async fn main() -> std::io::Result<()> {
|
||||||
HttpServer::new(|| App::new().service(index))
|
HttpServer::new(|| {
|
||||||
.bind(("127.0.0.1", 8080))?
|
App::new().service(greet)
|
||||||
.run()
|
})
|
||||||
.await
|
.bind(("127.0.0.1", 8080))?
|
||||||
|
.run()
|
||||||
|
.await
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
||||||
@ -78,6 +79,7 @@ async fn main() -> std::io::Result<()> {
|
|||||||
- [Application State](https://github.com/actix/examples/tree/master/basics/state)
|
- [Application State](https://github.com/actix/examples/tree/master/basics/state)
|
||||||
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json)
|
- [JSON Handling](https://github.com/actix/examples/tree/master/json/json)
|
||||||
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart)
|
- [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart)
|
||||||
|
- [MongoDB Integration](https://github.com/actix/examples/tree/master/databases/mongodb)
|
||||||
- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel)
|
- [Diesel Integration](https://github.com/actix/examples/tree/master/databases/diesel)
|
||||||
- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite)
|
- [SQLite Integration](https://github.com/actix/examples/tree/master/databases/sqlite)
|
||||||
- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres)
|
- [Postgres Integration](https://github.com/actix/examples/tree/master/databases/postgres)
|
||||||
|
@ -60,7 +60,7 @@ where
|
|||||||
/// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime.
|
/// [`HttpRequest::app_data`](crate::HttpRequest::app_data) method at runtime.
|
||||||
///
|
///
|
||||||
/// # [`Data<T>`]
|
/// # [`Data<T>`]
|
||||||
/// Any [`Data<T>`] type added here can utilize it's extractor implementation in handlers.
|
/// Any [`Data<T>`] type added here can utilize its extractor implementation in handlers.
|
||||||
/// Types not wrapped in `Data<T>` cannot use this extractor. See [its docs](Data<T>) for more
|
/// Types not wrapped in `Data<T>` cannot use this extractor. See [its docs](Data<T>) for more
|
||||||
/// about its usage and patterns.
|
/// about its usage and patterns.
|
||||||
///
|
///
|
||||||
@ -185,10 +185,17 @@ where
|
|||||||
F: FnOnce(&mut ServiceConfig),
|
F: FnOnce(&mut ServiceConfig),
|
||||||
{
|
{
|
||||||
let mut cfg = ServiceConfig::new();
|
let mut cfg = ServiceConfig::new();
|
||||||
|
|
||||||
f(&mut cfg);
|
f(&mut cfg);
|
||||||
|
|
||||||
self.services.extend(cfg.services);
|
self.services.extend(cfg.services);
|
||||||
self.external.extend(cfg.external);
|
self.external.extend(cfg.external);
|
||||||
self.extensions.extend(cfg.app_data);
|
self.extensions.extend(cfg.app_data);
|
||||||
|
|
||||||
|
if let Some(default) = cfg.default {
|
||||||
|
self.default = Some(default);
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -267,7 +274,6 @@ where
|
|||||||
{
|
{
|
||||||
let svc = svc
|
let svc = svc
|
||||||
.into_factory()
|
.into_factory()
|
||||||
.map(|res| res.map_into_boxed_body())
|
|
||||||
.map_init_err(|e| log::error!("Can not construct default service: {:?}", e));
|
.map_init_err(|e| log::error!("Can not construct default service: {:?}", e));
|
||||||
|
|
||||||
self.default = Some(Rc::new(boxed::factory(svc)));
|
self.default = Some(Rc::new(boxed::factory(svc)));
|
||||||
@ -308,7 +314,7 @@ where
|
|||||||
|
|
||||||
/// Registers an app-wide middleware.
|
/// Registers an app-wide middleware.
|
||||||
///
|
///
|
||||||
/// Registers middleware, in the form of a middleware compo nen t (type), that runs during
|
/// Registers middleware, in the form of a middleware component (type), that runs during
|
||||||
/// inbound and/or outbound processing in the request life-cycle (request -> response),
|
/// inbound and/or outbound processing in the request life-cycle (request -> response),
|
||||||
/// modifying request/response as necessary, across all requests managed by the `App`.
|
/// modifying request/response as necessary, across all requests managed by the `App`.
|
||||||
///
|
///
|
||||||
|
@ -257,7 +257,7 @@ impl ServiceFactory<ServiceRequest> for AppRoutingFactory {
|
|||||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||||
|
|
||||||
fn new_service(&self, _: ()) -> Self::Future {
|
fn new_service(&self, _: ()) -> Self::Future {
|
||||||
// construct all services factory future with it's resource def and guards.
|
// construct all services factory future with its resource def and guards.
|
||||||
let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| {
|
let factory_fut = join_all(self.services.iter().map(|(path, factory, guards)| {
|
||||||
let path = path.clone();
|
let path = path.clone();
|
||||||
let guards = guards.borrow_mut().take().unwrap_or_default();
|
let guards = guards.borrow_mut().take().unwrap_or_default();
|
||||||
|
@ -1,33 +1,32 @@
|
|||||||
use std::net::SocketAddr;
|
use std::{net::SocketAddr, rc::Rc};
|
||||||
use std::rc::Rc;
|
|
||||||
|
|
||||||
use actix_http::Extensions;
|
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
|
||||||
use actix_router::ResourceDef;
|
|
||||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
|
|
||||||
|
|
||||||
use crate::data::Data;
|
use crate::{
|
||||||
use crate::error::Error;
|
data::Data,
|
||||||
use crate::guard::Guard;
|
dev::{Extensions, ResourceDef},
|
||||||
use crate::resource::Resource;
|
error::Error,
|
||||||
use crate::rmap::ResourceMap;
|
guard::Guard,
|
||||||
use crate::route::Route;
|
resource::Resource,
|
||||||
use crate::service::{
|
rmap::ResourceMap,
|
||||||
AppServiceFactory, HttpServiceFactory, ServiceFactoryWrapper, ServiceRequest,
|
route::Route,
|
||||||
ServiceResponse,
|
service::{
|
||||||
|
AppServiceFactory, BoxedHttpServiceFactory, HttpServiceFactory, ServiceFactoryWrapper,
|
||||||
|
ServiceRequest, ServiceResponse,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
type Guards = Vec<Box<dyn Guard>>;
|
type Guards = Vec<Box<dyn Guard>>;
|
||||||
type HttpNewService = boxed::BoxServiceFactory<(), ServiceRequest, ServiceResponse, Error, ()>;
|
|
||||||
|
|
||||||
/// Application configuration
|
/// Application configuration
|
||||||
pub struct AppService {
|
pub struct AppService {
|
||||||
config: AppConfig,
|
config: AppConfig,
|
||||||
root: bool,
|
root: bool,
|
||||||
default: Rc<HttpNewService>,
|
default: Rc<BoxedHttpServiceFactory>,
|
||||||
#[allow(clippy::type_complexity)]
|
#[allow(clippy::type_complexity)]
|
||||||
services: Vec<(
|
services: Vec<(
|
||||||
ResourceDef,
|
ResourceDef,
|
||||||
HttpNewService,
|
BoxedHttpServiceFactory,
|
||||||
Option<Guards>,
|
Option<Guards>,
|
||||||
Option<Rc<ResourceMap>>,
|
Option<Rc<ResourceMap>>,
|
||||||
)>,
|
)>,
|
||||||
@ -35,7 +34,7 @@ pub struct AppService {
|
|||||||
|
|
||||||
impl AppService {
|
impl AppService {
|
||||||
/// Crate server settings instance.
|
/// Crate server settings instance.
|
||||||
pub(crate) fn new(config: AppConfig, default: Rc<HttpNewService>) -> Self {
|
pub(crate) fn new(config: AppConfig, default: Rc<BoxedHttpServiceFactory>) -> Self {
|
||||||
AppService {
|
AppService {
|
||||||
config,
|
config,
|
||||||
default,
|
default,
|
||||||
@ -56,7 +55,7 @@ impl AppService {
|
|||||||
AppConfig,
|
AppConfig,
|
||||||
Vec<(
|
Vec<(
|
||||||
ResourceDef,
|
ResourceDef,
|
||||||
HttpNewService,
|
BoxedHttpServiceFactory,
|
||||||
Option<Guards>,
|
Option<Guards>,
|
||||||
Option<Rc<ResourceMap>>,
|
Option<Rc<ResourceMap>>,
|
||||||
)>,
|
)>,
|
||||||
@ -81,7 +80,7 @@ impl AppService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Returns default handler factory.
|
/// Returns default handler factory.
|
||||||
pub fn default_service(&self) -> Rc<HttpNewService> {
|
pub fn default_service(&self) -> Rc<BoxedHttpServiceFactory> {
|
||||||
self.default.clone()
|
self.default.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -154,6 +153,16 @@ impl AppConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
impl Default for AppConfig {
|
impl Default for AppConfig {
|
||||||
|
/// Returns the default AppConfig.
|
||||||
|
/// Note: The included socket address is "127.0.0.1".
|
||||||
|
///
|
||||||
|
/// 127.0.0.1: non-routable meta address that denotes an unknown, invalid or non-applicable target.
|
||||||
|
/// If you need a service only accessed by itself, use a loopback address.
|
||||||
|
/// A loopback address for IPv4 is any loopback address that begins with "127".
|
||||||
|
/// Loopback addresses should be only used to test your application locally.
|
||||||
|
/// The default configuration provides a loopback address.
|
||||||
|
///
|
||||||
|
/// 0.0.0.0: if configured to use this special address, the application will listen to any IP address configured on the machine.
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
AppConfig::new(
|
AppConfig::new(
|
||||||
false,
|
false,
|
||||||
@ -187,6 +196,7 @@ pub struct ServiceConfig {
|
|||||||
pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
|
pub(crate) services: Vec<Box<dyn AppServiceFactory>>,
|
||||||
pub(crate) external: Vec<ResourceDef>,
|
pub(crate) external: Vec<ResourceDef>,
|
||||||
pub(crate) app_data: Extensions,
|
pub(crate) app_data: Extensions,
|
||||||
|
pub(crate) default: Option<Rc<BoxedHttpServiceFactory>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ServiceConfig {
|
impl ServiceConfig {
|
||||||
@ -195,6 +205,7 @@ impl ServiceConfig {
|
|||||||
services: Vec::new(),
|
services: Vec::new(),
|
||||||
external: Vec::new(),
|
external: Vec::new(),
|
||||||
app_data: Extensions::new(),
|
app_data: Extensions::new(),
|
||||||
|
default: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -215,6 +226,29 @@ impl ServiceConfig {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Default service to be used if no matching resource could be found.
|
||||||
|
///
|
||||||
|
/// Counterpart to [`App::default_service()`](crate::App::default_service).
|
||||||
|
pub fn default_service<F, U>(&mut self, f: F) -> &mut Self
|
||||||
|
where
|
||||||
|
F: IntoServiceFactory<U, ServiceRequest>,
|
||||||
|
U: ServiceFactory<
|
||||||
|
ServiceRequest,
|
||||||
|
Config = (),
|
||||||
|
Response = ServiceResponse,
|
||||||
|
Error = Error,
|
||||||
|
> + 'static,
|
||||||
|
U::InitError: std::fmt::Debug,
|
||||||
|
{
|
||||||
|
let svc = f
|
||||||
|
.into_factory()
|
||||||
|
.map_init_err(|err| log::error!("Can not construct default service: {:?}", err));
|
||||||
|
|
||||||
|
self.default = Some(Rc::new(boxed::factory(svc)));
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
/// Run external configuration as part of the application building process
|
/// Run external configuration as part of the application building process
|
||||||
///
|
///
|
||||||
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
|
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
|
||||||
@ -322,6 +356,38 @@ mod tests {
|
|||||||
assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
|
assert_eq!(body, Bytes::from_static(b"https://youtube.com/watch/12345"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[actix_rt::test]
|
||||||
|
async fn registers_default_service() {
|
||||||
|
let srv = init_service(
|
||||||
|
App::new()
|
||||||
|
.configure(|cfg| {
|
||||||
|
cfg.default_service(
|
||||||
|
web::get().to(|| HttpResponse::NotFound().body("four oh four")),
|
||||||
|
);
|
||||||
|
})
|
||||||
|
.service(web::scope("/scoped").configure(|cfg| {
|
||||||
|
cfg.default_service(
|
||||||
|
web::get().to(|| HttpResponse::NotFound().body("scoped four oh four")),
|
||||||
|
);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// app registers default service
|
||||||
|
let req = TestRequest::with_uri("/path/i/did/not-configure").to_request();
|
||||||
|
let resp = call_service(&srv, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
let body = read_body(resp).await;
|
||||||
|
assert_eq!(body, Bytes::from_static(b"four oh four"));
|
||||||
|
|
||||||
|
// scope registers default service
|
||||||
|
let req = TestRequest::with_uri("/scoped/path/i/did/not-configure").to_request();
|
||||||
|
let resp = call_service(&srv, req).await;
|
||||||
|
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||||
|
let body = read_body(resp).await;
|
||||||
|
assert_eq!(body, Bytes::from_static(b"scoped four oh four"));
|
||||||
|
}
|
||||||
|
|
||||||
#[actix_rt::test]
|
#[actix_rt::test]
|
||||||
async fn test_service() {
|
async fn test_service() {
|
||||||
let srv = init_service(App::new().configure(|cfg| {
|
let srv = init_service(App::new().configure(|cfg| {
|
||||||
|
@ -5,10 +5,7 @@ use actix_utils::future::{err, ok, Ready};
|
|||||||
use futures_core::future::LocalBoxFuture;
|
use futures_core::future::LocalBoxFuture;
|
||||||
use serde::Serialize;
|
use serde::Serialize;
|
||||||
|
|
||||||
use crate::{
|
use crate::{dev::Payload, error, Error, FromRequest, HttpRequest};
|
||||||
dev::Payload, error::ErrorInternalServerError, extract::FromRequest, request::HttpRequest,
|
|
||||||
Error,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// Data factory.
|
/// Data factory.
|
||||||
pub(crate) trait DataFactory {
|
pub(crate) trait DataFactory {
|
||||||
@ -121,7 +118,7 @@ impl<T: ?Sized> Deref for Data<T> {
|
|||||||
|
|
||||||
impl<T: ?Sized> Clone for Data<T> {
|
impl<T: ?Sized> Clone for Data<T> {
|
||||||
fn clone(&self) -> Data<T> {
|
fn clone(&self) -> Data<T> {
|
||||||
Data(self.0.clone())
|
Data(Arc::clone(&self.0))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -160,7 +157,7 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
|
|||||||
req.match_name().unwrap_or_else(|| req.path())
|
req.match_name().unwrap_or_else(|| req.path())
|
||||||
);
|
);
|
||||||
|
|
||||||
err(ErrorInternalServerError(
|
err(error::ErrorInternalServerError(
|
||||||
"Requested application data is not configured correctly. \
|
"Requested application data is not configured correctly. \
|
||||||
View/enable debug logs for more details.",
|
View/enable debug logs for more details.",
|
||||||
))
|
))
|
||||||
|
@ -51,12 +51,6 @@ impl StdError for Error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<std::convert::Infallible> for Error {
|
|
||||||
fn from(val: std::convert::Infallible) -> Self {
|
|
||||||
match val {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// `Error` for any error that implements `ResponseError`
|
/// `Error` for any error that implements `ResponseError`
|
||||||
impl<T: ResponseError + 'static> From<T> for Error {
|
impl<T: ResponseError + 'static> From<T> for Error {
|
||||||
fn from(err: T) -> Error {
|
fn from(err: T) -> Error {
|
||||||
|
@ -42,7 +42,7 @@ pub struct BlockingError;
|
|||||||
impl ResponseError for crate::error::BlockingError {}
|
impl ResponseError for crate::error::BlockingError {}
|
||||||
|
|
||||||
/// Errors which can occur when attempting to generate resource uri.
|
/// Errors which can occur when attempting to generate resource uri.
|
||||||
#[derive(Debug, PartialEq, Display, Error, From)]
|
#[derive(Debug, PartialEq, Eq, Display, Error, From)]
|
||||||
#[non_exhaustive]
|
#[non_exhaustive]
|
||||||
pub enum UrlGenerationError {
|
pub enum UrlGenerationError {
|
||||||
/// Resource not found.
|
/// Resource not found.
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
//! `ResponseError` trait and foreign impls.
|
//! `ResponseError` trait and foreign impls.
|
||||||
|
|
||||||
use std::{
|
use std::{
|
||||||
|
convert::Infallible,
|
||||||
error::Error as StdError,
|
error::Error as StdError,
|
||||||
fmt,
|
fmt,
|
||||||
io::{self, Write as _},
|
io::{self, Write as _},
|
||||||
@ -54,6 +55,15 @@ downcast_dyn!(ResponseError);
|
|||||||
|
|
||||||
impl ResponseError for Box<dyn StdError + 'static> {}
|
impl ResponseError for Box<dyn StdError + 'static> {}
|
||||||
|
|
||||||
|
impl ResponseError for Infallible {
|
||||||
|
fn status_code(&self) -> StatusCode {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
fn error_response(&self) -> HttpResponse<BoxBody> {
|
||||||
|
match *self {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(feature = "openssl")]
|
#[cfg(feature = "openssl")]
|
||||||
impl ResponseError for actix_tls::accept::openssl::reexports::Error {}
|
impl ResponseError for actix_tls::accept::openssl::reexports::Error {}
|
||||||
|
|
||||||
|
@ -18,9 +18,11 @@ use crate::{dev::Payload, Error, HttpRequest};
|
|||||||
/// A type that implements [`FromRequest`] is called an **extractor** and can extract data from
|
/// A type that implements [`FromRequest`] is called an **extractor** and can extract data from
|
||||||
/// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`].
|
/// the request. Some types that implement this trait are: [`Json`], [`Header`], and [`Path`].
|
||||||
///
|
///
|
||||||
|
/// Check out [`ServiceRequest::extract`](crate::dev::ServiceRequest::extract) if you want to
|
||||||
|
/// leverage extractors when implementing middlewares.
|
||||||
|
///
|
||||||
/// # Configuration
|
/// # Configuration
|
||||||
/// An extractor can be customized by injecting the corresponding configuration with one of:
|
/// An extractor can be customized by injecting the corresponding configuration with one of:
|
||||||
///
|
|
||||||
/// - [`App::app_data()`][crate::App::app_data]
|
/// - [`App::app_data()`][crate::App::app_data]
|
||||||
/// - [`Scope::app_data()`][crate::Scope::app_data]
|
/// - [`Scope::app_data()`][crate::Scope::app_data]
|
||||||
/// - [`Resource::app_data()`][crate::Resource::app_data]
|
/// - [`Resource::app_data()`][crate::Resource::app_data]
|
||||||
@ -64,13 +66,29 @@ pub trait FromRequest: Sized {
|
|||||||
/// The associated error which can be returned.
|
/// The associated error which can be returned.
|
||||||
type Error: Into<Error>;
|
type Error: Into<Error>;
|
||||||
|
|
||||||
/// Future that resolves to a Self.
|
/// Future that resolves to a `Self`.
|
||||||
|
///
|
||||||
|
/// To use an async function or block, the futures must be boxed. The following snippet will be
|
||||||
|
/// common when creating async/await extractors (that do not consume the body).
|
||||||
|
///
|
||||||
|
/// ```ignore
|
||||||
|
/// type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
|
||||||
|
/// // or
|
||||||
|
/// type Future = futures_util::future::LocalBoxFuture<'static, Result<Self, Self::Error>>;
|
||||||
|
///
|
||||||
|
/// fn from_request(req: HttpRequest, ...) -> Self::Future {
|
||||||
|
/// let req = req.clone();
|
||||||
|
/// Box::pin(async move {
|
||||||
|
/// ...
|
||||||
|
/// })
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
type Future: Future<Output = Result<Self, Self::Error>>;
|
type Future: Future<Output = Result<Self, Self::Error>>;
|
||||||
|
|
||||||
/// Create a Self from request parts asynchronously.
|
/// Create a `Self` from request parts asynchronously.
|
||||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future;
|
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future;
|
||||||
|
|
||||||
/// Create a Self from request head asynchronously.
|
/// Create a `Self` from request head asynchronously.
|
||||||
///
|
///
|
||||||
/// This method is short for `T::from_request(req, &mut Payload::None)`.
|
/// This method is short for `T::from_request(req, &mut Payload::None)`.
|
||||||
fn extract(req: &HttpRequest) -> Self::Future {
|
fn extract(req: &HttpRequest) -> Self::Future {
|
||||||
@ -78,9 +96,9 @@ pub trait FromRequest: Sized {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optionally extract a field from the request
|
/// Optionally extract from the request.
|
||||||
///
|
///
|
||||||
/// If the FromRequest for T fails, return None rather than returning an error response
|
/// If the inner `T::from_request` returns an error, handler will receive `None` instead.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
@ -165,9 +183,10 @@ where
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Optionally extract a field from the request or extract the Error if unsuccessful
|
/// Extract from the request, passing error type through to handler.
|
||||||
///
|
///
|
||||||
/// If the `FromRequest` for T fails, inject Err into handler rather than returning an error response
|
/// If the inner `T::from_request` returns an error, allow handler to receive the error rather than
|
||||||
|
/// immediately returning an error response.
|
||||||
///
|
///
|
||||||
/// # Examples
|
/// # Examples
|
||||||
/// ```
|
/// ```
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user