mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 09:36:36 +02:00
Compare commits
55 Commits
files-v0.6
...
camel-case
Author | SHA1 | Date | |
---|---|---|---|
1fe04cf51e | |||
3c7ccf5521 | |||
e7cae5a95b | |||
455d5c460d | |||
8faca783fa | |||
edbb9b047e | |||
32742d0715 | |||
d90c1a2331 | |||
2a12b41456 | |||
6c97d448b7 | |||
c3ce33df05 | |||
4431c8da65 | |||
2d11ab5977 | |||
4ebf16890d | |||
fe0bbfb3da | |||
2462b6dd5d | |||
49cfabeaf5 | |||
0f7292c69a | |||
8bbf2b5052 | |||
8c975bcc1f | |||
742ad56d30 | |||
bcc8d5c441 | |||
f659098d21 | |||
8621ae12f8 | |||
b338eb8473 | |||
5abd1c2c2c | |||
05336269f9 | |||
86df295ee2 | |||
85c9b1a263 | |||
577597a80a | |||
374dc9bfc9 | |||
93754f307f | |||
c7639bc3be | |||
0bc4ae9158 | |||
19a46e3925 | |||
68cd853aa2 | |||
25fe1bbaa5 | |||
e890307091 | |||
b708924590 | |||
5dcb250237 | |||
b4ff6addfe | |||
231a24ef8d | |||
6df4974234 | |||
a80e93d6db | |||
542c92c9a7 | |||
74738c63a7 | |||
a87e01f0d1 | |||
9779010a5a | |||
11d50d792b | |||
798e9911e9 | |||
2b2de29800 | |||
0f5c876c6b | |||
96a4dc9dec | |||
4616ca8ee6 | |||
36193b0a50 |
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@ -1,8 +1,6 @@
|
||||
name: Benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
|
87
.github/workflows/ci-master.yml
vendored
87
.github/workflows/ci-master.yml
vendored
@ -5,6 +5,93 @@ on:
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build_and_test_nightly:
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
target:
|
||||
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- nightly
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
env:
|
||||
CI: 1
|
||||
CARGO_INCREMENTAL: 0
|
||||
VCPKGRS_DYNAMIC: 1
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
# install OpenSSL on Windows
|
||||
# TODO: GitHub actions docs state that OpenSSL is
|
||||
# already installed on these Windows machines somewhere
|
||||
- name: Set vcpkg root
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
|
||||
- name: Install OpenSSL
|
||||
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
|
||||
run: vcpkg install openssl:x64-windows
|
||||
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
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: Install cargo-hack
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: install
|
||||
args: cargo-hack
|
||||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-min }
|
||||
|
||||
- name: check default
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-default }
|
||||
|
||||
- name: tests
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- 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
|
||||
run: |
|
||||
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
||||
|
||||
ci_feature_powerset_check:
|
||||
name: Verify Feature Combinations
|
||||
runs-on: ubuntu-latest
|
||||
|
3
.github/workflows/ci.yml
vendored
3
.github/workflows/ci.yml
vendored
@ -16,9 +16,8 @@ jobs:
|
||||
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
|
||||
- { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
|
||||
version:
|
||||
- 1.52.0 # MSRV
|
||||
- 1.54.0 # MSRV
|
||||
- stable
|
||||
- nightly
|
||||
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
11
.github/workflows/clippy-fmt.yml
vendored
11
.github/workflows/clippy-fmt.yml
vendored
@ -14,6 +14,7 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: rustfmt
|
||||
- name: Check with rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
@ -30,10 +31,18 @@ jobs:
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
profile: minimal
|
||||
components: clippy
|
||||
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: Check with Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --workspace --all-features --tests
|
||||
args: --workspace --tests --examples --all-features
|
||||
|
73
CHANGES.md
73
CHANGES.md
@ -3,6 +3,79 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 4.0.0-beta.20 - 2022-01-14
|
||||
### Added
|
||||
- `GuardContext::header` [#2569]
|
||||
- `ServiceConfig::configure` to allow easy nesting of configuration functions. [#1988]
|
||||
|
||||
### Changed
|
||||
- `HttpResponse` can now be used as a `Responder` with any body type. [#2567]
|
||||
- `Result` extractor wrapper can now convert error types. [#2581]
|
||||
- Associated types in `FromRequest` impl for `Option` and `Result` has changed. [#2581]
|
||||
- Maximum number of handler extractors has increased to 12. [#2582]
|
||||
- Removed bound `<B as MessageBody>::Error: Debug` in test utility functions in order to support returning opaque apps. [#2584]
|
||||
|
||||
[#1988]: https://github.com/actix/actix-web/pull/1988
|
||||
[#2567]: https://github.com/actix/actix-web/pull/2567
|
||||
[#2569]: https://github.com/actix/actix-web/pull/2569
|
||||
[#2581]: https://github.com/actix/actix-web/pull/2581
|
||||
[#2582]: https://github.com/actix/actix-web/pull/2582
|
||||
[#2584]: https://github.com/actix/actix-web/pull/2584
|
||||
|
||||
|
||||
## 4.0.0-beta.19 - 2022-01-04
|
||||
### Added
|
||||
- `impl Hash` for `http::header::Encoding`. [#2501]
|
||||
- `AcceptEncoding::negotiate()`. [#2501]
|
||||
|
||||
### Changed
|
||||
- `AcceptEncoding::preference` now returns `Option<Preference<Encoding>>`. [#2501]
|
||||
- Rename methods `BodyEncoding::{encoding => encode_with, get_encoding => preferred_encoding}`. [#2501]
|
||||
- `http::header::Encoding` now only represents `Content-Encoding` types. [#2501]
|
||||
|
||||
### Fixed
|
||||
- Auto-negotiation of content encoding is more fault-tolerant when using the `Compress` middleware. [#2501]
|
||||
|
||||
### Removed
|
||||
- `Compress::new`; restricting compression algorithm is done through feature flags. [#2501]
|
||||
- `BodyEncoding` trait; signalling content encoding is now only done via the `Content-Encoding` header. [#2565]
|
||||
|
||||
[#2501]: https://github.com/actix/actix-web/pull/2501
|
||||
[#2565]: https://github.com/actix/actix-web/pull/2565
|
||||
|
||||
|
||||
## 4.0.0-beta.18 - 2021-12-29
|
||||
### Changed
|
||||
- Update `cookie` dependency (re-exported) to `0.16`. [#2555]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
### Security
|
||||
- `cookie` upgrade addresses [`RUSTSEC-2020-0071`].
|
||||
|
||||
[#2555]: https://github.com/actix/actix-web/pull/2555
|
||||
[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html
|
||||
|
||||
|
||||
## 4.0.0-beta.17 - 2021-12-29
|
||||
### Added
|
||||
- `guard::GuardContext` for use with the `Guard` trait. [#2552]
|
||||
- `ServiceRequest::guard_ctx` for obtaining a guard context. [#2552]
|
||||
|
||||
### Changed
|
||||
- `Guard` trait now receives a `&GuardContext`. [#2552]
|
||||
- `guard::fn_guard` functions now receives a `&GuardContext`. [#2552]
|
||||
- Some guards now return `impl Guard` and their concrete types are made private: `guard::Header` and all the method guards. [#2552]
|
||||
- The `Not` guard is now generic over the type of guard it wraps. [#2552]
|
||||
|
||||
### Fixed
|
||||
- Rename `ConnectionInfo::{remote_addr => peer_addr}`, deprecating the old name. [#2554]
|
||||
- `ConnectionInfo::peer_addr` will not return the port number. [#2554]
|
||||
- `ConnectionInfo::realip_remote_addr` will not return the port number if sourcing the IP from the peer's socket address. [#2554]
|
||||
|
||||
[#2552]: https://github.com/actix/actix-web/pull/2552
|
||||
[#2554]: https://github.com/actix/actix-web/pull/2554
|
||||
|
||||
|
||||
## 4.0.0-beta.16 - 2021-12-27
|
||||
### Changed
|
||||
- No longer require `Scope` service body type to be boxed. [#2523]
|
||||
|
36
Cargo.toml
36
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.16"
|
||||
version = "4.0.0-beta.20"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
@ -28,15 +28,15 @@ path = "src/lib.rs"
|
||||
resolver = "2"
|
||||
members = [
|
||||
".",
|
||||
"awc",
|
||||
"actix-http",
|
||||
"actix-files",
|
||||
"actix-http-test",
|
||||
"actix-http",
|
||||
"actix-multipart",
|
||||
"actix-router",
|
||||
"actix-test",
|
||||
"actix-web-actors",
|
||||
"actix-web-codegen",
|
||||
"actix-http-test",
|
||||
"actix-test",
|
||||
"actix-router",
|
||||
"awc",
|
||||
]
|
||||
|
||||
[features]
|
||||
@ -71,20 +71,20 @@ experimental-io-uring = ["actix-server/io-uring"]
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-macros = "0.2.3"
|
||||
actix-rt = "2.3"
|
||||
actix-server = "2.0.0-rc.2"
|
||||
actix-rt = "2.6"
|
||||
actix-server = "2.0.0-rc.4"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-router = "0.5.0-beta.3"
|
||||
actix-web-codegen = "0.5.0-beta.6"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-router = "0.5.0-rc.1"
|
||||
actix-web-codegen = "0.5.0-rc.1"
|
||||
|
||||
ahash = "0.7"
|
||||
bytes = "1"
|
||||
cfg-if = "1"
|
||||
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||
derive_more = "0.99.5"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
@ -94,7 +94,6 @@ language-tags = "0.3"
|
||||
once_cell = "1.5"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
paste = "1"
|
||||
pin-project-lite = "0.2.7"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
@ -106,10 +105,12 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
||||
url = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.15", features = ["openssl"] }
|
||||
actix-files = "0.6.0-beta.14"
|
||||
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.18", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
@ -117,6 +118,7 @@ futures-util = { version = "0.3.7", default-features = false, features = ["std"]
|
||||
rand = "0.8"
|
||||
rcgen = "0.8"
|
||||
rustls-pemfile = "0.2"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
zstd = "0.9"
|
||||
@ -164,7 +166,7 @@ name = "uds"
|
||||
required-features = ["compress-gzip"]
|
||||
|
||||
[[example]]
|
||||
name = "on_connect"
|
||||
name = "on-connect"
|
||||
required-features = []
|
||||
|
||||
[[bench]]
|
||||
|
10
README.md
10
README.md
@ -6,12 +6,12 @@
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.16)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.20)
|
||||

|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.16)
|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.20)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions)
|
||||
[](https://github.com/actix/actix-web/actions/workflows/ci.yml)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||

|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
@ -32,7 +32,7 @@
|
||||
- SSL support using OpenSSL or Rustls
|
||||
- Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
- Includes an async [HTTP client](https://docs.rs/awc/)
|
||||
- Runs on stable Rust 1.52+
|
||||
- Runs on stable Rust 1.54+
|
||||
|
||||
## Documentation
|
||||
|
||||
|
@ -3,8 +3,26 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.6.0-beta.14 - 2022-01-14
|
||||
- The `prefer_utf8` option introduced in `0.4.0` is now true by default. [#2583]
|
||||
|
||||
[#2583]: https://github.com/actix/actix-web/pull/2583
|
||||
|
||||
|
||||
## 0.6.0-beta.13 - 2022-01-04
|
||||
- The `Files` service now rejects requests with URL paths that include `%2F` (decoded: `/`). [#2398]
|
||||
- The `Files` service now correctly decodes `%25` in the URL path to `%` for the file path. [#2398]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
[#2398]: https://github.com/actix/actix-web/pull/2398
|
||||
|
||||
|
||||
## 0.6.0-beta.12 - 2021-12-29
|
||||
- No significant changes since `0.6.0-beta.11`.
|
||||
|
||||
|
||||
## 0.6.0-beta.11 - 2021-12-27
|
||||
* No significant changes since `0.6.0-beta.10`.
|
||||
- No significant changes since `0.6.0-beta.10`.
|
||||
|
||||
|
||||
## 0.6.0-beta.10 - 2021-12-11
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0-beta.11"
|
||||
version = "0.6.0-beta.14"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@ -22,10 +22,10 @@ path = "src/lib.rs"
|
||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
actix-web = { version = "4.0.0-beta.16", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false }
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
@ -39,9 +39,10 @@ mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
|
||||
tokio-uring = { version = "0.1", optional = true }
|
||||
tokio-uring = { version = "0.2", optional = true, features = ["bytes"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.10"
|
||||
actix-web = "4.0.0-beta.16"
|
||||
actix-test = "0.1.0-beta.11"
|
||||
actix-web = "4.0.0-beta.20"
|
||||
tempfile = "3.2"
|
||||
|
@ -3,11 +3,11 @@
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.11)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.14)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.11)
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.14)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
@ -15,4 +15,4 @@
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-files/)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -10,6 +10,9 @@ use actix_web::{error::Error, web::Bytes};
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
use bytes::BytesMut;
|
||||
|
||||
use super::named::File;
|
||||
|
||||
pin_project! {
|
||||
@ -214,64 +217,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
use bytes_mut::BytesMut;
|
||||
|
||||
// TODO: remove new type and use bytes::BytesMut directly
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
mod bytes_mut {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use tokio_uring::buf::{IoBuf, IoBufMut};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BytesMut(bytes::BytesMut);
|
||||
|
||||
impl BytesMut {
|
||||
pub(super) fn new() -> Self {
|
||||
Self(bytes::BytesMut::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for BytesMut {
|
||||
type Target = bytes::BytesMut;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for BytesMut {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl IoBuf for BytesMut {
|
||||
fn stable_ptr(&self) -> *const u8 {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
|
||||
fn bytes_init(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn bytes_total(&self) -> usize {
|
||||
self.0.capacity()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl IoBufMut for BytesMut {
|
||||
fn stable_mut_ptr(&mut self) -> *mut u8 {
|
||||
self.0.as_mut_ptr()
|
||||
}
|
||||
|
||||
unsafe fn set_init(&mut self, init_len: usize) {
|
||||
if self.len() < init_len {
|
||||
self.0.set_len(init_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -40,14 +40,23 @@ impl Directory {
|
||||
pub(crate) type DirectoryRenderer =
|
||||
dyn Fn(&Directory, &HttpRequest) -> Result<ServiceResponse, io::Error>;
|
||||
|
||||
// show file url as relative to static path
|
||||
/// Returns percent encoded file URL path.
|
||||
macro_rules! encode_file_url {
|
||||
($path:ident) => {
|
||||
utf8_percent_encode(&$path, CONTROLS)
|
||||
};
|
||||
}
|
||||
|
||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||
/// Returns HTML entity encoded formatter.
|
||||
///
|
||||
/// ```plain
|
||||
/// " => "
|
||||
/// & => &
|
||||
/// ' => '
|
||||
/// < => <
|
||||
/// > => >
|
||||
/// / => /
|
||||
/// ```
|
||||
macro_rules! encode_file_name {
|
||||
($entry:ident) => {
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||
|
@ -23,16 +23,23 @@ impl ResponseError for FilesError {
|
||||
|
||||
#[allow(clippy::enum_variant_names)]
|
||||
#[derive(Display, Debug, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum UriSegmentError {
|
||||
/// The segment started with the wrapped invalid character.
|
||||
#[display(fmt = "The segment started with the wrapped invalid character")]
|
||||
BadStart(char),
|
||||
|
||||
/// The segment contained the wrapped invalid character.
|
||||
#[display(fmt = "The segment contained the wrapped invalid character")]
|
||||
BadChar(char),
|
||||
|
||||
/// The segment ended with the wrapped invalid character.
|
||||
#[display(fmt = "The segment ended with the wrapped invalid character")]
|
||||
BadEnd(char),
|
||||
|
||||
/// The path is not a valid UTF-8 string after doing percent decoding.
|
||||
#[display(fmt = "The path is not a valid UTF-8 string after percent-decoding")]
|
||||
NotValidUtf8,
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UriSegmentError`
|
||||
|
@ -28,6 +28,7 @@ use crate::{
|
||||
///
|
||||
/// `Files` service must be registered with `App::service()` method.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::App;
|
||||
/// use actix_files::Files;
|
||||
|
@ -67,8 +67,8 @@ mod tests {
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_web::{
|
||||
dev::ServiceFactory,
|
||||
guard,
|
||||
http::{
|
||||
header::{self, ContentDisposition, DispositionParam, DispositionType},
|
||||
@ -303,7 +303,7 @@ mod tests {
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"application/javascript"
|
||||
"application/javascript; charset=utf-8"
|
||||
);
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||
@ -597,7 +597,8 @@ mod tests {
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, request).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
||||
assert!(res.headers().contains_key(header::CONTENT_ENCODING));
|
||||
assert!(!test::read_body(res).await.is_empty());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@ -802,6 +803,38 @@ mod tests {
|
||||
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
// `%2F` == `/`
|
||||
let req = TestRequest::get().uri("/test%2Ftest.binary").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::get().uri("/test/Cargo.toml%00").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_percent_encoding_2() {
|
||||
let tmpdir = tempfile::tempdir().unwrap();
|
||||
let filename = match cfg!(unix) {
|
||||
true => "ض:?#[]{}<>()@!$&'`|*+,;= %20.test",
|
||||
false => "ض#[]{}()@!$&'`+,;= %20.test",
|
||||
};
|
||||
let filename_encoded = filename
|
||||
.as_bytes()
|
||||
.iter()
|
||||
.map(|c| format!("%{:02X}", c))
|
||||
.collect::<String>();
|
||||
std::fs::File::create(tmpdir.path().join(filename)).unwrap();
|
||||
|
||||
let srv = test::init_service(App::new().service(Files::new("", tmpdir.path()))).await;
|
||||
|
||||
let req = TestRequest::get()
|
||||
.uri(&format!("/{}", filename_encoded))
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
@ -1,22 +1,20 @@
|
||||
use std::{
|
||||
fmt,
|
||||
fs::Metadata,
|
||||
io,
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use actix_web::{
|
||||
body::{self, BoxBody, SizedStream},
|
||||
dev::{
|
||||
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
|
||||
ServiceResponse,
|
||||
self, AppService, HttpServiceFactory, ResourceDef, Service, ServiceFactory,
|
||||
ServiceRequest, ServiceResponse,
|
||||
},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
|
||||
DispositionType, ExtendedValue,
|
||||
DispositionType, ExtendedValue, HeaderValue,
|
||||
},
|
||||
StatusCode,
|
||||
},
|
||||
@ -40,7 +38,7 @@ bitflags! {
|
||||
|
||||
impl Default for Flags {
|
||||
fn default() -> Self {
|
||||
Flags::from_bits_truncate(0b0000_0111)
|
||||
Flags::from_bits_truncate(0b0000_1111)
|
||||
}
|
||||
}
|
||||
|
||||
@ -68,12 +66,12 @@ impl Default for Flags {
|
||||
/// NamedFile::open_async("./static/index.html").await
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Deref, DerefMut)]
|
||||
#[derive(Debug, Deref, DerefMut)]
|
||||
pub struct NamedFile {
|
||||
path: PathBuf,
|
||||
#[deref]
|
||||
#[deref_mut]
|
||||
file: File,
|
||||
path: PathBuf,
|
||||
modified: Option<SystemTime>,
|
||||
pub(crate) md: Metadata,
|
||||
pub(crate) flags: Flags,
|
||||
@ -83,32 +81,6 @@ pub struct NamedFile {
|
||||
pub(crate) encoding: Option<ContentEncoding>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for NamedFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NamedFile")
|
||||
.field("path", &self.path)
|
||||
.field(
|
||||
"file",
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
&"tokio_uring::File"
|
||||
},
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
&self.file
|
||||
},
|
||||
)
|
||||
.field("modified", &self.modified)
|
||||
.field("md", &self.md)
|
||||
.field("flags", &self.flags)
|
||||
.field("status_code", &self.status_code)
|
||||
.field("content_type", &self.content_type)
|
||||
.field("content_disposition", &self.content_disposition)
|
||||
.field("encoding", &self.encoding)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub(crate) use std::fs::File;
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
@ -224,7 +196,6 @@ impl NamedFile {
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
@ -232,6 +203,7 @@ impl NamedFile {
|
||||
/// use actix_files::NamedFile;
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = File::open(&path)?;
|
||||
Self::from_file(file, path)
|
||||
@ -295,23 +267,21 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the MIME Content-Type for serving this file. By default
|
||||
/// the Content-Type is inferred from the filename extension.
|
||||
/// Set the MIME Content-Type for serving this file. By default the Content-Type is inferred
|
||||
/// from the filename extension.
|
||||
#[inline]
|
||||
pub fn set_content_type(mut self, mime_type: mime::Mime) -> Self {
|
||||
self.content_type = mime_type;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the Content-Disposition for serving this file. This allows
|
||||
/// changing the inline/attachment disposition as well as the filename
|
||||
/// sent to the peer.
|
||||
/// Set the Content-Disposition for serving this file. This allows changing the
|
||||
/// `inline/attachment` disposition as well as the filename sent to the peer.
|
||||
///
|
||||
/// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
|
||||
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
|
||||
/// and the filename is taken from the path provided in the `open` method
|
||||
/// after converting it to UTF-8 using.
|
||||
/// [`std::ffi::OsStr::to_string_lossy`]
|
||||
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise, and the
|
||||
/// filename is taken from the path provided in the `open` method after converting it to UTF-8
|
||||
/// (using `to_string_lossy`).
|
||||
#[inline]
|
||||
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
|
||||
self.content_disposition = cd;
|
||||
@ -337,7 +307,7 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use ETag or not.
|
||||
/// Specifies whether to return `ETag` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
@ -346,7 +316,7 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Specifies whether to use Last-Modified or not.
|
||||
/// Specifies whether to return `Last-Modified` header in response.
|
||||
///
|
||||
/// Default is true.
|
||||
#[inline]
|
||||
@ -364,7 +334,7 @@ impl NamedFile {
|
||||
self
|
||||
}
|
||||
|
||||
/// Creates a etag in a format is similar to Apache's.
|
||||
/// Creates an `ETag` in a format is similar to Apache's.
|
||||
pub(crate) fn etag(&self) -> Option<header::EntityTag> {
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
let ino = {
|
||||
@ -386,7 +356,7 @@ impl NamedFile {
|
||||
.duration_since(UNIX_EPOCH)
|
||||
.expect("modification time must be after epoch");
|
||||
|
||||
header::EntityTag::strong(format!(
|
||||
header::EntityTag::new_strong(format!(
|
||||
"{:x}:{:x}:{:x}:{:x}",
|
||||
ino,
|
||||
self.md.len(),
|
||||
@ -405,12 +375,13 @@ impl NamedFile {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
equiv_utf8_text(self.content_type.clone())
|
||||
} else {
|
||||
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
self.content_type
|
||||
};
|
||||
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.insert_header((
|
||||
@ -420,7 +391,7 @@ impl NamedFile {
|
||||
}
|
||||
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
}
|
||||
|
||||
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
||||
@ -478,12 +449,13 @@ impl NamedFile {
|
||||
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
let ct = if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
equiv_utf8_text(self.content_type.clone())
|
||||
} else {
|
||||
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
self.content_type
|
||||
};
|
||||
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.insert_header((
|
||||
@ -492,9 +464,8 @@ impl NamedFile {
|
||||
));
|
||||
}
|
||||
|
||||
// default compressing
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
res.insert_header((header::CONTENT_ENCODING, current_encoding.as_str()));
|
||||
}
|
||||
|
||||
if let Some(lm) = last_modified {
|
||||
@ -517,7 +488,12 @@ impl NamedFile {
|
||||
length = ranges[0].length;
|
||||
offset = ranges[0].start;
|
||||
|
||||
res.encoding(ContentEncoding::Identity);
|
||||
// don't allow compression middleware to modify partial content
|
||||
res.insert_header((
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static("identity"),
|
||||
));
|
||||
|
||||
res.insert_header((
|
||||
header::CONTENT_RANGE,
|
||||
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
||||
@ -626,7 +602,7 @@ impl Service<ServiceRequest> for NamedFileService {
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let (req, _) = req.into_parts();
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
path::{Component, Path, PathBuf},
|
||||
str::FromStr,
|
||||
};
|
||||
|
||||
@ -26,8 +26,23 @@ impl PathBufWrap {
|
||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
// equivalent to `path.split('/').count()`
|
||||
let mut segment_count = path.matches('/').count() + 1;
|
||||
|
||||
// we can decode the whole path here (instead of per-segment decoding)
|
||||
// because we will reject `%2F` in paths using `segement_count`.
|
||||
let path = percent_encoding::percent_decode_str(path)
|
||||
.decode_utf8()
|
||||
.map_err(|_| UriSegmentError::NotValidUtf8)?;
|
||||
|
||||
// disallow decoding `%2F` into `/`
|
||||
if segment_count != path.matches('/').count() + 1 {
|
||||
return Err(UriSegmentError::BadChar('/'));
|
||||
}
|
||||
|
||||
for segment in path.split('/') {
|
||||
if segment == ".." {
|
||||
segment_count -= 1;
|
||||
buf.pop();
|
||||
} else if !hidden_files && segment.starts_with('.') {
|
||||
return Err(UriSegmentError::BadStart('.'));
|
||||
@ -40,6 +55,7 @@ impl PathBufWrap {
|
||||
} else if segment.ends_with('<') {
|
||||
return Err(UriSegmentError::BadEnd('<'));
|
||||
} else if segment.is_empty() {
|
||||
segment_count -= 1;
|
||||
continue;
|
||||
} else if cfg!(windows) && segment.contains('\\') {
|
||||
return Err(UriSegmentError::BadChar('\\'));
|
||||
@ -48,6 +64,12 @@ impl PathBufWrap {
|
||||
}
|
||||
}
|
||||
|
||||
// make sure we agree with stdlib parser
|
||||
for (i, component) in buf.components().enumerate() {
|
||||
assert!(matches!(component, Component::Normal(_)));
|
||||
assert!(i < segment_count);
|
||||
}
|
||||
|
||||
Ok(PathBufWrap(buf))
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::Service;
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
body::BoxBody,
|
||||
dev::{self, Service, ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::{header, Method},
|
||||
@ -94,16 +94,16 @@ impl fmt::Debug for FilesService {
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for FilesService {
|
||||
type Response = ServiceResponse;
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
dev::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let is_method_valid = if let Some(guard) = &self.guards {
|
||||
// execute user defined guards
|
||||
(**guard).check(req.head())
|
||||
(**guard).check(&req.guard_ctx())
|
||||
} else {
|
||||
// default behavior
|
||||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
@ -114,7 +114,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
Box::pin(async move {
|
||||
if !is_method_valid {
|
||||
return Ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
HttpResponse::MethodNotAllowed()
|
||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
));
|
||||
@ -123,7 +123,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
let real_path =
|
||||
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Ok(req.error_response(e)),
|
||||
Err(err) => return Ok(req.error_response(err)),
|
||||
};
|
||||
|
||||
if let Some(filter) = &this.path_filter {
|
||||
@ -131,9 +131,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
if let Some(ref default) = this.default {
|
||||
return default.call(req).await;
|
||||
} else {
|
||||
return Ok(
|
||||
req.into_response(actix_web::HttpResponse::NotFound().finish())
|
||||
);
|
||||
return Ok(req.into_response(HttpResponse::NotFound().finish()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -19,12 +19,12 @@ async fn test_utf8_file_contents() {
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain")),
|
||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||
);
|
||||
|
||||
// prefer UTF-8 encoding
|
||||
// disable UTF-8 attribute
|
||||
let srv =
|
||||
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true)))
|
||||
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(false)))
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
@ -33,6 +33,6 @@ async fn test_utf8_file_contents() {
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers().get(header::CONTENT_TYPE),
|
||||
Some(&HeaderValue::from_static("text/plain; charset=utf-8")),
|
||||
Some(&HeaderValue::from_static("text/plain")),
|
||||
);
|
||||
}
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 3.0.0-beta.10 - 2021-12-27
|
||||
- Update `actix-server` to `2.0.0-rc.2`. [#2550]
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.10"
|
||||
version = "3.0.0-beta.11"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -35,7 +35,7 @@ actix-tls = "3.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2.0.0-rc.2"
|
||||
awc = { version = "3.0.0-beta.15", default-features = false }
|
||||
awc = { version = "3.0.0-beta.18", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
@ -48,8 +48,8 @@ serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.8", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.18"
|
||||
|
@ -3,15 +3,15 @@
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.10)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.11)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.11)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http-test)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -1,6 +1,41 @@
|
||||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
### Added
|
||||
- Response headers can be sent as camel case using `res.head_mut().set_camel_case_headers(true)`. [#2587]
|
||||
|
||||
### Changed
|
||||
- Brotli (de)compression support is now provided by the `brotli` crate. [#2538]
|
||||
|
||||
[#2538]: https://github.com/actix/actix-web/pull/2538
|
||||
[#2587]: https://github.com/actix/actix-web/pull/2587
|
||||
|
||||
|
||||
## 3.0.0-beta.18 - 2022-01-04
|
||||
### Added
|
||||
- `impl Eq` for `header::ContentEncoding`. [#2501]
|
||||
- `impl Copy` for `QualityItem` where `T: Copy`. [#2501]
|
||||
- `Quality::ZERO` equivalent to `q=0`. [#2501]
|
||||
- `QualityItem::zero` that uses `Quality::ZERO`. [#2501]
|
||||
- `ContentEncoding::to_header_value()`. [#2501]
|
||||
|
||||
### Changed
|
||||
- `Quality::MIN` is now the smallest non-zero value. [#2501]
|
||||
- `QualityItem::min` semantics changed with `QualityItem::MIN`. [#2501]
|
||||
- Rename `ContentEncoding::{Br => Brotli}`. [#2501]
|
||||
- Rename `header::EntityTag::{weak => new_weak, strong => new_strong}`. [#2565]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
### Fixed
|
||||
- `ContentEncoding::Identity` can now be parsed from a string. [#2501]
|
||||
- A `Vary` header is now correctly sent along with compressed content. [#2501]
|
||||
|
||||
### Removed
|
||||
- `ContentEncoding::Auto` variant. [#2501]
|
||||
- `ContentEncoding::is_compression()`. [#2501]
|
||||
|
||||
[#2501]: https://github.com/actix/actix-web/pull/2501
|
||||
[#2565]: https://github.com/actix/actix-web/pull/2565
|
||||
|
||||
|
||||
## 3.0.0-beta.17 - 2021-12-27
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.17"
|
||||
version = "3.0.0-beta.18"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
@ -33,7 +33,7 @@ openssl = ["actix-tls/accept", "actix-tls/openssl"]
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls"]
|
||||
|
||||
# enable compression support
|
||||
compress-brotli = ["brotli2", "__compress"]
|
||||
compress-brotli = ["brotli", "__compress"]
|
||||
compress-gzip = ["flate2", "__compress"]
|
||||
compress-zstd = ["zstd", "__compress"]
|
||||
|
||||
@ -74,20 +74,21 @@ smallvec = "1.6.1"
|
||||
actix-tls = { version = "3.0.0", default-features = false, optional = true }
|
||||
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
brotli = { version = "3.3.3", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.9", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
actix-server = "2.0.0-rc.2"
|
||||
actix-tls = { version = "3.0.0", features = ["openssl"] }
|
||||
actix-web = "4.0.0-beta.16"
|
||||
actix-web = "4.0.0-beta.20"
|
||||
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
memchr = "2.4"
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
@ -96,7 +97,7 @@ serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.8", features = ["net", "rt", "macros"] }
|
||||
tokio = { version = "1.8.4", features = ["net", "rt", "macros"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
|
@ -3,18 +3,18 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.17)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.18)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.17)
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.18)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-http)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
||||
## Example
|
||||
|
||||
|
@ -42,32 +42,37 @@ mod _new {
|
||||
if x < 10 {
|
||||
f.write_str("00")?;
|
||||
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||
itoa::fmt(f, x)
|
||||
itoa_fmt(f, x)
|
||||
} else if x < 100 {
|
||||
f.write_str("0")?;
|
||||
if x % 10 == 0 {
|
||||
// trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
itoa_fmt(f, x)
|
||||
}
|
||||
} else {
|
||||
// x is in range 101–999
|
||||
|
||||
if x % 100 == 0 {
|
||||
// two trailing 0s, divide by 100 and write
|
||||
itoa::fmt(f, x / 100)
|
||||
itoa_fmt(f, x / 100)
|
||||
} else if x % 10 == 0 {
|
||||
// one trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
itoa_fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
itoa_fmt(f, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn itoa_fmt<W: fmt::Write, V: itoa::Integer>(mut wr: W, value: V) -> fmt::Result {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
wr.write_str(buf.format(value))
|
||||
}
|
||||
}
|
||||
|
||||
mod _naive {
|
||||
|
@ -11,9 +11,6 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
use brotli2::write::BrotliDecoder;
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||
|
||||
@ -47,9 +44,9 @@ where
|
||||
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
|
||||
let decoder = match encoding {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(BrotliDecoder::new(
|
||||
Writer::new(),
|
||||
)))),
|
||||
ContentEncoding::Brotli => Some(ContentDecoder::Brotli(Box::new(
|
||||
brotli::DecompressorWriter::new(Writer::new(), 8_096),
|
||||
))),
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
|
||||
ZlibDecoder::new(Writer::new()),
|
||||
@ -165,7 +162,7 @@ enum ContentDecoder {
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
Gzip(Box<GzDecoder<Writer>>),
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
Br(Box<BrotliDecoder<Writer>>),
|
||||
Brotli(Box<brotli::DecompressorWriter<Writer>>),
|
||||
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
|
||||
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
@ -176,7 +173,7 @@ impl ContentDecoder {
|
||||
fn feed_eof(&mut self) -> io::Result<Option<Bytes>> {
|
||||
match self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
|
||||
ContentDecoder::Brotli(ref mut decoder) => match decoder.flush() {
|
||||
Ok(()) => {
|
||||
let b = decoder.get_mut().take();
|
||||
|
||||
@ -234,7 +231,7 @@ impl ContentDecoder {
|
||||
fn feed_data(&mut self, data: Bytes) -> io::Result<Option<Bytes>> {
|
||||
match self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentDecoder::Br(ref mut decoder) => match decoder.write_all(&data) {
|
||||
ContentDecoder::Brotli(ref mut decoder) => match decoder.write_all(&data) {
|
||||
Ok(_) => {
|
||||
decoder.flush()?;
|
||||
let b = decoder.get_mut().take();
|
||||
|
@ -14,9 +14,6 @@ use derive_more::Display;
|
||||
use futures_core::ready;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
use brotli2::write::BrotliEncoder;
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||
|
||||
@ -56,25 +53,24 @@ impl<B: MessageBody> Encoder<B> {
|
||||
}
|
||||
|
||||
pub fn response(encoding: ContentEncoding, head: &mut ResponseHead, body: B) -> Self {
|
||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||
|| head.status == StatusCode::NO_CONTENT
|
||||
|| encoding == ContentEncoding::Identity
|
||||
|| encoding == ContentEncoding::Auto);
|
||||
|
||||
// no need to compress an empty body
|
||||
if matches!(body.size(), BodySize::None) {
|
||||
return Self::none();
|
||||
}
|
||||
|
||||
let should_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||
|| head.status == StatusCode::NO_CONTENT
|
||||
|| encoding == ContentEncoding::Identity);
|
||||
|
||||
let body = match body.try_into_bytes() {
|
||||
Ok(body) => EncoderBody::Full { body },
|
||||
Err(body) => EncoderBody::Stream { body },
|
||||
};
|
||||
|
||||
if can_encode {
|
||||
// Modify response body only if encoder is set
|
||||
if let Some(enc) = ContentEncoder::encoder(encoding) {
|
||||
if should_encode {
|
||||
// wrap body only if encoder is feature-enabled
|
||||
if let Some(enc) = ContentEncoder::select(encoding) {
|
||||
update_head(encoding, head);
|
||||
|
||||
return Encoder {
|
||||
@ -169,6 +165,7 @@ where
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
let mut this = self.project();
|
||||
|
||||
loop {
|
||||
if *this.eof {
|
||||
return Poll::Ready(None);
|
||||
@ -252,10 +249,10 @@ where
|
||||
}
|
||||
|
||||
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
||||
head.headers_mut().insert(
|
||||
header::CONTENT_ENCODING,
|
||||
HeaderValue::from_static(encoding.as_str()),
|
||||
);
|
||||
head.headers_mut()
|
||||
.insert(header::CONTENT_ENCODING, encoding.to_header_value());
|
||||
head.headers_mut()
|
||||
.insert(header::VARY, HeaderValue::from_static("accept-encoding"));
|
||||
|
||||
head.no_chunking(false);
|
||||
}
|
||||
@ -268,7 +265,7 @@ enum ContentEncoder {
|
||||
Gzip(GzEncoder<Writer>),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
Br(BrotliEncoder<Writer>),
|
||||
Brotli(Box<brotli::CompressorWriter<Writer>>),
|
||||
|
||||
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
|
||||
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
|
||||
@ -277,7 +274,7 @@ enum ContentEncoder {
|
||||
}
|
||||
|
||||
impl ContentEncoder {
|
||||
fn encoder(encoding: ContentEncoding) -> Option<Self> {
|
||||
fn select(encoding: ContentEncoding) -> Option<Self> {
|
||||
match encoding {
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Deflate => Some(ContentEncoder::Deflate(ZlibEncoder::new(
|
||||
@ -292,9 +289,7 @@ impl ContentEncoder {
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoding::Br => {
|
||||
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
||||
}
|
||||
ContentEncoding::Brotli => Some(ContentEncoder::Brotli(new_brotli_compressor())),
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoding::Zstd => {
|
||||
@ -310,7 +305,7 @@ impl ContentEncoder {
|
||||
pub(crate) fn take(&mut self) -> Bytes {
|
||||
match *self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
||||
ContentEncoder::Brotli(ref mut encoder) => encoder.get_mut().take(),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
||||
@ -326,8 +321,8 @@ impl ContentEncoder {
|
||||
fn finish(self) -> Result<Bytes, io::Error> {
|
||||
match self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(encoder) => match encoder.finish() {
|
||||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
ContentEncoder::Brotli(mut encoder) => match encoder.flush() {
|
||||
Ok(()) => Ok(encoder.into_inner().buf.freeze()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
@ -354,7 +349,7 @@ impl ContentEncoder {
|
||||
fn write(&mut self, data: &[u8]) -> Result<(), io::Error> {
|
||||
match *self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(ref mut encoder) => match encoder.write_all(data) {
|
||||
ContentEncoder::Brotli(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
Err(err) => {
|
||||
trace!("Error decoding br encoding: {}", err);
|
||||
@ -392,6 +387,16 @@ impl ContentEncoder {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
fn new_brotli_compressor() -> Box<brotli::CompressorWriter<Writer>> {
|
||||
Box::new(brotli::CompressorWriter::new(
|
||||
Writer::new(),
|
||||
8 * 1024, // 32 KiB buffer
|
||||
3, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
))
|
||||
}
|
||||
|
||||
#[derive(Debug, Display)]
|
||||
#[non_exhaustive]
|
||||
pub enum EncoderError {
|
||||
|
@ -250,6 +250,7 @@ impl From<ParseError> for Response<BoxBody> {
|
||||
/// A set of errors that can occur running blocking tasks in thread pool.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "Blocking thread pool is gone")]
|
||||
// TODO: non-exhaustive
|
||||
pub struct BlockingError;
|
||||
|
||||
/// A set of errors that can occur during payload parsing.
|
||||
|
@ -258,6 +258,12 @@ impl MessageType for Response<()> {
|
||||
None
|
||||
}
|
||||
|
||||
fn camel_case(&self) -> bool {
|
||||
self.head()
|
||||
.flags
|
||||
.contains(crate::message::Flags::CAMEL_CASE)
|
||||
}
|
||||
|
||||
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
|
||||
let head = self.head();
|
||||
let reason = head.reason().as_bytes();
|
||||
|
@ -6,7 +6,7 @@ use ahash::AHashMap;
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
use smallvec::{smallvec, SmallVec};
|
||||
|
||||
use crate::header::AsHeaderName;
|
||||
use super::AsHeaderName;
|
||||
|
||||
/// A multi-map of HTTP headers.
|
||||
///
|
||||
@ -605,6 +605,13 @@ impl<'a> IntoIterator for &'a HeaderMap {
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert `http::HeaderMap` to our `HeaderMap`.
|
||||
impl From<http::HeaderMap> for HeaderMap {
|
||||
fn from(mut map: http::HeaderMap) -> HeaderMap {
|
||||
HeaderMap::from_drain(map.drain())
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterator over removed, owned values with the same associated name.
|
||||
///
|
||||
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
|
||||
|
@ -50,20 +50,13 @@ pub use self::utils::{
|
||||
|
||||
/// An interface for types that already represent a valid header.
|
||||
pub trait Header: TryIntoHeaderValue {
|
||||
/// Returns the name of the header field
|
||||
/// Returns the name of the header field.
|
||||
fn name() -> HeaderName;
|
||||
|
||||
/// Parse a header
|
||||
/// Parse the header from a HTTP message.
|
||||
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
|
||||
}
|
||||
|
||||
/// Convert `http::HeaderMap` to our `HeaderMap`.
|
||||
impl From<http::HeaderMap> for HeaderMap {
|
||||
fn from(mut map: http::HeaderMap) -> HeaderMap {
|
||||
HeaderMap::from_drain(map.drain())
|
||||
}
|
||||
}
|
||||
|
||||
/// This encode set is used for HTTP header values and is defined at
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
|
||||
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||
|
@ -20,14 +20,16 @@ pub struct ContentEncodingParseError;
|
||||
/// See [IANA HTTP Content Coding Registry].
|
||||
///
|
||||
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentEncoding {
|
||||
/// Automatically select encoding based on encoding negotiation.
|
||||
Auto,
|
||||
/// Indicates the no-op identity encoding.
|
||||
///
|
||||
/// I.e., no compression or modification.
|
||||
Identity,
|
||||
|
||||
/// A format using the Brotli algorithm.
|
||||
Br,
|
||||
Brotli,
|
||||
|
||||
/// A format using the zlib structure with deflate algorithm.
|
||||
Deflate,
|
||||
@ -37,32 +39,36 @@ pub enum ContentEncoding {
|
||||
|
||||
/// Zstd algorithm.
|
||||
Zstd,
|
||||
|
||||
/// Indicates the identity function (i.e. no compression, nor modification).
|
||||
Identity,
|
||||
}
|
||||
|
||||
impl ContentEncoding {
|
||||
/// Is the content compressed?
|
||||
#[inline]
|
||||
pub const fn is_compression(self) -> bool {
|
||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||
}
|
||||
|
||||
/// Convert content encoding to string.
|
||||
#[inline]
|
||||
pub const fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ContentEncoding::Br => "br",
|
||||
ContentEncoding::Brotli => "br",
|
||||
ContentEncoding::Gzip => "gzip",
|
||||
ContentEncoding::Deflate => "deflate",
|
||||
ContentEncoding::Zstd => "zstd",
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
||||
ContentEncoding::Identity => "identity",
|
||||
}
|
||||
}
|
||||
|
||||
/// Convert content encoding to header value.
|
||||
#[inline]
|
||||
pub const fn to_header_value(self) -> HeaderValue {
|
||||
match self {
|
||||
ContentEncoding::Brotli => HeaderValue::from_static("br"),
|
||||
ContentEncoding::Gzip => HeaderValue::from_static("gzip"),
|
||||
ContentEncoding::Deflate => HeaderValue::from_static("deflate"),
|
||||
ContentEncoding::Zstd => HeaderValue::from_static("zstd"),
|
||||
ContentEncoding::Identity => HeaderValue::from_static("identity"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ContentEncoding {
|
||||
#[inline]
|
||||
fn default() -> Self {
|
||||
Self::Identity
|
||||
}
|
||||
@ -71,16 +77,18 @@ impl Default for ContentEncoding {
|
||||
impl FromStr for ContentEncoding {
|
||||
type Err = ContentEncodingParseError;
|
||||
|
||||
fn from_str(val: &str) -> Result<Self, Self::Err> {
|
||||
let val = val.trim();
|
||||
fn from_str(enc: &str) -> Result<Self, Self::Err> {
|
||||
let enc = enc.trim();
|
||||
|
||||
if val.eq_ignore_ascii_case("br") {
|
||||
Ok(ContentEncoding::Br)
|
||||
} else if val.eq_ignore_ascii_case("gzip") {
|
||||
if enc.eq_ignore_ascii_case("br") {
|
||||
Ok(ContentEncoding::Brotli)
|
||||
} else if enc.eq_ignore_ascii_case("gzip") {
|
||||
Ok(ContentEncoding::Gzip)
|
||||
} else if val.eq_ignore_ascii_case("deflate") {
|
||||
} else if enc.eq_ignore_ascii_case("deflate") {
|
||||
Ok(ContentEncoding::Deflate)
|
||||
} else if val.eq_ignore_ascii_case("zstd") {
|
||||
} else if enc.eq_ignore_ascii_case("identity") {
|
||||
Ok(ContentEncoding::Identity)
|
||||
} else if enc.eq_ignore_ascii_case("zstd") {
|
||||
Ok(ContentEncoding::Zstd)
|
||||
} else {
|
||||
Err(ContentEncodingParseError)
|
||||
|
@ -27,7 +27,8 @@ const MAX_QUALITY_FLOAT: f32 = 1.0;
|
||||
///
|
||||
/// assert_eq!(q(0.42).to_string(), "0.42");
|
||||
/// assert_eq!(q(1.0).to_string(), "1");
|
||||
/// assert_eq!(Quality::MIN.to_string(), "0");
|
||||
/// assert_eq!(Quality::MIN.to_string(), "0.001");
|
||||
/// assert_eq!(Quality::ZERO.to_string(), "0");
|
||||
/// ```
|
||||
///
|
||||
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
|
||||
@ -38,8 +39,11 @@ impl Quality {
|
||||
/// The maximum quality value, equivalent to `q=1.0`.
|
||||
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
|
||||
|
||||
/// The minimum quality value, equivalent to `q=0.0`.
|
||||
pub const MIN: Quality = Quality(0);
|
||||
/// The minimum, non-zero quality value, equivalent to `q=0.001`.
|
||||
pub const MIN: Quality = Quality(1);
|
||||
|
||||
/// The zero quality value, equivalent to `q=0.0`.
|
||||
pub const ZERO: Quality = Quality(0);
|
||||
|
||||
/// Converts a float in the range 0.0–1.0 to a `Quality`.
|
||||
///
|
||||
@ -51,7 +55,7 @@ impl Quality {
|
||||
// Check that `value` is within range should be done before calling this method.
|
||||
// Just in case, this debug_assert should catch if we were forgetful.
|
||||
debug_assert!(
|
||||
(0.0f32..=1.0f32).contains(&value),
|
||||
(0.0..=MAX_QUALITY_FLOAT).contains(&value),
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
|
||||
@ -154,10 +158,13 @@ impl TryFrom<f32> for Quality {
|
||||
/// let q1 = q(1.0);
|
||||
/// assert_eq!(q1, Quality::MAX);
|
||||
///
|
||||
/// let q2 = q(0.0);
|
||||
/// let q2 = q(0.001);
|
||||
/// assert_eq!(q2, Quality::MIN);
|
||||
///
|
||||
/// let q3 = q(0.42);
|
||||
/// let q3 = q(0.0);
|
||||
/// assert_eq!(q3, Quality::ZERO);
|
||||
///
|
||||
/// let q4 = q(0.42);
|
||||
/// ```
|
||||
///
|
||||
/// An out-of-range `f32` quality will panic.
|
||||
@ -185,6 +192,10 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn display_output() {
|
||||
assert_eq!(Quality::ZERO.to_string(), "0");
|
||||
assert_eq!(Quality::MIN.to_string(), "0.001");
|
||||
assert_eq!(Quality::MAX.to_string(), "1");
|
||||
|
||||
assert_eq!(q(0.0).to_string(), "0");
|
||||
assert_eq!(q(1.0).to_string(), "1");
|
||||
assert_eq!(q(0.001).to_string(), "0.001");
|
||||
|
@ -31,7 +31,7 @@ use super::Quality;
|
||||
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
|
||||
/// assert!(q_item > q_item_fallback);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub struct QualityItem<T> {
|
||||
/// The wrapped contents of the field.
|
||||
pub item: T,
|
||||
@ -53,10 +53,15 @@ impl<T> QualityItem<T> {
|
||||
Self::new(item, Quality::MAX)
|
||||
}
|
||||
|
||||
/// Constructs a new `QualityItem` from an item, using the minimum q-value.
|
||||
/// Constructs a new `QualityItem` from an item, using the minimum, non-zero q-value.
|
||||
pub fn min(item: T) -> Self {
|
||||
Self::new(item, Quality::MIN)
|
||||
}
|
||||
|
||||
/// Constructs a new `QualityItem` from an item, using zero q-value of zero.
|
||||
pub fn zero(item: T) -> Self {
|
||||
Self::new(item, Quality::ZERO)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> PartialOrd for QualityItem<T> {
|
||||
@ -73,7 +78,10 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
||||
// q-factor value is implied for max value
|
||||
Quality::MAX => Ok(()),
|
||||
|
||||
Quality::MIN => f.write_str("; q=0"),
|
||||
// fast path for zero
|
||||
Quality::ZERO => f.write_str("; q=0"),
|
||||
|
||||
// quality formatting is already using itoa
|
||||
q => write!(f, "; q={}", q),
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{cell::RefCell, rc::Rc};
|
||||
use std::{cell::RefCell, ops, rc::Rc};
|
||||
|
||||
use bitflags::bitflags;
|
||||
|
||||
@ -49,7 +49,7 @@ impl<T: Head> Message<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Head> std::ops::Deref for Message<T> {
|
||||
impl<T: Head> ops::Deref for Message<T> {
|
||||
type Target = T;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
@ -57,7 +57,7 @@ impl<T: Head> std::ops::Deref for Message<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Head> std::ops::DerefMut for Message<T> {
|
||||
impl<T: Head> ops::DerefMut for Message<T> {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
Rc::get_mut(&mut self.head).expect("Multiple copies exist")
|
||||
}
|
||||
|
@ -20,7 +20,7 @@ pub struct ResponseHead {
|
||||
pub headers: HeaderMap,
|
||||
pub reason: Option<&'static str>,
|
||||
pub(crate) extensions: RefCell<Extensions>,
|
||||
flags: Flags,
|
||||
pub(crate) flags: Flags,
|
||||
}
|
||||
|
||||
impl ResponseHead {
|
||||
@ -49,6 +49,18 @@ impl ResponseHead {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// Sets the flag that controls wether to send headers formatted as Camel-Case.
|
||||
///
|
||||
/// Only applicable to HTTP/1.x responses; HTTP/2 header names are always lowercase.
|
||||
#[inline]
|
||||
pub fn set_camel_case_headers(&mut self, camel_case: bool) {
|
||||
if camel_case {
|
||||
self.flags.insert(Flags::CAMEL_CASE);
|
||||
} else {
|
||||
self.flags.remove(Flags::CAMEL_CASE);
|
||||
}
|
||||
}
|
||||
|
||||
/// Message extensions
|
||||
#[inline]
|
||||
pub fn extensions(&self) -> Ref<'_, Extensions> {
|
||||
@ -206,3 +218,57 @@ impl BoxedResponsePool {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
io::{Read as _, Write as _},
|
||||
net,
|
||||
};
|
||||
|
||||
use memchr::memmem;
|
||||
|
||||
use crate::{
|
||||
header::{HeaderName, HeaderValue},
|
||||
Error, HttpService, Request, Response,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn camel_case_headers() {
|
||||
let mut srv = actix_http_test::test_server(|| {
|
||||
HttpService::new(|req: Request| async move {
|
||||
let mut res = Response::ok();
|
||||
|
||||
if req.path().contains("camel") {
|
||||
res.head_mut().set_camel_case_headers(true);
|
||||
}
|
||||
|
||||
res.headers_mut().insert(
|
||||
HeaderName::from_static("foo-bar"),
|
||||
HeaderValue::from_static("baz"),
|
||||
);
|
||||
Ok::<_, Error>(res)
|
||||
})
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /camel HTTP/1.1\r\nConnection: Close\r\n\r\n");
|
||||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
assert!(memmem::find(&data, b"Foo-Bar").is_some());
|
||||
assert!(!memmem::find(&data, b"foo-bar").is_some());
|
||||
|
||||
let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
|
||||
let _ = stream.write_all(b"GET /lower HTTP/1.1\r\nConnection: Close\r\n\r\n");
|
||||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
assert!(!memmem::find(&data, b"Foo-Bar").is_some());
|
||||
assert!(memmem::find(&data, b"foo-bar").is_some());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,12 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.4.0-beta.12 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 0.4.0-beta.11 - 2021-12-27
|
||||
* No significant changes since `0.4.0-beta.10`.
|
||||
- No significant changes since `0.4.0-beta.10`.
|
||||
|
||||
|
||||
## 0.4.0-beta.10 - 2021-12-11
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0-beta.11"
|
||||
version = "0.4.0-beta.12"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
@ -15,7 +15,7 @@ path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.16", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
@ -28,7 +28,7 @@ twoway = "0.2"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1.8", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
@ -3,15 +3,15 @@
|
||||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.11)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.12)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.11)
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.12)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-multipart)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -3,6 +3,20 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.5.0-rc.1 - 2022-01-14
|
||||
- `Resource` trait now have an associated type, `Path`, instead of the generic parameter. [#2568]
|
||||
- `Resource` is now implemented for `&mut Path<_>` and `RefMut<Path<_>>`. [#2568]
|
||||
|
||||
[#2568]: https://github.com/actix/actix-web/pull/2568
|
||||
|
||||
|
||||
## 0.5.0-beta.4 - 2022-01-04
|
||||
- `PathDeserializer` now decodes all percent encoded characters in dynamic segments. [#2566]
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
[#2566]: https://github.com/actix/actix-net/pull/2566
|
||||
|
||||
|
||||
## 0.5.0-beta.3 - 2021-12-17
|
||||
- Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-router"
|
||||
version = "0.5.0-beta.3"
|
||||
version = "0.5.0-rc.1"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
|
||||
|
@ -1,8 +1,14 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use serde::de::{self, Deserializer, Error as DeError, Visitor};
|
||||
use serde::forward_to_deserialize_any;
|
||||
|
||||
use crate::path::{Path, PathIter};
|
||||
use crate::ResourcePath;
|
||||
use crate::{Quoter, ResourcePath};
|
||||
|
||||
thread_local! {
|
||||
static FULL_QUOTER: Quoter = Quoter::new(b"+/%", b"");
|
||||
}
|
||||
|
||||
macro_rules! unsupported_type {
|
||||
($trait_fn:ident, $name:expr) => {
|
||||
@ -10,16 +16,13 @@ macro_rules! unsupported_type {
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
Err(de::value::Error::custom(concat!(
|
||||
"unsupported type: ",
|
||||
$name
|
||||
)))
|
||||
Err(de::Error::custom(concat!("unsupported type: ", $name)))
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_single_value {
|
||||
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
|
||||
($trait_fn:ident) => {
|
||||
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
@ -33,18 +36,35 @@ macro_rules! parse_single_value {
|
||||
.as_str(),
|
||||
))
|
||||
} else {
|
||||
let v = self.path[0].parse().map_err(|_| {
|
||||
de::value::Error::custom(format!(
|
||||
"can not parse {:?} to a {}",
|
||||
&self.path[0], $tp
|
||||
))
|
||||
})?;
|
||||
visitor.$visit_fn(v)
|
||||
Value {
|
||||
value: &self.path[0],
|
||||
}
|
||||
.$trait_fn(visitor)
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! parse_value {
|
||||
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
|
||||
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let decoded = FULL_QUOTER
|
||||
.with(|q| q.requote(self.value.as_bytes()))
|
||||
.map(Cow::Owned)
|
||||
.unwrap_or(Cow::Borrowed(self.value));
|
||||
|
||||
let v = decoded.parse().map_err(|_| {
|
||||
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
|
||||
})?;
|
||||
|
||||
visitor.$visit_fn(v)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
pub struct PathDeserializer<'de, T: ResourcePath> {
|
||||
path: &'de Path<T>,
|
||||
}
|
||||
@ -172,23 +192,6 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
if self.path.segment_count() != 1 {
|
||||
Err(de::value::Error::custom(
|
||||
format!(
|
||||
"wrong number of parameters: {} expected 1",
|
||||
self.path.segment_count()
|
||||
)
|
||||
.as_str(),
|
||||
))
|
||||
} else {
|
||||
visitor.visit_str(&self.path[0])
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_seq<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
@ -199,25 +202,26 @@ impl<'de, T: ResourcePath + 'de> Deserializer<'de> for PathDeserializer<'de, T>
|
||||
}
|
||||
|
||||
unsupported_type!(deserialize_any, "'any'");
|
||||
unsupported_type!(deserialize_bytes, "bytes");
|
||||
unsupported_type!(deserialize_option, "Option<T>");
|
||||
unsupported_type!(deserialize_identifier, "identifier");
|
||||
unsupported_type!(deserialize_ignored_any, "ignored_any");
|
||||
|
||||
parse_single_value!(deserialize_bool, visit_bool, "bool");
|
||||
parse_single_value!(deserialize_i8, visit_i8, "i8");
|
||||
parse_single_value!(deserialize_i16, visit_i16, "i16");
|
||||
parse_single_value!(deserialize_i32, visit_i32, "i32");
|
||||
parse_single_value!(deserialize_i64, visit_i64, "i64");
|
||||
parse_single_value!(deserialize_u8, visit_u8, "u8");
|
||||
parse_single_value!(deserialize_u16, visit_u16, "u16");
|
||||
parse_single_value!(deserialize_u32, visit_u32, "u32");
|
||||
parse_single_value!(deserialize_u64, visit_u64, "u64");
|
||||
parse_single_value!(deserialize_f32, visit_f32, "f32");
|
||||
parse_single_value!(deserialize_f64, visit_f64, "f64");
|
||||
parse_single_value!(deserialize_string, visit_string, "String");
|
||||
parse_single_value!(deserialize_byte_buf, visit_string, "String");
|
||||
parse_single_value!(deserialize_char, visit_char, "char");
|
||||
parse_single_value!(deserialize_bool);
|
||||
parse_single_value!(deserialize_i8);
|
||||
parse_single_value!(deserialize_i16);
|
||||
parse_single_value!(deserialize_i32);
|
||||
parse_single_value!(deserialize_i64);
|
||||
parse_single_value!(deserialize_u8);
|
||||
parse_single_value!(deserialize_u16);
|
||||
parse_single_value!(deserialize_u32);
|
||||
parse_single_value!(deserialize_u64);
|
||||
parse_single_value!(deserialize_f32);
|
||||
parse_single_value!(deserialize_f64);
|
||||
parse_single_value!(deserialize_str);
|
||||
parse_single_value!(deserialize_string);
|
||||
parse_single_value!(deserialize_bytes);
|
||||
parse_single_value!(deserialize_byte_buf);
|
||||
parse_single_value!(deserialize_char);
|
||||
}
|
||||
|
||||
struct ParamsDeserializer<'de, T: ResourcePath> {
|
||||
@ -279,20 +283,6 @@ impl<'de> Deserializer<'de> for Key<'de> {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! parse_value {
|
||||
($trait_fn:ident, $visit_fn:ident, $tp:tt) => {
|
||||
fn $trait_fn<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
let v = self.value.parse().map_err(|_| {
|
||||
de::value::Error::custom(format!("can not parse {:?} to a {}", self.value, $tp))
|
||||
})?;
|
||||
visitor.$visit_fn(v)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
struct Value<'de> {
|
||||
value: &'de str,
|
||||
}
|
||||
@ -311,8 +301,6 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
parse_value!(deserialize_u64, visit_u64, "u64");
|
||||
parse_value!(deserialize_f32, visit_f32, "f32");
|
||||
parse_value!(deserialize_f64, visit_f64, "f64");
|
||||
parse_value!(deserialize_string, visit_string, "String");
|
||||
parse_value!(deserialize_byte_buf, visit_string, "String");
|
||||
parse_value!(deserialize_char, visit_char, "char");
|
||||
|
||||
fn deserialize_ignored_any<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
@ -340,18 +328,38 @@ impl<'de> Deserializer<'de> for Value<'de> {
|
||||
visitor.visit_unit()
|
||||
}
|
||||
|
||||
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_borrowed_bytes(self.value.as_bytes())
|
||||
}
|
||||
|
||||
fn deserialize_str<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
visitor.visit_borrowed_str(self.value)
|
||||
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
|
||||
Some(s) => visitor.visit_string(s),
|
||||
None => visitor.visit_borrowed_str(self.value),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_bytes<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
match FULL_QUOTER.with(|q| q.requote(self.value.as_bytes())) {
|
||||
Some(s) => visitor.visit_byte_buf(s.into()),
|
||||
None => visitor.visit_borrowed_bytes(self.value.as_bytes()),
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_byte_buf<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_bytes(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_string<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
where
|
||||
V: Visitor<'de>,
|
||||
{
|
||||
self.deserialize_str(visitor)
|
||||
}
|
||||
|
||||
fn deserialize_option<V>(self, visitor: V) -> Result<V::Value, Self::Error>
|
||||
@ -497,6 +505,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::path::Path;
|
||||
use crate::router::Router;
|
||||
use crate::ResourceDef;
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct MyStruct {
|
||||
@ -657,6 +666,79 @@ mod tests {
|
||||
assert!(format!("{:?}", s).contains("can not parse"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_path_decode_string() {
|
||||
let rdef = ResourceDef::new("/{key}");
|
||||
|
||||
let mut path = Path::new("/%25");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let segment: String = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(segment, "%");
|
||||
|
||||
let mut path = Path::new("/%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let segment: String = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(segment, "/")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_path_decode_seq() {
|
||||
let rdef = ResourceDef::new("/{key}/{value}");
|
||||
|
||||
let mut path = Path::new("/%30%25/%30%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let segment: (String, String) = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(segment.0, "0%");
|
||||
assert_eq!(segment.1, "0/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_path_decode_map() {
|
||||
#[derive(Deserialize)]
|
||||
struct Vals {
|
||||
key: String,
|
||||
value: String,
|
||||
}
|
||||
|
||||
let rdef = ResourceDef::new("/{key}/{value}");
|
||||
|
||||
let mut path = Path::new("/%25/%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let vals: Vals = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(vals.key, "%");
|
||||
assert_eq!(vals.value, "/");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn deserialize_borrowed() {
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct Params<'a> {
|
||||
val: &'a str,
|
||||
}
|
||||
|
||||
let rdef = ResourceDef::new("/{val}");
|
||||
|
||||
let mut path = Path::new("/X");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
let params: Params<'_> = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(params.val, "X");
|
||||
let de = PathDeserializer::new(&path);
|
||||
let params: &str = serde::Deserialize::deserialize(de).unwrap();
|
||||
assert_eq!(params, "X");
|
||||
|
||||
let mut path = Path::new("/%2F");
|
||||
rdef.capture_match_info(&mut path);
|
||||
let de = PathDeserializer::new(&path);
|
||||
assert!(<Params<'_> as serde::Deserialize>::deserialize(de).is_err());
|
||||
let de = PathDeserializer::new(&path);
|
||||
assert!(<&str as serde::Deserialize>::deserialize(de).is_err());
|
||||
}
|
||||
|
||||
// #[test]
|
||||
// fn test_extract_path_decode() {
|
||||
// let mut router = Router::<()>::default();
|
||||
|
@ -8,6 +8,7 @@
|
||||
mod de;
|
||||
mod path;
|
||||
mod pattern;
|
||||
mod quoter;
|
||||
mod resource;
|
||||
mod resource_path;
|
||||
mod router;
|
||||
@ -18,9 +19,10 @@ mod url;
|
||||
pub use self::de::PathDeserializer;
|
||||
pub use self::path::Path;
|
||||
pub use self::pattern::{IntoPatterns, Patterns};
|
||||
pub use self::quoter::Quoter;
|
||||
pub use self::resource::ResourceDef;
|
||||
pub use self::resource_path::{Resource, ResourcePath};
|
||||
pub use self::router::{ResourceInfo, Router, RouterBuilder};
|
||||
|
||||
#[cfg(feature = "http")]
|
||||
pub use self::url::{Quoter, Url};
|
||||
pub use self::url::Url;
|
||||
|
@ -1,5 +1,5 @@
|
||||
use std::borrow::Cow;
|
||||
use std::ops::Index;
|
||||
use std::ops::{DerefMut, Index};
|
||||
|
||||
use firestorm::profile_method;
|
||||
use serde::de;
|
||||
@ -213,8 +213,38 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: ResourcePath> Resource<T> for Path<T> {
|
||||
fn resource_path(&mut self) -> &mut Self {
|
||||
impl<T: ResourcePath> Resource for Path<T> {
|
||||
type Path = T;
|
||||
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path> {
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, P> Resource for T
|
||||
where
|
||||
T: DerefMut<Target = Path<P>>,
|
||||
P: ResourcePath,
|
||||
{
|
||||
type Path = P;
|
||||
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path> {
|
||||
&mut *self
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::cell::RefCell;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn deref_impls() {
|
||||
let mut foo = Path::new("/foo");
|
||||
let _ = (&mut foo).resource_path();
|
||||
|
||||
let foo = RefCell::new(foo);
|
||||
let _ = foo.borrow_mut().resource_path();
|
||||
}
|
||||
}
|
||||
|
219
actix-router/src/quoter.rs
Normal file
219
actix-router/src/quoter.rs
Normal file
@ -0,0 +1,219 @@
|
||||
#[allow(dead_code)]
|
||||
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
||||
|
||||
#[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 {
|
||||
/// 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.
|
||||
protected_table: [u8; 16],
|
||||
}
|
||||
|
||||
impl Quoter {
|
||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
||||
let mut quoter = Quoter {
|
||||
safe_table: [0; 16],
|
||||
protected_table: [0; 16],
|
||||
};
|
||||
|
||||
// prepare safe table
|
||||
for ch in 0..128 {
|
||||
if ALLOWED.contains(&ch) {
|
||||
set_bit(&mut quoter.safe_table, ch);
|
||||
}
|
||||
|
||||
if QS.contains(&ch) {
|
||||
set_bit(&mut quoter.safe_table, ch);
|
||||
}
|
||||
}
|
||||
|
||||
for &ch in safe {
|
||||
set_bit(&mut quoter.safe_table, ch)
|
||||
}
|
||||
|
||||
// prepare protected table
|
||||
for &ch in protected {
|
||||
set_bit(&mut quoter.safe_table, ch);
|
||||
set_bit(&mut quoter.protected_table, ch);
|
||||
}
|
||||
|
||||
quoter
|
||||
}
|
||||
|
||||
/// Re-quotes... ?
|
||||
///
|
||||
/// Returns `None` when no modification to the original string was required.
|
||||
pub fn requote(&self, val: &[u8]) -> Option<String> {
|
||||
let mut has_pct = 0;
|
||||
let mut pct = [b'%', 0, 0];
|
||||
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.map(|data| String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
///
|
||||
/// Returns `None` if either portion of the decoded pair does not evaluate to a valid hex value.
|
||||
///
|
||||
/// - `0x33 ('3'), 0x30 ('0') => 0x30 ('0')`
|
||||
/// - `0x34 ('4'), 0x31 ('1') => 0x41 ('A')`
|
||||
/// - `0x36 ('6'), 0x31 ('1') => 0x61 ('a')`
|
||||
fn hex_pair_to_char(d1: u8, d2: u8) -> Option<u8> {
|
||||
let (d_high, d_low) = (from_ascii_hex(d1)?, from_ascii_hex(d2)?);
|
||||
|
||||
// left shift high nibble by 4 bits
|
||||
Some(d_high << 4 | d_low)
|
||||
}
|
||||
|
||||
/// Sets bit in given bit-map to 1=true.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `ch` index is out of bounds.
|
||||
fn set_bit(array: &mut [u8], ch: u8) {
|
||||
array[(ch >> 3) as usize] |= 0b1 << (ch & 0b111)
|
||||
}
|
||||
|
||||
/// Returns true if bit to true in given bit-map.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if `ch` index is out of bounds.
|
||||
fn bit_at(array: &[u8], ch: u8) -> bool {
|
||||
array[(ch >> 3) as usize] & (0b1 << (ch & 0b111)) != 0
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
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]
|
||||
fn custom_quoter() {
|
||||
let q = Quoter::new(b"", b"+");
|
||||
assert_eq!(q.requote(b"/a%25c").unwrap(), "/a%c");
|
||||
assert_eq!(q.requote(b"/a%2Bc").unwrap(), "/a%2Bc");
|
||||
|
||||
let q = Quoter::new(b"%+", b"/");
|
||||
assert_eq!(q.requote(b"/a%25b%2Bc").unwrap(), "/a%b+c");
|
||||
assert_eq!(q.requote(b"/a%2fb").unwrap(), "/a%2fb");
|
||||
assert_eq!(q.requote(b"/a%2Fb").unwrap(), "/a%2Fb");
|
||||
assert_eq!(q.requote(b"/a%0Ab").unwrap(), "/a\nb");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn quoter_no_modification() {
|
||||
let q = Quoter::new(b"", b"");
|
||||
assert_eq!(q.requote(b"/abc/../efg"), None);
|
||||
}
|
||||
}
|
@ -678,15 +678,14 @@ impl ResourceDef {
|
||||
/// assert!(!try_match(&resource, &mut path));
|
||||
/// assert_eq!(path.unprocessed(), "/user/admin/stars");
|
||||
/// ```
|
||||
pub fn capture_match_info_fn<R, T, F, U>(
|
||||
pub fn capture_match_info_fn<R, F, U>(
|
||||
&self,
|
||||
resource: &mut R,
|
||||
check_fn: F,
|
||||
user_data: U,
|
||||
) -> bool
|
||||
where
|
||||
R: Resource<T>,
|
||||
T: ResourcePath,
|
||||
R: Resource,
|
||||
F: FnOnce(&R, U) -> bool,
|
||||
{
|
||||
profile_method!(capture_match_info_fn);
|
||||
|
@ -2,8 +2,11 @@ use crate::Path;
|
||||
|
||||
// TODO: this trait is necessary, document it
|
||||
// see impl Resource for ServiceRequest
|
||||
pub trait Resource<T: ResourcePath> {
|
||||
fn resource_path(&mut self) -> &mut Path<T>;
|
||||
pub trait Resource {
|
||||
/// Type of resource's path returned in `resource_path`.
|
||||
type Path: ResourcePath;
|
||||
|
||||
fn resource_path(&mut self) -> &mut Path<Self::Path>;
|
||||
}
|
||||
|
||||
pub trait ResourcePath {
|
||||
|
@ -1,6 +1,6 @@
|
||||
use firestorm::profile_method;
|
||||
|
||||
use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath};
|
||||
use crate::{IntoPatterns, Resource, ResourceDef};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq)]
|
||||
pub struct ResourceId(pub u16);
|
||||
@ -26,10 +26,9 @@ impl<T, U> Router<T, U> {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn recognize<R, P>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
|
||||
pub fn recognize<R>(&self, resource: &mut R) -> Option<(&T, ResourceId)>
|
||||
where
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize);
|
||||
|
||||
@ -42,10 +41,9 @@ impl<T, U> Router<T, U> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn recognize_mut<R, P>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
|
||||
pub fn recognize_mut<R>(&mut self, resource: &mut R) -> Option<(&mut T, ResourceId)>
|
||||
where
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_mut);
|
||||
|
||||
@ -58,11 +56,10 @@ impl<T, U> Router<T, U> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn recognize_fn<R, P, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
|
||||
pub fn recognize_fn<R, F>(&self, resource: &mut R, check: F) -> Option<(&T, ResourceId)>
|
||||
where
|
||||
F: Fn(&R, &Option<U>) -> bool,
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_checked);
|
||||
|
||||
@ -75,15 +72,14 @@ impl<T, U> Router<T, U> {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn recognize_mut_fn<R, P, F>(
|
||||
pub fn recognize_mut_fn<R, F>(
|
||||
&mut self,
|
||||
resource: &mut R,
|
||||
check: F,
|
||||
) -> Option<(&mut T, ResourceId)>
|
||||
where
|
||||
F: Fn(&R, &Option<U>) -> bool,
|
||||
R: Resource<P>,
|
||||
P: ResourcePath,
|
||||
R: Resource,
|
||||
{
|
||||
profile_method!(recognize_mut_checked);
|
||||
|
||||
|
@ -1,40 +1,6 @@
|
||||
use crate::ResourcePath;
|
||||
|
||||
#[allow(dead_code)]
|
||||
const GEN_DELIMS: &[u8] = b":/?#[]@";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS_WITHOUT_QS: &[u8] = b"!$'()*,";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const SUB_DELIMS: &[u8] = b"!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const RESERVED: &[u8] = b":/?#[]@!$'()*,+?=;";
|
||||
|
||||
#[allow(dead_code)]
|
||||
const UNRESERVED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
1234567890
|
||||
-._~";
|
||||
|
||||
const ALLOWED: &[u8] = b"abcdefghijklmnopqrstuvwxyz
|
||||
ABCDEFGHIJKLMNOPQRSTUVWXYZ
|
||||
1234567890
|
||||
-._~
|
||||
!$'()*,";
|
||||
|
||||
const QS: &[u8] = b"+&=;b";
|
||||
|
||||
#[inline]
|
||||
fn bit_at(array: &[u8], ch: u8) -> bool {
|
||||
array[(ch >> 3) as usize] & (1 << (ch & 7)) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_bit(array: &mut [u8], ch: u8) {
|
||||
array[(ch >> 3) as usize] |= 1 << (ch & 7)
|
||||
}
|
||||
use crate::Quoter;
|
||||
|
||||
thread_local! {
|
||||
static DEFAULT_QUOTER: Quoter = Quoter::new(b"@:", b"%/+");
|
||||
@ -54,18 +20,20 @@ impl Url {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
|
||||
pub fn new_with_quoter(uri: http::Uri, quoter: &Quoter) -> Url {
|
||||
Url {
|
||||
path: quoter.requote(uri.path().as_bytes()),
|
||||
uri,
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns URI.
|
||||
#[inline]
|
||||
pub fn uri(&self) -> &http::Uri {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Returns path.
|
||||
#[inline]
|
||||
pub fn path(&self) -> &str {
|
||||
match self.path {
|
||||
@ -94,113 +62,6 @@ impl ResourcePath for Url {
|
||||
}
|
||||
}
|
||||
|
||||
/// A quoter
|
||||
pub struct Quoter {
|
||||
safe_table: [u8; 16],
|
||||
protected_table: [u8; 16],
|
||||
}
|
||||
|
||||
impl Quoter {
|
||||
pub fn new(safe: &[u8], protected: &[u8]) -> Quoter {
|
||||
let mut quoter = Quoter {
|
||||
safe_table: [0; 16],
|
||||
protected_table: [0; 16],
|
||||
};
|
||||
|
||||
// prepare safe table
|
||||
for i in 0..128 {
|
||||
if ALLOWED.contains(&i) {
|
||||
set_bit(&mut quoter.safe_table, i);
|
||||
}
|
||||
if QS.contains(&i) {
|
||||
set_bit(&mut quoter.safe_table, i);
|
||||
}
|
||||
}
|
||||
|
||||
for ch in safe {
|
||||
set_bit(&mut quoter.safe_table, *ch)
|
||||
}
|
||||
|
||||
// prepare protected table
|
||||
for ch in protected {
|
||||
set_bit(&mut quoter.safe_table, *ch);
|
||||
set_bit(&mut quoter.protected_table, *ch);
|
||||
}
|
||||
|
||||
quoter
|
||||
}
|
||||
|
||||
pub fn requote(&self, val: &[u8]) -> Option<String> {
|
||||
let mut has_pct = 0;
|
||||
let mut pct = [b'%', 0, 0];
|
||||
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) = restore_ch(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.map(|data| String::from_utf8_lossy(&data).into_owned())
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn from_hex(v: u8) -> Option<u8> {
|
||||
if (b'0'..=b'9').contains(&v) {
|
||||
Some(v - 0x30) // ord('0') == 0x30
|
||||
} else if (b'A'..=b'F').contains(&v) {
|
||||
Some(v - 0x41 + 10) // ord('A') == 0x41
|
||||
} else if (b'a'..=b'f').contains(&v) {
|
||||
Some(v - 0x61 + 10) // ord('a') == 0x61
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn restore_ch(d1: u8, d2: u8) -> Option<u8> {
|
||||
from_hex(d1).and_then(|d1| from_hex(d2).map(move |d2| d1 << 4 | d2))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use http::Uri;
|
||||
@ -229,6 +90,16 @@ mod tests {
|
||||
|
||||
let path = match_url(re, "/user/2345/test");
|
||||
assert_eq!(path.get("id").unwrap(), "2345");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protected_chars() {
|
||||
let re = "/user/{id}/test";
|
||||
|
||||
let encoded = percent_encode(PROTECTED);
|
||||
let path = match_url(re, format!("/user/{}/test", encoded));
|
||||
// characters in captured segment remain unencoded
|
||||
assert_eq!(path.get("id").unwrap(), &encoded);
|
||||
|
||||
// "%25" should never be decoded into '%' to guarantee the output is a valid
|
||||
// percent-encoded format
|
||||
@ -239,13 +110,6 @@ mod tests {
|
||||
assert_eq!(path.get("id").unwrap(), "qwe%25rty");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn protected_chars() {
|
||||
let encoded = percent_encode(PROTECTED);
|
||||
let path = match_url("/user/{id}/test", format!("/user/{}/test", encoded));
|
||||
assert_eq!(path.get("id").unwrap(), &encoded);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn non_protected_ascii() {
|
||||
let non_protected_ascii = ('\u{0}'..='\u{7F}')
|
||||
@ -273,25 +137,4 @@ mod tests {
|
||||
// We should always get a valid utf8 string
|
||||
assert!(String::from_utf8(path.path().as_bytes().to_owned()).is_ok());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn hex_encoding() {
|
||||
let hex = b"0123456789abcdefABCDEF";
|
||||
|
||||
for i in 0..256 {
|
||||
let c = i as u8;
|
||||
if hex.contains(&c) {
|
||||
assert!(from_hex(c).is_some())
|
||||
} else {
|
||||
assert!(from_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_hex(hex[i]).unwrap(), expected[i]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -3,8 +3,12 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.1.0-beta.11 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 0.1.0-beta.10 - 2021-12-27
|
||||
* No significant changes since `0.1.0-beta.9`.
|
||||
- No significant changes since `0.1.0-beta.9`.
|
||||
|
||||
|
||||
## 0.1.0-beta.9 - 2021-12-17
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-test"
|
||||
version = "0.1.0-beta.10"
|
||||
version = "0.1.0-beta.11"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
@ -29,13 +29,13 @@ openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-http-test = "3.0.0-beta.10"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-http-test = "3.0.0-beta.11"
|
||||
actix-rt = "2.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.16", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.15", default-features = false, features = ["cookies"] }
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.18", default-features = false, features = ["cookies"] }
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||
@ -45,4 +45,4 @@ serde_json = "1"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||
tokio = { version = "1.8", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
@ -3,8 +3,12 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 4.0.0-beta.10 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 4.0.0-beta.9 - 2021-12-27
|
||||
* No significant changes since `4.0.0-beta.8`.
|
||||
- No significant changes since `4.0.0-beta.8`.
|
||||
|
||||
|
||||
## 4.0.0-beta.8 - 2021-12-11
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-actors"
|
||||
version = "4.0.0-beta.9"
|
||||
version = "4.0.0-beta.10"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix actors support for Actix Web"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
@ -16,19 +16,19 @@ path = "src/lib.rs"
|
||||
[dependencies]
|
||||
actix = { version = "0.12.0", default-features = false }
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-web = { version = "4.0.0-beta.16", default-features = false }
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-web = { version = "4.0.0-beta.20", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
pin-project-lite = "0.2"
|
||||
tokio = { version = "1.8", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.10"
|
||||
awc = { version = "3.0.0-beta.15", default-features = false }
|
||||
actix-test = "0.1.0-beta.11"
|
||||
awc = { version = "3.0.0-beta.18", default-features = false }
|
||||
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
|
@ -3,15 +3,15 @@
|
||||
> Actix actors support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://docs.rs/actix-web-actors/4.0.0-beta.9)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-web-actors/4.0.0-beta.10)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.9)
|
||||
[](https://deps.rs/crate/actix-web-actors/4.0.0-beta.10)
|
||||
[](https://crates.io/crates/actix-web-actors)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-web-actors)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
@ -3,6 +3,10 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.5.0-rc.1 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 0.5.0-beta.6 - 2021-12-11
|
||||
- No significant changes since `0.5.0-beta.5`.
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web-codegen"
|
||||
version = "0.5.0-beta.6"
|
||||
version = "0.5.0-rc.1"
|
||||
description = "Routing and runtime macros for Actix Web"
|
||||
homepage = "https://actix.rs"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
@ -15,17 +15,17 @@ edition = "2018"
|
||||
proc-macro = true
|
||||
|
||||
[dependencies]
|
||||
actix-router = "0.5.0-beta.4"
|
||||
proc-macro2 = "1"
|
||||
quote = "1"
|
||||
syn = { version = "1", features = ["full", "parsing"] }
|
||||
proc-macro2 = "1"
|
||||
actix-router = "0.5.0-beta.3"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-macros = "0.2.3"
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.10"
|
||||
actix-test = "0.1.0-beta.11"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = "4.0.0-beta.16"
|
||||
actix-web = "4.0.0-beta.20"
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
trybuild = "1"
|
||||
|
@ -3,18 +3,18 @@
|
||||
> Routing and runtime macros for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-web-codegen)
|
||||
[](https://docs.rs/actix-web-codegen/0.5.0-beta.6)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||
[](https://docs.rs/actix-web-codegen/0.5.0-rc.1)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.54.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.6)
|
||||
[](https://deps.rs/crate/actix-web-codegen/0.5.0-rc.1)
|
||||
[](https://crates.io/crates/actix-web-codegen)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-web-codegen)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
||||
## Compile Testing
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
#[rustversion::stable(1.52)] // MSRV
|
||||
#[rustversion::stable(1.54)] // MSRV
|
||||
#[test]
|
||||
fn compile_macros() {
|
||||
let t = trybuild::TestCases::new();
|
||||
|
@ -1,13 +1,13 @@
|
||||
error: The #[route(..)] macro requires at least one `method` attribute
|
||||
--> $DIR/route-missing-method-fail.rs:3:1
|
||||
--> tests/trybuild/route-missing-method-fail.rs:3:1
|
||||
|
|
||||
3 | #[route("/")]
|
||||
| ^^^^^^^^^^^^^
|
||||
|
|
||||
= note: this error originates in an attribute macro (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
= note: this error originates in the attribute macro `route` (in Nightly builds, run with -Z macro-backtrace for more info)
|
||||
|
||||
error[E0277]: the trait bound `fn() -> impl std::future::Future {index}: HttpServiceFactory` is not satisfied
|
||||
--> $DIR/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));
|
||||
| ^^^^^ the trait `HttpServiceFactory` is not implemented for `fn() -> impl std::future::Future {index}`
|
||||
|
@ -3,6 +3,29 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.18 - 2022-01-04
|
||||
- Minimum supported Rust version (MSRV) is now 1.54.
|
||||
|
||||
|
||||
## 3.0.0-beta.17 - 2021-12-29
|
||||
### Changed
|
||||
- Update `cookie` dependency (re-exported) to `0.16`. [#2555]
|
||||
|
||||
### Security
|
||||
- `cookie` upgrade addresses [`RUSTSEC-2020-0071`].
|
||||
|
||||
[#2555]: https://github.com/actix/actix-web/pull/2555
|
||||
[`RUSTSEC-2020-0071`]: https://rustsec.org/advisories/RUSTSEC-2020-0071.html
|
||||
|
||||
|
||||
## 3.0.0-beta.16 - 2021-12-29
|
||||
- `*::send_json` and `*::send_form` methods now receive `impl Serialize`. [#2553]
|
||||
- `FrozenClientRequest::extra_header` now uses receives an `impl TryIntoHeaderPair`. [#2553]
|
||||
- Remove unnecessary `Unpin` bounds on `*::send_stream`. [#2553]
|
||||
|
||||
[#2553]: https://github.com/actix/actix-web/pull/2553
|
||||
|
||||
|
||||
## 3.0.0-beta.15 - 2021-12-27
|
||||
- Rename `Connector::{ssl => openssl}`. [#2503]
|
||||
- Improve `Client` instantiation efficiency when using `openssl` by only building connectors once. [#2503]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "awc"
|
||||
version = "3.0.0-beta.15"
|
||||
version = "3.0.0-beta.18"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
@ -60,7 +60,7 @@ dangerous-h2c = []
|
||||
[dependencies]
|
||||
actix-codec = "0.4.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-http = "3.0.0-beta.17"
|
||||
actix-http = "3.0.0-beta.18"
|
||||
actix-rt = { version = "2.1", default-features = false }
|
||||
actix-tls = { version = "3.0.0", features = ["connect", "uri"] }
|
||||
actix-utils = "3.0.0"
|
||||
@ -83,9 +83,9 @@ rand = "0.8"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7"
|
||||
tokio = { version = "1.8", features = ["sync"] }
|
||||
tokio = { version = "1.8.4", features = ["sync"] }
|
||||
|
||||
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
|
||||
cookie = { version = "0.16", features = ["percent-encode"], optional = true }
|
||||
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features = ["dangerous_configuration"] }
|
||||
@ -93,21 +93,23 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
|
||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-http = { version = "3.0.0-beta.17", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.10", features = ["openssl"] }
|
||||
actix-http = { version = "3.0.0-beta.18", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
actix-server = "2.0.0-rc.2"
|
||||
actix-test = { version = "0.1.0-beta.10", features = ["openssl", "rustls"] }
|
||||
actix-test = { version = "0.1.0-beta.11", features = ["openssl", "rustls"] }
|
||||
actix-tls = { version = "3.0.0", features = ["openssl", "rustls"] }
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.16", features = ["openssl"] }
|
||||
actix-web = { version = "4.0.0-beta.20", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
brotli = "3.3.3"
|
||||
const-str = "0.3"
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
static_assertions = "1.1"
|
||||
rcgen = "0.8"
|
||||
rustls-pemfile = "0.2"
|
||||
zstd = "0.9"
|
||||
|
||||
[[example]]
|
||||
name = "client"
|
||||
|
@ -3,16 +3,16 @@
|
||||
> Async HTTP and WebSocket client library.
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.0.0-beta.15)
|
||||
[](https://docs.rs/awc/3.0.0-beta.18)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.15)
|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.18)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/awc)
|
||||
- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https)
|
||||
- Minimum Supported Rust Version (MSRV): 1.52
|
||||
- Minimum Supported Rust Version (MSRV): 1.54
|
||||
|
||||
## Example
|
||||
|
||||
|
@ -1,17 +1,13 @@
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
fmt, mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use bytes::Bytes;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream};
|
||||
|
||||
use crate::BoxError;
|
||||
use actix_http::body::{BodySize, BoxBody, MessageBody};
|
||||
|
||||
pin_project! {
|
||||
/// Represents various types of HTTP message body.
|
||||
@ -160,91 +156,6 @@ impl<S: fmt::Debug> fmt::Debug for AnyBody<S> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<&'static str> for AnyBody<B> {
|
||||
fn from(string: &'static str) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from_static(string.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<&'static [u8]> for AnyBody<B> {
|
||||
fn from(bytes: &'static [u8]) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from_static(bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Vec<u8>> for AnyBody<B> {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from(vec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<String> for AnyBody<B> {
|
||||
fn from(string: String) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<&'_ String> for AnyBody<B> {
|
||||
fn from(string: &String) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Cow<'_, str>> for AnyBody<B> {
|
||||
fn from(string: Cow<'_, str>) -> Self {
|
||||
match string {
|
||||
Cow::Owned(s) => Self::from(s),
|
||||
Cow::Borrowed(s) => Self::Bytes {
|
||||
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Bytes> for AnyBody<B> {
|
||||
fn from(bytes: Bytes) -> Self {
|
||||
Self::Bytes { body: bytes }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<BytesMut> for AnyBody<B> {
|
||||
fn from(bytes: BytesMut) -> Self {
|
||||
Self::Bytes {
|
||||
body: bytes.freeze(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<SizedStream<S>> for AnyBody
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
fn from(stream: SizedStream<S>) -> Self {
|
||||
AnyBody::new_boxed(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<BodyStream<S>> for AnyBody
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
fn from(stream: BodyStream<S>) -> Self {
|
||||
AnyBody::new_boxed(stream)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::marker::PhantomPinned;
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::{convert::TryFrom, net, rc::Rc, time::Duration};
|
||||
use std::{net, rc::Rc, time::Duration};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
@ -7,7 +7,7 @@ use serde::Serialize;
|
||||
use actix_http::{
|
||||
body::MessageBody,
|
||||
error::HttpError,
|
||||
header::{HeaderMap, HeaderName, TryIntoHeaderValue},
|
||||
header::{HeaderMap, TryIntoHeaderPair},
|
||||
Method, RequestHead, Uri,
|
||||
};
|
||||
|
||||
@ -18,6 +18,7 @@ use crate::{
|
||||
};
|
||||
|
||||
/// `FrozenClientRequest` struct represents cloneable client request.
|
||||
///
|
||||
/// It could be used to send same request multiple times.
|
||||
#[derive(Clone)]
|
||||
pub struct FrozenClientRequest {
|
||||
@ -83,7 +84,7 @@ impl FrozenClientRequest {
|
||||
/// Send a streaming body.
|
||||
pub fn send_stream<S, E>(&self, stream: S) -> SendClientRequest
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
RequestSender::Rc(self.head.clone(), None).send_stream(
|
||||
@ -105,20 +106,14 @@ impl FrozenClientRequest {
|
||||
)
|
||||
}
|
||||
|
||||
/// Create a `FrozenSendBuilder` with extra headers
|
||||
/// Clones this `FrozenClientRequest`, returning a new one with extra headers added.
|
||||
pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder {
|
||||
FrozenSendBuilder::new(self.clone(), extra_headers)
|
||||
}
|
||||
|
||||
/// Create a `FrozenSendBuilder` with an extra header
|
||||
pub fn extra_header<K, V>(&self, key: K, value: V) -> FrozenSendBuilder
|
||||
where
|
||||
HeaderName: TryFrom<K>,
|
||||
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
||||
V: TryIntoHeaderValue,
|
||||
{
|
||||
self.extra_headers(HeaderMap::new())
|
||||
.extra_header(key, value)
|
||||
/// Clones this `FrozenClientRequest`, returning a new one with the extra header added.
|
||||
pub fn extra_header(&self, header: impl TryIntoHeaderPair) -> FrozenSendBuilder {
|
||||
self.extra_headers(HeaderMap::new()).extra_header(header)
|
||||
}
|
||||
}
|
||||
|
||||
@ -139,29 +134,20 @@ impl FrozenSendBuilder {
|
||||
}
|
||||
|
||||
/// Insert a header, it overrides existing header in `FrozenClientRequest`.
|
||||
pub fn extra_header<K, V>(mut self, key: K, value: V) -> Self
|
||||
where
|
||||
HeaderName: TryFrom<K>,
|
||||
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
|
||||
V: TryIntoHeaderValue,
|
||||
{
|
||||
match HeaderName::try_from(key) {
|
||||
Ok(key) => match value.try_into_value() {
|
||||
Ok(value) => {
|
||||
self.extra_headers.insert(key, value);
|
||||
}
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
},
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
pub fn extra_header(mut self, header: impl TryIntoHeaderPair) -> Self {
|
||||
match header.try_into_pair() {
|
||||
Ok((key, value)) => {
|
||||
self.extra_headers.insert(key, value);
|
||||
}
|
||||
|
||||
Err(err) => self.err = Some(err.into()),
|
||||
}
|
||||
|
||||
self
|
||||
}
|
||||
|
||||
/// Complete request construction and send a body.
|
||||
pub fn send_body<B>(self, body: B) -> SendClientRequest
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
pub fn send_body(self, body: impl MessageBody + 'static) -> SendClientRequest {
|
||||
if let Some(e) = self.err {
|
||||
return e.into();
|
||||
}
|
||||
@ -176,9 +162,9 @@ impl FrozenSendBuilder {
|
||||
}
|
||||
|
||||
/// Complete request construction and send a json body.
|
||||
pub fn send_json<T: Serialize>(self, value: &T) -> SendClientRequest {
|
||||
if let Some(e) = self.err {
|
||||
return e.into();
|
||||
pub fn send_json(self, value: impl Serialize) -> SendClientRequest {
|
||||
if let Some(err) = self.err {
|
||||
return err.into();
|
||||
}
|
||||
|
||||
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json(
|
||||
@ -191,7 +177,7 @@ impl FrozenSendBuilder {
|
||||
}
|
||||
|
||||
/// Complete request construction and send an urlencoded body.
|
||||
pub fn send_form<T: Serialize>(self, value: &T) -> SendClientRequest {
|
||||
pub fn send_form(self, value: impl Serialize) -> SendClientRequest {
|
||||
if let Some(e) = self.err {
|
||||
return e.into();
|
||||
}
|
||||
@ -208,7 +194,7 @@ impl FrozenSendBuilder {
|
||||
/// Complete request construction and send a streaming body.
|
||||
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
if let Some(e) = self.err {
|
||||
|
@ -190,7 +190,9 @@ where
|
||||
let body_new = if is_redirect {
|
||||
// try to reuse body
|
||||
match body {
|
||||
Some(ref bytes) => AnyBody::from(bytes.clone()),
|
||||
Some(ref bytes) => AnyBody::Bytes {
|
||||
body: bytes.clone(),
|
||||
},
|
||||
// TODO: should this be AnyBody::Empty or AnyBody::None.
|
||||
_ => AnyBody::empty(),
|
||||
}
|
||||
|
@ -385,7 +385,7 @@ impl ClientRequest {
|
||||
/// Set an streaming body and generate `ClientRequest`.
|
||||
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
let slf = match self.prep_for_sending() {
|
||||
|
@ -7,8 +7,8 @@ use std::{
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
error::PayloadError, header, header::HeaderMap, BoxedPayloadStream, Extensions,
|
||||
HttpMessage, Payload, ResponseHead, StatusCode, Version,
|
||||
error::PayloadError, header::HeaderMap, BoxedPayloadStream, Extensions, HttpMessage,
|
||||
Payload, ResponseHead, StatusCode, Version,
|
||||
};
|
||||
use actix_rt::time::{sleep, Sleep};
|
||||
use bytes::Bytes;
|
||||
@ -119,12 +119,13 @@ impl<S> ClientResponse<S> {
|
||||
|
||||
if self.extensions().get::<Cookies>().is_none() {
|
||||
let mut cookies = Vec::new();
|
||||
for hdr in self.headers().get_all(&header::SET_COOKIE) {
|
||||
for hdr in self.headers().get_all(&actix_http::header::SET_COOKIE) {
|
||||
let s = std::str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
|
||||
cookies.push(Cookie::parse_encoded(s)?.into_owned());
|
||||
}
|
||||
self.extensions_mut().insert(Cookies(cookies));
|
||||
}
|
||||
|
||||
Ok(Ref::map(self.extensions(), |ext| {
|
||||
&ext.get::<Cookies>().unwrap().0
|
||||
}))
|
||||
|
@ -181,17 +181,14 @@ pub(crate) enum RequestSender {
|
||||
}
|
||||
|
||||
impl RequestSender {
|
||||
pub(crate) fn send_body<B>(
|
||||
pub(crate) fn send_body(
|
||||
self,
|
||||
addr: Option<net::SocketAddr>,
|
||||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
body: B,
|
||||
) -> SendClientRequest
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
body: impl MessageBody + 'static,
|
||||
) -> SendClientRequest {
|
||||
let req = match self {
|
||||
RequestSender::Owned(head) => ConnectRequest::Client(
|
||||
RequestHeadType::Owned(head),
|
||||
@ -210,15 +207,15 @@ impl RequestSender {
|
||||
SendClientRequest::new(fut, response_decompress, timeout.or(config.timeout))
|
||||
}
|
||||
|
||||
pub(crate) fn send_json<T: Serialize>(
|
||||
pub(crate) fn send_json(
|
||||
mut self,
|
||||
addr: Option<net::SocketAddr>,
|
||||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
value: &T,
|
||||
value: impl Serialize,
|
||||
) -> SendClientRequest {
|
||||
let body = match serde_json::to_string(value) {
|
||||
let body = match serde_json::to_string(&value) {
|
||||
Ok(body) => body,
|
||||
Err(err) => return PrepForSendingError::Json(err).into(),
|
||||
};
|
||||
@ -227,22 +224,16 @@ impl RequestSender {
|
||||
return e.into();
|
||||
}
|
||||
|
||||
self.send_body(
|
||||
addr,
|
||||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
AnyBody::from_message_body(body.into_bytes()),
|
||||
)
|
||||
self.send_body(addr, response_decompress, timeout, config, body)
|
||||
}
|
||||
|
||||
pub(crate) fn send_form<T: Serialize>(
|
||||
pub(crate) fn send_form(
|
||||
mut self,
|
||||
addr: Option<net::SocketAddr>,
|
||||
response_decompress: bool,
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
value: &T,
|
||||
value: impl Serialize,
|
||||
) -> SendClientRequest {
|
||||
let body = match serde_urlencoded::to_string(value) {
|
||||
Ok(body) => body,
|
||||
@ -250,19 +241,13 @@ impl RequestSender {
|
||||
};
|
||||
|
||||
// set content-type
|
||||
if let Err(e) =
|
||||
if let Err(err) =
|
||||
self.set_header_if_none(header::CONTENT_TYPE, "application/x-www-form-urlencoded")
|
||||
{
|
||||
return e.into();
|
||||
return err.into();
|
||||
}
|
||||
|
||||
self.send_body(
|
||||
addr,
|
||||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
AnyBody::from_message_body(body.into_bytes()),
|
||||
)
|
||||
self.send_body(addr, response_decompress, timeout, config, body)
|
||||
}
|
||||
|
||||
pub(crate) fn send_stream<S, E>(
|
||||
@ -274,7 +259,7 @@ impl RequestSender {
|
||||
stream: S,
|
||||
) -> SendClientRequest
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
self.send_body(
|
||||
@ -282,7 +267,7 @@ impl RequestSender {
|
||||
response_decompress,
|
||||
timeout,
|
||||
config,
|
||||
AnyBody::new_boxed(BodyStream::new(stream)),
|
||||
BodyStream::new(stream),
|
||||
)
|
||||
}
|
||||
|
||||
@ -293,7 +278,7 @@ impl RequestSender {
|
||||
timeout: Option<Duration>,
|
||||
config: &ClientConfig,
|
||||
) -> SendClientRequest {
|
||||
self.send_body(addr, response_decompress, timeout, config, AnyBody::empty())
|
||||
self.send_body(addr, response_decompress, timeout, config, ())
|
||||
}
|
||||
|
||||
fn set_header_if_none<V>(&mut self, key: HeaderName, value: V) -> Result<(), HttpError>
|
||||
|
@ -1,5 +1,6 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::Infallible,
|
||||
io::{Read, Write},
|
||||
net::{IpAddr, Ipv4Addr},
|
||||
sync::{
|
||||
@ -15,43 +16,16 @@ use cookie::Cookie;
|
||||
use futures_util::stream;
|
||||
use rand::Rng;
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
use brotli2::write::BrotliEncoder;
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
|
||||
use actix_http::{ContentEncoding, HttpService, StatusCode};
|
||||
use actix_http::{HttpService, StatusCode};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::{fn_service, map_config, ServiceFactoryExt as _};
|
||||
use actix_web::{
|
||||
dev::{AppConfig, BodyEncoding},
|
||||
http::header,
|
||||
web, App, Error, HttpRequest, HttpResponse,
|
||||
};
|
||||
use actix_web::{dev::AppConfig, http::header, web, App, Error, HttpRequest, HttpResponse};
|
||||
use awc::error::{JsonPayloadError, PayloadError, SendRequestError};
|
||||
|
||||
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World \
|
||||
Hello World Hello World Hello World Hello World Hello World";
|
||||
mod utils;
|
||||
|
||||
const S: &str = "Hello World ";
|
||||
const STR: &str = const_str::repeat!(S, 100);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() {
|
||||
@ -471,15 +445,12 @@ async fn test_no_decompress() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new()
|
||||
.wrap(actix_web::middleware::Compress::default())
|
||||
.service(web::resource("/").route(web::to(|| {
|
||||
let mut res = HttpResponse::Ok().body(STR);
|
||||
res.encoding(header::ContentEncoding::Gzip);
|
||||
res
|
||||
})))
|
||||
.service(web::resource("/").route(web::to(|| HttpResponse::Ok().body(STR))))
|
||||
});
|
||||
|
||||
let mut res = awc::Client::new()
|
||||
.get(srv.url("/"))
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.no_decompress()
|
||||
.send()
|
||||
.await
|
||||
@ -488,15 +459,12 @@ async fn test_no_decompress() {
|
||||
|
||||
// read response
|
||||
let bytes = res.body().await.unwrap();
|
||||
|
||||
let mut e = GzDecoder::new(&bytes[..]);
|
||||
let mut dec = Vec::new();
|
||||
e.read_to_end(&mut dec).unwrap();
|
||||
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
|
||||
assert_eq!(utils::gzip::decode(bytes), STR.as_bytes());
|
||||
|
||||
// POST
|
||||
let mut res = awc::Client::new()
|
||||
.post(srv.url("/"))
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.no_decompress()
|
||||
.send()
|
||||
.await
|
||||
@ -504,10 +472,7 @@ async fn test_no_decompress() {
|
||||
assert!(res.status().is_success());
|
||||
|
||||
let bytes = res.body().await.unwrap();
|
||||
let mut e = GzDecoder::new(&bytes[..]);
|
||||
let mut dec = Vec::new();
|
||||
e.read_to_end(&mut dec).unwrap();
|
||||
assert_eq!(Bytes::from(dec), Bytes::from_static(STR.as_ref()));
|
||||
assert_eq!(utils::gzip::decode(bytes), STR.as_bytes());
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
@ -515,13 +480,9 @@ async fn test_no_decompress() {
|
||||
async fn test_client_gzip_encoding() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| {
|
||||
let mut e = GzEncoder::new(Vec::new(), Compression::default());
|
||||
e.write_all(STR.as_ref()).unwrap();
|
||||
let data = e.finish().unwrap();
|
||||
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-encoding", "gzip"))
|
||||
.body(data)
|
||||
.insert_header(header::ContentEncoding::Gzip)
|
||||
.body(utils::gzip::encode(STR))
|
||||
})))
|
||||
});
|
||||
|
||||
@ -531,7 +492,7 @@ async fn test_client_gzip_encoding() {
|
||||
|
||||
// read response
|
||||
let bytes = response.body().await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
assert_eq!(bytes, STR);
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
@ -539,13 +500,9 @@ async fn test_client_gzip_encoding() {
|
||||
async fn test_client_gzip_encoding_large() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|| {
|
||||
let mut e = GzEncoder::new(Vec::new(), Compression::default());
|
||||
e.write_all(STR.repeat(10).as_ref()).unwrap();
|
||||
let data = e.finish().unwrap();
|
||||
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-encoding", "gzip"))
|
||||
.body(data)
|
||||
.insert_header(header::ContentEncoding::Gzip)
|
||||
.body(utils::gzip::encode(STR.repeat(10)))
|
||||
})))
|
||||
});
|
||||
|
||||
@ -555,7 +512,7 @@ async fn test_client_gzip_encoding_large() {
|
||||
|
||||
// read response
|
||||
let bytes = response.body().await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from(STR.repeat(10)));
|
||||
assert_eq!(bytes, STR.repeat(10));
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
@ -569,12 +526,9 @@ async fn test_client_gzip_encoding_large_random() {
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
|
||||
let mut e = GzEncoder::new(Vec::new(), Compression::default());
|
||||
e.write_all(&data).unwrap();
|
||||
let data = e.finish().unwrap();
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-encoding", "gzip"))
|
||||
.body(data)
|
||||
.insert_header(header::ContentEncoding::Gzip)
|
||||
.body(utils::gzip::encode(data))
|
||||
})))
|
||||
});
|
||||
|
||||
@ -584,7 +538,7 @@ async fn test_client_gzip_encoding_large_random() {
|
||||
|
||||
// read response
|
||||
let bytes = response.body().await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from(data));
|
||||
assert_eq!(bytes, data);
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
@ -592,12 +546,9 @@ async fn test_client_gzip_encoding_large_random() {
|
||||
async fn test_client_brotli_encoding() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
|
||||
let mut e = BrotliEncoder::new(Vec::new(), 5);
|
||||
e.write_all(&data).unwrap();
|
||||
let data = e.finish().unwrap();
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-encoding", "br"))
|
||||
.body(data)
|
||||
.body(utils::brotli::encode(data))
|
||||
})))
|
||||
});
|
||||
|
||||
@ -621,12 +572,9 @@ async fn test_client_brotli_encoding_large_random() {
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").route(web::to(|data: Bytes| {
|
||||
let mut e = BrotliEncoder::new(Vec::new(), 5);
|
||||
e.write_all(&data).unwrap();
|
||||
let data = e.finish().unwrap();
|
||||
HttpResponse::Ok()
|
||||
.insert_header(("content-encoding", "br"))
|
||||
.body(data)
|
||||
.insert_header(header::ContentEncoding::Brotli)
|
||||
.body(utils::brotli::encode(&data))
|
||||
})))
|
||||
});
|
||||
|
||||
@ -636,25 +584,25 @@ async fn test_client_brotli_encoding_large_random() {
|
||||
|
||||
// read response
|
||||
let bytes = response.body().await.unwrap();
|
||||
assert_eq!(bytes.len(), data.len());
|
||||
assert_eq!(bytes, Bytes::from(data));
|
||||
assert_eq!(bytes, data);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_client_deflate_encoding() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|body: Bytes| {
|
||||
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body)
|
||||
}))
|
||||
App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
|
||||
});
|
||||
|
||||
let req = srv.post("/").send_body(STR);
|
||||
let req = srv
|
||||
.post("/")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.send_body(STR);
|
||||
|
||||
let mut res = req.await.unwrap();
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let bytes = res.body().await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
assert_eq!(bytes, STR);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
@ -666,12 +614,13 @@ async fn test_client_deflate_encoding_large_random() {
|
||||
.collect::<String>();
|
||||
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|body: Bytes| {
|
||||
HttpResponse::Ok().encoding(ContentEncoding::Br).body(body)
|
||||
}))
|
||||
App::new().default_service(web::to(|body: Bytes| HttpResponse::Ok().body(body)))
|
||||
});
|
||||
|
||||
let req = srv.post("/").send_body(data.clone());
|
||||
let req = srv
|
||||
.post("/")
|
||||
.insert_header((header::ACCEPT_ENCODING, "br"))
|
||||
.send_body(data.clone());
|
||||
|
||||
let mut res = req.await.unwrap();
|
||||
let bytes = res.body().await.unwrap();
|
||||
@ -684,15 +633,16 @@ async fn test_client_deflate_encoding_large_random() {
|
||||
async fn test_client_streaming_explicit() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|body: web::Payload| {
|
||||
HttpResponse::Ok()
|
||||
.encoding(ContentEncoding::Identity)
|
||||
.streaming(body)
|
||||
HttpResponse::Ok().streaming(body)
|
||||
}))
|
||||
});
|
||||
|
||||
let body =
|
||||
stream::once(async { Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes())) });
|
||||
let req = srv.post("/").send_stream(Box::pin(body));
|
||||
let req = srv
|
||||
.post("/")
|
||||
.insert_header((header::ACCEPT_ENCODING, "identity"))
|
||||
.send_stream(Box::pin(body));
|
||||
|
||||
let mut res = req.await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
@ -705,17 +655,16 @@ async fn test_client_streaming_explicit() {
|
||||
async fn test_body_streaming_implicit() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().default_service(web::to(|| {
|
||||
let body = stream::once(async {
|
||||
Ok::<_, actix_http::Error>(Bytes::from_static(STR.as_bytes()))
|
||||
});
|
||||
|
||||
HttpResponse::Ok()
|
||||
.encoding(ContentEncoding::Gzip)
|
||||
.streaming(Box::pin(body))
|
||||
let body =
|
||||
stream::once(async { Ok::<_, Infallible>(Bytes::from_static(STR.as_bytes())) });
|
||||
HttpResponse::Ok().streaming(body)
|
||||
}))
|
||||
});
|
||||
|
||||
let req = srv.get("/").send();
|
||||
let req = srv
|
||||
.get("/")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.send();
|
||||
|
||||
let mut res = req.await.unwrap();
|
||||
assert!(res.status().is_success());
|
||||
|
82
awc/tests/utils.rs
Normal file
82
awc/tests/utils.rs
Normal file
@ -0,0 +1,82 @@
|
||||
// compiling some tests will trigger unused function warnings even though other tests use them
|
||||
#![allow(dead_code)]
|
||||
|
||||
use std::io::{Read as _, Write as _};
|
||||
|
||||
pub mod gzip {
|
||||
use super::*;
|
||||
use flate2::{read::GzDecoder, write::GzEncoder, Compression};
|
||||
|
||||
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut encoder = GzEncoder::new(Vec::new(), Compression::fast());
|
||||
encoder.write_all(bytes.as_ref()).unwrap();
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut decoder = GzDecoder::new(bytes.as_ref());
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub mod deflate {
|
||||
use super::*;
|
||||
use flate2::{read::ZlibDecoder, write::ZlibEncoder, Compression};
|
||||
|
||||
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut encoder = ZlibEncoder::new(Vec::new(), Compression::fast());
|
||||
encoder.write_all(bytes.as_ref()).unwrap();
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut decoder = ZlibDecoder::new(bytes.as_ref());
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub mod brotli {
|
||||
use super::*;
|
||||
use ::brotli::{reader::Decompressor as BrotliDecoder, CompressorWriter as BrotliEncoder};
|
||||
|
||||
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut encoder = BrotliEncoder::new(
|
||||
Vec::new(),
|
||||
8 * 1024, // 32 KiB buffer
|
||||
3, // BROTLI_PARAM_QUALITY
|
||||
22, // BROTLI_PARAM_LGWIN
|
||||
);
|
||||
encoder.write_all(bytes.as_ref()).unwrap();
|
||||
encoder.flush().unwrap();
|
||||
encoder.into_inner()
|
||||
}
|
||||
|
||||
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut decoder = BrotliDecoder::new(bytes.as_ref(), 8_096);
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
||||
|
||||
pub mod zstd {
|
||||
use super::*;
|
||||
use ::zstd::stream::{read::Decoder, write::Encoder};
|
||||
|
||||
pub fn encode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut encoder = Encoder::new(Vec::new(), 3).unwrap();
|
||||
encoder.write_all(bytes.as_ref()).unwrap();
|
||||
encoder.finish().unwrap()
|
||||
}
|
||||
|
||||
pub fn decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
let mut decoder = Decoder::new(bytes.as_ref()).unwrap();
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
}
|
@ -1 +1 @@
|
||||
msrv = "1.52"
|
||||
msrv = "1.54"
|
||||
|
@ -15,6 +15,7 @@ digraph {
|
||||
|
||||
"actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" }
|
||||
"awc" -> { "actix-http" }
|
||||
"actix-web-codegen" -> { "actix-router" }
|
||||
"actix-web-actors" -> { "actix" "actix-web" "actix-http" }
|
||||
"actix-multipart" -> { "actix-web" }
|
||||
"actix-files" -> { "actix-web" }
|
||||
|
13
scripts/bump
13
scripts/bump
@ -17,9 +17,18 @@ if [ "$(uname)" = "Darwin" ]; then
|
||||
fi
|
||||
|
||||
CARGO_MANIFEST=$DIR/Cargo.toml
|
||||
CHANGELOG_FILE=$DIR/CHANGES.md
|
||||
README_FILE=$DIR/README.md
|
||||
|
||||
# determine changelog file name
|
||||
if [ -f "$DIR/CHANGES.md" ]; then
|
||||
CHANGELOG_FILE=$DIR/CHANGES.md
|
||||
elif [ -f "$DIR/CHANGELOG.md" ]; then
|
||||
CHANGELOG_FILE=$DIR/CHANGELOG.md
|
||||
else
|
||||
echo "No changelog file found"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# get current version
|
||||
PACKAGE_NAME="$(sed -nE 's/^name ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST" | head -n 1)"
|
||||
CURRENT_VERSION="$(sed -nE 's/^version ?= ?"([^"]+)"$/\1/ p' "$CARGO_MANIFEST")"
|
||||
@ -40,7 +49,7 @@ cat "$CHANGELOG_FILE" |
|
||||
|
||||
# if word count of changelog chunk is 0 then insert filler changelog chunk
|
||||
if [ "$(wc -w "$CHANGE_CHUNK_FILE" | awk '{ print $1 }')" = "0" ]; then
|
||||
echo "* No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE"
|
||||
echo "- No significant changes since \`$CURRENT_VERSION\`." >"$CHANGE_CHUNK_FILE"
|
||||
echo >>"$CHANGE_CHUNK_FILE"
|
||||
echo >>"$CHANGE_CHUNK_FILE"
|
||||
fi
|
||||
|
@ -12,16 +12,16 @@ save_exit_code() {
|
||||
[ "$CMD_EXIT" = "0" ] || EXIT=$CMD_EXIT
|
||||
}
|
||||
|
||||
save_exit_code cargo test --lib --tests -p=actix-router --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-http --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
save_exit_code cargo test --lib --tests -p=awc --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-test --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-files
|
||||
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
save_exit_code cargo test --lib --tests -p=actix-router --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-http --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --nocapture --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-codegen --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=awc --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-http-test --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-test --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-files -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-multipart --all-features -- --nocapture
|
||||
save_exit_code cargo test --lib --tests -p=actix-web-actors --all-features -- --nocapture
|
||||
|
||||
save_exit_code cargo test --workspace --doc
|
||||
|
||||
|
151
src/app.rs
151
src/app.rs
@ -51,7 +51,10 @@ impl App<AppEntry> {
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> App<T> {
|
||||
impl<T> App<T>
|
||||
where
|
||||
T: ServiceFactory<ServiceRequest, Config = (), Error = Error, InitError = ()>,
|
||||
{
|
||||
/// Set application (root level) data.
|
||||
///
|
||||
/// Application data stored with `App::app_data()` method is available through the
|
||||
@ -109,6 +112,7 @@ impl<T> App<T> {
|
||||
/// .route("/", web::get().to(handler))
|
||||
/// })
|
||||
/// ```
|
||||
#[doc(alias = "manage")]
|
||||
pub fn app_data<U: 'static>(mut self, ext: U) -> Self {
|
||||
self.extensions.insert(ext);
|
||||
self
|
||||
@ -316,65 +320,63 @@ impl<T> App<T> {
|
||||
self
|
||||
}
|
||||
|
||||
/// Registers middleware, in the form of a middleware component (type),
|
||||
/// that runs during inbound and/or outbound processing in the request
|
||||
/// life-cycle (request -> response), modifying request/response as
|
||||
/// necessary, across all requests managed by the *Application*.
|
||||
/// Registers an app-wide middleware.
|
||||
///
|
||||
/// Use middleware when you need to read or modify *every* request or
|
||||
/// response in some way.
|
||||
/// Registers middleware, in the form of a middleware compo nen t (type), that runs during
|
||||
/// inbound and/or outbound processing in the request life-cycle (request -> response),
|
||||
/// modifying request/response as necessary, across all requests managed by the `App`.
|
||||
///
|
||||
/// Notice that the keyword for registering middleware is `wrap`. As you
|
||||
/// register middleware using `wrap` in the App builder, imagine wrapping
|
||||
/// layers around an inner App. The first middleware layer exposed to a
|
||||
/// Request is the outermost layer-- the *last* registered in
|
||||
/// the builder chain. Consequently, the *first* middleware registered
|
||||
/// in the builder chain is the *last* to execute during request processing.
|
||||
/// Use middleware when you need to read or modify *every* request or response in some way.
|
||||
///
|
||||
/// Middleware can be applied similarly to individual `Scope`s and `Resource`s.
|
||||
/// See [`Scope::wrap`](crate::Scope::wrap) and [`Resource::wrap`].
|
||||
///
|
||||
/// # Middleware Order
|
||||
/// Notice that the keyword for registering middleware is `wrap`. As you register middleware
|
||||
/// using `wrap` in the App builder, imagine wrapping layers around an inner App. The first
|
||||
/// middleware layer exposed to a Request is the outermost layer (i.e., the *last* registered in
|
||||
/// the builder chain). Consequently, the *first* middleware registered in the builder chain is
|
||||
/// the *last* to start executing during request processing.
|
||||
///
|
||||
/// Ordering is less obvious when wrapped services also have middleware applied. In this case,
|
||||
/// middlewares are run in reverse order for `App` _and then_ in reverse order for the
|
||||
/// wrapped service.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_service::Service;
|
||||
/// use actix_web::{middleware, web, App};
|
||||
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
|
||||
///
|
||||
/// async fn index() -> &'static str {
|
||||
/// "Welcome!"
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = App::new()
|
||||
/// .wrap(middleware::Logger::default())
|
||||
/// .route("/index.html", web::get().to(index));
|
||||
/// }
|
||||
/// let app = App::new()
|
||||
/// .wrap(middleware::Logger::default())
|
||||
/// .route("/index.html", web::get().to(index));
|
||||
/// ```
|
||||
pub fn wrap<M, B, B1>(
|
||||
#[doc(alias = "middleware")]
|
||||
#[doc(alias = "use")] // nodejs terminology
|
||||
pub fn wrap<M, B>(
|
||||
self,
|
||||
mw: M,
|
||||
) -> App<
|
||||
impl ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B1>,
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
>
|
||||
where
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
Config = (),
|
||||
InitError = (),
|
||||
>,
|
||||
B: MessageBody,
|
||||
M: Transform<
|
||||
T::Service,
|
||||
ServiceRequest,
|
||||
Response = ServiceResponse<B1>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
B1: MessageBody,
|
||||
T::Service,
|
||||
ServiceRequest,
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
> + 'static,
|
||||
B: MessageBody,
|
||||
{
|
||||
App {
|
||||
endpoint: apply(mw, self.endpoint),
|
||||
@ -387,61 +389,57 @@ impl<T> App<T> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Registers middleware, in the form of a closure, that runs during inbound
|
||||
/// and/or outbound processing in the request life-cycle (request -> response),
|
||||
/// modifying request/response as necessary, across all requests managed by
|
||||
/// the *Application*.
|
||||
/// Registers an app-wide function middleware.
|
||||
///
|
||||
/// `mw` is a closure that runs during inbound and/or outbound processing in the request
|
||||
/// life-cycle (request -> response), modifying request/response as necessary, across all
|
||||
/// requests handled by the `App`.
|
||||
///
|
||||
/// Use middleware when you need to read or modify *every* request or response in some way.
|
||||
///
|
||||
/// Middleware can also be applied to individual `Scope`s and `Resource`s.
|
||||
///
|
||||
/// See [`App::wrap`] for details on how middlewares compose with each other.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_service::Service;
|
||||
/// use actix_web::{web, App};
|
||||
/// use actix_web::{dev::Service as _, middleware, web, App};
|
||||
/// use actix_web::http::header::{CONTENT_TYPE, HeaderValue};
|
||||
///
|
||||
/// async fn index() -> &'static str {
|
||||
/// "Welcome!"
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
/// let app = App::new()
|
||||
/// .wrap_fn(|req, srv| {
|
||||
/// let fut = srv.call(req);
|
||||
/// async {
|
||||
/// let mut res = fut.await?;
|
||||
/// res.headers_mut().insert(
|
||||
/// CONTENT_TYPE, HeaderValue::from_static("text/plain"),
|
||||
/// );
|
||||
/// Ok(res)
|
||||
/// }
|
||||
/// })
|
||||
/// .route("/index.html", web::get().to(index));
|
||||
/// }
|
||||
/// let app = App::new()
|
||||
/// .wrap_fn(|req, srv| {
|
||||
/// let fut = srv.call(req);
|
||||
/// async {
|
||||
/// let mut res = fut.await?;
|
||||
/// res.headers_mut()
|
||||
/// .insert(CONTENT_TYPE, HeaderValue::from_static("text/plain"));
|
||||
/// Ok(res)
|
||||
/// }
|
||||
/// })
|
||||
/// .route("/index.html", web::get().to(index));
|
||||
/// ```
|
||||
pub fn wrap_fn<F, R, B, B1>(
|
||||
#[doc(alias = "middleware")]
|
||||
#[doc(alias = "use")] // nodejs terminology
|
||||
pub fn wrap_fn<F, R, B>(
|
||||
self,
|
||||
mw: F,
|
||||
) -> App<
|
||||
impl ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B1>,
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
>
|
||||
where
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
Config = (),
|
||||
InitError = (),
|
||||
>,
|
||||
F: Fn(ServiceRequest, &T::Service) -> R + Clone + 'static,
|
||||
R: Future<Output = Result<ServiceResponse<B>, Error>>,
|
||||
B: MessageBody,
|
||||
F: Fn(ServiceRequest, &T::Service) -> R + Clone,
|
||||
R: Future<Output = Result<ServiceResponse<B1>, Error>>,
|
||||
B1: MessageBody,
|
||||
{
|
||||
App {
|
||||
endpoint: apply_fn_factory(self.endpoint, mw),
|
||||
@ -457,15 +455,14 @@ impl<T> App<T> {
|
||||
|
||||
impl<T, B> IntoServiceFactory<AppInit<T, B>, Request> for App<T>
|
||||
where
|
||||
B: MessageBody,
|
||||
T: ServiceFactory<
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
>,
|
||||
T::Future: 'static,
|
||||
ServiceRequest,
|
||||
Config = (),
|
||||
Response = ServiceResponse<B>,
|
||||
Error = Error,
|
||||
InitError = (),
|
||||
> + 'static,
|
||||
B: MessageBody,
|
||||
{
|
||||
fn into_factory(self) -> AppInit<T, B> {
|
||||
AppInit {
|
||||
|
@ -1,14 +1,16 @@
|
||||
use std::{cell::RefCell, mem, rc::Rc};
|
||||
|
||||
use actix_http::{Extensions, Request};
|
||||
use actix_http::Request;
|
||||
use actix_router::{Path, ResourceDef, Router, Url};
|
||||
use actix_service::{boxed, fn_service, Service, ServiceFactory};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use futures_util::future::join_all;
|
||||
|
||||
use crate::{
|
||||
body::BoxBody,
|
||||
config::{AppConfig, AppService},
|
||||
data::FnDataFactory,
|
||||
dev::Extensions,
|
||||
guard::Guard,
|
||||
request::{HttpRequest, HttpRequestPool},
|
||||
rmap::ResourceMap,
|
||||
@ -297,7 +299,7 @@ pub struct AppRouting {
|
||||
}
|
||||
|
||||
impl Service<ServiceRequest> for AppRouting {
|
||||
type Response = ServiceResponse;
|
||||
type Response = ServiceResponse<BoxBody>;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
@ -306,12 +308,15 @@ impl Service<ServiceRequest> for AppRouting {
|
||||
fn call(&self, mut req: ServiceRequest) -> Self::Future {
|
||||
let res = self.router.recognize_fn(&mut req, |req, guards| {
|
||||
if let Some(ref guards) = guards {
|
||||
for f in guards {
|
||||
if !f.check(req.head()) {
|
||||
let guard_ctx = req.guard_ctx();
|
||||
|
||||
for guard in guards {
|
||||
if !guard.check(&guard_ctx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
true
|
||||
});
|
||||
|
||||
|
@ -128,7 +128,7 @@ impl AppConfig {
|
||||
|
||||
/// Server host name.
|
||||
///
|
||||
/// Host name is used by application router as a hostname for url generation.
|
||||
/// Host name is used by application router as a hostname for URL generation.
|
||||
/// Check [ConnectionInfo](super::dev::ConnectionInfo::host())
|
||||
/// documentation for more information.
|
||||
///
|
||||
@ -137,7 +137,7 @@ impl AppConfig {
|
||||
&self.host
|
||||
}
|
||||
|
||||
/// Returns true if connection is secure(https)
|
||||
/// Returns true if connection is secure (i.e., running over `https:`).
|
||||
pub fn secure(&self) -> bool {
|
||||
self.secure
|
||||
}
|
||||
@ -215,6 +215,17 @@ impl ServiceConfig {
|
||||
self
|
||||
}
|
||||
|
||||
/// Run external configuration as part of the application building process
|
||||
///
|
||||
/// Counterpart to [`App::configure()`](crate::App::configure) that allows for easy nesting.
|
||||
pub fn configure<F>(&mut self, f: F) -> &mut Self
|
||||
where
|
||||
F: FnOnce(&mut ServiceConfig),
|
||||
{
|
||||
f(self);
|
||||
self
|
||||
}
|
||||
|
||||
/// Configure route for a specific path.
|
||||
///
|
||||
/// Counterpart to [`App::route()`](crate::App::route).
|
||||
@ -264,7 +275,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::http::{Method, StatusCode};
|
||||
use crate::test::{call_service, init_service, read_body, TestRequest};
|
||||
use crate::test::{assert_body_eq, call_service, init_service, read_body, TestRequest};
|
||||
use crate::{web, App, HttpRequest, HttpResponse};
|
||||
|
||||
// allow deprecated `ServiceConfig::data`
|
||||
@ -288,38 +299,6 @@ mod tests {
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
// #[actix_rt::test]
|
||||
// async fn test_data_factory() {
|
||||
// let cfg = |cfg: &mut ServiceConfig| {
|
||||
// cfg.data_factory(|| {
|
||||
// sleep(std::time::Duration::from_millis(50)).then(|_| {
|
||||
// println!("READY");
|
||||
// Ok::<_, ()>(10usize)
|
||||
// })
|
||||
// });
|
||||
// };
|
||||
|
||||
// let srv =
|
||||
// init_service(App::new().configure(cfg).service(
|
||||
// web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()),
|
||||
// ));
|
||||
// let req = TestRequest::default().to_request();
|
||||
// let resp = srv.call(req).await.unwrap();
|
||||
// assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
// let cfg2 = |cfg: &mut ServiceConfig| {
|
||||
// cfg.data_factory(|| Ok::<_, ()>(10u32));
|
||||
// };
|
||||
// let srv = init_service(
|
||||
// App::new()
|
||||
// .service(web::resource("/").to(|_: web::Data<usize>| HttpResponse::Ok()))
|
||||
// .configure(cfg2),
|
||||
// );
|
||||
// let req = TestRequest::default().to_request();
|
||||
// let resp = srv.call(req).await.unwrap();
|
||||
// assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
// }
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_external_resource() {
|
||||
let srv = init_service(
|
||||
@ -363,4 +342,22 @@ mod tests {
|
||||
let resp = call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn nested_service_configure() {
|
||||
fn cfg_root(cfg: &mut ServiceConfig) {
|
||||
cfg.configure(cfg_sub);
|
||||
}
|
||||
|
||||
fn cfg_sub(cfg: &mut ServiceConfig) {
|
||||
cfg.route("/", web::get().to(|| async { "hello world" }));
|
||||
}
|
||||
|
||||
let srv = init_service(App::new().configure(cfg_root)).await;
|
||||
|
||||
let req = TestRequest::with_uri("/").to_request();
|
||||
let res = call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_body_eq!(res, b"hello world");
|
||||
}
|
||||
}
|
||||
|
45
src/data.rs
45
src/data.rs
@ -19,23 +19,32 @@ pub(crate) trait DataFactory {
|
||||
pub(crate) type FnDataFactory =
|
||||
Box<dyn Fn() -> LocalBoxFuture<'static, Result<Box<dyn DataFactory>, ()>>>;
|
||||
|
||||
/// Application data.
|
||||
/// Application data wrapper and extractor.
|
||||
///
|
||||
/// Application level data is a piece of arbitrary data attached to the app, scope, or resource.
|
||||
/// Application data is available to all routes and can be added during the application
|
||||
/// configuration process via `App::data()`.
|
||||
/// # Setting Data
|
||||
/// Data is set using the `app_data` methods on `App`, `Scope`, and `Resource`. If data is wrapped
|
||||
/// in this `Data` type for those calls, it can be used as an extractor.
|
||||
///
|
||||
/// Application data can be accessed by using `Data<T>` extractor where `T` is data type.
|
||||
/// Note that `Data` should be constructed _outside_ the `HttpServer::new` closure if shared,
|
||||
/// potentially mutable state is desired. `Data` is cheap to clone; internally, it uses an `Arc`.
|
||||
///
|
||||
/// **Note**: HTTP server accepts an application factory rather than an application instance. HTTP
|
||||
/// server constructs an application instance for each thread, thus application data must be
|
||||
/// constructed multiple times. If you want to share data between different threads, a shareable
|
||||
/// object should be used, e.g. `Send + Sync`. Application data does not need to be `Send`
|
||||
/// or `Sync`. Internally `Data` contains an `Arc`.
|
||||
/// See also [`App::app_data`](crate::App::app_data), [`Scope::app_data`](crate::Scope::app_data),
|
||||
/// and [`Resource::app_data`](crate::Resource::app_data).
|
||||
///
|
||||
/// # Extracting `Data`
|
||||
/// Since the Actix Web router layers application data, the returned object will reference the
|
||||
/// "closest" instance of the type. For example, if an `App` stores a `u32`, a nested `Scope`
|
||||
/// also stores a `u32`, and the delegated request handler falls within that `Scope`, then
|
||||
/// extracting a `web::<Data<u32>>` for that handler will return the `Scope`'s instance.
|
||||
/// However, using the same router set up and a request that does not get captured by the `Scope`,
|
||||
/// `web::<Data<u32>>` would return the `App`'s instance.
|
||||
///
|
||||
/// If route data is not set for a handler, using `Data<T>` extractor would cause a `500 Internal
|
||||
/// Server Error` response.
|
||||
///
|
||||
/// See also [`HttpRequest::app_data`]
|
||||
/// and [`ServiceRequest::app_data`](crate::dev::ServiceRequest::app_data).
|
||||
///
|
||||
/// # Unsized Data
|
||||
/// For types that are unsized, most commonly `dyn T`, `Data` can wrap these types by first
|
||||
/// constructing an `Arc<dyn T>` and using the `From` implementation to convert it.
|
||||
@ -79,6 +88,7 @@ pub(crate) type FnDataFactory =
|
||||
/// .route("/index.html", web::get().to(index))
|
||||
/// .route("/index-alt.html", web::get().to(index_alt));
|
||||
/// ```
|
||||
#[doc(alias = "state")]
|
||||
#[derive(Debug)]
|
||||
pub struct Data<T: ?Sized>(Arc<T>);
|
||||
|
||||
@ -90,12 +100,12 @@ impl<T> Data<T> {
|
||||
}
|
||||
|
||||
impl<T: ?Sized> Data<T> {
|
||||
/// Get reference to inner app data.
|
||||
/// Returns reference to inner `T`.
|
||||
pub fn get_ref(&self) -> &T {
|
||||
self.0.as_ref()
|
||||
}
|
||||
|
||||
/// Convert to the internal Arc<T>
|
||||
/// Unwraps to the internal `Arc<T>`
|
||||
pub fn into_inner(self) -> Arc<T> {
|
||||
self.0
|
||||
}
|
||||
@ -143,13 +153,16 @@ impl<T: ?Sized + 'static> FromRequest for Data<T> {
|
||||
ok(st.clone())
|
||||
} else {
|
||||
log::debug!(
|
||||
"Failed to construct App-level Data extractor. \
|
||||
Request path: {:?} (type: {})",
|
||||
req.path(),
|
||||
"Failed to extract `Data<{}>` for `{}` handler. For the Data extractor to work \
|
||||
correctly, wrap the data with `Data::new()` and pass it to `App::app_data()`. \
|
||||
Ensure that types align in both the set and retrieve calls.",
|
||||
type_name::<T>(),
|
||||
req.match_name().unwrap_or_else(|| req.path())
|
||||
);
|
||||
|
||||
err(ErrorInternalServerError(
|
||||
"App data is not configured, to configure construct it with web::Data::new() and pass it to App::app_data()",
|
||||
"Requested application data is not configured correctly. \
|
||||
View/enable debug logs for more details.",
|
||||
))
|
||||
}
|
||||
}
|
||||
|
76
src/dev.rs
76
src/dev.rs
@ -3,17 +3,6 @@
|
||||
//! Most users will not have to interact with the types in this module, but it is useful for those
|
||||
//! writing extractors, middleware, libraries, or interacting with the service API directly.
|
||||
|
||||
pub use crate::config::{AppConfig, AppService};
|
||||
#[doc(hidden)]
|
||||
pub use crate::handler::Handler;
|
||||
pub use crate::info::{ConnectionInfo, PeerAddr};
|
||||
pub use crate::rmap::ResourceMap;
|
||||
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService};
|
||||
|
||||
pub use crate::types::form::UrlEncoded;
|
||||
pub use crate::types::json::JsonBody;
|
||||
pub use crate::types::readlines::Readlines;
|
||||
|
||||
pub use actix_http::{Extensions, Payload, RequestHead, Response, ResponseHead};
|
||||
pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
|
||||
pub use actix_server::{Server, ServerHandle};
|
||||
@ -24,7 +13,14 @@ pub use actix_service::{
|
||||
#[cfg(feature = "__compress")]
|
||||
pub use actix_http::encoding::Decoder as Decompress;
|
||||
|
||||
use crate::http::header::ContentEncoding;
|
||||
pub use crate::config::{AppConfig, AppService};
|
||||
#[doc(hidden)]
|
||||
pub use crate::handler::Handler;
|
||||
pub use crate::info::{ConnectionInfo, PeerAddr};
|
||||
pub use crate::rmap::ResourceMap;
|
||||
pub use crate::service::{HttpServiceFactory, ServiceRequest, ServiceResponse, WebService};
|
||||
|
||||
pub use crate::types::{JsonBody, Readlines, UrlEncoded};
|
||||
|
||||
use actix_router::Patterns;
|
||||
|
||||
@ -46,59 +42,3 @@ pub(crate) fn ensure_leading_slash(mut patterns: Patterns) -> Patterns {
|
||||
|
||||
patterns
|
||||
}
|
||||
struct Enc(ContentEncoding);
|
||||
|
||||
/// Helper trait that allows to set specific encoding for response.
|
||||
pub trait BodyEncoding {
|
||||
/// Get content encoding
|
||||
fn get_encoding(&self) -> Option<ContentEncoding>;
|
||||
|
||||
/// Set content encoding
|
||||
///
|
||||
/// Must be used with [`crate::middleware::Compress`] to take effect.
|
||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self;
|
||||
}
|
||||
|
||||
impl BodyEncoding for actix_http::ResponseBuilder {
|
||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||
}
|
||||
|
||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
|
||||
self.extensions_mut().insert(Enc(encoding));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> BodyEncoding for actix_http::Response<B> {
|
||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||
}
|
||||
|
||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
|
||||
self.extensions_mut().insert(Enc(encoding));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl BodyEncoding for crate::HttpResponseBuilder {
|
||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||
}
|
||||
|
||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
|
||||
self.extensions_mut().insert(Enc(encoding));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> BodyEncoding for crate::HttpResponse<B> {
|
||||
fn get_encoding(&self) -> Option<ContentEncoding> {
|
||||
self.extensions().get::<Enc>().map(|enc| enc.0)
|
||||
}
|
||||
|
||||
fn encoding(&mut self, encoding: ContentEncoding) -> &mut Self {
|
||||
self.extensions_mut().insert(Enc(encoding));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
@ -118,15 +118,13 @@ where
|
||||
|
||||
macro_rules! error_helper {
|
||||
($name:ident, $status:ident) => {
|
||||
paste::paste! {
|
||||
#[doc = "Helper function that wraps any error and generates a `" $status "` response."]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn $name<T>(err: T) -> Error
|
||||
where
|
||||
T: fmt::Debug + fmt::Display + 'static,
|
||||
{
|
||||
InternalError::new(err, StatusCode::$status).into()
|
||||
}
|
||||
#[doc = concat!("Helper function that wraps any error and generates a `", stringify!($status), "` response.")]
|
||||
#[allow(non_snake_case)]
|
||||
pub fn $name<T>(err: T) -> Error
|
||||
where
|
||||
T: fmt::Debug + fmt::Display + 'static,
|
||||
{
|
||||
InternalError::new(err, StatusCode::$status).into()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
@ -124,12 +125,11 @@ pub trait FromRequest: Sized {
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
impl<T: 'static> FromRequest for Option<T>
|
||||
impl<T> FromRequest for Option<T>
|
||||
where
|
||||
T: FromRequest,
|
||||
T::Future: 'static,
|
||||
{
|
||||
type Error = Error;
|
||||
type Error = Infallible;
|
||||
type Future = FromRequestOptFuture<T::Future>;
|
||||
|
||||
#[inline]
|
||||
@ -152,7 +152,7 @@ where
|
||||
Fut: Future<Output = Result<T, E>>,
|
||||
E: Into<Error>,
|
||||
{
|
||||
type Output = Result<Option<T>, Error>;
|
||||
type Output = Result<Option<T>, Infallible>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
@ -211,40 +211,42 @@ where
|
||||
/// );
|
||||
/// }
|
||||
/// ```
|
||||
impl<T> FromRequest for Result<T, T::Error>
|
||||
impl<T, E> FromRequest for Result<T, E>
|
||||
where
|
||||
T: FromRequest + 'static,
|
||||
T::Error: 'static,
|
||||
T::Future: 'static,
|
||||
T: FromRequest,
|
||||
T::Error: Into<E>,
|
||||
{
|
||||
type Error = Error;
|
||||
type Future = FromRequestResFuture<T::Future>;
|
||||
type Error = Infallible;
|
||||
type Future = FromRequestResFuture<T::Future, E>;
|
||||
|
||||
#[inline]
|
||||
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
|
||||
FromRequestResFuture {
|
||||
fut: T::from_request(req, payload),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
pub struct FromRequestResFuture<Fut> {
|
||||
pub struct FromRequestResFuture<Fut, E> {
|
||||
#[pin]
|
||||
fut: Fut,
|
||||
_phantom: PhantomData<E>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<Fut, T, E> Future for FromRequestResFuture<Fut>
|
||||
impl<Fut, T, Ei, E> Future for FromRequestResFuture<Fut, E>
|
||||
where
|
||||
Fut: Future<Output = Result<T, E>>,
|
||||
Fut: Future<Output = Result<T, Ei>>,
|
||||
Ei: Into<E>,
|
||||
{
|
||||
type Output = Result<Result<T, E>, Error>;
|
||||
type Output = Result<Result<T, E>, Infallible>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.project();
|
||||
let res = ready!(this.fut.poll(cx));
|
||||
Poll::Ready(Ok(res))
|
||||
Poll::Ready(Ok(res.map_err(Into::into)))
|
||||
}
|
||||
}
|
||||
|
||||
@ -290,16 +292,6 @@ impl FromRequest for Method {
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl FromRequest for () {
|
||||
type Error = Infallible;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[allow(non_snake_case)]
|
||||
mod tuple_from_req {
|
||||
@ -388,6 +380,15 @@ mod tuple_from_req {
|
||||
}
|
||||
}
|
||||
|
||||
impl FromRequest for () {
|
||||
type Error = Infallible;
|
||||
type Future = Ready<Result<Self, Self::Error>>;
|
||||
|
||||
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
|
||||
ok(())
|
||||
}
|
||||
}
|
||||
|
||||
tuple_from_req! { TupleFromRequest1; A }
|
||||
tuple_from_req! { TupleFromRequest2; A, B }
|
||||
tuple_from_req! { TupleFromRequest3; A, B, C }
|
||||
@ -398,6 +399,8 @@ mod tuple_from_req {
|
||||
tuple_from_req! { TupleFromRequest8; A, B, C, D, E, F, G, H }
|
||||
tuple_from_req! { TupleFromRequest9; A, B, C, D, E, F, G, H, I }
|
||||
tuple_from_req! { TupleFromRequest10; A, B, C, D, E, F, G, H, I, J }
|
||||
tuple_from_req! { TupleFromRequest11; A, B, C, D, E, F, G, H, I, J, K }
|
||||
tuple_from_req! { TupleFromRequest12; A, B, C, D, E, F, G, H, I, J, K, L }
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -480,7 +483,14 @@ mod tests {
|
||||
.set_payload(Bytes::from_static(b"bye=world"))
|
||||
.to_http_parts();
|
||||
|
||||
let r = Result::<Form<Info>, Error>::from_request(&req, &mut pl)
|
||||
struct MyError;
|
||||
impl From<Error> for MyError {
|
||||
fn from(_: Error) -> Self {
|
||||
Self
|
||||
}
|
||||
}
|
||||
|
||||
let r = Result::<Form<Info>, MyError>::from_request(&req, &mut pl)
|
||||
.await
|
||||
.unwrap();
|
||||
assert!(r.is_err());
|
||||
|
780
src/guard.rs
780
src/guard.rs
@ -1,160 +1,252 @@
|
||||
//! Route match guards.
|
||||
//! Route guards.
|
||||
//!
|
||||
//! Guards are one of the ways how actix-web router chooses a
|
||||
//! handler service. In essence it is just a function that accepts a
|
||||
//! reference to a `RequestHead` instance and returns a boolean.
|
||||
//! It is possible to add guards to *scopes*, *resources*
|
||||
//! and *routes*. Actix provide several guards by default, like various
|
||||
//! http methods, header, etc. To become a guard, type must implement `Guard`
|
||||
//! trait. Simple functions could be guards as well.
|
||||
//! Guards are used during routing to help select a matching service or handler using some aspect of
|
||||
//! the request; though guards should not be used for path matching since it is a built-in function
|
||||
//! of the Actix Web router.
|
||||
//!
|
||||
//! Guards can not modify the request object. But it is possible
|
||||
//! to store extra attributes on a request by using the `Extensions` container.
|
||||
//! Extensions containers are available via the `RequestHead::extensions()` method.
|
||||
//! Guards can be used on [`Scope`]s, [`Resource`]s, [`Route`]s, and other custom services.
|
||||
//!
|
||||
//! Fundamentally, a guard is a predicate function that receives a reference to a request context
|
||||
//! object and returns a boolean; true if the request _should_ be handled by the guarded service
|
||||
//! or handler. This interface is defined by the [`Guard`] trait.
|
||||
//!
|
||||
//! Commonly-used guards are provided in this module as well as a way of creating a guard from a
|
||||
//! closure ([`fn_guard`]). The [`Not`], [`Any`], and [`All`] guards are noteworthy, as they can be
|
||||
//! used to compose other guards in a more flexible and semantic way than calling `.guard(...)` on
|
||||
//! services multiple times (which might have different combining behavior than you want).
|
||||
//!
|
||||
//! There are shortcuts for routes with method guards in the [`web`](crate::web) module:
|
||||
//! [`web::get()`](crate::web::get), [`web::post()`](crate::web::post), etc. The routes created by
|
||||
//! the following calls are equivalent:
|
||||
//! - `web::get()` (recommended form)
|
||||
//! - `web::route().guard(guard::Get())`
|
||||
//!
|
||||
//! Guards can not modify anything about the request. However, it is possible to store extra
|
||||
//! attributes in the request-local data container obtained with [`GuardContext::req_data_mut`].
|
||||
//!
|
||||
//! Guards can prevent resource definitions from overlapping which, when only considering paths,
|
||||
//! would result in inaccessible routes. See the [`Host`] guard for an example of virtual hosting.
|
||||
//!
|
||||
//! # Examples
|
||||
//! In the following code, the `/guarded` resource has one defined route whose handler will only be
|
||||
//! called if the request method is `POST` and there is a request header with name and value equal
|
||||
//! to `x-guarded` and `secret`, respectively.
|
||||
//! ```
|
||||
//! use actix_web::{web, http, dev, guard, App, HttpResponse};
|
||||
//! use actix_web::{web, http::Method, guard, HttpResponse};
|
||||
//!
|
||||
//! App::new().service(web::resource("/index.html").route(
|
||||
//! web::resource("/guarded").route(
|
||||
//! web::route()
|
||||
//! .guard(guard::Post())
|
||||
//! .guard(guard::fn_guard(|head| head.method == http::Method::GET))
|
||||
//! .to(|| HttpResponse::MethodNotAllowed()))
|
||||
//! .guard(guard::Any(guard::Get()).or(guard::Post()))
|
||||
//! .guard(guard::Header("x-guarded", "secret"))
|
||||
//! .to(|| HttpResponse::Ok())
|
||||
//! );
|
||||
//! ```
|
||||
//!
|
||||
//! [`Scope`]: crate::Scope::guard()
|
||||
//! [`Resource`]: crate::Resource::guard()
|
||||
//! [`Route`]: crate::Route::guard()
|
||||
|
||||
#![allow(non_snake_case)]
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
convert::TryFrom,
|
||||
rc::Rc,
|
||||
};
|
||||
|
||||
use std::rc::Rc;
|
||||
use std::{convert::TryFrom, ops::Deref};
|
||||
use actix_http::{header, uri::Uri, Extensions, Method as HttpMethod, RequestHead};
|
||||
|
||||
use actix_http::{header, uri::Uri, Method as HttpMethod, RequestHead};
|
||||
use crate::{http::header::Header, service::ServiceRequest};
|
||||
|
||||
/// Trait defines resource guards. Guards are used for route selection.
|
||||
///
|
||||
/// Guards can not modify the request object. But it is possible
|
||||
/// to store extra attributes on a request by using the `Extensions` container.
|
||||
/// Extensions containers are available via the `RequestHead::extensions()` method.
|
||||
pub trait Guard {
|
||||
/// Check if request matches predicate
|
||||
fn check(&self, request: &RequestHead) -> bool;
|
||||
/// Provides access to request parts that are useful during routing.
|
||||
#[derive(Debug)]
|
||||
pub struct GuardContext<'a> {
|
||||
pub(crate) req: &'a ServiceRequest,
|
||||
}
|
||||
|
||||
impl Guard for Rc<dyn Guard> {
|
||||
fn check(&self, request: &RequestHead) -> bool {
|
||||
self.deref().check(request)
|
||||
impl<'a> GuardContext<'a> {
|
||||
/// Returns reference to the request head.
|
||||
#[inline]
|
||||
pub fn head(&self) -> &RequestHead {
|
||||
self.req.head()
|
||||
}
|
||||
|
||||
/// Returns reference to the request-local data container.
|
||||
#[inline]
|
||||
pub fn req_data(&self) -> Ref<'a, Extensions> {
|
||||
self.req.req_data()
|
||||
}
|
||||
|
||||
/// Returns mutable reference to the request-local data container.
|
||||
#[inline]
|
||||
pub fn req_data_mut(&self) -> RefMut<'a, Extensions> {
|
||||
self.req.req_data_mut()
|
||||
}
|
||||
|
||||
/// Extracts a typed header from the request.
|
||||
///
|
||||
/// Returns `None` if parsing `H` fails.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{guard::fn_guard, http::header};
|
||||
///
|
||||
/// let image_accept_guard = fn_guard(|ctx| {
|
||||
/// match ctx.header::<header::Accept>() {
|
||||
/// Some(hdr) => hdr.preference() == "image/*",
|
||||
/// None => false,
|
||||
/// }
|
||||
/// });
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn header<H: Header>(&self) -> Option<H> {
|
||||
H::parse(self.req).ok()
|
||||
}
|
||||
}
|
||||
|
||||
/// Create guard object for supplied function.
|
||||
/// Interface for routing guards.
|
||||
///
|
||||
/// See [module level documentation](self) for more.
|
||||
pub trait Guard {
|
||||
/// Returns true if predicate condition is met for a given request.
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool;
|
||||
}
|
||||
|
||||
impl Guard for Rc<dyn Guard> {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
(**self).check(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a guard using the given function.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::{guard, web, App, HttpResponse};
|
||||
/// use actix_web::{guard, web, HttpResponse};
|
||||
///
|
||||
/// App::new().service(web::resource("/index.html").route(
|
||||
/// web::route()
|
||||
/// .guard(
|
||||
/// guard::fn_guard(
|
||||
/// |req| req.headers()
|
||||
/// .contains_key("content-type")))
|
||||
/// .to(|| HttpResponse::MethodNotAllowed()))
|
||||
/// );
|
||||
/// web::route()
|
||||
/// .guard(guard::fn_guard(|ctx| {
|
||||
/// ctx.head().headers().contains_key("content-type")
|
||||
/// }))
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
pub fn fn_guard<F>(f: F) -> impl Guard
|
||||
where
|
||||
F: Fn(&RequestHead) -> bool,
|
||||
F: Fn(&GuardContext<'_>) -> bool,
|
||||
{
|
||||
FnGuard(f)
|
||||
}
|
||||
|
||||
struct FnGuard<F: Fn(&RequestHead) -> bool>(F);
|
||||
struct FnGuard<F: Fn(&GuardContext<'_>) -> bool>(F);
|
||||
|
||||
impl<F> Guard for FnGuard<F>
|
||||
where
|
||||
F: Fn(&RequestHead) -> bool,
|
||||
F: Fn(&GuardContext<'_>) -> bool,
|
||||
{
|
||||
fn check(&self, head: &RequestHead) -> bool {
|
||||
(self.0)(head)
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
(self.0)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<F> Guard for F
|
||||
where
|
||||
F: Fn(&RequestHead) -> bool,
|
||||
F: Fn(&GuardContext<'_>) -> bool,
|
||||
{
|
||||
fn check(&self, head: &RequestHead) -> bool {
|
||||
(self)(head)
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
(self)(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// Return guard that matches if any of supplied guards.
|
||||
/// Creates a guard that matches if any added guards match.
|
||||
///
|
||||
/// # Examples
|
||||
/// The handler below will be called for either request method `GET` or `POST`.
|
||||
/// ```
|
||||
/// use actix_web::{web, guard, App, HttpResponse};
|
||||
/// use actix_web::{web, guard, HttpResponse};
|
||||
///
|
||||
/// App::new().service(web::resource("/index.html").route(
|
||||
/// web::route()
|
||||
/// .guard(guard::Any(guard::Get()).or(guard::Post()))
|
||||
/// .to(|| HttpResponse::MethodNotAllowed()))
|
||||
/// );
|
||||
/// web::route()
|
||||
/// .guard(
|
||||
/// guard::Any(guard::Get())
|
||||
/// .or(guard::Post()))
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Any<F: Guard + 'static>(guard: F) -> AnyGuard {
|
||||
AnyGuard(vec![Box::new(guard)])
|
||||
AnyGuard {
|
||||
guards: vec![Box::new(guard)],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches any of supplied guards.
|
||||
pub struct AnyGuard(Vec<Box<dyn Guard>>);
|
||||
/// A collection of guards that match if the disjunction of their `check` outcomes is true.
|
||||
///
|
||||
/// That is, only one contained guard needs to match in order for the aggregate guard to match.
|
||||
///
|
||||
/// Construct an `AnyGuard` using [`Any`].
|
||||
pub struct AnyGuard {
|
||||
guards: Vec<Box<dyn Guard>>,
|
||||
}
|
||||
|
||||
impl AnyGuard {
|
||||
/// Add guard to a list of guards to check
|
||||
/// Adds new guard to the collection of guards to check.
|
||||
pub fn or<F: Guard + 'static>(mut self, guard: F) -> Self {
|
||||
self.0.push(Box::new(guard));
|
||||
self.guards.push(Box::new(guard));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for AnyGuard {
|
||||
fn check(&self, req: &RequestHead) -> bool {
|
||||
for p in &self.0 {
|
||||
if p.check(req) {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
for guard in &self.guards {
|
||||
if guard.check(ctx) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Return guard that matches if all of the supplied guards.
|
||||
/// Creates a guard that matches if all added guards match.
|
||||
///
|
||||
/// # Examples
|
||||
/// The handler below will only be called if the request method is `GET` **and** the specified
|
||||
/// header name and value match exactly.
|
||||
/// ```
|
||||
/// use actix_web::{guard, web, App, HttpResponse};
|
||||
/// use actix_web::{guard, web, HttpResponse};
|
||||
///
|
||||
/// App::new().service(web::resource("/index.html").route(
|
||||
/// web::route()
|
||||
/// .guard(
|
||||
/// guard::All(guard::Get()).and(guard::Header("content-type", "text/plain")))
|
||||
/// .to(|| HttpResponse::MethodNotAllowed()))
|
||||
/// );
|
||||
/// web::route()
|
||||
/// .guard(
|
||||
/// guard::All(guard::Get())
|
||||
/// .and(guard::Header("accept", "text/plain"))
|
||||
/// )
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn All<F: Guard + 'static>(guard: F) -> AllGuard {
|
||||
AllGuard(vec![Box::new(guard)])
|
||||
AllGuard {
|
||||
guards: vec![Box::new(guard)],
|
||||
}
|
||||
}
|
||||
|
||||
/// Matches if all of supplied guards.
|
||||
pub struct AllGuard(Vec<Box<dyn Guard>>);
|
||||
/// A collection of guards that match if the conjunction of their `check` outcomes is true.
|
||||
///
|
||||
/// That is, **all** contained guard needs to match in order for the aggregate guard to match.
|
||||
///
|
||||
/// Construct an `AllGuard` using [`All`].
|
||||
pub struct AllGuard {
|
||||
guards: Vec<Box<dyn Guard>>,
|
||||
}
|
||||
|
||||
impl AllGuard {
|
||||
/// Add new guard to the list of guards to check
|
||||
/// Adds new guard to the collection of guards to check.
|
||||
pub fn and<F: Guard + 'static>(mut self, guard: F) -> Self {
|
||||
self.0.push(Box::new(guard));
|
||||
self.guards.push(Box::new(guard));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for AllGuard {
|
||||
fn check(&self, request: &RequestHead) -> bool {
|
||||
for p in &self.0 {
|
||||
if !p.check(request) {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
for guard in &self.guards {
|
||||
if !guard.check(ctx) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -162,159 +254,210 @@ impl Guard for AllGuard {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return guard that matches if supplied guard does not match.
|
||||
pub fn Not<F: Guard + 'static>(guard: F) -> NotGuard {
|
||||
NotGuard(Box::new(guard))
|
||||
}
|
||||
/// Wraps a guard and inverts the outcome of it's `Guard` implementation.
|
||||
///
|
||||
/// # Examples
|
||||
/// The handler below will be called for any request method apart from `GET`.
|
||||
/// ```
|
||||
/// use actix_web::{guard, web, HttpResponse};
|
||||
///
|
||||
/// web::route()
|
||||
/// .guard(guard::Not(guard::Get()))
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
pub struct Not<G>(pub G);
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct NotGuard(Box<dyn Guard>);
|
||||
|
||||
impl Guard for NotGuard {
|
||||
fn check(&self, request: &RequestHead) -> bool {
|
||||
!self.0.check(request)
|
||||
impl<G: Guard> Guard for Not<G> {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
!self.0.check(ctx)
|
||||
}
|
||||
}
|
||||
|
||||
/// HTTP method guard.
|
||||
#[doc(hidden)]
|
||||
pub struct MethodGuard(HttpMethod);
|
||||
|
||||
impl Guard for MethodGuard {
|
||||
fn check(&self, request: &RequestHead) -> bool {
|
||||
request.method == self.0
|
||||
}
|
||||
}
|
||||
|
||||
/// Guard to match *GET* HTTP method.
|
||||
pub fn Get() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::GET)
|
||||
}
|
||||
|
||||
/// Predicate to match *POST* HTTP method.
|
||||
pub fn Post() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::POST)
|
||||
}
|
||||
|
||||
/// Predicate to match *PUT* HTTP method.
|
||||
pub fn Put() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::PUT)
|
||||
}
|
||||
|
||||
/// Predicate to match *DELETE* HTTP method.
|
||||
pub fn Delete() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::DELETE)
|
||||
}
|
||||
|
||||
/// Predicate to match *HEAD* HTTP method.
|
||||
pub fn Head() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::HEAD)
|
||||
}
|
||||
|
||||
/// Predicate to match *OPTIONS* HTTP method.
|
||||
pub fn Options() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::OPTIONS)
|
||||
}
|
||||
|
||||
/// Predicate to match *CONNECT* HTTP method.
|
||||
pub fn Connect() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::CONNECT)
|
||||
}
|
||||
|
||||
/// Predicate to match *PATCH* HTTP method.
|
||||
pub fn Patch() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::PATCH)
|
||||
}
|
||||
|
||||
/// Predicate to match *TRACE* HTTP method.
|
||||
pub fn Trace() -> MethodGuard {
|
||||
MethodGuard(HttpMethod::TRACE)
|
||||
}
|
||||
|
||||
/// Predicate to match specified HTTP method.
|
||||
pub fn Method(method: HttpMethod) -> MethodGuard {
|
||||
/// Creates a guard that matches a specified HTTP method.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Method(method: HttpMethod) -> impl Guard {
|
||||
MethodGuard(method)
|
||||
}
|
||||
|
||||
/// Return predicate that matches if request contains specified header and
|
||||
/// value.
|
||||
pub fn Header(name: &'static str, value: &'static str) -> HeaderGuard {
|
||||
/// HTTP method guard.
|
||||
struct MethodGuard(HttpMethod);
|
||||
|
||||
impl Guard for MethodGuard {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
ctx.head().method == self.0
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! method_guard {
|
||||
($method_fn:ident, $method_const:ident) => {
|
||||
#[doc = concat!("Creates a guard that matches the `", stringify!($method_const), "` request method.")]
|
||||
///
|
||||
/// # Examples
|
||||
#[doc = concat!("The route in this example will only respond to `", stringify!($method_const), "` requests.")]
|
||||
/// ```
|
||||
/// use actix_web::{guard, web, HttpResponse};
|
||||
///
|
||||
/// web::route()
|
||||
#[doc = concat!(" .guard(guard::", stringify!($method_fn), "())")]
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn $method_fn() -> impl Guard {
|
||||
MethodGuard(HttpMethod::$method_const)
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
method_guard!(Get, GET);
|
||||
method_guard!(Post, POST);
|
||||
method_guard!(Put, PUT);
|
||||
method_guard!(Delete, DELETE);
|
||||
method_guard!(Head, HEAD);
|
||||
method_guard!(Options, OPTIONS);
|
||||
method_guard!(Connect, CONNECT);
|
||||
method_guard!(Patch, PATCH);
|
||||
method_guard!(Trace, TRACE);
|
||||
|
||||
/// Creates a guard that matches if request contains given header name and value.
|
||||
///
|
||||
/// # Examples
|
||||
/// The handler below will be called when the request contains an `x-guarded` header with value
|
||||
/// equal to `secret`.
|
||||
/// ```
|
||||
/// use actix_web::{guard, web, HttpResponse};
|
||||
///
|
||||
/// web::route()
|
||||
/// .guard(guard::Header("x-guarded", "secret"))
|
||||
/// .to(|| HttpResponse::Ok());
|
||||
/// ```
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Header(name: &'static str, value: &'static str) -> impl Guard {
|
||||
HeaderGuard(
|
||||
header::HeaderName::try_from(name).unwrap(),
|
||||
header::HeaderValue::from_static(value),
|
||||
)
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct HeaderGuard(header::HeaderName, header::HeaderValue);
|
||||
struct HeaderGuard(header::HeaderName, header::HeaderValue);
|
||||
|
||||
impl Guard for HeaderGuard {
|
||||
fn check(&self, req: &RequestHead) -> bool {
|
||||
if let Some(val) = req.headers.get(&self.0) {
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
if let Some(val) = ctx.head().headers.get(&self.0) {
|
||||
return val == self.1;
|
||||
}
|
||||
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// Return predicate that matches if request contains specified Host name.
|
||||
/// Creates a guard that matches requests targetting a specific host.
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::{web, guard::Host, App, HttpResponse};
|
||||
/// # Matching Host
|
||||
/// This guard will:
|
||||
/// - match against the `Host` header, if present;
|
||||
/// - fall-back to matching against the request target's host, if present;
|
||||
/// - return false if host cannot be determined;
|
||||
///
|
||||
/// App::new().service(
|
||||
/// web::resource("/index.html")
|
||||
/// .guard(Host("www.rust-lang.org"))
|
||||
/// .to(|| HttpResponse::MethodNotAllowed())
|
||||
/// );
|
||||
/// # Matching Scheme
|
||||
/// Optionally, this guard can match against the host's scheme. Set the scheme for matching using
|
||||
/// `Host(host).scheme(protocol)`. If the request's scheme cannot be determined, it will not prevent
|
||||
/// the guard from matching successfully.
|
||||
///
|
||||
/// # Examples
|
||||
/// The [module-level documentation](self) has an example of virtual hosting using `Host` guards.
|
||||
///
|
||||
/// The example below additionally guards on the host URI's scheme. This could allow routing to
|
||||
/// different handlers for `http:` vs `https:` visitors; to redirect, for example.
|
||||
/// ```
|
||||
pub fn Host<H: AsRef<str>>(host: H) -> HostGuard {
|
||||
HostGuard(host.as_ref().to_string(), None)
|
||||
/// use actix_web::{web, guard::Host, HttpResponse};
|
||||
///
|
||||
/// web::scope("/admin")
|
||||
/// .guard(Host("admin.rust-lang.org").scheme("https"))
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("admin connection is secure")));
|
||||
/// ```
|
||||
///
|
||||
/// The `Host` guard can be used to set up some form of [virtual hosting] within a single app.
|
||||
/// Overlapping scope prefixes are usually discouraged, but when combined with non-overlapping guard
|
||||
/// definitions they become safe to use in this way. Without these host guards, only routes under
|
||||
/// the first-to-be-defined scope would be accessible. You can test this locally using `127.0.0.1`
|
||||
/// and `localhost` as the `Host` guards.
|
||||
/// ```
|
||||
/// use actix_web::{web, http::Method, guard, App, HttpResponse};
|
||||
///
|
||||
/// App::new()
|
||||
/// .service(
|
||||
/// web::scope("")
|
||||
/// .guard(guard::Host("www.rust-lang.org"))
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("marketing site"))),
|
||||
/// )
|
||||
/// .service(
|
||||
/// web::scope("")
|
||||
/// .guard(guard::Host("play.rust-lang.org"))
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("playground frontend"))),
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// [virtual hosting]: https://en.wikipedia.org/wiki/Virtual_hosting
|
||||
#[allow(non_snake_case)]
|
||||
pub fn Host(host: impl AsRef<str>) -> HostGuard {
|
||||
HostGuard {
|
||||
host: host.as_ref().to_string(),
|
||||
scheme: None,
|
||||
}
|
||||
}
|
||||
|
||||
fn get_host_uri(req: &RequestHead) -> Option<Uri> {
|
||||
use core::str::FromStr;
|
||||
req.headers
|
||||
.get(header::HOST)
|
||||
.and_then(|host_value| host_value.to_str().ok())
|
||||
.or_else(|| req.uri.host())
|
||||
.map(|host: &str| Uri::from_str(host).ok())
|
||||
.and_then(|host_success| host_success)
|
||||
.and_then(|host| host.parse().ok())
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
pub struct HostGuard(String, Option<String>);
|
||||
pub struct HostGuard {
|
||||
host: String,
|
||||
scheme: Option<String>,
|
||||
}
|
||||
|
||||
impl HostGuard {
|
||||
/// Set request scheme to match
|
||||
pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
|
||||
self.1 = Some(scheme.as_ref().to_string());
|
||||
self.scheme = Some(scheme.as_ref().to_string());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Guard for HostGuard {
|
||||
fn check(&self, req: &RequestHead) -> bool {
|
||||
let req_host_uri = if let Some(uri) = get_host_uri(req) {
|
||||
uri
|
||||
} else {
|
||||
return false;
|
||||
fn check(&self, ctx: &GuardContext<'_>) -> bool {
|
||||
// parse host URI from header or request target
|
||||
let req_host_uri = match get_host_uri(ctx.head()) {
|
||||
Some(uri) => uri,
|
||||
|
||||
// no match if host cannot be determined
|
||||
None => return false,
|
||||
};
|
||||
|
||||
if let Some(uri_host) = req_host_uri.host() {
|
||||
if self.0 != uri_host {
|
||||
return false;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
match req_host_uri.host() {
|
||||
// fall through to scheme checks
|
||||
Some(uri_host) if self.host == uri_host => {}
|
||||
|
||||
// Either:
|
||||
// - request's host does not match guard's host;
|
||||
// - It was possible that the parsed URI from request target did not contain a host.
|
||||
_ => return false,
|
||||
}
|
||||
|
||||
if let Some(ref scheme) = self.1 {
|
||||
if let Some(ref scheme) = self.scheme {
|
||||
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
|
||||
return scheme == req_host_uri_scheme;
|
||||
}
|
||||
|
||||
// TODO: is the the correct behavior?
|
||||
// falls through if scheme cannot be determined
|
||||
}
|
||||
|
||||
// all conditions passed
|
||||
true
|
||||
}
|
||||
}
|
||||
@ -327,171 +470,214 @@ mod tests {
|
||||
use crate::test::TestRequest;
|
||||
|
||||
#[test]
|
||||
fn test_header() {
|
||||
fn header_match() {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||
.to_http_request();
|
||||
.to_srv_request();
|
||||
|
||||
let pred = Header("transfer-encoding", "chunked");
|
||||
assert!(pred.check(req.head()));
|
||||
let hdr = Header("transfer-encoding", "chunked");
|
||||
assert!(hdr.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Header("transfer-encoding", "other");
|
||||
assert!(!pred.check(req.head()));
|
||||
let hdr = Header("transfer-encoding", "other");
|
||||
assert!(!hdr.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Header("content-type", "other");
|
||||
assert!(!pred.check(req.head()));
|
||||
let hdr = Header("content-type", "chunked");
|
||||
assert!(!hdr.check(&req.guard_ctx()));
|
||||
|
||||
let hdr = Header("content-type", "other");
|
||||
assert!(!hdr.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host() {
|
||||
fn host_from_header() {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((
|
||||
header::HOST,
|
||||
header::HeaderValue::from_static("www.rust-lang.org"),
|
||||
))
|
||||
.to_http_request();
|
||||
.to_srv_request();
|
||||
|
||||
let pred = Host("www.rust-lang.org");
|
||||
assert!(pred.check(req.head()));
|
||||
let host = Host("www.rust-lang.org");
|
||||
assert!(host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("www.rust-lang.org").scheme("https");
|
||||
assert!(pred.check(req.head()));
|
||||
let host = Host("www.rust-lang.org").scheme("https");
|
||||
assert!(host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("blog.rust-lang.org");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("blog.rust-lang.org");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("blog.rust-lang.org").scheme("https");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("blog.rust-lang.org").scheme("https");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("crates.io");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("crates.io");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("localhost");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("localhost");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_scheme() {
|
||||
fn host_without_header() {
|
||||
let req = TestRequest::default()
|
||||
.uri("www.rust-lang.org")
|
||||
.to_srv_request();
|
||||
|
||||
let host = Host("www.rust-lang.org");
|
||||
assert!(host.check(&req.guard_ctx()));
|
||||
|
||||
let host = Host("www.rust-lang.org").scheme("https");
|
||||
assert!(host.check(&req.guard_ctx()));
|
||||
|
||||
let host = Host("blog.rust-lang.org");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let host = Host("blog.rust-lang.org").scheme("https");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let host = Host("crates.io");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let host = Host("localhost");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn host_scheme() {
|
||||
let req = TestRequest::default()
|
||||
.insert_header((
|
||||
header::HOST,
|
||||
header::HeaderValue::from_static("https://www.rust-lang.org"),
|
||||
))
|
||||
.to_http_request();
|
||||
.to_srv_request();
|
||||
|
||||
let pred = Host("www.rust-lang.org").scheme("https");
|
||||
assert!(pred.check(req.head()));
|
||||
let host = Host("www.rust-lang.org").scheme("https");
|
||||
assert!(host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("www.rust-lang.org");
|
||||
assert!(pred.check(req.head()));
|
||||
let host = Host("www.rust-lang.org");
|
||||
assert!(host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("www.rust-lang.org").scheme("http");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("www.rust-lang.org").scheme("http");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("blog.rust-lang.org");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("blog.rust-lang.org");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("blog.rust-lang.org").scheme("https");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("blog.rust-lang.org").scheme("https");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("crates.io").scheme("https");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("crates.io").scheme("https");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
|
||||
let pred = Host("localhost");
|
||||
assert!(!pred.check(req.head()));
|
||||
let host = Host("localhost");
|
||||
assert!(!host.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_host_without_header() {
|
||||
fn method_guards() {
|
||||
let get_req = TestRequest::get().to_srv_request();
|
||||
let post_req = TestRequest::post().to_srv_request();
|
||||
|
||||
assert!(Get().check(&get_req.guard_ctx()));
|
||||
assert!(!Get().check(&post_req.guard_ctx()));
|
||||
|
||||
assert!(Post().check(&post_req.guard_ctx()));
|
||||
assert!(!Post().check(&get_req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::put().to_srv_request();
|
||||
assert!(Put().check(&req.guard_ctx()));
|
||||
assert!(!Put().check(&get_req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::patch().to_srv_request();
|
||||
assert!(Patch().check(&req.guard_ctx()));
|
||||
assert!(!Patch().check(&get_req.guard_ctx()));
|
||||
|
||||
let r = TestRequest::delete().to_srv_request();
|
||||
assert!(Delete().check(&r.guard_ctx()));
|
||||
assert!(!Delete().check(&get_req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::default().method(Method::HEAD).to_srv_request();
|
||||
assert!(Head().check(&req.guard_ctx()));
|
||||
assert!(!Head().check(&get_req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.uri("www.rust-lang.org")
|
||||
.to_http_request();
|
||||
|
||||
let pred = Host("www.rust-lang.org");
|
||||
assert!(pred.check(req.head()));
|
||||
|
||||
let pred = Host("www.rust-lang.org").scheme("https");
|
||||
assert!(pred.check(req.head()));
|
||||
|
||||
let pred = Host("blog.rust-lang.org");
|
||||
assert!(!pred.check(req.head()));
|
||||
|
||||
let pred = Host("blog.rust-lang.org").scheme("https");
|
||||
assert!(!pred.check(req.head()));
|
||||
|
||||
let pred = Host("crates.io");
|
||||
assert!(!pred.check(req.head()));
|
||||
|
||||
let pred = Host("localhost");
|
||||
assert!(!pred.check(req.head()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_methods() {
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let req2 = TestRequest::default()
|
||||
.method(Method::POST)
|
||||
.to_http_request();
|
||||
|
||||
assert!(Get().check(req.head()));
|
||||
assert!(!Get().check(req2.head()));
|
||||
assert!(Post().check(req2.head()));
|
||||
assert!(!Post().check(req.head()));
|
||||
|
||||
let r = TestRequest::default().method(Method::PUT).to_http_request();
|
||||
assert!(Put().check(r.head()));
|
||||
assert!(!Put().check(req.head()));
|
||||
|
||||
let r = TestRequest::default()
|
||||
.method(Method::DELETE)
|
||||
.to_http_request();
|
||||
assert!(Delete().check(r.head()));
|
||||
assert!(!Delete().check(req.head()));
|
||||
|
||||
let r = TestRequest::default()
|
||||
.method(Method::HEAD)
|
||||
.to_http_request();
|
||||
assert!(Head().check(r.head()));
|
||||
assert!(!Head().check(req.head()));
|
||||
|
||||
let r = TestRequest::default()
|
||||
.method(Method::OPTIONS)
|
||||
.to_http_request();
|
||||
assert!(Options().check(r.head()));
|
||||
assert!(!Options().check(req.head()));
|
||||
.to_srv_request();
|
||||
assert!(Options().check(&req.guard_ctx()));
|
||||
assert!(!Options().check(&get_req.guard_ctx()));
|
||||
|
||||
let r = TestRequest::default()
|
||||
let req = TestRequest::default()
|
||||
.method(Method::CONNECT)
|
||||
.to_http_request();
|
||||
assert!(Connect().check(r.head()));
|
||||
assert!(!Connect().check(req.head()));
|
||||
.to_srv_request();
|
||||
assert!(Connect().check(&req.guard_ctx()));
|
||||
assert!(!Connect().check(&get_req.guard_ctx()));
|
||||
|
||||
let r = TestRequest::default()
|
||||
.method(Method::PATCH)
|
||||
.to_http_request();
|
||||
assert!(Patch().check(r.head()));
|
||||
assert!(!Patch().check(req.head()));
|
||||
|
||||
let r = TestRequest::default()
|
||||
let req = TestRequest::default()
|
||||
.method(Method::TRACE)
|
||||
.to_http_request();
|
||||
assert!(Trace().check(r.head()));
|
||||
assert!(!Trace().check(req.head()));
|
||||
.to_srv_request();
|
||||
assert!(Trace().check(&req.guard_ctx()));
|
||||
assert!(!Trace().check(&get_req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_preds() {
|
||||
let r = TestRequest::default()
|
||||
fn aggregate_any() {
|
||||
let req = TestRequest::default()
|
||||
.method(Method::TRACE)
|
||||
.to_http_request();
|
||||
.to_srv_request();
|
||||
|
||||
assert!(Not(Get()).check(r.head()));
|
||||
assert!(!Not(Trace()).check(r.head()));
|
||||
assert!(Any(Trace()).check(&req.guard_ctx()));
|
||||
assert!(Any(Trace()).or(Get()).check(&req.guard_ctx()));
|
||||
assert!(!Any(Get()).or(Get()).check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
assert!(All(Trace()).and(Trace()).check(r.head()));
|
||||
assert!(!All(Get()).and(Trace()).check(r.head()));
|
||||
#[test]
|
||||
fn aggregate_all() {
|
||||
let req = TestRequest::default()
|
||||
.method(Method::TRACE)
|
||||
.to_srv_request();
|
||||
|
||||
assert!(Any(Get()).or(Trace()).check(r.head()));
|
||||
assert!(!Any(Get()).or(Get()).check(r.head()));
|
||||
assert!(All(Trace()).check(&req.guard_ctx()));
|
||||
assert!(All(Trace()).and(Trace()).check(&req.guard_ctx()));
|
||||
assert!(!All(Trace()).and(Get()).check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn nested_not() {
|
||||
let req = TestRequest::default().to_srv_request();
|
||||
|
||||
let get = Get();
|
||||
assert!(get.check(&req.guard_ctx()));
|
||||
|
||||
let not_get = Not(get);
|
||||
assert!(!not_get.check(&req.guard_ctx()));
|
||||
|
||||
let not_not_get = Not(not_get);
|
||||
assert!(not_not_get.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn function_guard() {
|
||||
let domain = "rust-lang.org".to_owned();
|
||||
let guard = fn_guard(|ctx| ctx.head().uri.host().unwrap().ends_with(&domain));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.uri("blog.rust-lang.org")
|
||||
.to_srv_request();
|
||||
assert!(guard.check(&req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::default().uri("crates.io").to_srv_request();
|
||||
assert!(!guard.check(&req.guard_ctx()));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mega_nesting() {
|
||||
let guard = fn_guard(|ctx| All(Not(Any(Not(Trace())))).check(ctx));
|
||||
|
||||
let req = TestRequest::default().to_srv_request();
|
||||
assert!(!guard.check(&req.guard_ctx()));
|
||||
|
||||
let req = TestRequest::default()
|
||||
.method(Method::TRACE)
|
||||
.to_srv_request();
|
||||
assert!(guard.check(&req.guard_ctx()));
|
||||
}
|
||||
}
|
||||
|
@ -12,17 +12,14 @@ use crate::{
|
||||
/// # What Is A Request Handler
|
||||
/// A request handler has three requirements:
|
||||
/// 1. It is an async function (or a function/closure that returns an appropriate future);
|
||||
/// 1. The function accepts zero or more parameters that implement [`FromRequest`];
|
||||
/// 1. The function parameters (up to 12) implement [`FromRequest`];
|
||||
/// 1. The async function (or future) resolves to a type that can be converted into an
|
||||
/// [`HttpResponse`] (i.e., it implements the [`Responder`] trait).
|
||||
///
|
||||
/// # Compiler Errors
|
||||
/// If you get the error `the trait Handler<_> is not implemented`, then your handler does not
|
||||
/// fulfill one or more of the above requirements.
|
||||
///
|
||||
/// Unfortunately we cannot provide a better compile error message (while keeping the trait's
|
||||
/// flexibility) unless a stable alternative to [`#[rustc_on_unimplemented]`][on_unimpl] is added
|
||||
/// to Rust.
|
||||
/// fulfill the _first_ of the above requirements. Missing other requirements manifest as errors on
|
||||
/// implementing [`FromRequest`] and [`Responder`], respectively.
|
||||
///
|
||||
/// # How Do Handlers Receive Variable Numbers Of Arguments
|
||||
/// Rest assured there is no macro magic here; it's just traits.
|
||||
@ -62,13 +59,15 @@ use crate::{
|
||||
/// This is the source code for the 2-parameter implementation of `Handler` to help illustrate the
|
||||
/// bounds of the handler call after argument extraction:
|
||||
/// ```ignore
|
||||
/// impl<Func, Arg1, Arg2, R> Handler<(Arg1, Arg2), R> for Func
|
||||
/// impl<Func, Arg1, Arg2, Fut> Handler<(Arg1, Arg2)> for Func
|
||||
/// where
|
||||
/// Func: Fn(Arg1, Arg2) -> R + Clone + 'static,
|
||||
/// R: Future,
|
||||
/// R::Output: Responder,
|
||||
/// Func: Fn(Arg1, Arg2) -> Fut + Clone + 'static,
|
||||
/// Fut: Future,
|
||||
/// {
|
||||
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> R {
|
||||
/// type Output = Fut::Output;
|
||||
/// type Future = Fut;
|
||||
///
|
||||
/// fn call(&self, (arg1, arg2): (Arg1, Arg2)) -> Self::Future {
|
||||
/// (self)(arg1, arg2)
|
||||
/// }
|
||||
/// }
|
||||
@ -76,7 +75,6 @@ use crate::{
|
||||
///
|
||||
/// [arity]: https://en.wikipedia.org/wiki/Arity
|
||||
/// [`from_request`]: FromRequest::from_request
|
||||
/// [on_unimpl]: https://github.com/rust-lang/rust/issues/29628
|
||||
pub trait Handler<Args>: Clone + 'static {
|
||||
type Output;
|
||||
type Future: Future<Output = Self::Output>;
|
||||
@ -121,8 +119,9 @@ where
|
||||
/// ```
|
||||
macro_rules! factory_tuple ({ $($param:ident)* } => {
|
||||
impl<Func, Fut, $($param,)*> Handler<($($param,)*)> for Func
|
||||
where Func: Fn($($param),*) -> Fut + Clone + 'static,
|
||||
Fut: Future,
|
||||
where
|
||||
Func: Fn($($param),*) -> Fut + Clone + 'static,
|
||||
Fut: Future,
|
||||
{
|
||||
type Output = Fut::Output;
|
||||
type Future = Fut;
|
||||
@ -148,3 +147,25 @@ factory_tuple! { A B C D E F G H I }
|
||||
factory_tuple! { A B C D E F G H I J }
|
||||
factory_tuple! { A B C D E F G H I J K }
|
||||
factory_tuple! { A B C D E F G H I J K L }
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn assert_impl_handler<T: FromRequest>(_: impl Handler<T>) {}
|
||||
|
||||
#[test]
|
||||
fn arg_number() {
|
||||
async fn handler_min() {}
|
||||
|
||||
#[rustfmt::skip]
|
||||
#[allow(clippy::too_many_arguments, clippy::just_underscores_and_digits)]
|
||||
async fn handler_max(
|
||||
_01: (), _02: (), _03: (), _04: (), _05: (), _06: (),
|
||||
_07: (), _08: (), _09: (), _10: (), _11: (), _12: (),
|
||||
) {}
|
||||
|
||||
assert_impl_handler(handler_min);
|
||||
assert_impl_handler(handler_max);
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,10 @@ use std::cmp::Ordering;
|
||||
|
||||
use mime::Mime;
|
||||
|
||||
use super::QualityItem;
|
||||
use super::{common_header, QualityItem};
|
||||
use crate::http::header;
|
||||
|
||||
crate::http::header::common_header! {
|
||||
common_header! {
|
||||
/// `Accept` header, defined
|
||||
/// in [RFC 7231 §5.3.2](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2)
|
||||
///
|
||||
@ -147,6 +147,39 @@ impl Accept {
|
||||
Accept(vec![QualityItem::max(mime::TEXT_HTML)])
|
||||
}
|
||||
|
||||
// TODO: method for getting best content encoding based on q-factors, available from server side
|
||||
// and if none are acceptable return None
|
||||
|
||||
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first mime type is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
|
||||
/// list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Mime {
|
||||
use actix_http::header::Quality;
|
||||
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::ZERO;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(mime::STAR_STAR)
|
||||
}
|
||||
|
||||
/// Returns a sorted list of mime types from highest to lowest preference, accounting for
|
||||
/// [q-factor weighting] and specificity.
|
||||
///
|
||||
@ -196,36 +229,6 @@ impl Accept {
|
||||
|
||||
types.into_iter().map(|qitem| qitem.item).collect()
|
||||
}
|
||||
|
||||
/// Extracts the most preferable mime type, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first mime type is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, will return [`mime::STAR_STAR`] (indicating no preference) if the contained
|
||||
/// list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Mime {
|
||||
use actix_http::header::Quality;
|
||||
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::MIN;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(mime::STAR_STAR)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -239,7 +242,7 @@ mod tests {
|
||||
assert!(test.ranked().is_empty());
|
||||
|
||||
let test = Accept(vec![QualityItem::max(mime::APPLICATION_JSON)]);
|
||||
assert_eq!(test.ranked(), vec!(mime::APPLICATION_JSON));
|
||||
assert_eq!(test.ranked(), vec![mime::APPLICATION_JSON]);
|
||||
|
||||
let test = Accept(vec
|
||||
common_header! {
|
||||
/// `Accept-Charset` header, defined in [RFC 7231 §5.3.3].
|
||||
///
|
||||
/// The `Accept-Charset` header field can be sent by a user agent to
|
||||
/// indicate what charsets are acceptable in textual response content.
|
||||
@ -52,10 +51,12 @@ crate::http::header::common_header! {
|
||||
/// AcceptCharset(vec![QualityItem::max(Charset::Ext("utf-8".to_owned()))])
|
||||
/// );
|
||||
/// ```
|
||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
||||
///
|
||||
/// [RFC 7231 §5.3.3]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.3
|
||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)*
|
||||
|
||||
test_parse_and_format {
|
||||
// Test case from RFC
|
||||
crate::http::header::common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
|
||||
common_header_test!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
|
||||
}
|
||||
}
|
||||
|
@ -1,17 +1,15 @@
|
||||
use actix_http::header::QualityItem;
|
||||
use std::collections::HashSet;
|
||||
|
||||
use super::{common_header, Encoding};
|
||||
use super::{common_header, ContentEncoding, Encoding, Preference, Quality, QualityItem};
|
||||
use crate::http::header;
|
||||
|
||||
common_header! {
|
||||
/// `Accept-Encoding` header, defined
|
||||
/// in [RFC 7231](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.4)
|
||||
///
|
||||
/// The `Accept-Encoding` header field can be used by user agents to
|
||||
/// indicate what response content-codings are
|
||||
/// acceptable in the response. An `identity` token is used as a synonym
|
||||
/// for "no encoding" in order to communicate when no encoding is
|
||||
/// preferred.
|
||||
/// The `Accept-Encoding` header field can be used by user agents to indicate what response
|
||||
/// content-codings are acceptable in the response. An `identity` token is used as a synonym
|
||||
/// for "no encoding" in order to communicate when no encoding is preferred.
|
||||
///
|
||||
/// # ABNF
|
||||
/// ```plain
|
||||
@ -29,11 +27,11 @@ common_header! {
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem};
|
||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, Preference, QualityItem};
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptEncoding(vec![QualityItem::max(Encoding::Chunked)])
|
||||
/// AcceptEncoding(vec![QualityItem::max(Preference::Specific(Encoding::gzip()))])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
@ -44,40 +42,388 @@ common_header! {
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptEncoding(vec![
|
||||
/// QualityItem::max(Encoding::Chunked),
|
||||
/// QualityItem::max(Encoding::Gzip),
|
||||
/// QualityItem::max(Encoding::Deflate),
|
||||
/// "gzip".parse().unwrap(),
|
||||
/// "br".parse().unwrap(),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```
|
||||
/// use actix_web::HttpResponse;
|
||||
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q};
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptEncoding(vec![
|
||||
/// QualityItem::max(Encoding::Chunked),
|
||||
/// QualityItem::new(Encoding::Gzip, q(0.60)),
|
||||
/// QualityItem::min(Encoding::EncodingExt("*".to_owned())),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Encoding>)*
|
||||
(AcceptEncoding, header::ACCEPT_ENCODING) => (QualityItem<Preference<Encoding>>)*
|
||||
|
||||
test_parse_and_format {
|
||||
// From the RFC
|
||||
common_header_test!(test1, vec![b"compress, gzip"]);
|
||||
common_header_test!(test2, vec![b""], Some(AcceptEncoding(vec![])));
|
||||
common_header_test!(test3, vec![b"*"]);
|
||||
common_header_test!(no_headers, vec![b""; 0], Some(AcceptEncoding(vec![])));
|
||||
common_header_test!(empty_header, vec![b""; 1], Some(AcceptEncoding(vec![])));
|
||||
|
||||
common_header_test!(
|
||||
order_of_appearance,
|
||||
vec![b"br, gzip"],
|
||||
Some(AcceptEncoding(vec![
|
||||
QualityItem::max(Preference::Specific(Encoding::brotli())),
|
||||
QualityItem::max(Preference::Specific(Encoding::gzip())),
|
||||
]))
|
||||
);
|
||||
|
||||
common_header_test!(any, vec![b"*"], Some(AcceptEncoding(vec![
|
||||
QualityItem::max(Preference::Any),
|
||||
])));
|
||||
|
||||
// Note: Removed quality 1 from gzip
|
||||
common_header_test!(test4, vec![b"compress;q=0.5, gzip"]);
|
||||
common_header_test!(implicit_quality, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
||||
|
||||
// Note: Removed quality 1 from gzip
|
||||
common_header_test!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
||||
common_header_test!(implicit_quality_out_of_order, vec![b"compress;q=0.5, gzip"]);
|
||||
|
||||
common_header_test!(
|
||||
only_gzip_no_identity,
|
||||
vec![b"gzip, *; q=0"],
|
||||
Some(AcceptEncoding(vec![
|
||||
QualityItem::max(Preference::Specific(Encoding::gzip())),
|
||||
QualityItem::zero(Preference::Any),
|
||||
]))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// TODO: shortcut for EncodingExt(*) = Any
|
||||
impl AcceptEncoding {
|
||||
/// Selects the most acceptable encoding according to client preference and supported types.
|
||||
///
|
||||
/// The "identity" encoding is not assumed and should be included in the `supported` iterator
|
||||
/// if a non-encoded representation can be selected.
|
||||
///
|
||||
/// If `None` is returned, this indicates that none of the supported encodings are acceptable to
|
||||
/// the client. The caller should generate a 406 Not Acceptable response (unencoded) that
|
||||
/// includes the server's supported encodings in the body plus a [`Vary`] header.
|
||||
///
|
||||
/// [`Vary`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Vary
|
||||
pub fn negotiate<'a>(
|
||||
&self,
|
||||
supported: impl Iterator<Item = &'a Encoding>,
|
||||
) -> Option<Encoding> {
|
||||
// 1. If no Accept-Encoding field is in the request, any content-coding is considered
|
||||
// acceptable by the user agent.
|
||||
|
||||
let supported_set = supported.collect::<HashSet<_>>();
|
||||
|
||||
if supported_set.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
if self.0.is_empty() {
|
||||
// though it is not recommended to encode in this case, return identity encoding
|
||||
return Some(Encoding::identity());
|
||||
}
|
||||
|
||||
// 2. If the representation has no content-coding, then it is acceptable by default unless
|
||||
// specifically excluded by the Accept-Encoding field stating either "identity;q=0" or
|
||||
// "*;q=0" without a more specific entry for "identity".
|
||||
|
||||
let acceptable_items = self.ranked_items().collect::<Vec<_>>();
|
||||
|
||||
let identity_acceptable = is_identity_acceptable(&acceptable_items);
|
||||
let identity_supported = supported_set.contains(&Encoding::identity());
|
||||
|
||||
if identity_acceptable && identity_supported && supported_set.len() == 1 {
|
||||
return Some(Encoding::identity());
|
||||
}
|
||||
|
||||
// 3. If the representation's content-coding is one of the content-codings listed in the
|
||||
// Accept-Encoding field, then it is acceptable unless it is accompanied by a qvalue of 0.
|
||||
|
||||
// 4. If multiple content-codings are acceptable, then the acceptable content-coding with
|
||||
// the highest non-zero qvalue is preferred.
|
||||
|
||||
let matched = acceptable_items
|
||||
.into_iter()
|
||||
.filter(|q| q.quality > Quality::ZERO)
|
||||
// search relies on item list being in descending order of quality
|
||||
.find(|q| {
|
||||
let enc = &q.item;
|
||||
matches!(enc, Preference::Specific(enc) if supported_set.contains(enc))
|
||||
})
|
||||
.map(|q| q.item);
|
||||
|
||||
match matched {
|
||||
Some(Preference::Specific(enc)) => Some(enc),
|
||||
|
||||
_ if identity_acceptable => Some(Encoding::identity()),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Extracts the most preferable encoding, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first encoding is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, returns [`Preference::Any`] if acceptable list is empty. Though, if this is
|
||||
/// returned, it is recommended to use an un-encoded representation.
|
||||
///
|
||||
/// If `None` is returned, it means that the client has signalled that no representations
|
||||
/// are acceptable. This should never occur for a well behaved user-agent.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Option<Preference<Encoding>> {
|
||||
// empty header indicates no preference
|
||||
if self.0.is_empty() {
|
||||
return Some(Preference::Any);
|
||||
}
|
||||
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::ZERO;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
// Return max_item if any items were above 0 quality...
|
||||
max_item.or_else(|| {
|
||||
// ...or else check for "*" or "identity". We can elide quality checks since
|
||||
// entering this block means all items had "q=0".
|
||||
match self.0.iter().find(|pref| {
|
||||
matches!(
|
||||
pref.item,
|
||||
Preference::Any
|
||||
| Preference::Specific(Encoding::Known(ContentEncoding::Identity))
|
||||
)
|
||||
}) {
|
||||
// "identity" or "*" found so no representation is acceptable
|
||||
Some(_) => None,
|
||||
|
||||
// implicit "identity" is acceptable
|
||||
None => Some(Preference::Specific(Encoding::identity())),
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/// Returns a sorted list of encodings from highest to lowest precedence, accounting
|
||||
/// for [q-factor weighting].
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn ranked(&self) -> Vec<Preference<Encoding>> {
|
||||
self.ranked_items().map(|q| q.item).collect()
|
||||
}
|
||||
|
||||
fn ranked_items(&self) -> impl Iterator<Item = QualityItem<Preference<Encoding>>> {
|
||||
if self.0.is_empty() {
|
||||
return vec![].into_iter();
|
||||
}
|
||||
|
||||
let mut types = self.0.clone();
|
||||
|
||||
// use stable sort so items with equal q-factor retain listed order
|
||||
types.sort_by(|a, b| {
|
||||
// sort by q-factor descending
|
||||
b.quality.cmp(&a.quality)
|
||||
});
|
||||
|
||||
types.into_iter()
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if "identity" is an acceptable encoding.
|
||||
///
|
||||
/// Internal algorithm relies on item list being in descending order of quality.
|
||||
fn is_identity_acceptable(items: &'_ [QualityItem<Preference<Encoding>>]) -> bool {
|
||||
if items.is_empty() {
|
||||
return true;
|
||||
}
|
||||
|
||||
// Loop algorithm depends on items being sorted in descending order of quality. As such, it
|
||||
// is sufficient to return (q > 0) when reaching either an "identity" or "*" item.
|
||||
for q in items {
|
||||
match (q.quality, &q.item) {
|
||||
// occurrence of "identity;q=n"; return true if quality is non-zero
|
||||
(q, Preference::Specific(Encoding::Known(ContentEncoding::Identity))) => {
|
||||
return q > Quality::ZERO
|
||||
}
|
||||
|
||||
// occurrence of "*;q=n"; return true if quality is non-zero
|
||||
(q, Preference::Any) => return q > Quality::ZERO,
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
// implicit acceptable identity
|
||||
true
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::header::*;
|
||||
|
||||
macro_rules! accept_encoding {
|
||||
() => { AcceptEncoding(vec![]) };
|
||||
($($q:expr),+ $(,)?) => { AcceptEncoding(vec![$($q.parse().unwrap()),+]) };
|
||||
}
|
||||
|
||||
/// Parses an encoding string.
|
||||
fn enc(enc: &str) -> Preference<Encoding> {
|
||||
enc.parse().unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn detect_identity_acceptable() {
|
||||
macro_rules! accept_encoding_ranked {
|
||||
() => { accept_encoding!().ranked_items().collect::<Vec<_>>() };
|
||||
($($q:expr),+ $(,)?) => { accept_encoding!($($q),+).ranked_items().collect::<Vec<_>>() };
|
||||
}
|
||||
|
||||
let test = accept_encoding_ranked!();
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "br");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0.1");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0.1");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0.1", "*;q=0");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0.1");
|
||||
assert!(is_identity_acceptable(&test));
|
||||
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "identity;q=0", "*;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
let test = accept_encoding_ranked!("gzip", "*;q=0", "identity;q=0");
|
||||
assert!(!is_identity_acceptable(&test));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn encoding_negotiation() {
|
||||
// no preference
|
||||
let test = accept_encoding!();
|
||||
assert_eq!(test.negotiate([].iter()), None);
|
||||
|
||||
let test = accept_encoding!();
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::identity()].iter()),
|
||||
Some(Encoding::identity()),
|
||||
);
|
||||
|
||||
let test = accept_encoding!("identity;q=0");
|
||||
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
|
||||
|
||||
let test = accept_encoding!("*;q=0");
|
||||
assert_eq!(test.negotiate([Encoding::identity()].iter()), None);
|
||||
|
||||
let test = accept_encoding!();
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::identity()),
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
Some(Encoding::identity()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip", "identity;q=0");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
None
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip", "*;q=0");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
None
|
||||
);
|
||||
|
||||
let test = accept_encoding!("gzip", "deflate", "br");
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip()),
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::identity()].iter()),
|
||||
Some(Encoding::brotli())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::deflate(), Encoding::identity()].iter()),
|
||||
Some(Encoding::deflate())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate(
|
||||
[Encoding::gzip(), Encoding::deflate(), Encoding::identity()].iter()
|
||||
),
|
||||
Some(Encoding::gzip())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::gzip(), Encoding::brotli(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip())
|
||||
);
|
||||
assert_eq!(
|
||||
test.negotiate([Encoding::brotli(), Encoding::gzip(), Encoding::identity()].iter()),
|
||||
Some(Encoding::gzip())
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ranking_precedence() {
|
||||
let test = accept_encoding!();
|
||||
assert!(test.ranked().is_empty());
|
||||
|
||||
let test = accept_encoding!("gzip");
|
||||
assert_eq!(test.ranked(), vec![enc("gzip")]);
|
||||
|
||||
let test = accept_encoding!("gzip;q=0.900", "*;q=0.700", "br;q=1.0");
|
||||
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
|
||||
|
||||
let test = accept_encoding!("br", "gzip", "*");
|
||||
assert_eq!(test.ranked(), vec![enc("br"), enc("gzip"), enc("*")]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preference_selection() {
|
||||
assert_eq!(accept_encoding!().preference(), Some(Preference::Any));
|
||||
|
||||
assert_eq!(accept_encoding!("identity;q=0").preference(), None);
|
||||
assert_eq!(accept_encoding!("*;q=0").preference(), None);
|
||||
assert_eq!(accept_encoding!("compress;q=0", "*;q=0").preference(), None);
|
||||
assert_eq!(accept_encoding!("identity;q=0", "*;q=0").preference(), None);
|
||||
|
||||
let test = accept_encoding!("*;q=0.5");
|
||||
assert_eq!(test.preference().unwrap(), enc("*"));
|
||||
|
||||
let test = accept_encoding!("br;q=0");
|
||||
assert_eq!(test.preference().unwrap(), enc("identity"));
|
||||
|
||||
let test = accept_encoding!("br;q=0.900", "gzip;q=1.0", "*;q=0.500");
|
||||
assert_eq!(test.preference().unwrap(), enc("gzip"));
|
||||
|
||||
let test = accept_encoding!("br", "gzip", "*");
|
||||
assert_eq!(test.preference().unwrap(), enc("br"));
|
||||
}
|
||||
}
|
||||
|
@ -37,7 +37,7 @@ common_header! {
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptLanguage(vec![
|
||||
/// QualityItem::max("en-US".parse().unwrap())
|
||||
/// "en-US".parse().unwrap(),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
@ -49,9 +49,9 @@ common_header! {
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// AcceptLanguage(vec![
|
||||
/// QualityItem::max("da".parse().unwrap()),
|
||||
/// QualityItem::new("en-GB".parse().unwrap(), q(0.8)),
|
||||
/// QualityItem::new("en".parse().unwrap(), q(0.7)),
|
||||
/// "da".parse().unwrap(),
|
||||
/// "en-GB;q=0.8".parse().unwrap(),
|
||||
/// "en;q=0.7".parse().unwrap(),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
@ -93,6 +93,33 @@ common_header! {
|
||||
}
|
||||
|
||||
impl AcceptLanguage {
|
||||
/// Extracts the most preferable language, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first language is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Preference<LanguageTag> {
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::ZERO;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(Preference::Any)
|
||||
}
|
||||
|
||||
/// Returns a sorted list of languages from highest to lowest precedence, accounting
|
||||
/// for [q-factor weighting].
|
||||
///
|
||||
@ -112,33 +139,6 @@ impl AcceptLanguage {
|
||||
|
||||
types.into_iter().map(|qitem| qitem.item).collect()
|
||||
}
|
||||
|
||||
/// Extracts the most preferable language, accounting for [q-factor weighting].
|
||||
///
|
||||
/// If no q-factors are provided, the first language is chosen. Note that items without
|
||||
/// q-factors are given the maximum preference value.
|
||||
///
|
||||
/// As per the spec, returns [`Preference::Any`] if contained list is empty.
|
||||
///
|
||||
/// [q-factor weighting]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.2
|
||||
pub fn preference(&self) -> Preference<LanguageTag> {
|
||||
let mut max_item = None;
|
||||
let mut max_pref = Quality::MIN;
|
||||
|
||||
// uses manual max lookup loop since we want the first occurrence in the case of same
|
||||
// preference but `Iterator::max_by_key` would give us the last occurrence
|
||||
|
||||
for pref in &self.0 {
|
||||
// only change if strictly greater
|
||||
// equal items, even while unsorted, still have higher preference if they appear first
|
||||
if pref.quality > max_pref {
|
||||
max_pref = pref.quality;
|
||||
max_item = Some(pref.item.clone());
|
||||
}
|
||||
}
|
||||
|
||||
max_item.unwrap_or(Preference::Any)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -152,7 +152,7 @@ mod tests {
|
||||
assert!(test.ranked().is_empty());
|
||||
|
||||
let test = AcceptLanguage(vec![QualityItem::max("fr-CH".parse().unwrap())]);
|
||||
assert_eq!(test.ranked(), vec!("fr-CH".parse().unwrap()));
|
||||
assert_eq!(test.ranked(), vec!["fr-CH".parse().unwrap()]);
|
||||
|
||||
let test = AcceptLanguage(vec.
|
||||
// TODO: think about using private fields and smallvec
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ContentDisposition {
|
||||
/// The disposition type
|
||||
|
@ -1,69 +1,55 @@
|
||||
use std::{fmt, str};
|
||||
|
||||
pub use self::Encoding::{
|
||||
Brotli, Chunked, Compress, Deflate, EncodingExt, Gzip, Identity, Trailers, Zstd,
|
||||
};
|
||||
use actix_http::ContentEncoding;
|
||||
|
||||
/// A value to represent an encoding used in `Transfer-Encoding` or `Accept-Encoding` header.
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
/// A value to represent an encoding used in the `Accept-Encoding` and `Content-Encoding` header.
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Encoding {
|
||||
/// The `chunked` encoding.
|
||||
Chunked,
|
||||
/// A supported content encoding. See [`ContentEncoding`] for variants.
|
||||
Known(ContentEncoding),
|
||||
|
||||
/// The `br` encoding.
|
||||
Brotli,
|
||||
/// Some other encoding that is less common, can be any string.
|
||||
Unknown(String),
|
||||
}
|
||||
|
||||
/// The `gzip` encoding.
|
||||
Gzip,
|
||||
impl Encoding {
|
||||
pub const fn identity() -> Self {
|
||||
Self::Known(ContentEncoding::Identity)
|
||||
}
|
||||
|
||||
/// The `deflate` encoding.
|
||||
Deflate,
|
||||
pub const fn brotli() -> Self {
|
||||
Self::Known(ContentEncoding::Brotli)
|
||||
}
|
||||
|
||||
/// The `compress` encoding.
|
||||
Compress,
|
||||
pub const fn deflate() -> Self {
|
||||
Self::Known(ContentEncoding::Deflate)
|
||||
}
|
||||
|
||||
/// The `identity` encoding.
|
||||
Identity,
|
||||
pub const fn gzip() -> Self {
|
||||
Self::Known(ContentEncoding::Gzip)
|
||||
}
|
||||
|
||||
/// The `trailers` encoding.
|
||||
Trailers,
|
||||
|
||||
/// The `zstd` encoding.
|
||||
Zstd,
|
||||
|
||||
/// Some other encoding that is less common, can be any String.
|
||||
EncodingExt(String),
|
||||
pub const fn zstd() -> Self {
|
||||
Self::Known(ContentEncoding::Zstd)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Encoding {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(match *self {
|
||||
Chunked => "chunked",
|
||||
Brotli => "br",
|
||||
Gzip => "gzip",
|
||||
Deflate => "deflate",
|
||||
Compress => "compress",
|
||||
Identity => "identity",
|
||||
Trailers => "trailers",
|
||||
Zstd => "zstd",
|
||||
EncodingExt(ref s) => s.as_ref(),
|
||||
f.write_str(match self {
|
||||
Encoding::Known(enc) => enc.as_str(),
|
||||
Encoding::Unknown(enc) => enc.as_str(),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl str::FromStr for Encoding {
|
||||
type Err = crate::error::ParseError;
|
||||
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
||||
match s {
|
||||
"chunked" => Ok(Chunked),
|
||||
"br" => Ok(Brotli),
|
||||
"deflate" => Ok(Deflate),
|
||||
"gzip" => Ok(Gzip),
|
||||
"compress" => Ok(Compress),
|
||||
"identity" => Ok(Identity),
|
||||
"trailers" => Ok(Trailers),
|
||||
"zstd" => Ok(Zstd),
|
||||
_ => Ok(EncodingExt(s.to_owned())),
|
||||
|
||||
fn from_str(enc: &str) -> Result<Self, crate::error::ParseError> {
|
||||
match enc.parse::<ContentEncoding>() {
|
||||
Ok(enc) => Ok(Self::Known(enc)),
|
||||
Err(_) => Ok(Self::Unknown(enc.to_owned())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -17,8 +17,7 @@ fn check_slice_validity(slice: &str) -> bool {
|
||||
slice.bytes().all(entity_validate_char)
|
||||
}
|
||||
|
||||
/// An entity tag, defined
|
||||
/// in [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
|
||||
/// An entity tag, defined in [RFC 7232 §2.3].
|
||||
///
|
||||
/// An entity tag consists of a string enclosed by two literal double quotes.
|
||||
/// Preceding the first double quote is an optional weakness indicator,
|
||||
@ -48,16 +47,20 @@ fn check_slice_validity(slice: &str) -> bool {
|
||||
/// | `W/"1"` | `W/"2"` | no match | no match |
|
||||
/// | `W/"1"` | `"1"` | no match | match |
|
||||
/// | `"1"` | `"1"` | match | match |
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
///
|
||||
/// [RFC 7232 §2.3](https://datatracker.ietf.org/doc/html/rfc7232#section-2.3)
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct EntityTag {
|
||||
/// Weakness indicator for the tag
|
||||
pub weak: bool,
|
||||
|
||||
/// The opaque string in between the DQUOTEs
|
||||
tag: String,
|
||||
}
|
||||
|
||||
impl EntityTag {
|
||||
/// Constructs a new EntityTag.
|
||||
/// Constructs a new `EntityTag`.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the tag contains invalid characters.
|
||||
pub fn new(weak: bool, tag: String) -> EntityTag {
|
||||
@ -66,51 +69,64 @@ impl EntityTag {
|
||||
}
|
||||
|
||||
/// Constructs a new weak EntityTag.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the tag contains invalid characters.
|
||||
pub fn weak(tag: String) -> EntityTag {
|
||||
pub fn new_weak(tag: String) -> EntityTag {
|
||||
EntityTag::new(true, tag)
|
||||
}
|
||||
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `new_weak`.")]
|
||||
pub fn weak(tag: String) -> EntityTag {
|
||||
Self::new_weak(tag)
|
||||
}
|
||||
|
||||
/// Constructs a new strong EntityTag.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the tag contains invalid characters.
|
||||
pub fn strong(tag: String) -> EntityTag {
|
||||
pub fn new_strong(tag: String) -> EntityTag {
|
||||
EntityTag::new(false, tag)
|
||||
}
|
||||
|
||||
/// Get the tag.
|
||||
#[deprecated(since = "3.0.0", note = "Renamed to `new_strong`.")]
|
||||
pub fn strong(tag: String) -> EntityTag {
|
||||
Self::new_strong(tag)
|
||||
}
|
||||
|
||||
/// Returns tag.
|
||||
pub fn tag(&self) -> &str {
|
||||
self.tag.as_ref()
|
||||
}
|
||||
|
||||
/// Set the tag.
|
||||
/// Sets tag.
|
||||
///
|
||||
/// # Panics
|
||||
/// If the tag contains invalid characters.
|
||||
pub fn set_tag(&mut self, tag: String) {
|
||||
pub fn set_tag(&mut self, tag: impl Into<String>) {
|
||||
let tag = tag.into();
|
||||
assert!(check_slice_validity(&tag), "Invalid tag: {:?}", tag);
|
||||
self.tag = tag
|
||||
}
|
||||
|
||||
/// For strong comparison two entity-tags are equivalent if both are not
|
||||
/// weak and their opaque-tags match character-by-character.
|
||||
/// For strong comparison two entity-tags are equivalent if both are not weak and their
|
||||
/// opaque-tags match character-by-character.
|
||||
pub fn strong_eq(&self, other: &EntityTag) -> bool {
|
||||
!self.weak && !other.weak && self.tag == other.tag
|
||||
}
|
||||
|
||||
/// For weak comparison two entity-tags are equivalent if their
|
||||
/// opaque-tags match character-by-character, regardless of either or
|
||||
/// both being tagged as "weak".
|
||||
/// For weak comparison two entity-tags are equivalent if their opaque-tags match
|
||||
/// character-by-character, regardless of either or both being tagged as "weak".
|
||||
pub fn weak_eq(&self, other: &EntityTag) -> bool {
|
||||
self.tag == other.tag
|
||||
}
|
||||
|
||||
/// The inverse of `EntityTag.strong_eq()`.
|
||||
/// Returns the inverse of `strong_eq()`.
|
||||
pub fn strong_ne(&self, other: &EntityTag) -> bool {
|
||||
!self.strong_eq(other)
|
||||
}
|
||||
|
||||
/// The inverse of `EntityTag.weak_eq()`.
|
||||
/// Returns inverse of `weak_eq()`.
|
||||
pub fn weak_ne(&self, other: &EntityTag) -> bool {
|
||||
!self.weak_eq(other)
|
||||
}
|
||||
@ -178,23 +194,23 @@ mod tests {
|
||||
// Expected success
|
||||
assert_eq!(
|
||||
"\"foobar\"".parse::<EntityTag>().unwrap(),
|
||||
EntityTag::strong("foobar".to_owned())
|
||||
EntityTag::new_strong("foobar".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
"\"\"".parse::<EntityTag>().unwrap(),
|
||||
EntityTag::strong("".to_owned())
|
||||
EntityTag::new_strong("".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
"W/\"weaktag\"".parse::<EntityTag>().unwrap(),
|
||||
EntityTag::weak("weaktag".to_owned())
|
||||
EntityTag::new_weak("weaktag".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
"W/\"\x65\x62\"".parse::<EntityTag>().unwrap(),
|
||||
EntityTag::weak("\x65\x62".to_owned())
|
||||
EntityTag::new_weak("\x65\x62".to_owned())
|
||||
);
|
||||
assert_eq!(
|
||||
"W/\"\"".parse::<EntityTag>().unwrap(),
|
||||
EntityTag::weak("".to_owned())
|
||||
EntityTag::new_weak("".to_owned())
|
||||
);
|
||||
}
|
||||
|
||||
@ -214,19 +230,19 @@ mod tests {
|
||||
#[test]
|
||||
fn test_etag_fmt() {
|
||||
assert_eq!(
|
||||
format!("{}", EntityTag::strong("foobar".to_owned())),
|
||||
format!("{}", EntityTag::new_strong("foobar".to_owned())),
|
||||
"\"foobar\""
|
||||
);
|
||||
assert_eq!(format!("{}", EntityTag::strong("".to_owned())), "\"\"");
|
||||
assert_eq!(format!("{}", EntityTag::new_strong("".to_owned())), "\"\"");
|
||||
assert_eq!(
|
||||
format!("{}", EntityTag::weak("weak-etag".to_owned())),
|
||||
format!("{}", EntityTag::new_weak("weak-etag".to_owned())),
|
||||
"W/\"weak-etag\""
|
||||
);
|
||||
assert_eq!(
|
||||
format!("{}", EntityTag::weak("\u{0065}".to_owned())),
|
||||
format!("{}", EntityTag::new_weak("\u{0065}".to_owned())),
|
||||
"W/\"\x65\""
|
||||
);
|
||||
assert_eq!(format!("{}", EntityTag::weak("".to_owned())), "W/\"\"");
|
||||
assert_eq!(format!("{}", EntityTag::new_weak("".to_owned())), "W/\"\"");
|
||||
}
|
||||
|
||||
#[test]
|
||||
@ -237,29 +253,29 @@ mod tests {
|
||||
// | `W/"1"` | `W/"2"` | no match | no match |
|
||||
// | `W/"1"` | `"1"` | no match | match |
|
||||
// | `"1"` | `"1"` | match | match |
|
||||
let mut etag1 = EntityTag::weak("1".to_owned());
|
||||
let mut etag2 = EntityTag::weak("1".to_owned());
|
||||
let mut etag1 = EntityTag::new_weak("1".to_owned());
|
||||
let mut etag2 = EntityTag::new_weak("1".to_owned());
|
||||
assert!(!etag1.strong_eq(&etag2));
|
||||
assert!(etag1.weak_eq(&etag2));
|
||||
assert!(etag1.strong_ne(&etag2));
|
||||
assert!(!etag1.weak_ne(&etag2));
|
||||
|
||||
etag1 = EntityTag::weak("1".to_owned());
|
||||
etag2 = EntityTag::weak("2".to_owned());
|
||||
etag1 = EntityTag::new_weak("1".to_owned());
|
||||
etag2 = EntityTag::new_weak("2".to_owned());
|
||||
assert!(!etag1.strong_eq(&etag2));
|
||||
assert!(!etag1.weak_eq(&etag2));
|
||||
assert!(etag1.strong_ne(&etag2));
|
||||
assert!(etag1.weak_ne(&etag2));
|
||||
|
||||
etag1 = EntityTag::weak("1".to_owned());
|
||||
etag2 = EntityTag::strong("1".to_owned());
|
||||
etag1 = EntityTag::new_weak("1".to_owned());
|
||||
etag2 = EntityTag::new_strong("1".to_owned());
|
||||
assert!(!etag1.strong_eq(&etag2));
|
||||
assert!(etag1.weak_eq(&etag2));
|
||||
assert!(etag1.strong_ne(&etag2));
|
||||
assert!(!etag1.weak_ne(&etag2));
|
||||
|
||||
etag1 = EntityTag::strong("1".to_owned());
|
||||
etag2 = EntityTag::strong("1".to_owned());
|
||||
etag1 = EntityTag::new_strong("1".to_owned());
|
||||
etag2 = EntityTag::new_strong("1".to_owned());
|
||||
assert!(etag1.strong_eq(&etag2));
|
||||
assert!(etag1.weak_eq(&etag2));
|
||||
assert!(!etag1.strong_ne(&etag2));
|
||||
|
@ -31,7 +31,7 @@ crate::http::header::common_header! {
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// ETag(EntityTag::new(false, "xyzzy".to_owned()))
|
||||
/// ETag(EntityTag::new_strong("xyzzy".to_owned()))
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
@ -41,7 +41,7 @@ crate::http::header::common_header! {
|
||||
///
|
||||
/// let mut builder = HttpResponse::Ok();
|
||||
/// builder.insert_header(
|
||||
/// ETag(EntityTag::new(true, "xyzzy".to_owned()))
|
||||
/// ETag(EntityTag::new_weak("xyzzy".to_owned()))
|
||||
/// );
|
||||
/// ```
|
||||
(ETag, ETAG) => [EntityTag]
|
||||
@ -50,29 +50,29 @@ crate::http::header::common_header! {
|
||||
// From the RFC
|
||||
crate::http::header::common_header_test!(test1,
|
||||
vec![b"\"xyzzy\""],
|
||||
Some(ETag(EntityTag::new(false, "xyzzy".to_owned()))));
|
||||
Some(ETag(EntityTag::new_strong("xyzzy".to_owned()))));
|
||||
crate::http::header::common_header_test!(test2,
|
||||
vec![b"W/\"xyzzy\""],
|
||||
Some(ETag(EntityTag::new(true, "xyzzy".to_owned()))));
|
||||
Some(ETag(EntityTag::new_weak("xyzzy".to_owned()))));
|
||||
crate::http::header::common_header_test!(test3,
|
||||
vec![b"\"\""],
|
||||
Some(ETag(EntityTag::new(false, "".to_owned()))));
|
||||
Some(ETag(EntityTag::new_strong("".to_owned()))));
|
||||
// Own tests
|
||||
crate::http::header::common_header_test!(test4,
|
||||
vec![b"\"foobar\""],
|
||||
Some(ETag(EntityTag::new(false, "foobar".to_owned()))));
|
||||
Some(ETag(EntityTag::new_strong("foobar".to_owned()))));
|
||||
crate::http::header::common_header_test!(test5,
|
||||
vec![b"\"\""],
|
||||
Some(ETag(EntityTag::new(false, "".to_owned()))));
|
||||
Some(ETag(EntityTag::new_strong("".to_owned()))));
|
||||
crate::http::header::common_header_test!(test6,
|
||||
vec![b"W/\"weak-etag\""],
|
||||
Some(ETag(EntityTag::new(true, "weak-etag".to_owned()))));
|
||||
Some(ETag(EntityTag::new_weak("weak-etag".to_owned()))));
|
||||
crate::http::header::common_header_test!(test7,
|
||||
vec![b"W/\"\x65\x62\""],
|
||||
Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned()))));
|
||||
Some(ETag(EntityTag::new_weak("\u{0065}\u{0062}".to_owned()))));
|
||||
crate::http::header::common_header_test!(test8,
|
||||
vec![b"W/\"\""],
|
||||
Some(ETag(EntityTag::new(true, "".to_owned()))));
|
||||
Some(ETag(EntityTag::new_weak("".to_owned()))));
|
||||
crate::http::header::common_header_test!(test9,
|
||||
vec![b"no-dquotes"],
|
||||
None::<ETag>);
|
||||
|
@ -54,14 +54,15 @@ common_header! {
|
||||
test1,
|
||||
vec![b"\"xyzzy\""],
|
||||
Some(HeaderField::Items(
|
||||
vec![EntityTag::new(false, "xyzzy".to_owned())])));
|
||||
vec![EntityTag::new_strong("xyzzy".to_owned())])));
|
||||
|
||||
crate::http::header::common_header_test!(
|
||||
test2,
|
||||
vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""],
|
||||
Some(HeaderField::Items(
|
||||
vec![EntityTag::new(false, "xyzzy".to_owned()),
|
||||
EntityTag::new(false, "r2d2xxxx".to_owned()),
|
||||
EntityTag::new(false, "c3piozzzz".to_owned())])));
|
||||
vec![EntityTag::new_strong("xyzzy".to_owned()),
|
||||
EntityTag::new_strong("r2d2xxxx".to_owned()),
|
||||
EntityTag::new_strong("c3piozzzz".to_owned())])));
|
||||
crate::http::header::common_header_test!(test3, vec![b"*"], Some(IfMatch::Any));
|
||||
}
|
||||
}
|
||||
|
@ -82,8 +82,8 @@ mod tests {
|
||||
|
||||
if_none_match = Header::parse(&req);
|
||||
let mut entities: Vec<EntityTag> = Vec::new();
|
||||
let foobar_etag = EntityTag::new(false, "foobar".to_owned());
|
||||
let weak_etag = EntityTag::new(true, "weak-etag".to_owned());
|
||||
let foobar_etag = EntityTag::new_strong("foobar".to_owned());
|
||||
let weak_etag = EntityTag::new_weak("weak-etag".to_owned());
|
||||
entities.push(foobar_etag);
|
||||
entities.push(weak_etag);
|
||||
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities)));
|
||||
|
125
src/info.rs
125
src/info.rs
@ -67,7 +67,7 @@ fn first_header_value<'a>(req: &'a RequestHead, name: &'_ HeaderName) -> Option<
|
||||
pub struct ConnectionInfo {
|
||||
host: String,
|
||||
scheme: String,
|
||||
remote_addr: Option<String>,
|
||||
peer_addr: Option<String>,
|
||||
realip_remote_addr: Option<String>,
|
||||
}
|
||||
|
||||
@ -134,67 +134,70 @@ impl ConnectionInfo {
|
||||
.or_else(|| first_header_value(req, &*X_FORWARDED_FOR))
|
||||
.map(str::to_owned);
|
||||
|
||||
let remote_addr = req.peer_addr.map(|addr| addr.to_string());
|
||||
let peer_addr = req.peer_addr.map(|addr| addr.ip().to_string());
|
||||
|
||||
ConnectionInfo {
|
||||
host,
|
||||
scheme,
|
||||
remote_addr,
|
||||
peer_addr,
|
||||
realip_remote_addr,
|
||||
}
|
||||
}
|
||||
|
||||
/// Real IP (remote address) of client that initiated request.
|
||||
///
|
||||
/// The address is resolved through the following, in order:
|
||||
/// - `Forwarded` header
|
||||
/// - `X-Forwarded-For` header
|
||||
/// - peer address of opened socket (same as [`remote_addr`](Self::remote_addr))
|
||||
///
|
||||
/// # Security
|
||||
/// Do not use this function for security purposes unless you can be sure that the `Forwarded`
|
||||
/// and `X-Forwarded-For` headers cannot be spoofed by the client. If you are running without a
|
||||
/// proxy then [obtaining the peer address](Self::peer_addr) would be more appropriate.
|
||||
#[inline]
|
||||
pub fn realip_remote_addr(&self) -> Option<&str> {
|
||||
self.realip_remote_addr
|
||||
.as_deref()
|
||||
.or_else(|| self.peer_addr.as_deref())
|
||||
}
|
||||
|
||||
/// Returns serialized IP address of the peer connection.
|
||||
///
|
||||
/// See [`HttpRequest::peer_addr`] for more details.
|
||||
#[inline]
|
||||
pub fn peer_addr(&self) -> Option<&str> {
|
||||
self.peer_addr.as_deref()
|
||||
}
|
||||
|
||||
/// Hostname of the request.
|
||||
///
|
||||
/// Hostname is resolved through the following, in order:
|
||||
/// - `Forwarded` header
|
||||
/// - `X-Forwarded-Host` header
|
||||
/// - `Host` header
|
||||
/// - request target / URI
|
||||
/// - configured server hostname
|
||||
#[inline]
|
||||
pub fn host(&self) -> &str {
|
||||
&self.host
|
||||
}
|
||||
|
||||
/// Scheme of the request.
|
||||
///
|
||||
/// Scheme is resolved through the following headers, in this order:
|
||||
///
|
||||
/// - Forwarded
|
||||
/// - X-Forwarded-Proto
|
||||
/// - Uri
|
||||
/// Scheme is resolved through the following, in order:
|
||||
/// - `Forwarded` header
|
||||
/// - `X-Forwarded-Proto` header
|
||||
/// - request target / URI
|
||||
#[inline]
|
||||
pub fn scheme(&self) -> &str {
|
||||
&self.scheme
|
||||
}
|
||||
|
||||
/// Hostname of the request.
|
||||
///
|
||||
/// Hostname is resolved through the following headers, in this order:
|
||||
///
|
||||
/// - Forwarded
|
||||
/// - X-Forwarded-Host
|
||||
/// - Host
|
||||
/// - Uri
|
||||
/// - Server hostname
|
||||
pub fn host(&self) -> &str {
|
||||
&self.host
|
||||
}
|
||||
|
||||
/// Remote address of the connection.
|
||||
///
|
||||
/// Get remote_addr address from socket address.
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `peer_addr`.")]
|
||||
pub fn remote_addr(&self) -> Option<&str> {
|
||||
self.remote_addr.as_deref()
|
||||
}
|
||||
|
||||
/// Real IP (remote address) of client that initiated request.
|
||||
///
|
||||
/// The address is resolved through the following headers, in this order:
|
||||
///
|
||||
/// - Forwarded
|
||||
/// - X-Forwarded-For
|
||||
/// - remote_addr name of opened socket
|
||||
///
|
||||
/// # Security
|
||||
/// Do not use this function for security purposes, unless you can ensure the Forwarded and
|
||||
/// X-Forwarded-For headers cannot be spoofed by the client. If you want the client's socket
|
||||
/// address explicitly, use [`HttpRequest::peer_addr()`][peer_addr] instead.
|
||||
///
|
||||
/// [peer_addr]: crate::web::HttpRequest::peer_addr()
|
||||
#[inline]
|
||||
pub fn realip_remote_addr(&self) -> Option<&str> {
|
||||
self.realip_remote_addr
|
||||
.as_deref()
|
||||
.or_else(|| self.remote_addr.as_deref())
|
||||
self.peer_addr()
|
||||
}
|
||||
}
|
||||
|
||||
@ -209,7 +212,7 @@ impl FromRequest for ConnectionInfo {
|
||||
|
||||
/// Extractor for peer's socket address.
|
||||
///
|
||||
/// Also see [`HttpRequest::peer_addr`].
|
||||
/// Also see [`HttpRequest::peer_addr`] and [`ConnectionInfo::peer_addr`].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
@ -432,13 +435,37 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn peer_addr_extract() {
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = PeerAddr::extract(&req).await;
|
||||
assert!(res.is_err());
|
||||
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
||||
let peer_addr = PeerAddr::extract(&req).await.unwrap();
|
||||
assert_eq!(peer_addr, PeerAddr(addr));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn remote_address() {
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = PeerAddr::extract(&req).await;
|
||||
assert!(res.is_err());
|
||||
let res = ConnectionInfo::extract(&req).await.unwrap();
|
||||
assert!(res.peer_addr().is_none());
|
||||
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
||||
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
|
||||
assert_eq!(conn_info.peer_addr().unwrap(), "127.0.0.1");
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn real_ip_from_socket_addr() {
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let res = ConnectionInfo::extract(&req).await.unwrap();
|
||||
assert!(res.realip_remote_addr().is_none());
|
||||
|
||||
let addr = "127.0.0.1:8080".parse().unwrap();
|
||||
let req = TestRequest::default().peer_addr(addr).to_http_request();
|
||||
let conn_info = ConnectionInfo::extract(&req).await.unwrap();
|
||||
assert_eq!(conn_info.realip_remote_addr().unwrap(), "127.0.0.1");
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +53,7 @@
|
||||
//! * SSL support using OpenSSL or Rustls
|
||||
//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
//! * Includes an async [HTTP client](https://docs.rs/awc/)
|
||||
//! * Runs on stable Rust 1.52+
|
||||
//! * Runs on stable Rust 1.54+
|
||||
//!
|
||||
//! # Crate Features
|
||||
//! * `cookies` - cookies support (enabled by default)
|
||||
|
@ -150,11 +150,13 @@ mod tests {
|
||||
|
||||
use actix_service::IntoService;
|
||||
|
||||
use crate::dev::ServiceRequest;
|
||||
use crate::http::StatusCode;
|
||||
use crate::middleware::{self, Condition, Logger};
|
||||
use crate::test::{call_service, init_service, TestRequest};
|
||||
use crate::{web, App, HttpResponse};
|
||||
use crate::{
|
||||
dev::ServiceRequest,
|
||||
http::StatusCode,
|
||||
middleware::{self, Condition, Logger},
|
||||
test::{self, call_service, init_service, TestRequest},
|
||||
web, App, HttpResponse,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
#[cfg(all(feature = "cookies", feature = "__compress"))]
|
||||
@ -219,4 +221,17 @@ mod tests {
|
||||
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn compat_noop_is_noop() {
|
||||
let srv = test::ok_service();
|
||||
|
||||
let mw = Compat::noop()
|
||||
.new_transform(srv.into_service())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let resp = call_service(&mw, TestRequest::default().to_srv_request()).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
@ -1,20 +1,13 @@
|
||||
//! For middleware documentation, see [`Compress`].
|
||||
|
||||
use std::{
|
||||
cmp,
|
||||
convert::TryFrom as _,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_http::{
|
||||
body::{EitherBody, MessageBody},
|
||||
encoding::Encoder,
|
||||
header::{ContentEncoding, ACCEPT_ENCODING},
|
||||
StatusCode,
|
||||
};
|
||||
use actix_http::encoding::Encoder;
|
||||
use actix_service::{Service, Transform};
|
||||
use actix_utils::future::{ok, Either, Ready};
|
||||
use futures_core::ready;
|
||||
@ -22,39 +15,65 @@ use once_cell::sync::Lazy;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
dev::BodyEncoding,
|
||||
body::{EitherBody, MessageBody},
|
||||
http::{
|
||||
header::{self, AcceptEncoding, Encoding, HeaderValue},
|
||||
StatusCode,
|
||||
},
|
||||
service::{ServiceRequest, ServiceResponse},
|
||||
Error, HttpResponse,
|
||||
Error, HttpMessage, HttpResponse,
|
||||
};
|
||||
|
||||
/// Middleware for compressing response payloads.
|
||||
///
|
||||
/// Use `BodyEncoding` trait for overriding response compression. To disable compression set
|
||||
/// encoding to `ContentEncoding::Identity`.
|
||||
/// # Encoding Negotiation
|
||||
/// `Compress` will read the `Accept-Encoding` header to negotiate which compression codec to use.
|
||||
/// Payloads are not compressed if the header is not sent. The `compress-*` [feature flags] are also
|
||||
/// considered in this selection process.
|
||||
///
|
||||
/// # Pre-compressed Payload
|
||||
/// If you are serving some data is already using a compressed representation (e.g., a gzip
|
||||
/// compressed HTML file from disk) you can signal this to `Compress` by setting an appropriate
|
||||
/// `Content-Encoding` header. In addition to preventing double compressing the payload, this header
|
||||
/// is required by the spec when using compressed representations and will inform the client that
|
||||
/// the content should be uncompressed.
|
||||
///
|
||||
/// However, it is not advised to unconditionally serve encoded representations of content because
|
||||
/// the client may not support it. The [`AcceptEncoding`] typed header has some utilities to help
|
||||
/// perform manual encoding negotiation, if required. When negotiating content encoding, it is also
|
||||
/// required by the spec to send a `Vary: Accept-Encoding` header.
|
||||
///
|
||||
/// A (naïve) example serving an pre-compressed Gzip file is included below.
|
||||
///
|
||||
/// # Examples
|
||||
/// To enable automatic payload compression just include `Compress` as a top-level middleware:
|
||||
/// ```
|
||||
/// use actix_web::{web, middleware, App, HttpResponse};
|
||||
/// use actix_web::{middleware, web, App, HttpResponse};
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .wrap(middleware::Compress::default())
|
||||
/// .default_service(web::to(|| HttpResponse::NotFound()));
|
||||
/// .default_service(web::to(|| HttpResponse::Ok().body("hello world")));
|
||||
/// ```
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Compress(ContentEncoding);
|
||||
|
||||
impl Compress {
|
||||
/// Create new `Compress` middleware with the specified encoding.
|
||||
pub fn new(encoding: ContentEncoding) -> Self {
|
||||
Compress(encoding)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Compress {
|
||||
fn default() -> Self {
|
||||
Compress::new(ContentEncoding::Auto)
|
||||
}
|
||||
}
|
||||
///
|
||||
/// Pre-compressed Gzip file being served from disk with correct headers added to bypass middleware:
|
||||
/// ```no_run
|
||||
/// use actix_web::{middleware, http::header, web, App, HttpResponse, Responder};
|
||||
///
|
||||
/// async fn index_handler() -> actix_web::Result<impl Responder> {
|
||||
/// Ok(actix_files::NamedFile::open_async("./assets/index.html.gz").await?
|
||||
/// .customize()
|
||||
/// .insert_header(header::ContentEncoding::Gzip))
|
||||
/// }
|
||||
///
|
||||
/// let app = App::new()
|
||||
/// .wrap(middleware::Compress::default())
|
||||
/// .default_service(web::to(index_handler));
|
||||
/// ```
|
||||
///
|
||||
/// [feature flags]: ../index.html#crate-features
|
||||
#[derive(Debug, Clone, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct Compress;
|
||||
|
||||
impl<S, B> Transform<S, ServiceRequest> for Compress
|
||||
where
|
||||
@ -68,44 +87,14 @@ where
|
||||
type Future = Ready<Result<Self::Transform, Self::InitError>>;
|
||||
|
||||
fn new_transform(&self, service: S) -> Self::Future {
|
||||
ok(CompressMiddleware {
|
||||
service,
|
||||
encoding: self.0,
|
||||
})
|
||||
ok(CompressMiddleware { service })
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CompressMiddleware<S> {
|
||||
service: S,
|
||||
encoding: ContentEncoding,
|
||||
}
|
||||
|
||||
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
|
||||
#[allow(unused_mut)] // only unused when no compress features enabled
|
||||
let mut encoding: Vec<&str> = vec![];
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
{
|
||||
encoding.push("br");
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
{
|
||||
encoding.push("gzip");
|
||||
encoding.push("deflate");
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
encoding.push("zstd");
|
||||
|
||||
assert!(
|
||||
!encoding.is_empty(),
|
||||
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
|
||||
);
|
||||
|
||||
encoding.join(", ")
|
||||
});
|
||||
|
||||
impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
|
||||
where
|
||||
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
|
||||
@ -121,39 +110,43 @@ where
|
||||
#[allow(clippy::borrow_interior_mutable_const)]
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
// negotiate content-encoding
|
||||
let encoding_result = req
|
||||
.headers()
|
||||
.get(&ACCEPT_ENCODING)
|
||||
.and_then(|val| val.to_str().ok())
|
||||
.map(|enc| AcceptEncoding::try_parse(enc, self.encoding));
|
||||
let accept_encoding = req.get_header::<AcceptEncoding>();
|
||||
|
||||
match encoding_result {
|
||||
// Missing header => fallback to identity
|
||||
None => Either::left(CompressResponse {
|
||||
encoding: ContentEncoding::Identity,
|
||||
fut: self.service.call(req),
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
let accept_encoding = match accept_encoding {
|
||||
// missing header; fallback to identity
|
||||
None => {
|
||||
return Either::left(CompressResponse {
|
||||
encoding: Encoding::identity(),
|
||||
fut: self.service.call(req),
|
||||
_phantom: PhantomData,
|
||||
})
|
||||
}
|
||||
|
||||
// Valid encoding
|
||||
Some(Ok(encoding)) => Either::left(CompressResponse {
|
||||
encoding,
|
||||
fut: self.service.call(req),
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
// valid accept-encoding header
|
||||
Some(accept_encoding) => accept_encoding,
|
||||
};
|
||||
|
||||
// There is an HTTP header but we cannot match what client as asked for
|
||||
Some(Err(_)) => {
|
||||
let res = HttpResponse::with_body(
|
||||
match accept_encoding.negotiate(SUPPORTED_ENCODINGS.iter()) {
|
||||
None => {
|
||||
let mut res = HttpResponse::with_body(
|
||||
StatusCode::NOT_ACCEPTABLE,
|
||||
SUPPORTED_ALGORITHM_NAMES.clone(),
|
||||
SUPPORTED_ENCODINGS_STRING.as_str(),
|
||||
);
|
||||
|
||||
res.headers_mut()
|
||||
.insert(header::VARY, HeaderValue::from_static("Accept-Encoding"));
|
||||
|
||||
Either::right(ok(req
|
||||
.into_response(res)
|
||||
.map_into_boxed_body()
|
||||
.map_into_right_body()))
|
||||
}
|
||||
|
||||
Some(encoding) => Either::left(CompressResponse {
|
||||
fut: self.service.call(req),
|
||||
encoding,
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -165,7 +158,7 @@ pin_project! {
|
||||
{
|
||||
#[pin]
|
||||
fut: S::Future,
|
||||
encoding: ContentEncoding,
|
||||
encoding: Encoding,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
}
|
||||
@ -182,10 +175,11 @@ where
|
||||
|
||||
match ready!(this.fut.poll(cx)) {
|
||||
Ok(resp) => {
|
||||
let enc = if let Some(enc) = resp.response().get_encoding() {
|
||||
enc
|
||||
} else {
|
||||
*this.encoding
|
||||
let enc = match this.encoding {
|
||||
Encoding::Known(enc) => *enc,
|
||||
Encoding::Unknown(enc) => {
|
||||
unimplemented!("encoding {} should not be here", enc);
|
||||
}
|
||||
};
|
||||
|
||||
Poll::Ready(Ok(resp.map_body(move |head, body| {
|
||||
@ -198,178 +192,117 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
struct AcceptEncoding {
|
||||
encoding: ContentEncoding,
|
||||
// TODO: use Quality or QualityItem<ContentEncoding>
|
||||
quality: f64,
|
||||
}
|
||||
static SUPPORTED_ENCODINGS_STRING: Lazy<String> = Lazy::new(|| {
|
||||
#[allow(unused_mut)] // only unused when no compress features enabled
|
||||
let mut encoding: Vec<&str> = vec![];
|
||||
|
||||
impl Eq for AcceptEncoding {}
|
||||
|
||||
impl Ord for AcceptEncoding {
|
||||
#[allow(clippy::comparison_chain)]
|
||||
fn cmp(&self, other: &AcceptEncoding) -> cmp::Ordering {
|
||||
if self.quality > other.quality {
|
||||
cmp::Ordering::Less
|
||||
} else if self.quality < other.quality {
|
||||
cmp::Ordering::Greater
|
||||
} else {
|
||||
cmp::Ordering::Equal
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialOrd for AcceptEncoding {
|
||||
fn partial_cmp(&self, other: &AcceptEncoding) -> Option<cmp::Ordering> {
|
||||
Some(self.cmp(other))
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AcceptEncoding {
|
||||
fn eq(&self, other: &AcceptEncoding) -> bool {
|
||||
self.encoding == other.encoding && self.quality == other.quality
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse q-factor from quality strings.
|
||||
///
|
||||
/// If parse fail, then fallback to default value which is 1.
|
||||
/// More details available here: <https://developer.mozilla.org/en-US/docs/Glossary/Quality_values>
|
||||
fn parse_quality(parts: &[&str]) -> f64 {
|
||||
for part in parts {
|
||||
if part.trim().starts_with("q=") {
|
||||
return part[2..].parse().unwrap_or(1.0);
|
||||
}
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
{
|
||||
encoding.push("br");
|
||||
}
|
||||
|
||||
1.0
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
enum AcceptEncodingError {
|
||||
/// This error occurs when client only support compressed response and server do not have any
|
||||
/// algorithm that match client accepted algorithms.
|
||||
CompressionAlgorithmMismatch,
|
||||
}
|
||||
|
||||
impl AcceptEncoding {
|
||||
fn new(tag: &str) -> Option<AcceptEncoding> {
|
||||
let parts: Vec<&str> = tag.split(';').collect();
|
||||
let encoding = match parts.len() {
|
||||
0 => return None,
|
||||
_ => match ContentEncoding::try_from(parts[0]) {
|
||||
Err(_) => return None,
|
||||
Ok(x) => x,
|
||||
},
|
||||
};
|
||||
|
||||
let quality = parse_quality(&parts[1..]);
|
||||
if quality <= 0.0 || quality > 1.0 {
|
||||
return None;
|
||||
}
|
||||
|
||||
Some(AcceptEncoding { encoding, quality })
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
{
|
||||
encoding.push("gzip");
|
||||
encoding.push("deflate");
|
||||
}
|
||||
|
||||
/// Parse a raw Accept-Encoding header value into an ordered list then return the best match
|
||||
/// based on middleware configuration.
|
||||
pub fn try_parse(
|
||||
raw: &str,
|
||||
encoding: ContentEncoding,
|
||||
) -> Result<ContentEncoding, AcceptEncodingError> {
|
||||
let mut encodings = raw
|
||||
.replace(' ', "")
|
||||
.split(',')
|
||||
.filter_map(AcceptEncoding::new)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
encodings.sort();
|
||||
|
||||
for enc in encodings {
|
||||
if encoding == ContentEncoding::Auto || encoding == enc.encoding {
|
||||
return Ok(enc.encoding);
|
||||
}
|
||||
}
|
||||
|
||||
// Special case if user cannot accept uncompressed data.
|
||||
// See: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Accept-Encoding
|
||||
// TODO: account for whitespace
|
||||
if raw.contains("*;q=0") || raw.contains("identity;q=0") {
|
||||
return Err(AcceptEncodingError::CompressionAlgorithmMismatch);
|
||||
}
|
||||
|
||||
Ok(ContentEncoding::Identity)
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
{
|
||||
encoding.push("zstd");
|
||||
}
|
||||
}
|
||||
|
||||
assert!(
|
||||
!encoding.is_empty(),
|
||||
"encoding can not be empty unless __compress feature has been explicitly enabled by itself"
|
||||
);
|
||||
|
||||
encoding.join(", ")
|
||||
});
|
||||
|
||||
static SUPPORTED_ENCODINGS: Lazy<Vec<Encoding>> = Lazy::new(|| {
|
||||
let mut encodings = vec![Encoding::identity()];
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
{
|
||||
encodings.push(Encoding::brotli());
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
{
|
||||
encodings.push(Encoding::gzip());
|
||||
encodings.push(Encoding::deflate());
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
{
|
||||
encodings.push(Encoding::zstd());
|
||||
}
|
||||
|
||||
assert!(
|
||||
!encodings.is_empty(),
|
||||
"encodings can not be empty unless __compress feature has been explicitly enabled by itself"
|
||||
);
|
||||
|
||||
encodings
|
||||
});
|
||||
|
||||
// move cfg(feature) to prevents_double_compressing if more tests are added
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{middleware::DefaultHeaders, test, web, App};
|
||||
|
||||
macro_rules! assert_parse_eq {
|
||||
($raw:expr, $result:expr) => {
|
||||
assert_eq!(
|
||||
AcceptEncoding::try_parse($raw, ContentEncoding::Auto),
|
||||
Ok($result)
|
||||
);
|
||||
};
|
||||
pub fn gzip_decode(bytes: impl AsRef<[u8]>) -> Vec<u8> {
|
||||
use std::io::Read as _;
|
||||
let mut decoder = flate2::read::GzDecoder::new(bytes.as_ref());
|
||||
let mut buf = Vec::new();
|
||||
decoder.read_to_end(&mut buf).unwrap();
|
||||
buf
|
||||
}
|
||||
|
||||
macro_rules! assert_parse_fail {
|
||||
($raw:expr) => {
|
||||
assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err());
|
||||
};
|
||||
}
|
||||
#[actix_rt::test]
|
||||
async fn prevents_double_compressing() {
|
||||
const D: &str = "hello world ";
|
||||
const DATA: &str = const_str::repeat!(D, 100);
|
||||
|
||||
#[test]
|
||||
fn test_parse_encoding() {
|
||||
// Test simple case
|
||||
assert_parse_eq!("br", ContentEncoding::Br);
|
||||
assert_parse_eq!("gzip", ContentEncoding::Gzip);
|
||||
assert_parse_eq!("deflate", ContentEncoding::Deflate);
|
||||
assert_parse_eq!("zstd", ContentEncoding::Zstd);
|
||||
let app = test::init_service({
|
||||
App::new()
|
||||
.wrap(Compress::default())
|
||||
.route(
|
||||
"/single",
|
||||
web::get().to(move || HttpResponse::Ok().body(DATA)),
|
||||
)
|
||||
.service(
|
||||
web::resource("/double")
|
||||
.wrap(Compress::default())
|
||||
.wrap(DefaultHeaders::new().add(("x-double", "true")))
|
||||
.route(web::get().to(move || HttpResponse::Ok().body(DATA))),
|
||||
)
|
||||
})
|
||||
.await;
|
||||
|
||||
// Test space, trim, missing values
|
||||
assert_parse_eq!("br,,,,", ContentEncoding::Br);
|
||||
assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip);
|
||||
let req = test::TestRequest::default()
|
||||
.uri("/single")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.to_request();
|
||||
let res = test::call_service(&app, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.headers().get("x-double"), None);
|
||||
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
|
||||
let bytes = test::read_body(res).await;
|
||||
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
|
||||
|
||||
// Test float number parsing
|
||||
assert_parse_eq!("br;q=1 ,", ContentEncoding::Br);
|
||||
assert_parse_eq!("br;q=1.0 , br", ContentEncoding::Br);
|
||||
|
||||
// Test wildcard
|
||||
assert_parse_eq!("*", ContentEncoding::Identity);
|
||||
assert_parse_eq!("*;q=1.0", ContentEncoding::Identity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_encoding_qfactor_ordering() {
|
||||
assert_parse_eq!("gzip, br, zstd", ContentEncoding::Gzip);
|
||||
assert_parse_eq!("zstd, br, gzip", ContentEncoding::Zstd);
|
||||
|
||||
assert_parse_eq!("gzip;q=0.4, br;q=0.6", ContentEncoding::Br);
|
||||
assert_parse_eq!("gzip;q=0.8, br;q=0.4", ContentEncoding::Gzip);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_encoding_qfactor_invalid() {
|
||||
// Out of range
|
||||
assert_parse_eq!("gzip;q=-5.0", ContentEncoding::Identity);
|
||||
assert_parse_eq!("gzip;q=5.0", ContentEncoding::Identity);
|
||||
|
||||
// Disabled
|
||||
assert_parse_eq!("gzip;q=0", ContentEncoding::Identity);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_compression_required() {
|
||||
// Check we fallback to identity if there is an unsupported compression algorithm
|
||||
assert_parse_eq!("compress", ContentEncoding::Identity);
|
||||
|
||||
// User do not want any compression
|
||||
assert_parse_fail!("compress, identity;q=0");
|
||||
assert_parse_fail!("compress, identity;q=0.0");
|
||||
assert_parse_fail!("compress, *;q=0");
|
||||
assert_parse_fail!("compress, *;q=0.0");
|
||||
let req = test::TestRequest::default()
|
||||
.uri("/double")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.to_request();
|
||||
let res = test::call_service(&app, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(res.headers().get("x-double").unwrap(), "true");
|
||||
assert_eq!(res.headers().get(header::CONTENT_ENCODING).unwrap(), "gzip");
|
||||
let bytes = test::read_body(res).await;
|
||||
assert_eq!(gzip_decode(bytes), DATA.as_bytes());
|
||||
}
|
||||
}
|
||||
|
@ -547,7 +547,7 @@ impl FormatText {
|
||||
*self = FormatText::Str(s.to_string());
|
||||
}
|
||||
FormatText::RemoteAddr => {
|
||||
let s = if let Some(peer) = req.connection_info().remote_addr() {
|
||||
let s = if let Some(peer) = req.connection_info().peer_addr() {
|
||||
FormatText::Str((*peer).to_string())
|
||||
} else {
|
||||
FormatText::Str("-".to_string())
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user