Merge branch 'master' into json-require-content-type

This commit is contained in:
Rob Ede 2021-09-11 01:26:29 +01:00 committed by GitHub
commit dc85ef09a5
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
99 changed files with 2328 additions and 1545 deletions

View File

@ -6,4 +6,4 @@ ci-min-test = "hack check --workspace --no-default-features --tests --examples"
ci-default = "check --workspace --bins --tests --examples" ci-default = "check --workspace --bins --tests --examples"
ci-full = "check --workspace --all-features --bins --tests --examples" ci-full = "check --workspace --all-features --bins --tests --examples"
ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture"
ci-doctest = "hack test --workspace --all-features --doc --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"

View File

@ -16,7 +16,7 @@ jobs:
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version: version:
- 1.46.0 # MSRV - 1.51.0 # MSRV
- stable - stable
- nightly - nightly
@ -24,6 +24,8 @@ jobs:
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env: env:
CI: 1
CARGO_INCREMENTAL: 0
VCPKGRS_DYNAMIC: 1 VCPKGRS_DYNAMIC: 1
steps: steps:
@ -80,13 +82,6 @@ jobs:
command: ci-test command: ci-test
args: --skip=test_reading_deflate_encoding_large_random_rustls args: --skip=test_reading_deflate_encoding_large_random_rustls
- name: doc tests
# due to unknown issue with running doc tests on macOS
if: matrix.target.os == 'ubuntu-latest'
uses: actions-rs/cargo@v1
timeout-minutes: 40
with: { command: ci-doctest }
- name: Generate coverage file - name: Generate coverage file
if: > if: >
matrix.target.os == 'ubuntu-latest' matrix.target.os == 'ubuntu-latest'
@ -106,5 +101,36 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
cargo-cache cargo-cache
rustdoc:
name: rustdoc
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust (nightly)
uses: actions-rs/toolchain@v1
with:
toolchain: nightly-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: doc tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with: { command: ci-doctest }

View File

@ -1,10 +1,29 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Changed
* Asscociated type `FromRequest::Config` was removed. [#2233]
[#2233]: https://github.com/actix/actix-web/pull/2233
## 4.0.0-beta.9 - 2021-09-09
### Added ### Added
* Re-export actix-service `ServiceFactory` in `dev` module. [#2325] * Re-export actix-service `ServiceFactory` in `dev` module. [#2325]
### Changed
* Compress middleware will return 406 Not Acceptable when no content encoding is acceptable to the client. [#2344]
* Move `BaseHttpResponse` to `dev::Response`. [#2379]
* Enable `TestRequest::param` to accept more than just static strings. [#2172]
* Minimum supported Rust version (MSRV) is now 1.51.
### Fixed
* Fix quality parse error in Accept-Encoding header. [#2344]
* Re-export correct type at `web::HttpResponse`. [#2379]
[#2172]: https://github.com/actix/actix-web/pull/2172
[#2325]: https://github.com/actix/actix-web/pull/2325 [#2325]: https://github.com/actix/actix-web/pull/2325
[#2344]: https://github.com/actix/actix-web/pull/2344
[#2379]: https://github.com/actix/actix-web/pull/2379
## 4.0.0-beta.8 - 2021-06-26 ## 4.0.0-beta.8 - 2021-06-26

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.8" version = "4.0.0-beta.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -18,12 +18,14 @@ edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
rustdoc-args = ["--cfg", "docsrs"]
[lib] [lib]
name = "actix_web" name = "actix_web"
path = "src/lib.rs" path = "src/lib.rs"
[workspace] [workspace]
resolver = "2"
members = [ members = [
".", ".",
"awc", "awc",
@ -68,15 +70,15 @@ __compress = []
[dependencies] [dependencies]
actix-codec = "0.4.0" actix-codec = "0.4.0"
actix-macros = "0.2.1" actix-macros = "0.2.1"
actix-router = "0.5.0-beta.1" actix-router = "0.5.0-beta.2"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.2" actix-web-codegen = "0.5.0-beta.4"
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
@ -98,14 +100,14 @@ regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
smallvec = "1.6" smallvec = "1.6.1"
socket2 = "0.4.0" socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.7", features = ["openssl"] } awc = { version = "3.0.0-beta.8", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
@ -117,6 +119,10 @@ rcgen = "0.8"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.19.0" } tls-rustls = { package = "rustls", version = "0.19.0" }
[profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
debug = 0
[profile.release] [profile.release]
lto = true lto = true
opt-level = 3 opt-level = 3

View File

@ -3,13 +3,16 @@
* The default `NormalizePath` behavior now strips trailing slashes by default. This was * The default `NormalizePath` behavior now strips trailing slashes by default. This was
previously documented to be the case in v3 but the behavior now matches. The effect is that previously documented to be the case in v3 but the behavior now matches. The effect is that
routes defined with trailing slashes will become inaccessible when routes defined with trailing slashes will become inaccessible when
using `NormalizePath::default()`. using `NormalizePath::default()`. As such, calling `NormalizePath::default()` will log a warning.
It is advised that the `new` method be used instead.
Before: `#[get("/test/")]` Before: `#[get("/test/")]`
After: `#[get("/test")]` After: `#[get("/test")]`
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
* The `type Config` of `FromRequest` was removed.
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). * Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
By default all compression algorithms are enabled. By default all compression algorithms are enabled.
To select algorithm you want to include with `middleware::Compress` use following flags: To select algorithm you want to include with `middleware::Compress` use following flags:

View File

@ -6,10 +6,10 @@
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.8)](https://docs.rs/actix-web/4.0.0-beta.8) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.9)](https://docs.rs/actix-web/4.0.0-beta.9)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.8) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.9/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.9)
<br /> <br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
@ -32,7 +32,7 @@
* SSL support using OpenSSL or Rustls * SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://docs.rs/awc/) * Includes an async [HTTP client](https://docs.rs/awc/)
* Runs on stable Rust 1.46+ * Runs on stable Rust 1.51+
## Documentation ## Documentation

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.7 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51.
## 0.6.0-beta.6 - 2021-06-26 ## 0.6.0-beta.6 - 2021-06-26
* Added `Files::path_filter()`. [#2274] * Added `Files::path_filter()`. [#2274]
* `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228] * `Files::show_files_listing()` can now be used with `Files::index_file()` to show files listing as a fallback when the index file is not found. [#2228]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.6" version = "0.6.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
@ -15,8 +15,8 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.8", default-features = false } actix-web = { version = "4.0.0-beta.9", default-features = false }
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
@ -33,5 +33,5 @@ percent-encoding = "2.1"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-web = "4.0.0-beta.8" actix-web = "4.0.0-beta.9"
actix-test = "0.1.0-beta.3" actix-test = "0.1.0-beta.3"

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.6)](https://docs.rs/actix-files/0.6.0-beta.6) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.7)](https://docs.rs/actix-files/0.6.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.6/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.6) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.7/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.7)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
@ -15,4 +15,4 @@
- [API Documentation](https://docs.rs/actix-files/) - [API Documentation](https://docs.rs/actix-files/)
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) - [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
- Minimum supported Rust version: 1.46 or later - Minimum supported Rust version: 1.51 or later

View File

@ -21,6 +21,7 @@ impl ResponseError for FilesError {
} }
} }
#[allow(clippy::enum_variant_names)]
#[derive(Display, Debug, PartialEq)] #[derive(Display, Debug, PartialEq)]
pub enum UriSegmentError { pub enum UriSegmentError {
/// The segment started with the wrapped invalid character. /// The segment started with the wrapped invalid character.

View File

@ -106,7 +106,7 @@ impl Files {
}; };
Files { Files {
path: mount_path.to_owned(), path: mount_path.trim_end_matches('/').to_owned(),
directory: dir, directory: dir,
index: None, index: None,
show_index: false, show_index: false,

View File

@ -59,7 +59,6 @@ impl AsRef<Path> for PathBufWrap {
impl FromRequest for PathBufWrap { impl FromRequest for PathBufWrap {
type Error = UriSegmentError; type Error = UriSegmentError;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.match_info().path().parse()) ready(req.match_info().path().parse())

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.5 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51.
## 3.0.0-beta.4 - 2021-04-02 ## 3.0.0-beta.4 - 2021-04-02
* Added `TestServer::client_headers` method. [#2097] * Added `TestServer::client_headers` method. [#2097]

View File

@ -1,18 +1,18 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.4" version = "3.0.0-beta.5"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-http-test/" categories = [
categories = ["network-programming", "asynchronous", "network-programming",
"asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket"] "web-programming::websocket",
]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
exclude = [".gitignore", ".cargo/config"]
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
@ -35,7 +35,7 @@ actix-tls = "3.0.0-beta.5"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
awc = { version = "3.0.0-beta.7", default-features = false } awc = { version = "3.0.0-beta.8", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
@ -47,9 +47,8 @@ serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"

View File

@ -3,15 +3,15 @@
> Various helpers for Actix applications to use during testing. > Various helpers for Actix applications to use during testing.
[![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test) [![crates.io](https://img.shields.io/crates/v/actix-http-test?label=latest)](https://crates.io/crates/actix-http-test)
[![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http-test/3.0.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http-test)
<br> <br>
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.5)
[![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test) [![Download](https://img.shields.io/crates/d/actix-http-test.svg)](https://crates.io/crates/actix-http-test)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http-test) - [API Documentation](https://docs.rs/actix-http-test)
- Minimum Supported Rust Version (MSRV): 1.46.0 - Minimum Supported Rust Version (MSRV): 1.51.0

View File

@ -7,8 +7,7 @@
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::sync::mpsc; use std::{net, sync::mpsc, thread, time::Duration};
use std::{net, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System}; use actix_rt::{net::TcpStream, System};
@ -95,15 +94,15 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
.ssl(builder.build()) .ssl(builder.build())
} }
#[cfg(not(feature = "openssl"))] #[cfg(not(feature = "openssl"))]
{ {
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
} }
}; };

View File

@ -3,6 +3,27 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.10 - 2021-09-09
### Changed
* `ContentEncoding` is now marked `#[non_exhaustive]`. [#2377]
* Minimum supported Rust version (MSRV) is now 1.51.
### Fixed
* Remove slice creation pointing to potential uninitialized data on h1 encoder. [#2364]
* Remove `Into<Error>` bound on `Encoder` body types. [#2375]
* Fix quality parse error in Accept-Encoding header. [#2344]
[#2364]: https://github.com/actix/actix-web/pull/2364
[#2375]: https://github.com/actix/actix-web/pull/2375
[#2344]: https://github.com/actix/actix-web/pull/2344
[#2377]: https://github.com/actix/actix-web/pull/2377
## 3.0.0-beta.9 - 2021-08-09
### Fixed
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
## 3.0.0-beta.8 - 2021-06-26 ## 3.0.0-beta.8 - 2021-06-26
### Changed ### Changed
* Change compression algorithm features flags. [#2250] * Change compression algorithm features flags. [#2250]
@ -210,6 +231,11 @@
[#1878]: https://github.com/actix/actix-web/pull/1878 [#1878]: https://github.com/actix/actix-web/pull/1878
## 2.2.1 - 2021-08-09
### Fixed
* Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
## 2.2.0 - 2020-11-25 ## 2.2.0 - 2020-11-25
### Added ### Added
* HttpResponse builders for 1xx status codes. [#1768] * HttpResponse builders for 1xx status codes. [#1768]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.8" version = "3.0.0-beta.10"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
@ -59,7 +59,8 @@ futures-core = { version = "0.3.7", default-features = false, features = ["alloc
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.1" h2 = "0.3.1"
http = "0.2.2" http = "0.2.2"
httparse = "1.3" httparse = "1.5.1"
httpdate = "1.0.1"
itoa = "0.4" itoa = "0.4"
language-tags = "0.3" language-tags = "0.3"
local-channel = "0.1" local-channel = "0.1"
@ -70,11 +71,8 @@ percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2" pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
regex = "1.3"
serde = "1.0"
sha-1 = "0.9" sha-1 = "0.9"
smallvec = "1.6" smallvec = "1.6.1"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tokio = { version = "1.2", features = ["sync"] } tokio = { version = "1.2", features = ["sync"] }
# compression # compression
@ -86,17 +84,18 @@ trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.8"
rcgen = "0.8" rcgen = "0.8"
regex = "1.3"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } tls-rustls = { version = "0.19", package = "rustls" }
webpki = { version = "0.21.0" } webpki = { version = "0.21" }
[[example]] [[example]]
name = "ws" name = "ws"

View File

@ -3,18 +3,18 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.8)](https://docs.rs/actix-http/3.0.0-beta.8) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.10)](https://docs.rs/actix-http/3.0.0-beta.10)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.8/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.8) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.10/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.10)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http) [![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http) - [API Documentation](https://docs.rs/actix-http)
- Minimum Supported Rust Version (MSRV): 1.46.0 - Minimum Supported Rust Version (MSRV): 1.51.0
## Example ## Example

View File

@ -18,7 +18,8 @@ fn bench_write_camel_case(c: &mut Criterion) {
group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| { group.bench_with_input(BenchmarkId::new("New", i), bts, |b, bts| {
b.iter(|| { b.iter(|| {
let mut buf = black_box([0; 24]); let mut buf = black_box([0; 24]);
_new::write_camel_case(black_box(bts), &mut buf) let len = black_box(bts.len());
_new::write_camel_case(black_box(bts), buf.as_mut_ptr(), len)
}); });
}); });
} }
@ -30,9 +31,12 @@ criterion_group!(benches, bench_write_camel_case);
criterion_main!(benches); criterion_main!(benches);
mod _new { mod _new {
pub fn write_camel_case(value: &[u8], buffer: &mut [u8]) { pub fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
// first copy entire (potentially wrong) slice to output // first copy entire (potentially wrong) slice to output
buffer[..value.len()].copy_from_slice(value); let buffer = unsafe {
std::ptr::copy_nonoverlapping(value.as_ptr(), buf, len);
std::slice::from_raw_parts_mut(buf, len)
};
let mut iter = value.iter(); let mut iter = value.iter();

View File

@ -7,7 +7,7 @@ use std::{
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream}; use futures_core::Stream;
use crate::error::Error; use crate::error::Error;
@ -74,14 +74,10 @@ impl MessageBody for AnyBody {
} }
} }
// TODO: MSRV 1.51: poll_map_err AnyBody::Message(body) => body
AnyBody::Message(body) => match ready!(body.as_pin_mut().poll_next(cx)) { .as_pin_mut()
Some(Err(err)) => { .poll_next(cx)
Poll::Ready(Some(Err(Error::new_body().with_cause(err)))) .map_err(|err| Error::new_body().with_cause(err)),
}
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
} }
} }
} }
@ -223,11 +219,9 @@ impl MessageBody for BoxAnyBody {
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
// TODO: MSRV 1.51: poll_map_err self.0
match ready!(self.0.as_mut().poll_next(cx)) { .as_mut()
Some(Err(err)) => Poll::Ready(Some(Err(Error::new_body().with_cause(err)))), .poll_next(cx)
Some(Ok(val)) => Poll::Ready(Some(Ok(val))), .map_err(|err| Error::new_body().with_cause(err))
None => Poll::Ready(None),
}
} }
} }

View File

@ -11,8 +11,6 @@ use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::error::Error;
use super::BodySize; use super::BodySize;
/// An interface for response bodies. /// An interface for response bodies.
@ -47,7 +45,6 @@ impl MessageBody for () {
impl<B> MessageBody for Box<B> impl<B> MessageBody for Box<B>
where where
B: MessageBody + Unpin, B: MessageBody + Unpin,
B::Error: Into<Error>,
{ {
type Error = B::Error; type Error = B::Error;
@ -66,7 +63,6 @@ where
impl<B> MessageBody for Pin<Box<B>> impl<B> MessageBody for Pin<Box<B>>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
type Error = B::Error; type Error = B::Error;

View File

@ -80,7 +80,7 @@ mod tests {
impl Body { impl Body {
pub(crate) fn get_ref(&self) -> &[u8] { pub(crate) fn get_ref(&self) -> &[u8] {
match *self { match *self {
Body::Bytes(ref bin) => &bin, Body::Bytes(ref bin) => bin,
_ => panic!(), _ => panic!(),
} }
} }

View File

@ -5,7 +5,7 @@ use std::{
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::Stream;
use pin_project::pin_project; use pin_project::pin_project;
use crate::error::Error; use crate::error::Error;
@ -77,12 +77,7 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
match self.project() { match self.project() {
// TODO: MSRV 1.51: poll_map_err ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into),
ResponseBodyProj::Body(body) => match ready!(body.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
} }
} }

View File

@ -1,18 +1,19 @@
use std::cell::Cell; use std::{
use std::fmt::Write; cell::Cell,
use std::rc::Rc; fmt::{self, Write},
use std::time::Duration; net,
use std::{fmt, net}; rc::Rc,
time::{Duration, SystemTime},
};
use actix_rt::{ use actix_rt::{
task::JoinHandle, task::JoinHandle,
time::{interval, sleep_until, Instant, Sleep}, time::{interval, sleep_until, Instant, Sleep},
}; };
use bytes::BytesMut; use bytes::BytesMut;
use time::OffsetDateTime;
/// "Sun, 06 Nov 1994 08:49:37 GMT".len() /// "Sun, 06 Nov 1994 08:49:37 GMT".len()
const DATE_VALUE_LENGTH: usize = 29; pub(crate) const DATE_VALUE_LENGTH: usize = 29;
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
/// Server keep-alive setting /// Server keep-alive setting
@ -206,12 +207,7 @@ impl Date {
fn update(&mut self) { fn update(&mut self) {
self.pos = 0; self.pos = 0;
write!( write!(self, "{}", httpdate::fmt_http_date(SystemTime::now())).unwrap();
self,
"{}",
OffsetDateTime::now_utc().format("%a, %d %b %Y %H:%M:%S GMT")
)
.unwrap();
} }
} }
@ -269,11 +265,11 @@ impl DateService {
} }
// TODO: move to a util module for testing all spawn handle drop style tasks. // TODO: move to a util module for testing all spawn handle drop style tasks.
#[cfg(test)]
/// Test Module for checking the drop state of certain async tasks that are spawned /// Test Module for checking the drop state of certain async tasks that are spawned
/// with `actix_rt::spawn` /// with `actix_rt::spawn`
/// ///
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task /// The target task must explicitly generate `NotifyOnDrop` when spawn the task
#[cfg(test)]
mod notify_on_drop { mod notify_on_drop {
use std::cell::RefCell; use std::cell::RefCell;
@ -283,9 +279,8 @@ mod notify_on_drop {
/// Check if the spawned task is dropped. /// Check if the spawned task is dropped.
/// ///
/// # Panic: /// # Panics
/// /// Panics when there was no `NotifyOnDrop` instance on current thread.
/// When there was no `NotifyOnDrop` instance on current thread
pub(crate) fn is_dropped() -> bool { pub(crate) fn is_dropped() -> bool {
NOTIFY_DROPPED.with(|bool| { NOTIFY_DROPPED.with(|bool| {
bool.borrow() bool.borrow()

View File

@ -80,7 +80,7 @@ where
let encoding = headers let encoding = headers
.get(&CONTENT_ENCODING) .get(&CONTENT_ENCODING)
.and_then(|val| val.to_str().ok()) .and_then(|val| val.to_str().ok())
.map(ContentEncoding::from) .and_then(|x| x.parse().ok())
.unwrap_or(ContentEncoding::Identity); .unwrap_or(ContentEncoding::Identity);
Self::new(stream, encoding) Self::new(stream, encoding)

View File

@ -29,7 +29,7 @@ use crate::{
header::{ContentEncoding, CONTENT_ENCODING}, header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode, HeaderValue, StatusCode,
}, },
Error, ResponseHead, ResponseHead,
}; };
use super::Writer; use super::Writer;
@ -107,7 +107,6 @@ enum EncoderBody<B> {
impl<B> MessageBody for EncoderBody<B> impl<B> MessageBody for EncoderBody<B>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
type Error = EncoderError<B::Error>; type Error = EncoderError<B::Error>;
@ -131,18 +130,9 @@ where
Poll::Ready(Some(Ok(std::mem::take(b)))) Poll::Ready(Some(Ok(std::mem::take(b))))
} }
} }
// TODO: MSRV 1.51: poll_map_err EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body),
EncoderBodyProj::Stream(b) => match ready!(b.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Body(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
EncoderBodyProj::BoxedStream(ref mut b) => { EncoderBodyProj::BoxedStream(ref mut b) => {
match ready!(b.as_pin_mut().poll_next(cx)) { b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed)
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
} }
} }
} }
@ -151,7 +141,6 @@ where
impl<B> MessageBody for Encoder<B> impl<B> MessageBody for Encoder<B>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
type Error = EncoderError<B::Error>; type Error = EncoderError<B::Error>;

View File

@ -196,7 +196,7 @@ pub enum ParseError {
#[display(fmt = "IO error: {}", _0)] #[display(fmt = "IO error: {}", _0)]
Io(io::Error), Io(io::Error),
/// Parsing a field as string failed /// Parsing a field as string failed.
#[display(fmt = "UTF8 error: {}", _0)] #[display(fmt = "UTF8 error: {}", _0)]
Utf8(Utf8Error), Utf8(Utf8Error),
} }

View File

@ -0,0 +1,432 @@
use std::{io, task::Poll};
use bytes::{Buf as _, Bytes, BytesMut};
macro_rules! byte (
($rdr:ident) => ({
if $rdr.len() > 0 {
let b = $rdr[0];
$rdr.advance(1);
b
} else {
return Poll::Pending
}
})
);
#[derive(Debug, PartialEq, Clone)]
pub(super) enum ChunkedState {
Size,
SizeLws,
Extension,
SizeLf,
Body,
BodyCr,
BodyLf,
EndCr,
EndLf,
End,
}
impl ChunkedState {
pub(super) fn step(
&self,
body: &mut BytesMut,
size: &mut u64,
buf: &mut Option<Bytes>,
) -> Poll<Result<ChunkedState, io::Error>> {
use self::ChunkedState::*;
match *self {
Size => ChunkedState::read_size(body, size),
SizeLws => ChunkedState::read_size_lws(body),
Extension => ChunkedState::read_extension(body),
SizeLf => ChunkedState::read_size_lf(body, *size),
Body => ChunkedState::read_body(body, size, buf),
BodyCr => ChunkedState::read_body_cr(body),
BodyLf => ChunkedState::read_body_lf(body),
EndCr => ChunkedState::read_end_cr(body),
EndLf => ChunkedState::read_end_lf(body),
End => Poll::Ready(Ok(ChunkedState::End)),
}
}
fn read_size(
rdr: &mut BytesMut,
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
let radix = 16;
let rem = match byte!(rdr) {
b @ b'0'..=b'9' => b - b'0',
b @ b'a'..=b'f' => b + 10 - b'a',
b @ b'A'..=b'F' => b + 10 - b'A',
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size line: Invalid Size",
)));
}
};
match size.checked_mul(radix) {
Some(n) => {
*size = n as u64;
*size += rem as u64;
Poll::Ready(Ok(ChunkedState::Size))
}
None => {
log::debug!("chunk size would overflow u64");
Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size line: Size is too big",
)))
}
}
}
fn read_size_lws(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
// LWS can follow the chunk size, but no more digits can come
b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size linear white space",
))),
}
}
fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
// strictly 0x20 (space) should be disallowed but we don't parse quoted strings here
0x00..=0x08 | 0x0a..=0x1f | 0x7f => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid character in chunk extension",
))),
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
}
}
fn read_size_lf(
rdr: &mut BytesMut,
size: u64,
) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\n' if size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
b'\n' if size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size LF",
))),
}
}
fn read_body(
rdr: &mut BytesMut,
rem: &mut u64,
buf: &mut Option<Bytes>,
) -> Poll<Result<ChunkedState, io::Error>> {
log::trace!("Chunked read, remaining={:?}", rem);
let len = rdr.len() as u64;
if len == 0 {
Poll::Ready(Ok(ChunkedState::Body))
} else {
let slice;
if *rem > len {
slice = rdr.split().freeze();
*rem -= len;
} else {
slice = rdr.split_to(*rem as usize).freeze();
*rem = 0;
}
*buf = Some(slice);
if *rem > 0 {
Poll::Ready(Ok(ChunkedState::Body))
} else {
Poll::Ready(Ok(ChunkedState::BodyCr))
}
}
}
fn read_body_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk body CR",
))),
}
}
fn read_body_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\n' => Poll::Ready(Ok(ChunkedState::Size)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk body LF",
))),
}
}
fn read_end_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk end CR",
))),
}
}
fn read_end_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\n' => Poll::Ready(Ok(ChunkedState::End)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk end LF",
))),
}
}
}
#[cfg(test)]
mod tests {
use actix_codec::Decoder as _;
use bytes::{Bytes, BytesMut};
use http::Method;
use crate::{
error::ParseError,
h1::decoder::{MessageDecoder, PayloadItem},
HttpMessage as _, Request,
};
macro_rules! parse_ready {
($e:expr) => {{
match MessageDecoder::<Request>::default().decode($e) {
Ok(Some((msg, _))) => msg,
Ok(_) => unreachable!("Eof during parsing http request"),
Err(err) => unreachable!("Error during parsing http request: {:?}", err),
}
}};
}
macro_rules! expect_parse_err {
($e:expr) => {{
match MessageDecoder::<Request>::default().decode($e) {
Err(err) => match err {
ParseError::Io(_) => unreachable!("Parse error expected"),
_ => {}
},
_ => unreachable!("Error expected"),
}
}};
}
#[test]
fn test_parse_chunked_payload_chunk_extension() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(msg.chunked().unwrap());
buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n")
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
assert_eq!(chunk, Bytes::from_static(b"data"));
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
assert_eq!(chunk, Bytes::from_static(b"line"));
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert!(msg.eof());
}
#[test]
fn test_request_chunked() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let req = parse_ready!(&mut buf);
if let Ok(val) = req.chunked() {
assert!(val);
} else {
unreachable!("Error");
}
// intentional typo in "chunked"
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chnked\r\n\r\n",
);
expect_parse_err!(&mut buf);
}
#[test]
fn test_http_request_chunked_payload() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(req.chunked().unwrap());
buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n");
assert_eq!(
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
b"data"
);
assert_eq!(
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
b"line"
);
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
}
#[test]
fn test_http_request_chunked_payload_and_next_message() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(req.chunked().unwrap());
buf.extend(
b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\
POST /test2 HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n"
.iter(),
);
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"data");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"line");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert!(msg.eof());
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert!(req.chunked().unwrap());
assert_eq!(*req.method(), Method::POST);
assert!(req.chunked().unwrap());
}
#[test]
fn test_http_request_chunked_payload_chunks() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(req.chunked().unwrap());
buf.extend(b"4\r\n1111\r\n");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"1111");
buf.extend(b"4\r\ndata\r");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"data");
buf.extend(b"\n4");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"\r");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"\n");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"li");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"li");
//trailers
//buf.feed_data("test: test\r\n");
//not_ready!(reader.parse(&mut buf, &mut readbuf));
buf.extend(b"ne\r\n0\r\n");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"ne");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"\r\n");
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
}
#[test]
fn chunk_extension_quoted() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
2;hello=b;one=\"1 2 3\"\r\n\
xx",
);
let mut reader = MessageDecoder::<Request>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
let chunk = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"xx")));
}
#[test]
fn hrs_chunk_extension_invalid() {
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: localhost:8080\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
2;x\nx\r\n\
4c\r\n\
0\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
let err = pl.decode(&mut buf).unwrap_err();
assert!(err
.to_string()
.contains("Invalid character in chunk extension"));
}
#[test]
fn hrs_chunk_size_overflow() {
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
f0000000000000003\r\n\
abc\r\n\
0\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
let err = pl.decode(&mut buf).unwrap_err();
assert!(err
.to_string()
.contains("Invalid chunk size line: Size is too big"));
}
}

View File

@ -1,18 +1,18 @@
use std::convert::TryFrom; use std::{convert::TryFrom, io, marker::PhantomData, mem::MaybeUninit, task::Poll};
use std::io;
use std::marker::PhantomData;
use std::task::Poll;
use actix_codec::Decoder; use actix_codec::Decoder;
use bytes::{Buf, Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::header::{HeaderName, HeaderValue}; use http::header::{HeaderName, HeaderValue};
use http::{header, Method, StatusCode, Uri, Version}; use http::{header, Method, StatusCode, Uri, Version};
use log::{debug, error, trace}; use log::{debug, error, trace};
use crate::error::ParseError; use super::chunked::ChunkedState;
use crate::header::HeaderMap; use crate::{
use crate::message::{ConnectionType, ResponseHead}; error::ParseError,
use crate::request::Request; header::HeaderMap,
message::{ConnectionType, ResponseHead},
request::Request,
};
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072; pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
const MAX_HEADERS: usize = 96; const MAX_HEADERS: usize = 96;
@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized {
let mut has_upgrade_websocket = false; let mut has_upgrade_websocket = false;
let mut expect = false; let mut expect = false;
let mut chunked = false; let mut chunked = false;
let mut seen_te = false;
let mut content_length = None; let mut content_length = None;
{ {
@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized {
}; };
match name { match name {
header::CONTENT_LENGTH => { header::CONTENT_LENGTH if content_length.is_some() => {
if let Ok(s) = value.to_str() { debug!("multiple Content-Length");
return Err(ParseError::Header);
}
header::CONTENT_LENGTH => match value.to_str() {
Ok(s) if s.trim().starts_with('+') => {
debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header);
}
Ok(s) => {
if let Ok(len) = s.parse::<u64>() { if let Ok(len) = s.parse::<u64>() {
if len != 0 { if len != 0 {
content_length = Some(len); content_length = Some(len);
@ -95,15 +105,31 @@ pub(crate) trait MessageType: Sized {
debug!("illegal Content-Length: {:?}", s); debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} else { }
Err(_) => {
debug!("illegal Content-Length: {:?}", value); debug!("illegal Content-Length: {:?}", value);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} },
// transfer-encoding // transfer-encoding
header::TRANSFER_ENCODING if seen_te => {
debug!("multiple Transfer-Encoding not allowed");
return Err(ParseError::Header);
}
header::TRANSFER_ENCODING => { header::TRANSFER_ENCODING => {
seen_te = true;
if let Ok(s) = value.to_str().map(str::trim) { if let Ok(s) = value.to_str().map(str::trim) {
chunked = s.eq_ignore_ascii_case("chunked"); if s.eq_ignore_ascii_case("chunked") {
chunked = true;
} else if s.eq_ignore_ascii_case("identity") {
// allow silently since multiple TE headers are already checked
} else {
debug!("illegal Transfer-Encoding: {:?}", s);
return Err(ParseError::Header);
}
} else { } else {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
@ -186,10 +212,17 @@ impl MessageType for Request {
let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY; let mut headers: [HeaderIndex; MAX_HEADERS] = EMPTY_HEADER_INDEX_ARRAY;
let (len, method, uri, ver, h_len) = { let (len, method, uri, ver, h_len) = {
let mut parsed: [httparse::Header<'_>; MAX_HEADERS] = EMPTY_HEADER_ARRAY; // SAFETY:
// Create an uninitialized array of `MaybeUninit`. The `assume_init` is
// safe because the type we are claiming to have initialized here is a
// bunch of `MaybeUninit`s, which do not require initialization.
let mut parsed = unsafe {
MaybeUninit::<[MaybeUninit<httparse::Header<'_>>; MAX_HEADERS]>::uninit()
.assume_init()
};
let mut req = httparse::Request::new(&mut parsed); let mut req = httparse::Request::new(&mut []);
match req.parse(src)? { match req.parse_with_uninit_headers(src, &mut parsed)? {
httparse::Status::Complete(len) => { httparse::Status::Complete(len) => {
let method = Method::from_bytes(req.method.unwrap().as_bytes()) let method = Method::from_bytes(req.method.unwrap().as_bytes())
.map_err(|_| ParseError::Method)?; .map_err(|_| ParseError::Method)?;
@ -408,20 +441,6 @@ enum Kind {
Eof, Eof,
} }
#[derive(Debug, PartialEq, Clone)]
enum ChunkedState {
Size,
SizeLws,
Extension,
SizeLf,
Body,
BodyCr,
BodyLf,
EndCr,
EndLf,
End,
}
impl Decoder for PayloadDecoder { impl Decoder for PayloadDecoder {
type Item = PayloadItem; type Item = PayloadItem;
type Error = io::Error; type Error = io::Error;
@ -451,19 +470,23 @@ impl Decoder for PayloadDecoder {
Kind::Chunked(ref mut state, ref mut size) => { Kind::Chunked(ref mut state, ref mut size) => {
loop { loop {
let mut buf = None; let mut buf = None;
// advances the chunked state // advances the chunked state
*state = match state.step(src, size, &mut buf) { *state = match state.step(src, size, &mut buf) {
Poll::Pending => return Ok(None), Poll::Pending => return Ok(None),
Poll::Ready(Ok(state)) => state, Poll::Ready(Ok(state)) => state,
Poll::Ready(Err(e)) => return Err(e), Poll::Ready(Err(e)) => return Err(e),
}; };
if *state == ChunkedState::End { if *state == ChunkedState::End {
trace!("End of chunked stream"); trace!("End of chunked stream");
return Ok(Some(PayloadItem::Eof)); return Ok(Some(PayloadItem::Eof));
} }
if let Some(buf) = buf { if let Some(buf) = buf {
return Ok(Some(PayloadItem::Chunk(buf))); return Ok(Some(PayloadItem::Chunk(buf)));
} }
if src.is_empty() { if src.is_empty() {
return Ok(None); return Ok(None);
} }
@ -480,201 +503,40 @@ impl Decoder for PayloadDecoder {
} }
} }
macro_rules! byte (
($rdr:ident) => ({
if $rdr.len() > 0 {
let b = $rdr[0];
$rdr.advance(1);
b
} else {
return Poll::Pending
}
})
);
impl ChunkedState {
fn step(
&self,
body: &mut BytesMut,
size: &mut u64,
buf: &mut Option<Bytes>,
) -> Poll<Result<ChunkedState, io::Error>> {
use self::ChunkedState::*;
match *self {
Size => ChunkedState::read_size(body, size),
SizeLws => ChunkedState::read_size_lws(body),
Extension => ChunkedState::read_extension(body),
SizeLf => ChunkedState::read_size_lf(body, size),
Body => ChunkedState::read_body(body, size, buf),
BodyCr => ChunkedState::read_body_cr(body),
BodyLf => ChunkedState::read_body_lf(body),
EndCr => ChunkedState::read_end_cr(body),
EndLf => ChunkedState::read_end_lf(body),
End => Poll::Ready(Ok(ChunkedState::End)),
}
}
fn read_size(
rdr: &mut BytesMut,
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
let radix = 16;
match byte!(rdr) {
b @ b'0'..=b'9' => {
*size *= radix;
*size += u64::from(b - b'0');
}
b @ b'a'..=b'f' => {
*size *= radix;
*size += u64::from(b + 10 - b'a');
}
b @ b'A'..=b'F' => {
*size *= radix;
*size += u64::from(b + 10 - b'A');
}
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => {
return Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size line: Invalid Size",
)));
}
}
Poll::Ready(Ok(ChunkedState::Size))
}
fn read_size_lws(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
trace!("read_size_lws");
match byte!(rdr) {
// LWS can follow the chunk size, but no more digits can come
b'\t' | b' ' => Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size linear white space",
))),
}
}
fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
}
}
fn read_size_lf(
rdr: &mut BytesMut,
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\n' if *size > 0 => Poll::Ready(Ok(ChunkedState::Body)),
b'\n' if *size == 0 => Poll::Ready(Ok(ChunkedState::EndCr)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size LF",
))),
}
}
fn read_body(
rdr: &mut BytesMut,
rem: &mut u64,
buf: &mut Option<Bytes>,
) -> Poll<Result<ChunkedState, io::Error>> {
trace!("Chunked read, remaining={:?}", rem);
let len = rdr.len() as u64;
if len == 0 {
Poll::Ready(Ok(ChunkedState::Body))
} else {
let slice;
if *rem > len {
slice = rdr.split().freeze();
*rem -= len;
} else {
slice = rdr.split_to(*rem as usize).freeze();
*rem = 0;
}
*buf = Some(slice);
if *rem > 0 {
Poll::Ready(Ok(ChunkedState::Body))
} else {
Poll::Ready(Ok(ChunkedState::BodyCr))
}
}
}
fn read_body_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::BodyLf)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk body CR",
))),
}
}
fn read_body_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\n' => Poll::Ready(Ok(ChunkedState::Size)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk body LF",
))),
}
}
fn read_end_cr(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::EndLf)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk end CR",
))),
}
}
fn read_end_lf(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\n' => Poll::Ready(Ok(ChunkedState::End)),
_ => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk end LF",
))),
}
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::{Method, Version}; use http::{Method, Version};
use super::*; use super::*;
use crate::error::ParseError; use crate::{
use crate::http::header::{HeaderName, SET_COOKIE}; error::ParseError,
use crate::HttpMessage; http::header::{HeaderName, SET_COOKIE},
HttpMessage as _,
};
impl PayloadType { impl PayloadType {
fn unwrap(self) -> PayloadDecoder { pub(crate) fn unwrap(self) -> PayloadDecoder {
match self { match self {
PayloadType::Payload(pl) => pl, PayloadType::Payload(pl) => pl,
_ => panic!(), _ => panic!(),
} }
} }
fn is_unhandled(&self) -> bool { pub(crate) fn is_unhandled(&self) -> bool {
matches!(self, PayloadType::Stream(_)) matches!(self, PayloadType::Stream(_))
} }
} }
impl PayloadItem { impl PayloadItem {
fn chunk(self) -> Bytes { pub(crate) fn chunk(self) -> Bytes {
match self { match self {
PayloadItem::Chunk(chunk) => chunk, PayloadItem::Chunk(chunk) => chunk,
_ => panic!("error"), _ => panic!("error"),
} }
} }
fn eof(&self) -> bool {
pub(crate) fn eof(&self) -> bool {
matches!(*self, PayloadItem::Eof) matches!(*self, PayloadItem::Eof)
} }
} }
@ -967,34 +829,6 @@ mod tests {
assert!(req.upgrade()); assert!(req.upgrade());
} }
#[test]
fn test_request_chunked() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let req = parse_ready!(&mut buf);
if let Ok(val) = req.chunked() {
assert!(val);
} else {
unreachable!("Error");
}
// intentional typo in "chunked"
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chnked\r\n\r\n",
);
let req = parse_ready!(&mut buf);
if let Ok(val) = req.chunked() {
assert!(!val);
} else {
unreachable!("Error");
}
}
#[test] #[test]
fn test_headers_content_length_err_1() { fn test_headers_content_length_err_1() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
@ -1112,126 +946,6 @@ mod tests {
expect_parse_err!(&mut buf); expect_parse_err!(&mut buf);
} }
#[test]
fn test_http_request_chunked_payload() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(req.chunked().unwrap());
buf.extend(b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n");
assert_eq!(
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
b"data"
);
assert_eq!(
pl.decode(&mut buf).unwrap().unwrap().chunk().as_ref(),
b"line"
);
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
}
#[test]
fn test_http_request_chunked_payload_and_next_message() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(req.chunked().unwrap());
buf.extend(
b"4\r\ndata\r\n4\r\nline\r\n0\r\n\r\n\
POST /test2 HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n"
.iter(),
);
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"data");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"line");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert!(msg.eof());
let (req, _) = reader.decode(&mut buf).unwrap().unwrap();
assert!(req.chunked().unwrap());
assert_eq!(*req.method(), Method::POST);
assert!(req.chunked().unwrap());
}
#[test]
fn test_http_request_chunked_payload_chunks() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (req, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(req.chunked().unwrap());
buf.extend(b"4\r\n1111\r\n");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"1111");
buf.extend(b"4\r\ndata\r");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"data");
buf.extend(b"\n4");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"\r");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"\n");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"li");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"li");
//trailers
//buf.feed_data("test: test\r\n");
//not_ready!(reader.parse(&mut buf, &mut readbuf));
buf.extend(b"ne\r\n0\r\n");
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(msg.chunk().as_ref(), b"ne");
assert!(pl.decode(&mut buf).unwrap().is_none());
buf.extend(b"\r\n");
assert!(pl.decode(&mut buf).unwrap().unwrap().eof());
}
#[test]
fn test_parse_chunked_payload_chunk_extension() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\
\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
assert!(msg.chunked().unwrap());
buf.extend(b"4;test\r\ndata\r\n4\r\nline\r\n0\r\n\r\n"); // test: test\r\n\r\n")
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
assert_eq!(chunk, Bytes::from_static(b"data"));
let chunk = pl.decode(&mut buf).unwrap().unwrap().chunk();
assert_eq!(chunk, Bytes::from_static(b"line"));
let msg = pl.decode(&mut buf).unwrap().unwrap();
assert!(msg.eof());
}
#[test] #[test]
fn test_response_http10_read_until_eof() { fn test_response_http10_read_until_eof() {
let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data"); let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data");
@ -1243,4 +957,84 @@ mod tests {
let chunk = pl.decode(&mut buf).unwrap().unwrap(); let chunk = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data"))); assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"test data")));
} }
#[test]
fn hrs_multiple_content_length() {
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 4\r\n\
Content-Length: 2\r\n\
\r\n\
abcd",
);
expect_parse_err!(&mut buf);
}
#[test]
fn hrs_content_length_plus() {
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: +3\r\n\
\r\n\
000",
);
expect_parse_err!(&mut buf);
}
#[test]
fn hrs_unknown_transfer_encoding() {
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Transfer-Encoding: JUNK\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
5\r\n\
hello\r\n\
0",
);
expect_parse_err!(&mut buf);
}
#[test]
fn hrs_multiple_transfer_encoding() {
let mut buf = BytesMut::from(
"GET / HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 51\r\n\
Transfer-Encoding: identity\r\n\
Transfer-Encoding: chunked\r\n\
\r\n\
0\r\n\
\r\n\
GET /forbidden HTTP/1.1\r\n\
Host: example.com\r\n\r\n",
);
expect_parse_err!(&mut buf);
}
#[test]
fn transfer_encoding_agrees() {
let mut buf = BytesMut::from(
"GET /test HTTP/1.1\r\n\
Host: example.com\r\n\
Content-Length: 3\r\n\
Transfer-Encoding: identity\r\n\
\r\n\
0\r\n",
);
let mut reader = MessageDecoder::<Request>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();
let mut pl = pl.unwrap();
let chunk = pl.decode(&mut buf).unwrap().unwrap();
assert_eq!(chunk, PayloadItem::Chunk(Bytes::from_static(b"0\r\n")));
}
} }

View File

@ -1060,7 +1060,7 @@ mod tests {
fn stabilize_date_header(payload: &mut [u8]) { fn stabilize_date_header(payload: &mut [u8]) {
let mut from = 0; let mut from = 0;
while let Some(pos) = find_slice(&payload, b"date", from) { while let Some(pos) = find_slice(payload, b"date", from) {
payload[(from + pos)..(from + pos + 35)] payload[(from + pos)..(from + pos + 35)]
.copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC"); .copy_from_slice(b"date: Thu, 01 Jan 1970 12:34:56 UTC");
from += 35; from += 35;

View File

@ -81,6 +81,7 @@ pub(crate) trait MessageType: Sized {
match length { match length {
BodySize::Stream => { BodySize::Stream => {
if chunked { if chunked {
skip_len = true;
if camel_case { if camel_case {
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
} else { } else {
@ -174,7 +175,7 @@ pub(crate) trait MessageType: Sized {
unsafe { unsafe {
if camel_case { if camel_case {
// use Camel-Case headers // use Camel-Case headers
write_camel_case(k, from_raw_parts_mut(buf, k_len)); write_camel_case(k, buf, k_len);
} else { } else {
write_data(k, buf, k_len); write_data(k, buf, k_len);
} }
@ -472,15 +473,22 @@ impl TransferEncoding {
} }
/// # Safety /// # Safety
/// Callers must ensure that the given length matches given value length. /// Callers must ensure that the given `len` matches the given `value` length and that `buf` is
/// valid for writes of at least `len` bytes.
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
debug_assert_eq!(value.len(), len); debug_assert_eq!(value.len(), len);
copy_nonoverlapping(value.as_ptr(), buf, len); copy_nonoverlapping(value.as_ptr(), buf, len);
} }
fn write_camel_case(value: &[u8], buffer: &mut [u8]) { /// # Safety
/// Callers must ensure that the given `len` matches the given `value` length and that `buf` is
/// valid for writes of at least `len` bytes.
unsafe fn write_camel_case(value: &[u8], buf: *mut u8, len: usize) {
// first copy entire (potentially wrong) slice to output // first copy entire (potentially wrong) slice to output
buffer[..value.len()].copy_from_slice(value); write_data(value, buf, len);
// SAFETY: We just initialized the buffer with `value`
let buffer = from_raw_parts_mut(buf, len);
let mut iter = value.iter(); let mut iter = value.iter();

View File

@ -1,6 +1,8 @@
//! HTTP/1 protocol implementation. //! HTTP/1 protocol implementation.
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
mod chunked;
mod client; mod client;
mod codec; mod codec;
mod decoder; mod decoder;

View File

@ -63,7 +63,6 @@ where
.is_write_buf_full() .is_write_buf_full()
{ {
let next = let next =
// TODO: MSRV 1.51: poll_map_err
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) { match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)), Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {

View File

@ -684,7 +684,7 @@ impl<'a> Iterator for Iter<'a> {
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
// handle in-progress multi value lists first // handle in-progress multi value lists first
if let Some((ref name, ref mut vals)) = self.multi_inner { if let Some((name, ref mut vals)) = self.multi_inner {
match vals.get(self.multi_idx) { match vals.get(self.multi_idx) {
Some(val) => { Some(val) => {
self.multi_idx += 1; self.multi_idx += 1;

View File

@ -1,5 +1,6 @@
use std::{convert::Infallible, str::FromStr}; use std::{convert::TryFrom, str::FromStr};
use derive_more::{Display, Error};
use http::header::InvalidHeaderValue; use http::header::InvalidHeaderValue;
use crate::{ use crate::{
@ -8,8 +9,16 @@ use crate::{
HttpMessage, HttpMessage,
}; };
/// Error return when a content encoding is unknown.
///
/// Example: 'compress'
#[derive(Debug, Display, Error)]
#[display(fmt = "unsupported content encoding")]
pub struct ContentEncodingParseError;
/// Represents a supported content encoding. /// Represents a supported content encoding.
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ContentEncoding { pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation. /// Automatically select encoding based on encoding negotiation.
Auto, Auto,
@ -37,7 +46,7 @@ impl ContentEncoding {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto) matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
} }
/// Convert content encoding to string /// Convert content encoding to string.
#[inline] #[inline]
pub fn as_str(self) -> &'static str { pub fn as_str(self) -> &'static str {
match self { match self {
@ -48,18 +57,6 @@ impl ContentEncoding {
ContentEncoding::Identity | ContentEncoding::Auto => "identity", ContentEncoding::Identity | ContentEncoding::Auto => "identity",
} }
} }
/// Default Q-factor (quality) value.
#[inline]
pub fn quality(self) -> f64 {
match self {
ContentEncoding::Br => 1.1,
ContentEncoding::Gzip => 1.0,
ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
ContentEncoding::Zstd => 0.0,
}
}
} }
impl Default for ContentEncoding { impl Default for ContentEncoding {
@ -69,31 +66,33 @@ impl Default for ContentEncoding {
} }
impl FromStr for ContentEncoding { impl FromStr for ContentEncoding {
type Err = Infallible; type Err = ContentEncodingParseError;
fn from_str(val: &str) -> Result<Self, Self::Err> { fn from_str(val: &str) -> Result<Self, Self::Err> {
Ok(Self::from(val))
}
}
impl From<&str> for ContentEncoding {
fn from(val: &str) -> ContentEncoding {
let val = val.trim(); let val = val.trim();
if val.eq_ignore_ascii_case("br") { if val.eq_ignore_ascii_case("br") {
ContentEncoding::Br Ok(ContentEncoding::Br)
} else if val.eq_ignore_ascii_case("gzip") { } else if val.eq_ignore_ascii_case("gzip") {
ContentEncoding::Gzip Ok(ContentEncoding::Gzip)
} else if val.eq_ignore_ascii_case("deflate") { } else if val.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate Ok(ContentEncoding::Deflate)
} else if val.eq_ignore_ascii_case("zstd") { } else if val.eq_ignore_ascii_case("zstd") {
ContentEncoding::Zstd Ok(ContentEncoding::Zstd)
} else { } else {
ContentEncoding::default() Err(ContentEncodingParseError)
} }
} }
} }
impl TryFrom<&str> for ContentEncoding {
type Error = ContentEncodingParseError;
fn try_from(val: &str) -> Result<Self, Self::Error> {
val.parse()
}
}
impl IntoHeaderValue for ContentEncoding { impl IntoHeaderValue for ContentEncoding {
type Error = InvalidHeaderValue; type Error = InvalidHeaderValue;

View File

@ -0,0 +1,82 @@
use std::{fmt, io::Write, str::FromStr, time::SystemTime};
use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue};
use crate::{
config::DATE_VALUE_LENGTH, error::ParseError, header::IntoHeaderValue,
helpers::MutWriter,
};
/// A timestamp with HTTP formatting and parsing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(SystemTime);
impl FromStr for HttpDate {
type Err = ParseError;
fn from_str(s: &str) -> Result<HttpDate, ParseError> {
match httpdate::parse_http_date(s) {
Ok(sys_time) => Ok(HttpDate(sys_time)),
Err(_) => Err(ParseError::Header),
}
}
}
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let date_str = httpdate::fmt_http_date(self.0);
f.write_str(&date_str)
}
}
impl IntoHeaderValue for HttpDate {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut buf = BytesMut::with_capacity(DATE_VALUE_LENGTH);
let mut wrt = MutWriter(&mut buf);
// unwrap: date output is known to be well formed and of known length
write!(wrt, "{}", httpdate::fmt_http_date(self.0)).unwrap();
HeaderValue::from_maybe_shared(buf.split().freeze())
}
}
impl From<SystemTime> for HttpDate {
fn from(sys_time: SystemTime) -> HttpDate {
HttpDate(sys_time)
}
}
impl From<HttpDate> for SystemTime {
fn from(HttpDate(sys_time): HttpDate) -> SystemTime {
sys_time
}
}
#[cfg(test)]
mod tests {
use std::time::Duration;
use super::*;
#[test]
fn date_header() {
macro_rules! assert_parsed_date {
($case:expr, $exp:expr) => {
assert_eq!($case.parse::<HttpDate>().unwrap(), $exp);
};
}
// 784198117 = SystemTime::from(datetime!(1994-11-07 08:48:37).assume_utc()).duration_since(SystemTime::UNIX_EPOCH));
let nov_07 = HttpDate(SystemTime::UNIX_EPOCH + Duration::from_secs(784198117));
assert_parsed_date!("Mon, 07 Nov 1994 08:48:37 GMT", nov_07);
assert_parsed_date!("Monday, 07-Nov-94 08:48:37 GMT", nov_07);
assert_parsed_date!("Mon Nov 7 08:48:37 1994", nov_07);
assert!("this-is-no-date".parse::<HttpDate>().is_err());
}
}

View File

@ -1,97 +0,0 @@
use std::{
fmt,
io::Write,
str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use bytes::buf::BufMut;
use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue};
use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
use crate::error::ParseError;
use crate::header::IntoHeaderValue;
use crate::time_parser;
/// A timestamp with HTTP formatting and parsing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(OffsetDateTime);
impl FromStr for HttpDate {
type Err = ParseError;
fn from_str(s: &str) -> Result<HttpDate, ParseError> {
match time_parser::parse_http_date(s) {
Some(t) => Ok(HttpDate(t.assume_utc())),
None => Err(ParseError::Header),
}
}
}
impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f)
}
}
impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate {
HttpDate(PrimitiveDateTime::from(sys).assume_utc())
}
}
impl IntoHeaderValue for HttpDate {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = BytesMut::with_capacity(29).writer();
write!(
wrt,
"{}",
self.0
.to_offset(UtcOffset::UTC)
.format("%a, %d %b %Y %H:%M:%S GMT")
)
.unwrap();
HeaderValue::from_maybe_shared(wrt.get_mut().split().freeze())
}
}
impl From<HttpDate> for SystemTime {
fn from(date: HttpDate) -> SystemTime {
let dt = date.0;
let epoch = OffsetDateTime::unix_epoch();
UNIX_EPOCH + (dt - epoch)
}
}
#[cfg(test)]
mod tests {
use super::HttpDate;
use time::{date, time, PrimitiveDateTime};
#[test]
fn test_date() {
let nov_07 = HttpDate(
PrimitiveDateTime::new(date!(1994 - 11 - 07), time!(8:48:37)).assume_utc(),
);
assert_eq!(
"Sun, 07 Nov 1994 08:48:37 GMT".parse::<HttpDate>().unwrap(),
nov_07
);
assert_eq!(
"Sunday, 07-Nov-94 08:48:37 GMT"
.parse::<HttpDate>()
.unwrap(),
nov_07
);
assert_eq!(
"Sun Nov 7 08:48:37 1994".parse::<HttpDate>().unwrap(),
nov_07
);
assert!("this-is-no-date".parse::<HttpDate>().is_err());
}
}

View File

@ -3,12 +3,12 @@
mod charset; mod charset;
mod content_encoding; mod content_encoding;
mod extended; mod extended;
mod httpdate; mod http_date;
mod quality_item; mod quality_item;
pub use self::charset::Charset; pub use self::charset::Charset;
pub use self::content_encoding::ContentEncoding; pub use self::content_encoding::ContentEncoding;
pub use self::extended::{parse_extended_value, ExtendedValue}; pub use self::extended::{parse_extended_value, ExtendedValue};
pub use self::httpdate::HttpDate; pub use self::http_date::HttpDate;
pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use self::quality_item::{q, qitem, Quality, QualityItem};
pub use language_tags::LanguageTag; pub use language_tags::LanguageTag;

View File

@ -1,11 +1,14 @@
use std::{ use std::{
cmp, cmp,
convert::{TryFrom, TryInto}, convert::{TryFrom, TryInto},
fmt, str, fmt,
str::{self, FromStr},
}; };
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use crate::error::ParseError;
const MAX_QUALITY: u16 = 1000; const MAX_QUALITY: u16 = 1000;
const MAX_FLOAT_QUALITY: f32 = 1.0; const MAX_FLOAT_QUALITY: f32 = 1.0;
@ -113,12 +116,12 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
} }
} }
impl<T: str::FromStr> str::FromStr for QualityItem<T> { impl<T: FromStr> FromStr for QualityItem<T> {
type Err = crate::error::ParseError; type Err = ParseError;
fn from_str(qitem_str: &str) -> Result<QualityItem<T>, crate::error::ParseError> { fn from_str(qitem_str: &str) -> Result<Self, Self::Err> {
if !qitem_str.is_ascii() { if !qitem_str.is_ascii() {
return Err(crate::error::ParseError::Header); return Err(ParseError::Header);
} }
// Set defaults used if parsing fails. // Set defaults used if parsing fails.
@ -139,7 +142,7 @@ impl<T: str::FromStr> str::FromStr for QualityItem<T> {
if parts[0].len() < 2 { if parts[0].len() < 2 {
// Can't possibly be an attribute since an attribute needs at least a name followed // Can't possibly be an attribute since an attribute needs at least a name followed
// by an equals sign. And bare identifiers are forbidden. // by an equals sign. And bare identifiers are forbidden.
return Err(crate::error::ParseError::Header); return Err(ParseError::Header);
} }
let start = &parts[0][0..2]; let start = &parts[0][0..2];
@ -148,25 +151,21 @@ impl<T: str::FromStr> str::FromStr for QualityItem<T> {
let q_val = &parts[0][2..]; let q_val = &parts[0][2..];
if q_val.len() > 5 { if q_val.len() > 5 {
// longer than 5 indicates an over-precise q-factor // longer than 5 indicates an over-precise q-factor
return Err(crate::error::ParseError::Header); return Err(ParseError::Header);
} }
let q_value = q_val let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
.parse::<f32>()
.map_err(|_| crate::error::ParseError::Header)?;
if (0f32..=1f32).contains(&q_value) { if (0f32..=1f32).contains(&q_value) {
quality = q_value; quality = q_value;
raw_item = parts[1]; raw_item = parts[1];
} else { } else {
return Err(crate::error::ParseError::Header); return Err(ParseError::Header);
} }
} }
} }
let item = raw_item let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
.parse::<T>()
.map_err(|_| crate::error::ParseError::Header)?;
// we already checked above that the quality is within range // we already checked above that the quality is within range
Ok(QualityItem::new(item, Quality::from_f32(quality))) Ok(QualityItem::new(item, Quality::from_f32(quality)))
@ -224,7 +223,7 @@ mod tests {
} }
} }
impl str::FromStr for Encoding { impl FromStr for Encoding {
type Err = crate::error::ParseError; type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> { fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
use Encoding::*; use Encoding::*;

View File

@ -14,7 +14,7 @@
//! [rustls]: https://crates.io/crates/rustls //! [rustls]: https://crates.io/crates/rustls
//! [trust-dns]: https://crates.io/crates/trust-dns //! [trust-dns]: https://crates.io/crates/trust-dns
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style, clippy::uninit_assumed_init)]
#![allow( #![allow(
clippy::type_complexity, clippy::type_complexity,
clippy::too_many_arguments, clippy::too_many_arguments,
@ -44,7 +44,6 @@ mod request;
mod response; mod response;
mod response_builder; mod response_builder;
mod service; mod service;
mod time_parser;
pub mod error; pub mod error;
pub mod h1; pub mod h1;

View File

@ -209,7 +209,7 @@ impl RequestHeadType {
impl AsRef<RequestHead> for RequestHeadType { impl AsRef<RequestHead> for RequestHeadType {
fn as_ref(&self) -> &RequestHead { fn as_ref(&self) -> &RequestHead {
match self { match self {
RequestHeadType::Owned(head) => &head, RequestHeadType::Owned(head) => head,
RequestHeadType::Rc(head, _) => head.as_ref(), RequestHeadType::Rc(head, _) => head.as_ref(),
} }
} }
@ -363,7 +363,7 @@ impl<T: Head> std::ops::Deref for Message<T> {
type Target = T; type Target = T;
fn deref(&self) -> &Self::Target { fn deref(&self) -> &Self::Target {
&self.head.as_ref() self.head.as_ref()
} }
} }

View File

@ -1,72 +0,0 @@
use time::{Date, OffsetDateTime, PrimitiveDateTime};
/// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime.
pub(crate) fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> {
try_parse_rfc_1123(time)
.or_else(|| try_parse_rfc_850(time))
.or_else(|| try_parse_asctime(time))
}
/// Attempt to parse a `time` string as a RFC 1123 formatted date time string.
///
/// Eg: `Fri, 12 Feb 2021 00:14:29 GMT`
fn try_parse_rfc_1123(time: &str) -> Option<PrimitiveDateTime> {
time::parse(time, "%a, %d %b %Y %H:%M:%S").ok()
}
/// Attempt to parse a `time` string as a RFC 850 formatted date time string.
///
/// Eg: `Wednesday, 11-Jan-21 13:37:41 UTC`
fn try_parse_rfc_850(time: &str) -> Option<PrimitiveDateTime> {
let dt = PrimitiveDateTime::parse(time, "%A, %d-%b-%y %H:%M:%S").ok()?;
// If the `time` string contains a two-digit year, then as per RFC 2616 § 19.3,
// we consider the year as part of this century if it's within the next 50 years,
// otherwise we consider as part of the previous century.
let now = OffsetDateTime::now_utc();
let century_start_year = (now.year() / 100) * 100;
let mut expanded_year = century_start_year + dt.year();
if expanded_year > now.year() + 50 {
expanded_year -= 100;
}
let date = Date::try_from_ymd(expanded_year, dt.month(), dt.day()).ok()?;
Some(PrimitiveDateTime::new(date, dt.time()))
}
/// Attempt to parse a `time` string using ANSI C's `asctime` format.
///
/// Eg: `Wed Feb 13 15:46:11 2013`
fn try_parse_asctime(time: &str) -> Option<PrimitiveDateTime> {
time::parse(time, "%a %b %_d %H:%M:%S %Y").ok()
}
#[cfg(test)]
mod tests {
use time::{date, time};
use super::*;
#[test]
fn test_rfc_850_year_shift() {
let date = try_parse_rfc_850("Friday, 19-Nov-82 16:14:55 EST").unwrap();
assert_eq!(date, date!(1982 - 11 - 19).with_time(time!(16:14:55)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-62 13:37:41 EST").unwrap();
assert_eq!(date, date!(2062 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-21 13:37:41 EST").unwrap();
assert_eq!(date, date!(2021 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-23 13:37:41 EST").unwrap();
assert_eq!(date, date!(2023 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-99 13:37:41 EST").unwrap();
assert_eq!(date, date!(1999 - 01 - 11).with_time(time!(13:37:41)));
let date = try_parse_rfc_850("Wednesday, 11-Jan-00 13:37:41 EST").unwrap();
assert_eq!(date, date!(2000 - 01 - 11).with_time(time!(13:37:41)));
}
}

View File

@ -183,6 +183,7 @@ async fn test_chunked_payload() {
Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(), Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(),
None => panic!("Failed to find size in HTTP Response: {}", data), None => panic!("Failed to find size in HTTP Response: {}", data),
}; };
size size
}; };

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.6 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51.
## 0.4.0-beta.5 - 2021-06-17 ## 0.4.0-beta.5 - 2021-06-17
* No notable changes. * No notable changes.

View File

@ -1,13 +1,11 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.5" version = "0.4.0-beta.6"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-multipart"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
@ -16,7 +14,7 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.8", default-features = false } actix-web = { version = "4.0.0-beta.9", default-features = false }
actix-utils = "3.0.0" actix-utils = "3.0.0"
bytes = "1" bytes = "1"
@ -31,6 +29,6 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -3,15 +3,15 @@
> Multipart form support for Actix Web. > Multipart form support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart) [![crates.io](https://img.shields.io/crates/v/actix-multipart?label=latest)](https://crates.io/crates/actix-multipart)
[![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.6)](https://docs.rs/actix-multipart/0.4.0-beta.6)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-multipart.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.6/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.6)
[![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart) [![Download](https://img.shields.io/crates/d/actix-multipart.svg)](https://crates.io/crates/actix-multipart)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-multipart) - [API Documentation](https://docs.rs/actix-multipart)
- Minimum Supported Rust Version (MSRV): 1.46.0 - Minimum Supported Rust Version (MSRV): 1.51.0

View File

@ -33,7 +33,6 @@ use crate::server::Multipart;
impl FromRequest for Multipart { impl FromRequest for Multipart {
type Error = Error; type Error = Error;
type Future = Ready<Result<Multipart, Error>>; type Future = Ready<Result<Multipart, Error>>;
type Config = ();
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {

View File

@ -1,15 +1,26 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-beta.2 - 2021-09-09
* Introduce `ResourceDef::join`. [#380] * Introduce `ResourceDef::join`. [#380]
* Disallow prefix routes with tail segments. [#379] * Disallow prefix routes with tail segments. [#379]
* Enforce path separators on dynamic prefixes. [#378] * Enforce path separators on dynamic prefixes. [#378]
* Improve malformed path error message. [#384] * Improve malformed path error message. [#384]
* Prefix segments now always end with with a segment delimiter or end-of-input. [#2355]
* Prefix segments with trailing slashes define a trailing empty segment. [#2355]
* Support multi-pattern prefixes and joins. [#2356]
* `ResourceDef::pattern` now returns the first pattern in multi-pattern resources. [#2356]
* Support `build_resource_path` on multi-pattern resources. [#2356]
* Minimum supported Rust version (MSRV) is now 1.51.
[#378]: https://github.com/actix/actix-net/pull/378 [#378]: https://github.com/actix/actix-net/pull/378
[#379]: https://github.com/actix/actix-net/pull/379 [#379]: https://github.com/actix/actix-net/pull/379
[#380]: https://github.com/actix/actix-net/pull/380 [#380]: https://github.com/actix/actix-net/pull/380
[#384]: https://github.com/actix/actix-net/pull/384 [#384]: https://github.com/actix/actix-net/pull/384
[#2355]: https://github.com/actix/actix-web/pull/2355
[#2356]: https://github.com/actix/actix-web/pull/2356
## 0.5.0-beta.1 - 2021-07-20 ## 0.5.0-beta.1 - 2021-07-20

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-router" name = "actix-router"
version = "0.5.0-beta.1" version = "0.5.0-beta.2"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>", "Ali MJ Al-Nasrawy <alimjalnasrawy@gmail.com>",
@ -8,7 +8,7 @@ authors = [
] ]
description = "Resource path matching and router" description = "Resource path matching and router"
keywords = ["actix", "router", "routing"] keywords = ["actix", "router", "routing"]
repository = "https://github.com/actix/actix-net.git" repository = "https://github.com/actix/actix-web.git"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"

View File

@ -125,7 +125,7 @@ impl<T: ResourcePath> Path<T> {
for (seg_name, val) in self.segments.iter() { for (seg_name, val) in self.segments.iter() {
if name == seg_name { if name == seg_name {
return match val { return match val {
PathItem::Static(ref s) => Some(&s), PathItem::Static(ref s) => Some(s),
PathItem::Segment(s, e) => { PathItem::Segment(s, e) => {
Some(&self.path.path()[(*s as usize)..(*e as usize)]) Some(&self.path.path()[(*s as usize)..(*e as usize)])
} }
@ -183,7 +183,7 @@ impl<'a, T: ResourcePath> Iterator for PathIter<'a, T> {
if self.idx < self.params.segment_count() { if self.idx < self.params.segment_count() {
let idx = self.idx; let idx = self.idx;
let res = match self.params.segments[idx].1 { let res = match self.params.segments[idx].1 {
PathItem::Static(ref s) => &s, PathItem::Static(ref s) => s,
PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)], PathItem::Segment(s, e) => &self.params.path.path()[(s as usize)..(e as usize)],
}; };
self.idx += 1; self.idx += 1;
@ -207,7 +207,7 @@ impl<T: ResourcePath> Index<usize> for Path<T> {
fn index(&self, idx: usize) -> &str { fn index(&self, idx: usize) -> &str {
match self.segments[idx].1 { match self.segments[idx].1 {
PathItem::Static(ref s) => &s, PathItem::Static(ref s) => s,
PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)], PathItem::Segment(s, e) => &self.path.path()[(s as usize)..(e as usize)],
} }
} }

View File

@ -28,9 +28,27 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// regex engine. /// regex engine.
/// ///
/// ///
/// # Pattern Format and Matching Behavior
///
/// Resource pattern is defined as a string of zero or more _segments_ where each segment is
/// preceded by a slash `/`.
///
/// This means that pattern string __must__ either be empty or begin with a slash (`/`).
/// This also implies that a trailing slash in pattern defines an empty segment.
/// For example, the pattern `"/user/"` has two segments: `["user", ""]`
///
/// A key point to underhand is that `ResourceDef` matches segments, not strings.
/// It matches segments individually.
/// For example, the pattern `/user/` is not considered a prefix for the path `/user/123/456`,
/// because the second segment doesn't match: `["user", ""]` vs `["user", "123", "456"]`.
///
/// This definition is consistent with the definition of absolute URL path in
/// [RFC 3986 (section 3.3)](https://datatracker.ietf.org/doc/html/rfc3986#section-3.3)
///
///
/// # Static Resources /// # Static Resources
/// A static resource is the most basic type of definition. Pass a regular string to /// A static resource is the most basic type of definition. Pass a pattern to
/// [new][Self::new]. Conforming paths must match the string exactly. /// [new][Self::new]. Conforming paths must match the pattern exactly.
/// ///
/// ## Examples /// ## Examples
/// ``` /// ```
@ -39,6 +57,7 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// ///
/// assert!(resource.is_match("/home")); /// assert!(resource.is_match("/home"));
/// ///
/// assert!(!resource.is_match("/home/"));
/// assert!(!resource.is_match("/home/new")); /// assert!(!resource.is_match("/home/new"));
/// assert!(!resource.is_match("/homes")); /// assert!(!resource.is_match("/homes"));
/// assert!(!resource.is_match("/search")); /// assert!(!resource.is_match("/search"));
@ -85,12 +104,13 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// ///
/// ///
/// # Prefix Resources /// # Prefix Resources
/// A prefix resource is defined as pattern that can match just the start of a path. /// A prefix resource is defined as pattern that can match just the start of a path, up to a
/// segment boundary.
/// ///
/// This library chooses to restrict that definition slightly. In particular, when matching, the /// Prefix patterns with a trailing slash may have an unexpected, though correct, behavior.
/// prefix must be separated from the remaining part of the path by a `/` character, either at the /// They define and therefore require an empty segment in order to match. Examples are given below.
/// end of the prefix pattern or at the start of the the remaining slice. In practice, this is not ///
/// much of a limitation. /// Empty pattern matches any path as a prefix.
/// ///
/// Prefix resources can contain dynamic segments. /// Prefix resources can contain dynamic segments.
/// ///
@ -102,9 +122,12 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// assert!(resource.is_match("/home/new")); /// assert!(resource.is_match("/home/new"));
/// assert!(!resource.is_match("/homes")); /// assert!(!resource.is_match("/homes"));
/// ///
/// // prefix pattern with a trailing slash
/// let resource = ResourceDef::prefix("/user/{id}/"); /// let resource = ResourceDef::prefix("/user/{id}/");
/// assert!(resource.is_match("/user/123/")); /// assert!(resource.is_match("/user/123/"));
/// assert!(resource.is_match("/user/123/stars")); /// assert!(resource.is_match("/user/123//stars"));
/// assert!(!resource.is_match("/user/123/stars"));
/// assert!(!resource.is_match("/user/123"));
/// ``` /// ```
/// ///
/// ///
@ -117,6 +140,10 @@ const REGEX_FLAGS: &str = "(?s-m)";
/// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID /// `{name:regex}`. For example, `/user/{id:\d+}` will only match paths where the user ID
/// is numeric. /// is numeric.
/// ///
/// The regex could potentially match multiple segments. If this is not wanted, then care must be
/// taken to avoid matching a slash `/`. It is guaranteed, however, that the match ends at a
/// segment boundary; the pattern `r"(/|$)` is always appended to the regex.
///
/// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in /// By default, dynamic segments use this regex: `[^/]+`. This shows why it is the case, as shown in
/// the earlier section, that segments capture a slice of the path up to the next `/` character. /// the earlier section, that segments capture a slice of the path up to the next `/` character.
/// ///
@ -193,17 +220,15 @@ pub struct ResourceDef {
name: Option<String>, name: Option<String>,
/// Pattern that generated the resource definition. /// Pattern that generated the resource definition.
///
/// `None` when pattern type is `DynamicSet`.
patterns: Patterns, patterns: Patterns,
is_prefix: bool,
/// Pattern type. /// Pattern type.
pat_type: PatternType, pat_type: PatternType,
/// List of segments that compose the pattern, in order. /// List of segments that compose the pattern, in order.
/// segments: Vec<PatternSegment>,
/// `None` when pattern type is `DynamicSet`.
segments: Option<Vec<PatternSegment>>,
} }
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@ -221,9 +246,6 @@ enum PatternType {
/// Single constant/literal segment. /// Single constant/literal segment.
Static(String), Static(String),
/// Single constant/literal prefix segment.
Prefix(String),
/// Single regular expression and list of dynamic segment names. /// Single regular expression and list of dynamic segment names.
Dynamic(Regex, Vec<&'static str>), Dynamic(Regex, Vec<&'static str>),
@ -257,48 +279,10 @@ impl ResourceDef {
/// ``` /// ```
pub fn new<T: IntoPatterns>(paths: T) -> Self { pub fn new<T: IntoPatterns>(paths: T) -> Self {
profile_method!(new); profile_method!(new);
Self::new2(paths, false)
match paths.patterns() {
Patterns::Single(pattern) => ResourceDef::from_single_pattern(&pattern, false),
// since zero length pattern sets are possible
// just return a useless `ResourceDef`
Patterns::List(patterns) if patterns.is_empty() => ResourceDef {
id: 0,
name: None,
patterns: Patterns::List(patterns),
pat_type: PatternType::DynamicSet(RegexSet::empty(), Vec::new()),
segments: None,
},
Patterns::List(patterns) => {
let mut re_set = Vec::with_capacity(patterns.len());
let mut pattern_data = Vec::new();
for pattern in &patterns {
match ResourceDef::parse(&pattern, false, true) {
(PatternType::Dynamic(re, names), _) => {
re_set.push(re.as_str().to_owned());
pattern_data.push((re, names));
}
_ => unreachable!(),
}
} }
let pattern_re_set = RegexSet::new(re_set).unwrap(); /// Constructs a new resource definition using a pattern that performs prefix matching.
ResourceDef {
id: 0,
name: None,
patterns: Patterns::List(patterns),
pat_type: PatternType::DynamicSet(pattern_re_set, pattern_data),
segments: None,
}
}
}
}
/// Constructs a new resource definition using a string pattern that performs prefix matching.
/// ///
/// More specifically, the regular expressions generated for matching are different when using /// More specifically, the regular expressions generated for matching are different when using
/// this method vs using `new`; they will not be appended with the `$` meta-character that /// this method vs using `new`; they will not be appended with the `$` meta-character that
@ -320,17 +304,10 @@ impl ResourceDef {
/// assert!(!resource.is_match("user/123")); /// assert!(!resource.is_match("user/123"));
/// assert!(!resource.is_match("user/123/stars")); /// assert!(!resource.is_match("user/123/stars"));
/// assert!(!resource.is_match("/foo")); /// assert!(!resource.is_match("/foo"));
///
/// let resource = ResourceDef::prefix("user/{id}");
/// assert!(resource.is_match("user/123"));
/// assert!(resource.is_match("user/123/stars"));
/// assert!(!resource.is_match("/user/123"));
/// assert!(!resource.is_match("/user/123/stars"));
/// assert!(!resource.is_match("foo"));
/// ``` /// ```
pub fn prefix(path: &str) -> Self { pub fn prefix<T: IntoPatterns>(paths: T) -> Self {
profile_method!(prefix); profile_method!(prefix);
ResourceDef::from_single_pattern(path, true) ResourceDef::new2(paths, true)
} }
/// Constructs a new resource definition using a string pattern that performs prefix matching, /// Constructs a new resource definition using a string pattern that performs prefix matching,
@ -355,7 +332,7 @@ impl ResourceDef {
/// ``` /// ```
pub fn root_prefix(path: &str) -> Self { pub fn root_prefix(path: &str) -> Self {
profile_method!(root_prefix); profile_method!(root_prefix);
ResourceDef::prefix(&insert_slash(path)) ResourceDef::prefix(insert_slash(path).into_owned())
} }
/// Returns a numeric resource ID. /// Returns a numeric resource ID.
@ -433,17 +410,14 @@ impl ResourceDef {
/// assert!(!ResourceDef::new("/user").is_prefix()); /// assert!(!ResourceDef::new("/user").is_prefix());
/// ``` /// ```
pub fn is_prefix(&self) -> bool { pub fn is_prefix(&self) -> bool {
match &self.pat_type { self.is_prefix
PatternType::Prefix(_) => true,
PatternType::Dynamic(re, _) if !re.as_str().ends_with('$') => true,
_ => false,
}
} }
/// Returns the pattern string that generated the resource definition. /// Returns the pattern string that generated the resource definition.
/// ///
/// Returns `None` if definition was constructed with multiple patterns. /// If definition is constructed with multiple patterns, the first pattern is returned. To get
/// See [`patterns_iter`][Self::pattern_iter]. /// all patterns, use [`patterns_iter`][Self::pattern_iter]. If resource has 0 patterns,
/// returns `None`.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -452,11 +426,11 @@ impl ResourceDef {
/// assert_eq!(resource.pattern().unwrap(), "/user/{id}"); /// assert_eq!(resource.pattern().unwrap(), "/user/{id}");
/// ///
/// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]); /// let mut resource = ResourceDef::new(["/profile", "/user/{id}"]);
/// assert!(resource.pattern().is_none()); /// assert_eq!(resource.pattern(), Some("/profile"));
pub fn pattern(&self) -> Option<&str> { pub fn pattern(&self) -> Option<&str> {
match &self.patterns { match &self.patterns {
Patterns::Single(pattern) => Some(pattern.as_str()), Patterns::Single(pattern) => Some(pattern.as_str()),
Patterns::List(_) => None, Patterns::List(patterns) => patterns.first().map(AsRef::as_ref),
} }
} }
@ -543,8 +517,8 @@ impl ResourceDef {
.collect::<Vec<_>>(); .collect::<Vec<_>>();
match patterns.len() { match patterns.len() {
1 => ResourceDef::from_single_pattern(&patterns[0], other.is_prefix()), 1 => ResourceDef::new2(&patterns[0], other.is_prefix()),
_ => ResourceDef::new(patterns), _ => ResourceDef::new2(patterns, other.is_prefix()),
} }
} }
@ -589,28 +563,10 @@ impl ResourceDef {
// `self.find_match(path).is_some()` // `self.find_match(path).is_some()`
// but this skips some checks and uses potentially faster regex methods // but this skips some checks and uses potentially faster regex methods
match self.pat_type { match &self.pat_type {
PatternType::Static(ref s) => s == path, PatternType::Static(pattern) => self.static_match(pattern, path).is_some(),
PatternType::Dynamic(re, _) => re.is_match(path),
PatternType::Prefix(ref prefix) if prefix == path => true, PatternType::DynamicSet(re, _) => re.is_match(path),
PatternType::Prefix(ref prefix) => is_strict_prefix(prefix, path),
// dynamic prefix
PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => {
match re.find(path) {
// prefix matches exactly
Some(m) if m.end() == path.len() => true,
// prefix matches part
Some(m) => is_strict_prefix(m.as_str(), path),
// prefix does not match
None => false,
}
}
PatternType::Dynamic(ref re, _) => re.is_match(path),
PatternType::DynamicSet(ref re, _) => re.is_match(path),
} }
} }
@ -653,33 +609,14 @@ impl ResourceDef {
profile_method!(find_match); profile_method!(find_match);
match &self.pat_type { match &self.pat_type {
PatternType::Static(segment) if path == segment => Some(segment.len()), PatternType::Static(pattern) => self.static_match(pattern, path),
PatternType::Static(_) => None,
PatternType::Prefix(prefix) if path == prefix => Some(prefix.len()), PatternType::Dynamic(re, _) => Some(re.captures(path)?[1].len()),
PatternType::Prefix(prefix) if is_strict_prefix(prefix, path) => Some(prefix.len()),
PatternType::Prefix(_) => None,
// dynamic prefix
PatternType::Dynamic(ref re, _) if !re.as_str().ends_with('$') => {
match re.find(path) {
// prefix matches exactly
Some(m) if m.end() == path.len() => Some(m.end()),
// prefix matches part
Some(m) if is_strict_prefix(m.as_str(), path) => Some(m.end()),
// prefix does not match
_ => None,
}
}
PatternType::Dynamic(re, _) => re.find(path).map(|m| m.end()),
PatternType::DynamicSet(re, params) => { PatternType::DynamicSet(re, params) => {
let idx = re.matches(path).into_iter().next()?; let idx = re.matches(path).into_iter().next()?;
let (ref pattern, _) = params[idx]; let (ref pattern, _) = params[idx];
pattern.find(path).map(|m| m.end()) Some(pattern.captures(path)?[1].len())
} }
} }
} }
@ -765,10 +702,10 @@ impl ResourceDef {
let path_str = path.path(); let path_str = path.path();
let (matched_len, matched_vars) = match &self.pat_type { let (matched_len, matched_vars) = match &self.pat_type {
PatternType::Static(_) | PatternType::Prefix(_) => { PatternType::Static(pattern) => {
profile_section!(pattern_static_or_prefix); profile_section!(pattern_static_or_prefix);
match self.find_match(path_str) { match self.static_match(pattern, path_str) {
Some(len) => (len, None), Some(len) => (len, None),
None => return false, None => return false,
} }
@ -790,7 +727,7 @@ impl ResourceDef {
profile_section!(pattern_dynamic_extract_captures); profile_section!(pattern_dynamic_extract_captures);
for (no, name) in names.iter().enumerate() { for (no, name) in names.iter().enumerate() {
if let Some(m) = captures.name(&name) { if let Some(m) = captures.name(name) {
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
} else { } else {
log::error!( log::error!(
@ -802,7 +739,7 @@ impl ResourceDef {
} }
}; };
(captures[0].len(), Some(names)) (captures[1].len(), Some(names))
} }
PatternType::DynamicSet(re, params) => { PatternType::DynamicSet(re, params) => {
@ -820,7 +757,7 @@ impl ResourceDef {
}; };
for (no, name) in names.iter().enumerate() { for (no, name) in names.iter().enumerate() {
if let Some(m) = captures.name(&name) { if let Some(m) = captures.name(name) {
segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16); segments[no] = PathItem::Segment(m.start() as u16, m.end() as u16);
} else { } else {
log::error!("Dynamic path match but not all segments found: {}", name); log::error!("Dynamic path match but not all segments found: {}", name);
@ -828,7 +765,7 @@ impl ResourceDef {
} }
} }
(captures[0].len(), Some(names)) (captures[1].len(), Some(names))
} }
}; };
@ -856,13 +793,10 @@ impl ResourceDef {
F: FnMut(&str) -> Option<I>, F: FnMut(&str) -> Option<I>,
I: AsRef<str>, I: AsRef<str>,
{ {
for el in match self.segments { for segment in &self.segments {
Some(ref segments) => segments, match segment {
None => return false, PatternSegment::Const(val) => path.push_str(val),
} { PatternSegment::Var(name) => match vars(name) {
match *el {
PatternSegment::Const(ref val) => path.push_str(val),
PatternSegment::Var(ref name) => match vars(name) {
Some(val) => path.push_str(val.as_ref()), Some(val) => path.push_str(val.as_ref()),
_ => return false, _ => return false,
}, },
@ -876,8 +810,8 @@ impl ResourceDef {
/// ///
/// Returns `true` on success. /// Returns `true` on success.
/// ///
/// Resource paths can not be built from multi-pattern resources; this call will always return /// For multi-pattern resources, the first pattern is used under the assumption that it would be
/// false and will not add anything to the string buffer. /// equivalent to any other choice.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -902,8 +836,8 @@ impl ResourceDef {
/// ///
/// Returns `true` on success. /// Returns `true` on success.
/// ///
/// Resource paths can not be built from multi-pattern resources; this call will always return /// For multi-pattern resources, the first pattern is used under the assumption that it would be
/// false and will not add anything to the string buffer. /// equivalent to any other choice.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -933,19 +867,69 @@ impl ResourceDef {
self.build_resource_path(path, |name| values.get(name).map(AsRef::<str>::as_ref)) self.build_resource_path(path, |name| values.get(name).map(AsRef::<str>::as_ref))
} }
/// Parse path pattern and create a new instance. /// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`.
fn from_single_pattern(pattern: &str, is_prefix: bool) -> Self { fn static_match(&self, pattern: &str, path: &str) -> Option<usize> {
profile_method!(from_single_pattern); let rem = path.strip_prefix(pattern)?;
let pattern = pattern.to_owned(); match self.is_prefix {
let (pat_type, segments) = ResourceDef::parse(&pattern, is_prefix, false); // resource is not a prefix so an exact match is needed
false if rem.is_empty() => Some(pattern.len()),
// resource is a prefix so rem should start with a path delimiter
true if rem.is_empty() || rem.starts_with('/') => Some(pattern.len()),
// otherwise, no match
_ => None,
}
}
fn new2<T: IntoPatterns>(paths: T, is_prefix: bool) -> Self {
profile_method!(new2);
let patterns = paths.patterns();
let (pat_type, segments) = match &patterns {
Patterns::Single(pattern) => ResourceDef::parse(pattern, is_prefix, false),
// since zero length pattern sets are possible
// just return a useless `ResourceDef`
Patterns::List(patterns) if patterns.is_empty() => (
PatternType::DynamicSet(RegexSet::empty(), Vec::new()),
Vec::new(),
),
Patterns::List(patterns) => {
let mut re_set = Vec::with_capacity(patterns.len());
let mut pattern_data = Vec::new();
let mut segments = None;
for pattern in patterns {
match ResourceDef::parse(pattern, is_prefix, true) {
(PatternType::Dynamic(re, names), segs) => {
re_set.push(re.as_str().to_owned());
pattern_data.push((re, names));
segments.get_or_insert(segs);
}
_ => unreachable!(),
}
}
let pattern_re_set = RegexSet::new(re_set).unwrap();
let segments = segments.unwrap_or_else(Vec::new);
(
PatternType::DynamicSet(pattern_re_set, pattern_data),
segments,
)
}
};
ResourceDef { ResourceDef {
id: 0, id: 0,
name: None, name: None,
patterns: Patterns::Single(pattern), patterns,
is_prefix,
pat_type, pat_type,
segments: Some(segments), segments,
} }
} }
@ -979,7 +963,10 @@ impl ResourceDef {
_ => false, _ => false,
}) })
.unwrap_or_else(|| { .unwrap_or_else(|| {
panic!(r#"path "{}" contains malformed dynamic segment"#, pattern) panic!(
r#"pattern "{}" contains malformed dynamic segment"#,
pattern
)
}); });
let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1); let (mut param, mut unprocessed) = pattern.split_at(close_idx + 1);
@ -1032,20 +1019,15 @@ impl ResourceDef {
) -> (PatternType, Vec<PatternSegment>) { ) -> (PatternType, Vec<PatternSegment>) {
profile_method!(parse); profile_method!(parse);
let mut unprocessed = pattern; if !force_dynamic && pattern.find('{').is_none() && !pattern.ends_with('*') {
if !force_dynamic && unprocessed.find('{').is_none() && !unprocessed.ends_with('*') {
// pattern is static // pattern is static
return (
let tp = if is_prefix { PatternType::Static(pattern.to_owned()),
PatternType::Prefix(unprocessed.to_owned()) vec![PatternSegment::Const(pattern.to_owned())],
} else { );
PatternType::Static(unprocessed.to_owned())
};
return (tp, vec![PatternSegment::Const(unprocessed.to_owned())]);
} }
let mut unprocessed = pattern;
let mut segments = Vec::new(); let mut segments = Vec::new();
let mut re = format!("{}^", REGEX_FLAGS); let mut re = format!("{}^", REGEX_FLAGS);
let mut dyn_segment_count = 0; let mut dyn_segment_count = 0;
@ -1112,9 +1094,17 @@ impl ResourceDef {
); );
} }
if !is_prefix && !has_tail_segment { // Store the pattern in capture group #1 to have context info outside it
let mut re = format!("({})", re);
// Ensure the match ends at a segment boundary
if !has_tail_segment {
if is_prefix {
re.push_str(r"(/|$)");
} else {
re.push('$'); re.push('$');
} }
}
let re = match Regex::new(&re) { let re = match Regex::new(&re) {
Ok(re) => re, Ok(re) => re,
@ -1138,18 +1128,7 @@ impl Eq for ResourceDef {}
impl PartialEq for ResourceDef { impl PartialEq for ResourceDef {
fn eq(&self, other: &ResourceDef) -> bool { fn eq(&self, other: &ResourceDef) -> bool {
self.patterns == other.patterns self.patterns == other.patterns && self.is_prefix == other.is_prefix
&& match &self.pat_type {
PatternType::Static(_) => matches!(&other.pat_type, PatternType::Static(_)),
PatternType::Prefix(_) => matches!(&other.pat_type, PatternType::Prefix(_)),
PatternType::Dynamic(re, _) => match &other.pat_type {
PatternType::Dynamic(other_re, _) => re.as_str() == other_re.as_str(),
_ => false,
},
PatternType::DynamicSet(_, _) => {
matches!(&other.pat_type, PatternType::DynamicSet(..))
}
}
} }
} }
@ -1184,13 +1163,6 @@ pub(crate) fn insert_slash(path: &str) -> Cow<'_, str> {
} }
} }
/// Returns true if `prefix` acts as a proper prefix (i.e., separated by a slash) in `path`.
///
/// The `strict` refers to the fact that this will return `false` if `prefix == path`.
fn is_strict_prefix(prefix: &str, path: &str) -> bool {
path.starts_with(prefix) && (prefix.ends_with('/') || path[prefix.len()..].starts_with('/'))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -1375,6 +1347,24 @@ mod tests {
assert!(!re.is_match("/user/2345/sdg")); assert!(!re.is_match("/user/2345/sdg"));
} }
#[test]
fn dynamic_set_prefix() {
let re = ResourceDef::prefix(vec!["/u/{id}", "/{id:[[:digit:]]{3}}"]);
assert_eq!(re.find_match("/u/abc"), Some(6));
assert_eq!(re.find_match("/u/abc/123"), Some(6));
assert_eq!(re.find_match("/s/user/profile"), None);
assert_eq!(re.find_match("/123"), Some(4));
assert_eq!(re.find_match("/123/456"), Some(4));
assert_eq!(re.find_match("/12345"), None);
let mut path = Path::new("/151/res");
assert!(re.capture_match_info(&mut path));
assert_eq!(path.get("id").unwrap(), "151");
assert_eq!(path.unprocessed(), "/res");
}
#[test] #[test]
fn parse_tail() { fn parse_tail() {
let re = ResourceDef::new("/user/-{id}*"); let re = ResourceDef::new("/user/-{id}*");
@ -1501,54 +1491,70 @@ mod tests {
let re = ResourceDef::prefix("/name/"); let re = ResourceDef::prefix("/name/");
assert!(re.is_match("/name/")); assert!(re.is_match("/name/"));
assert!(re.is_match("/name/gs")); assert!(re.is_match("/name//gs"));
assert!(!re.is_match("/name/gs"));
assert!(!re.is_match("/name")); assert!(!re.is_match("/name"));
let mut path = Path::new("/name/gs"); let mut path = Path::new("/name/gs");
assert!(!re.capture_match_info(&mut path));
let mut path = Path::new("/name//gs");
assert!(re.capture_match_info(&mut path)); assert!(re.capture_match_info(&mut path));
assert_eq!(path.unprocessed(), "gs"); assert_eq!(path.unprocessed(), "/gs");
let re = ResourceDef::root_prefix("name/"); let re = ResourceDef::root_prefix("name/");
assert!(re.is_match("/name/")); assert!(re.is_match("/name/"));
assert!(re.is_match("/name/gs")); assert!(re.is_match("/name//gs"));
assert!(!re.is_match("/name/gs"));
assert!(!re.is_match("/name")); assert!(!re.is_match("/name"));
let mut path = Path::new("/name/gs"); let mut path = Path::new("/name/gs");
assert!(re.capture_match_info(&mut path)); assert!(!re.capture_match_info(&mut path));
assert_eq!(path.unprocessed(), "gs");
} }
#[test] #[test]
fn prefix_dynamic() { fn prefix_dynamic() {
let re = ResourceDef::prefix("/{name}/"); let re = ResourceDef::prefix("/{name}");
assert!(re.is_prefix()); assert!(re.is_prefix());
assert!(re.is_match("/name/")); assert!(re.is_match("/name/"));
assert!(re.is_match("/name/gs")); assert!(re.is_match("/name/gs"));
assert!(!re.is_match("/name")); assert!(re.is_match("/name"));
assert_eq!(re.find_match("/name/"), Some(6)); assert_eq!(re.find_match("/name/"), Some(5));
assert_eq!(re.find_match("/name/gs"), Some(6)); assert_eq!(re.find_match("/name/gs"), Some(5));
assert_eq!(re.find_match("/name"), None); assert_eq!(re.find_match("/name"), Some(5));
assert_eq!(re.find_match(""), None);
let mut path = Path::new("/test2/"); let mut path = Path::new("/test2/");
assert!(re.capture_match_info(&mut path)); assert!(re.capture_match_info(&mut path));
assert_eq!(&path["name"], "test2"); assert_eq!(&path["name"], "test2");
assert_eq!(&path[0], "test2"); assert_eq!(&path[0], "test2");
assert_eq!(path.unprocessed(), ""); assert_eq!(path.unprocessed(), "/");
let mut path = Path::new("/test2/subpath1/subpath2/index.html"); let mut path = Path::new("/test2/subpath1/subpath2/index.html");
assert!(re.capture_match_info(&mut path)); assert!(re.capture_match_info(&mut path));
assert_eq!(&path["name"], "test2"); assert_eq!(&path["name"], "test2");
assert_eq!(&path[0], "test2"); assert_eq!(&path[0], "test2");
assert_eq!(path.unprocessed(), "subpath1/subpath2/index.html"); assert_eq!(path.unprocessed(), "/subpath1/subpath2/index.html");
let resource = ResourceDef::prefix("/user"); let resource = ResourceDef::prefix("/user");
// input string shorter than prefix // input string shorter than prefix
assert!(resource.find_match("/foo").is_none()); assert!(resource.find_match("/foo").is_none());
} }
#[test]
fn prefix_empty() {
let re = ResourceDef::prefix("");
assert!(re.is_prefix());
assert!(re.is_match(""));
assert!(re.is_match("/"));
assert!(re.is_match("/name/test/test"));
}
#[test] #[test]
fn build_path_list() { fn build_path_list() {
let mut s = String::new(); let mut s = String::new();
@ -1585,10 +1591,11 @@ mod tests {
} }
#[test] #[test]
fn multi_pattern_cannot_build_path() { fn multi_pattern_build_path() {
let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]); let resource = ResourceDef::new(["/user/{id}", "/profile/{id}"]);
let mut s = String::new(); let mut s = String::new();
assert!(!resource.resource_path_from_iter(&mut s, &mut ["123"].iter())); assert!(resource.resource_path_from_iter(&mut s, &mut ["123"].iter()));
assert_eq!(s, "/user/123");
} }
#[test] #[test]
@ -1667,14 +1674,17 @@ mod tests {
} }
#[test] #[test]
fn consistent_match_length() { fn prefix_trailing_slash() {
let result = Some(5); // The prefix "/abc/" matches two segments: ["user", ""]
// These are not prefixes
let re = ResourceDef::prefix("/abc/"); let re = ResourceDef::prefix("/abc/");
assert_eq!(re.find_match("/abc/def"), result); assert_eq!(re.find_match("/abc/def"), None);
assert_eq!(re.find_match("/abc//def"), Some(5));
let re = ResourceDef::prefix("/{id}/"); let re = ResourceDef::prefix("/{id}/");
assert_eq!(re.find_match("/abc/def"), result); assert_eq!(re.find_match("/abc/def"), None);
assert_eq!(re.find_match("/abc//def"), Some(5));
} }
#[test] #[test]
@ -1718,8 +1728,12 @@ mod tests {
join_test!("", "" => "", "/hello", "/"); join_test!("", "" => "", "/hello", "/");
join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123"); join_test!("/user", "" => "", "/user", "/user/123", "/user11", "user", "user/123");
join_test!("", "/user"=> "", "/user", "foo", "/user11", "user", "user/123"); join_test!("", "/user" => "", "/user", "foo", "/user11", "user", "user/123");
join_test!("/user", "/xx"=> "", "", "/", "/user", "/xx", "/userxx", "/user/xx"); join_test!("/user", "/xx" => "", "", "/", "/user", "/xx", "/userxx", "/user/xx");
join_test!(["/ver/{v}", "/v{v}"], ["/req/{req}", "/{req}"] => "/v1/abc",
"/ver/1/abc", "/v1/req/abc", "/ver/1/req/abc", "/v1/abc/def",
"/ver1/req/abc/def", "", "/", "/v1/");
} }
#[test] #[test]
@ -1757,6 +1771,7 @@ mod tests {
match_methods_agree!(prefix "" => "", "/", "/foo"); match_methods_agree!(prefix "" => "", "/", "/foo");
match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo"); match_methods_agree!(prefix "/user" => "user", "/user", "/users", "/user/123", "/foo");
match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234"); match_methods_agree!(prefix r"/id/{id:\d{3}}" => "/id/123", "/id/1234");
match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1");
} }
#[test] #[test]

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.1.0-beta.4 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51.
## 0.1.0-beta.3 - 2021-06-20 ## 0.1.0-beta.3 - 2021-06-20
* No significant changes from `0.1.0-beta.2`. * No significant changes from `0.1.0-beta.2`.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-test" name = "actix-test"
version = "0.1.0-beta.3" version = "0.1.0-beta.4"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>", "Rob Ede <robjtede@icloud.com>",
@ -20,13 +20,13 @@ openssl = ["tls-openssl", "actix-http/openssl"]
[dependencies] [dependencies]
actix-codec = "0.4.0" actix-codec = "0.4.0"
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"
actix-http-test = { version = "3.0.0-beta.4", features = [] } actix-http-test = "3.0.0-beta.5"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.8", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] }
actix-rt = "2.1" actix-rt = "2.1"
awc = { version = "3.0.0-beta.7", default-features = false, features = ["cookies"] } awc = { version = "3.0.0-beta.8", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] } futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] } futures-util = { version = "0.3.7", default-features = false, features = [] }

View File

@ -3,6 +3,10 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.7 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51.
## 4.0.0-beta.6 - 2021-06-26 ## 4.0.0-beta.6 - 2021-06-26
* Update `actix` to `0.12`. [#2277] * Update `actix` to `0.12`. [#2277]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "4.0.0-beta.6" version = "4.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for Actix Web" description = "Actix actors support for Actix Web"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@ -16,8 +16,8 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix = { version = "0.12.0", default-features = false } actix = { version = "0.12.0", default-features = false }
actix-codec = "0.4.0" actix-codec = "0.4.0"
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"
actix-web = { version = "4.0.0-beta.8", default-features = false } actix-web = { version = "4.0.0-beta.9", default-features = false }
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
@ -29,6 +29,6 @@ tokio = { version = "1", features = ["sync"] }
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.3" actix-test = "0.1.0-beta.3"
awc = { version = "3.0.0-beta.7", default-features = false } awc = { version = "3.0.0-beta.8", default-features = false }
env_logger = "0.8" env_logger = "0.8"
futures-util = { version = "0.3.7", default-features = false } futures-util = { version = "0.3.7", default-features = false }

View File

@ -3,15 +3,15 @@
> Actix actors support for Actix Web. > Actix actors support for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors) [![crates.io](https://img.shields.io/crates/v/actix-web-actors?label=latest)](https://crates.io/crates/actix-web-actors)
[![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.6)](https://docs.rs/actix-web-actors/4.0.0-beta.6) [![Documentation](https://docs.rs/actix-web-actors/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web-actors/4.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![License](https://img.shields.io/crates/l/actix-web-actors.svg) ![License](https://img.shields.io/crates/l/actix-web-actors.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.6) [![dependency status](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web-actors/4.0.0-beta.7)
[![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors) [![Download](https://img.shields.io/crates/d/actix-web-actors.svg)](https://crates.io/crates/actix-web-actors)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-actors) - [API Documentation](https://docs.rs/actix-web-actors)
- Minimum supported Rust version: 1.46 or later - Minimum supported Rust version: 1.51 or later

View File

@ -3,6 +3,13 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.5.0-beta.4 - 2021-09-09
* In routing macros, paths are now validated at compile time. [#2350]
* Minimum supported Rust version (MSRV) is now 1.51.
[#2350]: https://github.com/actix/actix-web/pull/2350
## 0.5.0-beta.3 - 2021-06-17 ## 0.5.0-beta.3 - 2021-06-17
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.5.0-beta.3" version = "0.5.0-beta.4"
description = "Routing and runtime macros for Actix Web" description = "Routing and runtime macros for Actix Web"
readme = "README.md" readme = "README.md"
homepage = "https://actix.rs" homepage = "https://actix.rs"
@ -17,12 +17,13 @@ proc-macro = true
quote = "1" quote = "1"
syn = { version = "1", features = ["full", "parsing"] } syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1" proc-macro2 = "1"
actix-router = "0.5.0-beta.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-test = "0.1.0-beta.3" actix-test = "0.1.0-beta.3"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-web = "4.0.0-beta.8" actix-web = "4.0.0-beta.9"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
trybuild = "1" trybuild = "1"

View File

@ -3,18 +3,18 @@
> Routing and runtime macros for Actix Web. > Routing and runtime macros for Actix Web.
[![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen) [![crates.io](https://img.shields.io/crates/v/actix-web-codegen?label=latest)](https://crates.io/crates/actix-web-codegen)
[![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.3)](https://docs.rs/actix-web-codegen/0.5.0-beta.3) [![Documentation](https://docs.rs/actix-web-codegen/badge.svg?version=0.5.0-beta.4)](https://docs.rs/actix-web-codegen/0.5.0-beta.4)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.51+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.51.html)
![License](https://img.shields.io/crates/l/actix-web-codegen.svg) ![License](https://img.shields.io/crates/l/actix-web-codegen.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.3) [![dependency status](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4/status.svg)](https://deps.rs/crate/actix-web-codegen/0.5.0-beta.4)
[![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen) [![Download](https://img.shields.io/crates/d/actix-web-codegen.svg)](https://crates.io/crates/actix-web-codegen)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-web-codegen) - [API Documentation](https://docs.rs/actix-web-codegen)
- Minimum supported Rust version: 1.46 or later. - Minimum supported Rust version: 1.51 or later.
## Compile Testing ## Compile Testing

View File

@ -3,6 +3,7 @@ extern crate proc_macro;
use std::collections::HashSet; use std::collections::HashSet;
use std::convert::TryFrom; use std::convert::TryFrom;
use actix_router::ResourceDef;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use proc_macro2::{Span, TokenStream as TokenStream2}; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{format_ident, quote, ToTokens, TokenStreamExt}; use quote::{format_ident, quote, ToTokens, TokenStreamExt};
@ -101,6 +102,7 @@ impl Args {
match arg { match arg {
NestedMeta::Lit(syn::Lit::Str(lit)) => match path { NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
None => { None => {
let _ = ResourceDef::new(lit.value());
path = Some(lit); path = Some(lit);
} }
_ => { _ => {

View File

@ -1,4 +1,4 @@
#[rustversion::stable(1.46)] // MSRV #[rustversion::stable(1.51)] // MSRV
#[test] #[test]
fn compile_macros() { fn compile_macros() {
let t = trybuild::TestCases::new(); let t = trybuild::TestCases::new();
@ -10,6 +10,7 @@ fn compile_macros() {
t.compile_fail("tests/trybuild/route-missing-method-fail.rs"); t.compile_fail("tests/trybuild/route-missing-method-fail.rs");
t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs"); t.compile_fail("tests/trybuild/route-duplicate-method-fail.rs");
t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs"); t.compile_fail("tests/trybuild/route-unexpected-method-fail.rs");
t.compile_fail("tests/trybuild/route-malformed-path-fail.rs");
t.pass("tests/trybuild/docstring-ok.rs"); t.pass("tests/trybuild/docstring-ok.rs");
} }

View File

@ -0,0 +1,33 @@
use actix_web_codegen::get;
#[get("/{")]
async fn zero() -> &'static str {
"malformed resource def"
}
#[get("/{foo")]
async fn one() -> &'static str {
"malformed resource def"
}
#[get("/{}")]
async fn two() -> &'static str {
"malformed resource def"
}
#[get("/*")]
async fn three() -> &'static str {
"malformed resource def"
}
#[get("/{tail:\\d+}*")]
async fn four() -> &'static str {
"malformed resource def"
}
#[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")]
async fn five() -> &'static str {
"malformed resource def"
}
fn main() {}

View File

@ -0,0 +1,42 @@
error: custom attribute panicked
--> $DIR/route-malformed-path-fail.rs:3:1
|
3 | #[get("/{")]
| ^^^^^^^^^^^^
|
= help: message: pattern "{" contains malformed dynamic segment
error: custom attribute panicked
--> $DIR/route-malformed-path-fail.rs:8:1
|
8 | #[get("/{foo")]
| ^^^^^^^^^^^^^^^
|
= help: message: pattern "{foo" contains malformed dynamic segment
error: custom attribute panicked
--> $DIR/route-malformed-path-fail.rs:13:1
|
13 | #[get("/{}")]
| ^^^^^^^^^^^^^
|
= help: message: Wrong path pattern: "/{}" regex parse error:
((?s-m)^/(?P<>[^/]+))$
^
error: empty capture group name
error: custom attribute panicked
--> $DIR/route-malformed-path-fail.rs:23:1
|
23 | #[get("/{tail:\\d+}*")]
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= help: message: custom regex is not supported for tail match
error: custom attribute panicked
--> $DIR/route-malformed-path-fail.rs:28:1
|
28 | #[get("/{a}/{b}/{c}/{d}/{e}/{f}/{g}/{h}/{i}/{j}/{k}/{l}/{m}/{n}/{o}/{p}/{q}")]
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
= help: message: Only 16 dynamic segments are allowed, provided: 17

View File

@ -3,6 +3,13 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.8 - 2021-09-09
### Changed
* Send headers within the redirect requests. [#2310]
[#2310]: https://github.com/actix/actix-web/pull/2310
## 3.0.0-beta.7 - 2021-06-26 ## 3.0.0-beta.7 - 2021-06-26
### Changed ### Changed
* Change compression algorithm features flags. [#2250] * Change compression algorithm features flags. [#2250]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "3.0.0-beta.7" version = "3.0.0-beta.8"
authors = [ authors = [
"Nikolay Kim <fafhrd91@gmail.com>", "Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>", "fakeshadow <24548779@qq.com>",
@ -55,7 +55,7 @@ __compress = []
[dependencies] [dependencies]
actix-codec = "0.4.0" actix-codec = "0.4.0"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-http = "3.0.0-beta.8" actix-http = "3.0.0-beta.10"
actix-rt = { version = "2.1", default-features = false } actix-rt = { version = "2.1", default-features = false }
base64 = "0.13" base64 = "0.13"
@ -77,9 +77,9 @@ tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] } tls-rustls = { version = "0.19.0", package = "rustls", optional = true, features = ["dangerous_configuration"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.8", features = ["openssl"] } actix-web = { version = "4.0.0-beta.9", features = ["openssl"] }
actix-http = { version = "3.0.0-beta.8", features = ["openssl"] } actix-http = { version = "3.0.0-beta.10", features = ["openssl"] }
actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] }
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl", "rustls"] }

View File

@ -3,16 +3,16 @@
> Async HTTP and WebSocket client library. > Async HTTP and WebSocket client library.
[![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc) [![crates.io](https://img.shields.io/crates/v/awc?label=latest)](https://crates.io/crates/awc)
[![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.7)](https://docs.rs/awc/3.0.0-beta.7) [![Documentation](https://docs.rs/awc/badge.svg?version=3.0.0-beta.8)](https://docs.rs/awc/3.0.0-beta.8)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc) ![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/awc)
[![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.7/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.7) [![Dependency Status](https://deps.rs/crate/awc/3.0.0-beta.8/status.svg)](https://deps.rs/crate/awc/3.0.0-beta.8)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
## Documentation & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/awc) - [API Documentation](https://docs.rs/awc)
- [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https) - [Example Project](https://github.com/actix/examples/tree/HEAD/security/awc_https)
- Minimum Supported Rust Version (MSRV): 1.46.0 - Minimum Supported Rust Version (MSRV): 1.51.0
## Example ## Example

View File

@ -85,10 +85,12 @@ where
let max_redirect_times = self.max_redirect_times; let max_redirect_times = self.max_redirect_times;
// backup the uri and method for reuse schema and authority. // backup the uri and method for reuse schema and authority.
let (uri, method) = match head { let (uri, method, headers) = match head {
RequestHeadType::Owned(ref head) => (head.uri.clone(), head.method.clone()), RequestHeadType::Owned(ref head) => {
(head.uri.clone(), head.method.clone(), head.headers.clone())
}
RequestHeadType::Rc(ref head, ..) => { RequestHeadType::Rc(ref head, ..) => {
(head.uri.clone(), head.method.clone()) (head.uri.clone(), head.method.clone(), head.headers.clone())
} }
}; };
@ -104,6 +106,7 @@ where
max_redirect_times, max_redirect_times,
uri: Some(uri), uri: Some(uri),
method: Some(method), method: Some(method),
headers: Some(headers),
body: body_opt, body: body_opt,
addr, addr,
connector: Some(connector), connector: Some(connector),
@ -127,9 +130,10 @@ pin_project_lite::pin_project! {
max_redirect_times: u8, max_redirect_times: u8,
uri: Option<Uri>, uri: Option<Uri>,
method: Option<Method>, method: Option<Method>,
headers: Option<header::HeaderMap>,
body: Option<Bytes>, body: Option<Bytes>,
addr: Option<SocketAddr>, addr: Option<SocketAddr>,
connector: Option<Rc<S>> connector: Option<Rc<S>>,
} }
} }
} }
@ -148,6 +152,7 @@ where
max_redirect_times, max_redirect_times,
uri, uri,
method, method,
headers,
body, body,
addr, addr,
connector, connector,
@ -156,79 +161,60 @@ where
StatusCode::MOVED_PERMANENTLY StatusCode::MOVED_PERMANENTLY
| StatusCode::FOUND | StatusCode::FOUND
| StatusCode::SEE_OTHER | StatusCode::SEE_OTHER
| StatusCode::TEMPORARY_REDIRECT
| StatusCode::PERMANENT_REDIRECT
if *max_redirect_times > 0 => if *max_redirect_times > 0 =>
{ {
let org_uri = uri.take().unwrap(); let is_redirect = res.head().status == StatusCode::TEMPORARY_REDIRECT
// rebuild uri from the location header value. || res.head().status == StatusCode::PERMANENT_REDIRECT;
let uri = rebuild_uri(&res, org_uri)?;
// reset method let prev_uri = uri.take().unwrap();
let method = method.take().unwrap();
let method = match method { // rebuild uri from the location header value.
Method::GET | Method::HEAD => method, let next_uri = build_next_uri(&res, &prev_uri)?;
_ => Method::GET,
};
// take ownership of states that could be reused // take ownership of states that could be reused
let addr = addr.take(); let addr = addr.take();
let connector = connector.take(); let connector = connector.take();
let mut max_redirect_times = *max_redirect_times;
// use a new request head. // reset method
let mut head = RequestHead::default(); let method = if is_redirect {
head.uri = uri.clone(); method.take().unwrap()
head.method = method.clone(); } else {
let method = method.take().unwrap();
let head = RequestHeadType::Owned(head); match method {
Method::GET | Method::HEAD => method,
max_redirect_times -= 1; _ => Method::GET,
let fut = connector
.as_ref()
.unwrap()
// remove body
.call(ConnectRequest::Client(head, Body::None, addr));
self.set(RedirectServiceFuture::Client {
fut,
max_redirect_times,
uri: Some(uri),
method: Some(method),
// body is dropped on 301,302,303
body: None,
addr,
connector,
});
self.poll(cx)
} }
StatusCode::TEMPORARY_REDIRECT | StatusCode::PERMANENT_REDIRECT };
if *max_redirect_times > 0 =>
{
let org_uri = uri.take().unwrap();
// rebuild uri from the location header value.
let uri = rebuild_uri(&res, org_uri)?;
let mut body = body.take();
let body_new = if is_redirect {
// try to reuse body // try to reuse body
let body = body.take(); match body {
let body_new = match body {
Some(ref bytes) => Body::Bytes(bytes.clone()), Some(ref bytes) => Body::Bytes(bytes.clone()),
// TODO: should this be Body::Empty or Body::None. // TODO: should this be Body::Empty or Body::None.
_ => Body::Empty, _ => Body::Empty,
}
} else {
body = None;
// remove body
Body::None
}; };
let addr = addr.take(); let mut headers = headers.take().unwrap();
let method = method.take().unwrap();
let connector = connector.take(); remove_sensitive_headers(&mut headers, &prev_uri, &next_uri);
let mut max_redirect_times = *max_redirect_times;
// use a new request head. // use a new request head.
let mut head = RequestHead::default(); let mut head = RequestHead::default();
head.uri = uri.clone(); head.uri = next_uri.clone();
head.method = method.clone(); head.method = method.clone();
head.headers = headers.clone();
let head = RequestHeadType::Owned(head); let head = RequestHeadType::Owned(head);
let mut max_redirect_times = *max_redirect_times;
max_redirect_times -= 1; max_redirect_times -= 1;
let fut = connector let fut = connector
@ -239,8 +225,9 @@ where
self.set(RedirectServiceFuture::Client { self.set(RedirectServiceFuture::Client {
fut, fut,
max_redirect_times, max_redirect_times,
uri: Some(uri), uri: Some(next_uri),
method: Some(method), method: Some(method),
headers: Some(headers),
body, body,
addr, addr,
connector, connector,
@ -256,7 +243,7 @@ where
} }
} }
fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result<Uri, SendRequestError> { fn build_next_uri(res: &ClientResponse, prev_uri: &Uri) -> Result<Uri, SendRequestError> {
let uri = res let uri = res
.headers() .headers()
.get(header::LOCATION) .get(header::LOCATION)
@ -266,8 +253,8 @@ fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result<Uri, SendRequestErr
.map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?; .map_err(|e| SendRequestError::Url(InvalidUrl::HttpError(e.into())))?;
if uri.scheme().is_none() || uri.authority().is_none() { if uri.scheme().is_none() || uri.authority().is_none() {
let uri = Uri::builder() let uri = Uri::builder()
.scheme(org_uri.scheme().cloned().unwrap()) .scheme(prev_uri.scheme().cloned().unwrap())
.authority(org_uri.authority().cloned().unwrap()) .authority(prev_uri.authority().cloned().unwrap())
.path_and_query(value.as_bytes()) .path_and_query(value.as_bytes())
.build()?; .build()?;
Ok::<_, SendRequestError>(uri) Ok::<_, SendRequestError>(uri)
@ -281,12 +268,25 @@ fn rebuild_uri(res: &ClientResponse, org_uri: Uri) -> Result<Uri, SendRequestErr
Ok(uri) Ok(uri)
} }
fn remove_sensitive_headers(headers: &mut header::HeaderMap, prev_uri: &Uri, next_uri: &Uri) {
if next_uri.host() != prev_uri.host()
|| next_uri.port() != prev_uri.port()
|| next_uri.scheme() != prev_uri.scheme()
{
headers.remove(header::COOKIE);
headers.remove(header::AUTHORIZATION);
headers.remove(header::PROXY_AUTHORIZATION);
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use actix_web::{web, App, Error, HttpResponse}; use actix_web::{web, App, Error, HttpRequest, HttpResponse};
use super::*; use super::*;
use crate::http::HeaderValue;
use crate::ClientBuilder; use crate::ClientBuilder;
use std::str::FromStr;
#[actix_rt::test] #[actix_rt::test]
async fn test_basic_redirect() { async fn test_basic_redirect() {
@ -347,4 +347,239 @@ mod tests {
assert_eq!(res.status().as_u16(), 302); assert_eq!(res.status().as_u16(), 302);
} }
#[actix_rt::test]
async fn test_redirect_status_kind_307_308() {
let srv = actix_test::start(|| {
async fn root() -> HttpResponse {
HttpResponse::TemporaryRedirect()
.append_header(("location", "/test"))
.finish()
}
async fn test(req: HttpRequest, body: Bytes) -> HttpResponse {
if req.method() == Method::POST && !body.is_empty() {
HttpResponse::Ok().finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
App::new()
.service(web::resource("/").route(web::to(root)))
.service(web::resource("/test").route(web::to(test)))
});
let res = srv.post("/").send_body("Hello").await.unwrap();
assert_eq!(res.status().as_u16(), 200);
}
#[actix_rt::test]
async fn test_redirect_status_kind_301_302_303() {
let srv = actix_test::start(|| {
async fn root() -> HttpResponse {
HttpResponse::Found()
.append_header(("location", "/test"))
.finish()
}
async fn test(req: HttpRequest, body: Bytes) -> HttpResponse {
if (req.method() == Method::GET || req.method() == Method::HEAD)
&& body.is_empty()
{
HttpResponse::Ok().finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
App::new()
.service(web::resource("/").route(web::to(root)))
.service(web::resource("/test").route(web::to(test)))
});
let res = srv.post("/").send_body("Hello").await.unwrap();
assert_eq!(res.status().as_u16(), 200);
let res = srv.post("/").send().await.unwrap();
assert_eq!(res.status().as_u16(), 200);
}
#[actix_rt::test]
async fn test_redirect_headers() {
let srv = actix_test::start(|| {
async fn root(req: HttpRequest) -> HttpResponse {
if req
.headers()
.get("custom")
.unwrap_or(&HeaderValue::from_str("").unwrap())
== "value"
{
HttpResponse::Found()
.append_header(("location", "/test"))
.finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
async fn test(req: HttpRequest) -> HttpResponse {
if req
.headers()
.get("custom")
.unwrap_or(&HeaderValue::from_str("").unwrap())
== "value"
{
HttpResponse::Ok().finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
App::new()
.service(web::resource("/").route(web::to(root)))
.service(web::resource("/test").route(web::to(test)))
});
let client = ClientBuilder::new()
.header("custom", "value")
.disable_redirects()
.finish();
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 302);
let client = ClientBuilder::new().header("custom", "value").finish();
let res = client.get(srv.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 200);
let client = ClientBuilder::new().finish();
let res = client
.get(srv.url("/"))
.insert_header(("custom", "value"))
.send()
.await
.unwrap();
assert_eq!(res.status().as_u16(), 200);
}
#[actix_rt::test]
async fn test_redirect_cross_origin_headers() {
// defining two services to have two different origins
let srv2 = actix_test::start(|| {
async fn root(req: HttpRequest) -> HttpResponse {
if req.headers().get(header::AUTHORIZATION).is_none() {
HttpResponse::Ok().finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
App::new().service(web::resource("/").route(web::to(root)))
});
let srv2_port: u16 = srv2.addr().port();
let srv1 = actix_test::start(move || {
async fn root(req: HttpRequest) -> HttpResponse {
let port = *req.app_data::<u16>().unwrap();
if req.headers().get(header::AUTHORIZATION).is_some() {
HttpResponse::Found()
.append_header((
"location",
format!("http://localhost:{}/", port).as_str(),
))
.finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
async fn test1(req: HttpRequest) -> HttpResponse {
if req.headers().get(header::AUTHORIZATION).is_some() {
HttpResponse::Found()
.append_header(("location", "/test2"))
.finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
async fn test2(req: HttpRequest) -> HttpResponse {
if req.headers().get(header::AUTHORIZATION).is_some() {
HttpResponse::Ok().finish()
} else {
HttpResponse::InternalServerError().finish()
}
}
App::new()
.app_data(srv2_port)
.service(web::resource("/").route(web::to(root)))
.service(web::resource("/test1").route(web::to(test1)))
.service(web::resource("/test2").route(web::to(test2)))
});
// send a request to different origins, http://srv1/ then http://srv2/. So it should remove the header
let client = ClientBuilder::new()
.header(header::AUTHORIZATION, "auth_key_value")
.finish();
let res = client.get(srv1.url("/")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 200);
// send a request to same origin, http://srv1/test1 then http://srv1/test2. So it should NOT remove any header
let res = client.get(srv1.url("/test1")).send().await.unwrap();
assert_eq!(res.status().as_u16(), 200);
}
#[actix_rt::test]
async fn test_remove_sensitive_headers() {
fn gen_headers() -> header::HeaderMap {
let mut headers = header::HeaderMap::new();
headers.insert(header::USER_AGENT, HeaderValue::from_str("value").unwrap());
headers.insert(
header::AUTHORIZATION,
HeaderValue::from_str("value").unwrap(),
);
headers.insert(
header::PROXY_AUTHORIZATION,
HeaderValue::from_str("value").unwrap(),
);
headers.insert(header::COOKIE, HeaderValue::from_str("value").unwrap());
headers
}
// Same origin
let prev_uri = Uri::from_str("https://host/path1").unwrap();
let next_uri = Uri::from_str("https://host/path2").unwrap();
let mut headers = gen_headers();
remove_sensitive_headers(&mut headers, &prev_uri, &next_uri);
assert_eq!(headers.len(), 4);
// different schema
let prev_uri = Uri::from_str("http://host/").unwrap();
let next_uri = Uri::from_str("https://host/").unwrap();
let mut headers = gen_headers();
remove_sensitive_headers(&mut headers, &prev_uri, &next_uri);
assert_eq!(headers.len(), 1);
// different host
let prev_uri = Uri::from_str("https://host1/").unwrap();
let next_uri = Uri::from_str("https://host2/").unwrap();
let mut headers = gen_headers();
remove_sensitive_headers(&mut headers, &prev_uri, &next_uri);
assert_eq!(headers.len(), 1);
// different port
let prev_uri = Uri::from_str("https://host:12/").unwrap();
let next_uri = Uri::from_str("https://host:23/").unwrap();
let mut headers = gen_headers();
remove_sensitive_headers(&mut headers, &prev_uri, &next_uri);
assert_eq!(headers.len(), 1);
// different everything!
let prev_uri = Uri::from_str("http://host1:12/path1").unwrap();
let next_uri = Uri::from_str("https://host2:23/path2").unwrap();
let mut headers = gen_headers();
remove_sensitive_headers(&mut headers, &prev_uri, &next_uri);
assert_eq!(headers.len(), 1);
}
} }

View File

@ -1 +1 @@
msrv = "1.46" msrv = "1.51"

View File

@ -4,7 +4,7 @@ digraph {
subgraph cluster_net { subgraph cluster_net {
label="actix-net" label="actix-net"
"actix-codec" "actix-macros" "actix-rt" "actix-server" "actix-service" "actix-codec" "actix-macros" "actix-rt" "actix-server" "actix-service"
"actix-tls" "actix-tracing" "actix-utils" "actix-router" "actix-tls" "actix-tracing" "actix-utils"
} }
subgraph cluster_other { subgraph cluster_other {
@ -25,7 +25,6 @@ digraph {
"actix-tls" -> { "tokio-util" }[color="#009900"] "actix-tls" -> { "tokio-util" }[color="#009900"]
"actix-server" -> { "actix-service" "actix-rt" "actix-utils" "tokio" } "actix-server" -> { "actix-service" "actix-rt" "actix-utils" "tokio" }
"actix-rt" -> { "actix-macros" "tokio" } "actix-rt" -> { "actix-macros" "tokio" }
"actix-router" -> { "bytestring" }
"local-channel" -> { "local-waker" } "local-channel" -> { "local-waker" }

View File

@ -10,6 +10,7 @@ digraph {
"web-actors" "web-actors"
"web-codegen" "web-codegen"
"http-test" "http-test"
"router"
{ rank=same; "multipart" "web-actors" "http-test" }; { rank=same; "multipart" "web-actors" "http-test" };
{ rank=same; "files" "awc" "web" }; { rank=same; "files" "awc" "web" };
@ -36,7 +37,7 @@ digraph {
"rt" -> { "macros" } "rt" -> { "macros" }
{ rank=same; "utils" "codec" }; { rank=same; "utils" "codec" };
{ rank=same; "rt" "macros" "service" "router" }; { rank=same; "rt" "macros" "service" };
// actix // actix

View File

@ -10,9 +10,10 @@ digraph {
"actix-web-codegen" "actix-web-codegen"
"actix-http-test" "actix-http-test"
"actix-test" "actix-test"
"actix-router"
} }
"actix-web" -> { "actix-web-codegen" "actix-http" } "actix-web" -> { "actix-web-codegen" "actix-http" "actix-router" }
"awc" -> { "actix-http" } "awc" -> { "actix-http" }
"actix-web-actors" -> { "actix" "actix-web" "actix-http" } "actix-web-actors" -> { "actix" "actix-web" "actix-http" }
"actix-multipart" -> { "actix-web" } "actix-multipart" -> { "actix-web" }

View File

@ -79,7 +79,7 @@ where
.into_iter() .into_iter()
.for_each(|mut srv| srv.register(&mut config)); .for_each(|mut srv| srv.register(&mut config));
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
let (config, services) = config.into_services(); let (config, services) = config.into_services();
@ -104,7 +104,7 @@ where
// complete ResourceMap tree creation // complete ResourceMap tree creation
let rmap = Rc::new(rmap); let rmap = Rc::new(rmap);
rmap.finish(rmap.clone()); ResourceMap::finish(&rmap);
// construct all async data factory futures // construct all async data factory futures
let factory_futs = join_all(self.async_data_factories.iter().map(|f| f())); let factory_futs = join_all(self.async_data_factories.iter().map(|f| f()));

View File

@ -120,7 +120,6 @@ where
} }
impl<T: ?Sized + 'static> FromRequest for Data<T> { impl<T: ?Sized + 'static> FromRequest for Data<T> {
type Config = ();
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Error>>; type Future = Ready<Result<Self, Error>>;

View File

@ -18,7 +18,7 @@ pub use actix_http::body::{AnyBody, Body, BodySize, MessageBody, ResponseBody, S
#[cfg(feature = "__compress")] #[cfg(feature = "__compress")]
pub use actix_http::encoding::Decoder as Decompress; pub use actix_http::encoding::Decoder as Decompress;
pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, ResponseHead}; pub use actix_http::{Extensions, Payload, PayloadStream, RequestHead, Response, ResponseHead};
pub use actix_router::{Path, ResourceDef, ResourcePath, Url}; pub use actix_router::{Path, ResourceDef, ResourcePath, Url};
pub use actix_server::Server; pub use actix_server::Server;
pub use actix_service::{ pub use actix_service::{
@ -26,7 +26,7 @@ pub use actix_service::{
}; };
use crate::http::header::ContentEncoding; use crate::http::header::ContentEncoding;
use actix_http::{Response, ResponseBuilder}; use actix_http::ResponseBuilder;
use actix_router::Patterns; use actix_router::Patterns;

View File

@ -13,13 +13,42 @@ use futures_core::ready;
use crate::{dev::Payload, Error, HttpRequest}; use crate::{dev::Payload, Error, HttpRequest};
/// Trait implemented by types that can be extracted from request. /// A type that implements [`FromRequest`] is called an **extractor** and can extract data
/// from the request. Examples of types that implement this trait are [`Json`], [`Form`], [`Path`].
/// ///
/// Types that implement this trait can be used with `Route` handlers. /// An extractor can be customized by injecting the corresponding configuration with one of:
///
/// - [`App::app_data()`](`crate::App::app_data`)
/// - [`Scope::app_data()`](`crate::Scope::app_data`)
/// - [`Resource::app_data()`](`crate::Resource::app_data`)
///
/// Here are some built-in extractors and their corresponding configuration.
/// Please refer to the respective documentation for details.
///
/// | Extractor | Configuration |
/// |-------------|-------------------|
/// | [`Json`] | [`JsonConfig`] |
/// | [`Form`] | [`FormConfig`] |
/// | [`Path`] | [`PathConfig`] |
/// | [`Query`] | [`QueryConfig`] |
/// | [`Payload`] | [`PayloadConfig`] |
/// | [`String`] | [`PayloadConfig`] |
/// | [`Bytes`] | [`PayloadConfig`] |
///
/// [`Json`]: crate::web::Json
/// [`JsonConfig`]: crate::web::JsonConfig
/// [`Form`]: crate::web::Form
/// [`FormConfig`]: crate::web::FormConfig
/// [`Path`]: crate::web::Path
/// [`PathConfig`]: crate::web::PathConfig
/// [`Query`]: crate::web::Query
/// [`QueryConfig`]: crate::web::QueryConfig
/// [`Payload`]: crate::web::Payload
/// [`PayloadConfig`]: crate::web::PayloadConfig
/// [`String`]: FromRequest#impl-FromRequest-for-String
/// [`Bytes`]: crate::web::Bytes#impl-FromRequest
#[cfg_attr(docsrs, doc(alias = "Extractor"))]
pub trait FromRequest: Sized { pub trait FromRequest: Sized {
/// Configuration for this extractor.
type Config: Default + 'static;
/// The associated error which can be returned. /// The associated error which can be returned.
type Error: Into<Error>; type Error: Into<Error>;
@ -35,14 +64,6 @@ pub trait FromRequest: Sized {
fn extract(req: &HttpRequest) -> Self::Future { fn extract(req: &HttpRequest) -> Self::Future {
Self::from_request(req, &mut Payload::None) Self::from_request(req, &mut Payload::None)
} }
/// Create and configure config instance.
fn configure<F>(f: F) -> Self::Config
where
F: FnOnce(Self::Config) -> Self::Config,
{
f(Self::Config::default())
}
} }
/// Optionally extract a field from the request /// Optionally extract a field from the request
@ -65,7 +86,6 @@ pub trait FromRequest: Sized {
/// impl FromRequest for Thing { /// impl FromRequest for Thing {
/// type Error = Error; /// type Error = Error;
/// type Future = Ready<Result<Self, Self::Error>>; /// type Future = Ready<Result<Self, Self::Error>>;
/// type Config = ();
/// ///
/// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
/// if rand::random() { /// if rand::random() {
@ -100,7 +120,6 @@ where
{ {
type Error = Error; type Error = Error;
type Future = FromRequestOptFuture<T::Future>; type Future = FromRequestOptFuture<T::Future>;
type Config = T::Config;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
@ -156,7 +175,6 @@ where
/// impl FromRequest for Thing { /// impl FromRequest for Thing {
/// type Error = Error; /// type Error = Error;
/// type Future = Ready<Result<Thing, Error>>; /// type Future = Ready<Result<Thing, Error>>;
/// type Config = ();
/// ///
/// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { /// fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
/// if rand::random() { /// if rand::random() {
@ -189,7 +207,6 @@ where
{ {
type Error = Error; type Error = Error;
type Future = FromRequestResFuture<T::Future>; type Future = FromRequestResFuture<T::Future>;
type Config = T::Config;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
@ -233,7 +250,6 @@ where
impl FromRequest for Uri { impl FromRequest for Uri {
type Error = Infallible; type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.uri().clone()) ok(req.uri().clone())
@ -255,7 +271,6 @@ impl FromRequest for Uri {
impl FromRequest for Method { impl FromRequest for Method {
type Error = Infallible; type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.method().clone()) ok(req.method().clone())
@ -266,7 +281,6 @@ impl FromRequest for Method {
impl FromRequest for () { impl FromRequest for () {
type Error = Infallible; type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(_: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(()) ok(())
@ -306,7 +320,6 @@ macro_rules! tuple_from_req ({$fut_type:ident, $(($n:tt, $T:ident)),+} => {
{ {
type Error = Error; type Error = Error;
type Future = $fut_type<$($T),+>; type Future = $fut_type<$($T),+>;
type Config = ($($T::Config),+);
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
$fut_type { $fut_type {

View File

@ -1,10 +1,10 @@
//! # References //! # References
//! //!
//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt //! "The Content-Disposition Header Field" <https://www.ietf.org/rfc/rfc2183.txt>
//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt //! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" <https://www.ietf.org/rfc/rfc6266.txt>
//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt //! "Returning Values from Forms: multipart/form-data" <https://www.ietf.org/rfc/rfc7578.txt>
//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/ //! Browser conformance tests at: <http://greenbytes.de/tech/tc2231/>
//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml //! IANA assignment: <http://www.iana.org/assignments/cont-disp/cont-disp.xhtml>
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
@ -457,7 +457,7 @@ impl Header for ContentDisposition {
fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> { fn parse<T: crate::HttpMessage>(msg: &T) -> Result<Self, crate::error::ParseError> {
if let Some(h) = msg.headers().get(&Self::name()) { if let Some(h) = msg.headers().get(&Self::name()) {
Self::from_raw(&h) Self::from_raw(h)
} else { } else {
Err(crate::error::ParseError::Header) Err(crate::error::ParseError::Header)
} }

View File

@ -209,7 +209,6 @@ impl ConnectionInfo {
impl FromRequest for ConnectionInfo { impl FromRequest for ConnectionInfo {
type Error = Infallible; type Error = Infallible;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ok(req.connection_info().clone()) ok(req.connection_info().clone())
@ -252,7 +251,6 @@ impl ResponseError for MissingPeerAddr {}
impl FromRequest for PeerAddr { impl FromRequest for PeerAddr {
type Error = MissingPeerAddr; type Error = MissingPeerAddr;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
match req.peer_addr() { match req.peer_addr() {

View File

@ -53,7 +53,7 @@
//! * SSL support using OpenSSL or Rustls //! * SSL support using OpenSSL or Rustls
//! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) //! * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
//! * Includes an async [HTTP client](https://docs.rs/awc/) //! * Includes an async [HTTP client](https://docs.rs/awc/)
//! * Runs on stable Rust 1.46+ //! * Runs on stable Rust 1.51+
//! //!
//! # Crate Features //! # Crate Features
//! * `cookies` - cookies support (enabled by default) //! * `cookies` - cookies support (enabled by default)
@ -96,7 +96,6 @@ pub mod test;
pub(crate) mod types; pub(crate) mod types;
pub mod web; pub mod web;
pub use actix_http::Response as BaseHttpResponse;
pub use actix_http::{body, HttpMessage}; pub use actix_http::{body, HttpMessage};
#[doc(inline)] #[doc(inline)]
pub use actix_rt as rt; pub use actix_rt as rt;

View File

@ -2,10 +2,10 @@
use std::{ use std::{
cmp, cmp,
convert::TryFrom,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
pin::Pin, pin::Pin,
str::FromStr,
task::{Context, Poll}, task::{Context, Poll},
}; };
@ -13,16 +13,18 @@ use actix_http::{
body::{MessageBody, ResponseBody}, body::{MessageBody, ResponseBody},
encoding::Encoder, encoding::Encoder,
http::header::{ContentEncoding, ACCEPT_ENCODING}, http::header::{ContentEncoding, ACCEPT_ENCODING},
StatusCode,
}; };
use actix_service::{Service, Transform}; use actix_service::{Service, Transform};
use actix_utils::future::{ok, Ready}; use actix_utils::future::{ok, Either, Ready};
use futures_core::ready; use futures_core::ready;
use once_cell::sync::Lazy;
use pin_project::pin_project; use pin_project::pin_project;
use crate::{ use crate::{
dev::BodyEncoding, dev::BodyEncoding,
service::{ServiceRequest, ServiceResponse}, service::{ServiceRequest, ServiceResponse},
Error, Error, HttpResponse,
}; };
/// Middleware for compressing response payloads. /// Middleware for compressing response payloads.
@ -78,34 +80,78 @@ pub struct CompressMiddleware<S> {
encoding: ContentEncoding, encoding: ContentEncoding,
} }
static SUPPORTED_ALGORITHM_NAMES: Lazy<String> = Lazy::new(|| {
let mut encoding = 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> impl<S, B> Service<ServiceRequest> for CompressMiddleware<S>
where where
B: MessageBody,
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>, S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = Error>,
B: MessageBody,
{ {
type Response = ServiceResponse<ResponseBody<Encoder<B>>>; type Response = ServiceResponse<ResponseBody<Encoder<B>>>;
type Error = Error; type Error = Error;
type Future = CompressResponse<S, B>; type Future = Either<CompressResponse<S, B>, Ready<Result<Self::Response, Self::Error>>>;
actix_service::forward_ready!(service); actix_service::forward_ready!(service);
#[allow(clippy::borrow_interior_mutable_const)] #[allow(clippy::borrow_interior_mutable_const)]
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
// negotiate content-encoding // negotiate content-encoding
let encoding = if let Some(val) = req.headers().get(&ACCEPT_ENCODING) { let encoding_result = req
if let Ok(enc) = val.to_str() { .headers()
AcceptEncoding::parse(enc, self.encoding) .get(&ACCEPT_ENCODING)
} else { .and_then(|val| val.to_str().ok())
ContentEncoding::Identity .map(|enc| AcceptEncoding::try_parse(enc, self.encoding));
}
} else {
ContentEncoding::Identity
};
CompressResponse { match encoding_result {
// Missing header => fallback to identity
None => Either::left(CompressResponse {
encoding: ContentEncoding::Identity,
fut: self.service.call(req),
_phantom: PhantomData,
}),
// Valid encoding
Some(Ok(encoding)) => Either::left(CompressResponse {
encoding, encoding,
fut: self.service.call(req), fut: self.service.call(req),
_phantom: PhantomData, _phantom: PhantomData,
}),
// There is an HTTP header but we cannot match what client as asked for
Some(Err(_)) => {
let res = HttpResponse::with_body(
StatusCode::NOT_ACCEPTABLE,
SUPPORTED_ALGORITHM_NAMES.as_str(),
);
let enc = ContentEncoding::Identity;
Either::right(ok(req.into_response(res.map_body(move |head, body| {
Encoder::response(enc, head, ResponseBody::Other(body.into()))
}))))
}
} }
} }
} }
@ -114,7 +160,6 @@ where
pub struct CompressResponse<S, B> pub struct CompressResponse<S, B>
where where
S: Service<ServiceRequest>, S: Service<ServiceRequest>,
B: MessageBody,
{ {
#[pin] #[pin]
fut: S::Future, fut: S::Future,
@ -151,6 +196,7 @@ where
struct AcceptEncoding { struct AcceptEncoding {
encoding: ContentEncoding, encoding: ContentEncoding,
// TODO: use Quality or QualityItem<ContentEncoding>
quality: f64, quality: f64,
} }
@ -177,26 +223,56 @@ impl PartialOrd for AcceptEncoding {
impl PartialEq for AcceptEncoding { impl PartialEq for AcceptEncoding {
fn eq(&self, other: &AcceptEncoding) -> bool { fn eq(&self, other: &AcceptEncoding) -> bool {
self.quality == other.quality 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);
}
}
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 { impl AcceptEncoding {
fn new(tag: &str) -> Option<AcceptEncoding> { fn new(tag: &str) -> Option<AcceptEncoding> {
let parts: Vec<&str> = tag.split(';').collect(); let parts: Vec<&str> = tag.split(';').collect();
let encoding = match parts.len() { let encoding = match parts.len() {
0 => return None, 0 => return None,
_ => ContentEncoding::from(parts[0]), _ => match ContentEncoding::try_from(parts[0]) {
}; Err(_) => return None,
let quality = match parts.len() { Ok(x) => x,
1 => encoding.quality(), },
_ => f64::from_str(parts[1]).unwrap_or(0.0),
}; };
let quality = parse_quality(&parts[1..]);
if quality <= 0.0 || quality > 1.0 {
return None;
}
Some(AcceptEncoding { encoding, quality }) Some(AcceptEncoding { encoding, quality })
} }
/// Parse a raw Accept-Encoding header value into an ordered list. /// Parse a raw Accept-Encoding header value into an ordered list then return the best match
pub fn parse(raw: &str, encoding: ContentEncoding) -> ContentEncoding { /// based on middleware configuration.
pub fn try_parse(
raw: &str,
encoding: ContentEncoding,
) -> Result<ContentEncoding, AcceptEncodingError> {
let mut encodings = raw let mut encodings = raw
.replace(' ', "") .replace(' ', "")
.split(',') .split(',')
@ -206,13 +282,90 @@ impl AcceptEncoding {
encodings.sort(); encodings.sort();
for enc in encodings { for enc in encodings {
if encoding == ContentEncoding::Auto { if encoding == ContentEncoding::Auto || encoding == enc.encoding {
return enc.encoding; return Ok(enc.encoding);
} else if encoding == enc.encoding {
return encoding;
} }
} }
ContentEncoding::Identity // 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(test)]
mod tests {
use super::*;
macro_rules! assert_parse_eq {
($raw:expr, $result:expr) => {
assert_eq!(
AcceptEncoding::try_parse($raw, ContentEncoding::Auto),
Ok($result)
);
};
}
macro_rules! assert_parse_fail {
($raw:expr) => {
assert!(AcceptEncoding::try_parse($raw, ContentEncoding::Auto).is_err());
};
}
#[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);
// Test space, trim, missing values
assert_parse_eq!("br,,,,", ContentEncoding::Br);
assert_parse_eq!("gzip , br, zstd", ContentEncoding::Gzip);
// 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");
} }
} }

View File

@ -18,7 +18,7 @@ use bytes::Bytes;
use futures_core::ready; use futures_core::ready;
use log::{debug, warn}; use log::{debug, warn};
use regex::{Regex, RegexSet}; use regex::{Regex, RegexSet};
use time::OffsetDateTime; use time::{format_description::well_known::Rfc3339, OffsetDateTime};
use crate::{ use crate::{
dev::{BodySize, MessageBody}, dev::{BodySize, MessageBody},
@ -341,7 +341,6 @@ where
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
let this = self.project(); let this = self.project();
// TODO: MSRV 1.51: poll_map_err
match ready!(this.body.poll_next(cx)) { match ready!(this.body.poll_next(cx)) {
Some(Ok(chunk)) => { Some(Ok(chunk)) => {
*this.size += chunk.len(); *this.size += chunk.len();
@ -539,7 +538,7 @@ impl FormatText {
}; };
} }
FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()), FormatText::UrlPath => *self = FormatText::Str(req.path().to_string()),
FormatText::RequestTime => *self = FormatText::Str(now.format("%Y-%m-%dT%H:%M:%S")), FormatText::RequestTime => *self = FormatText::Str(now.format(&Rfc3339).unwrap()),
FormatText::RequestHeader(ref name) => { FormatText::RequestHeader(ref name) => {
let s = if let Some(val) = req.headers().get(name) { let s = if let Some(val) = req.headers().get(name) {
if let Ok(s) = val.to_str() { if let Ok(s) = val.to_str() {
@ -553,7 +552,7 @@ impl FormatText {
*self = FormatText::Str(s.to_string()); *self = FormatText::Str(s.to_string());
} }
FormatText::RemoteAddr => { FormatText::RemoteAddr => {
let s = if let Some(ref peer) = req.connection_info().remote_addr() { let s = if let Some(peer) = req.connection_info().remote_addr() {
FormatText::Str((*peer).to_string()) FormatText::Str((*peer).to_string())
} else { } else {
FormatText::Str("-".to_string()) FormatText::Str("-".to_string())
@ -768,7 +767,7 @@ mod tests {
Ok(()) Ok(())
}; };
let s = format!("{}", FormatDisplay(&render)); let s = format!("{}", FormatDisplay(&render));
assert!(s.contains(&now.format("%Y-%m-%dT%H:%M:%S"))); assert!(s.contains(&now.format(&Rfc3339).unwrap()));
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -19,3 +19,43 @@ mod compress;
#[cfg(feature = "__compress")] #[cfg(feature = "__compress")]
pub use self::compress::Compress; pub use self::compress::Compress;
#[cfg(test)]
mod tests {
use crate::{http::StatusCode, App};
use super::*;
#[test]
fn common_combinations() {
// ensure there's no reason that the built-in middleware cannot compose
let _ = App::new()
.wrap(Compat::new(Logger::default()))
.wrap(Condition::new(true, DefaultHeaders::new()))
.wrap(DefaultHeaders::new().header("X-Test2", "X-Value2"))
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
Ok(ErrorHandlerResponse::Response(res))
}))
.wrap(Logger::default())
.wrap(NormalizePath::new(TrailingSlash::Trim));
let _ = App::new()
.wrap(NormalizePath::new(TrailingSlash::Trim))
.wrap(Logger::default())
.wrap(ErrorHandlers::new().handler(StatusCode::FORBIDDEN, |res| {
Ok(ErrorHandlerResponse::Response(res))
}))
.wrap(DefaultHeaders::new().header("X-Test2", "X-Value2"))
.wrap(Condition::new(true, DefaultHeaders::new()))
.wrap(Compat::new(Logger::default()));
#[cfg(feature = "__compress")]
{
let _ = App::new().wrap(Compress::default()).wrap(Logger::default());
let _ = App::new().wrap(Logger::default()).wrap(Compress::default());
let _ = App::new().wrap(Compat::new(Compress::default()));
let _ = App::new().wrap(Condition::new(true, Compat::new(Compress::default())));
}
}
}

View File

@ -59,7 +59,7 @@ impl Default for TrailingSlash {
/// ///
/// # actix_web::rt::System::new().block_on(async { /// # actix_web::rt::System::new().block_on(async {
/// let app = App::new() /// let app = App::new()
/// .wrap(middleware::NormalizePath::default()) /// .wrap(middleware::NormalizePath::trim())
/// .route("/test", web::get().to(|| async { "test" })) /// .route("/test", web::get().to(|| async { "test" }))
/// .route("/unmatchable/", web::get().to(|| async { "unmatchable" })); /// .route("/unmatchable/", web::get().to(|| async { "unmatchable" }));
/// ///
@ -85,13 +85,31 @@ impl Default for TrailingSlash {
/// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// # }) /// # })
/// ``` /// ```
#[derive(Debug, Clone, Copy, Default)] #[derive(Debug, Clone, Copy)]
pub struct NormalizePath(TrailingSlash); pub struct NormalizePath(TrailingSlash);
impl Default for NormalizePath {
fn default() -> Self {
log::warn!(
"`NormalizePath::default()` is deprecated. The default trailing slash behavior changed \
in v4 from `Always` to `Trim`. Update your call to `NormalizePath::new(...)`."
);
Self(TrailingSlash::Trim)
}
}
impl NormalizePath { impl NormalizePath {
/// Create new `NormalizePath` middleware with the specified trailing slash style. /// Create new `NormalizePath` middleware with the specified trailing slash style.
pub fn new(trailing_slash_style: TrailingSlash) -> Self { pub fn new(trailing_slash_style: TrailingSlash) -> Self {
NormalizePath(trailing_slash_style) Self(trailing_slash_style)
}
/// Constructs a new `NormalizePath` middleware with [trim](TrailingSlash::Trim) semantics.
///
/// Use this instead of `NormalizePath::default()` to avoid deprecation warning.
pub fn trim() -> Self {
Self::new(TrailingSlash::Trim)
} }
} }

View File

@ -184,7 +184,7 @@ impl HttpRequest {
U: IntoIterator<Item = I>, U: IntoIterator<Item = I>,
I: AsRef<str>, I: AsRef<str>,
{ {
self.resource_map().url_for(&self, name, elements) self.resource_map().url_for(self, name, elements)
} }
/// Generate url for named resource /// Generate url for named resource
@ -199,7 +199,7 @@ impl HttpRequest {
#[inline] #[inline]
/// Get a reference to a `ResourceMap` of current application. /// Get a reference to a `ResourceMap` of current application.
pub fn resource_map(&self) -> &ResourceMap { pub fn resource_map(&self) -> &ResourceMap {
&self.app_state().rmap() self.app_state().rmap()
} }
/// Peer socket address. /// Peer socket address.
@ -358,7 +358,6 @@ impl Drop for HttpRequest {
/// } /// }
/// ``` /// ```
impl FromRequest for HttpRequest { impl FromRequest for HttpRequest {
type Config = ();
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Error>>; type Future = Ready<Result<Self, Error>>;
@ -511,7 +510,7 @@ mod tests {
let mut res = ResourceDef::new("/user/{name}.{ext}"); let mut res = ResourceDef::new("/user/{name}.{ext}");
res.set_name("index"); res.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut res, None); rmap.add(&mut res, None);
assert!(rmap.has_resource("/user/test.html")); assert!(rmap.has_resource("/user/test.html"));
assert!(!rmap.has_resource("/test/unknown")); assert!(!rmap.has_resource("/test/unknown"));
@ -541,7 +540,7 @@ mod tests {
let mut rdef = ResourceDef::new("/index.html"); let mut rdef = ResourceDef::new("/index.html");
rdef.set_name("index"); rdef.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
assert!(rmap.has_resource("/index.html")); assert!(rmap.has_resource("/index.html"));
@ -562,7 +561,7 @@ mod tests {
let mut rdef = ResourceDef::new("/index.html"); let mut rdef = ResourceDef::new("/index.html");
rdef.set_name("index"); rdef.set_name("index");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
assert!(rmap.has_resource("/index.html")); assert!(rmap.has_resource("/index.html"));
@ -581,9 +580,8 @@ mod tests {
rdef.set_name("youtube"); rdef.set_name("youtube");
let mut rmap = ResourceMap::new(ResourceDef::new("")); let mut rmap = ResourceMap::new(ResourceDef::prefix(""));
rmap.add(&mut rdef, None); rmap.add(&mut rdef, None);
assert!(rmap.has_resource("https://youtube.com/watch/unknown"));
let req = TestRequest::default().rmap(rmap).to_http_request(); let req = TestRequest::default().rmap(rmap).to_http_request();
let url = req.url_for("youtube", &["oHg5SJYRHA0"]); let url = req.url_for("youtube", &["oHg5SJYRHA0"]);

View File

@ -64,7 +64,6 @@ impl<T: Clone + 'static> Deref for ReqData<T> {
} }
impl<T: Clone + 'static> FromRequest for ReqData<T> { impl<T: Clone + 'static> FromRequest for ReqData<T> {
type Config = ();
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Error>>; type Future = Ready<Result<Self, Error>>;

View File

@ -270,7 +270,7 @@ pub(crate) mod tests {
impl BodyTest for Body { impl BodyTest for Body {
fn bin_ref(&self) -> &[u8] { fn bin_ref(&self) -> &[u8] {
match self { match self {
Body::Bytes(ref bin) => &bin, Body::Bytes(ref bin) => bin,
_ => unreachable!("bug in test impl"), _ => unreachable!("bug in test impl"),
} }
} }
@ -283,11 +283,11 @@ pub(crate) mod tests {
fn bin_ref(&self) -> &[u8] { fn bin_ref(&self) -> &[u8] {
match self { match self {
ResponseBody::Body(ref b) => match b { ResponseBody::Body(ref b) => match b {
Body::Bytes(ref bin) => &bin, Body::Bytes(ref bin) => bin,
_ => unreachable!("bug in test impl"), _ => unreachable!("bug in test impl"),
}, },
ResponseBody::Other(ref b) => match b { ResponseBody::Other(ref b) => match b {
Body::Bytes(ref bin) => &bin, Body::Bytes(ref bin) => bin,
_ => unreachable!("bug in test impl"), _ => unreachable!("bug in test impl"),
}, },
} }

View File

@ -10,43 +10,75 @@ use crate::request::HttpRequest;
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
pub struct ResourceMap { pub struct ResourceMap {
root: ResourceDef, pattern: ResourceDef,
/// Named resources within the tree or, for external resources,
/// it points to isolated nodes outside the tree.
named: AHashMap<String, Rc<ResourceMap>>,
parent: RefCell<Weak<ResourceMap>>, parent: RefCell<Weak<ResourceMap>>,
named: AHashMap<String, ResourceDef>,
patterns: Vec<(ResourceDef, Option<Rc<ResourceMap>>)>, /// Must be `None` for "edge" nodes.
nodes: Option<Vec<Rc<ResourceMap>>>,
} }
impl ResourceMap { impl ResourceMap {
/// Creates a _container_ node in the `ResourceMap` tree.
pub fn new(root: ResourceDef) -> Self { pub fn new(root: ResourceDef) -> Self {
ResourceMap { ResourceMap {
root, pattern: root,
parent: RefCell::new(Weak::new()),
named: AHashMap::default(), named: AHashMap::default(),
patterns: Vec::new(), parent: RefCell::new(Weak::new()),
nodes: Some(Vec::new()),
} }
} }
/// Adds a (possibly nested) resource.
///
/// To add a non-prefix pattern, `nested` must be `None`.
/// To add external resource, supply a pattern without a leading `/`.
/// The root pattern of `nested`, if present, should match `pattern`.
pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) { pub fn add(&mut self, pattern: &mut ResourceDef, nested: Option<Rc<ResourceMap>>) {
pattern.set_id(self.patterns.len() as u16); pattern.set_id(self.nodes.as_ref().unwrap().len() as u16);
self.patterns.push((pattern.clone(), nested));
if let Some(new_node) = nested {
assert_eq!(&new_node.pattern, pattern, "`patern` and `nested` mismatch");
self.named.extend(new_node.named.clone().into_iter());
self.nodes.as_mut().unwrap().push(new_node);
} else {
let new_node = Rc::new(ResourceMap {
pattern: pattern.clone(),
named: AHashMap::default(),
parent: RefCell::new(Weak::new()),
nodes: None,
});
if let Some(name) = pattern.name() { if let Some(name) = pattern.name() {
self.named.insert(name.to_owned(), pattern.clone()); self.named.insert(name.to_owned(), Rc::clone(&new_node));
}
let is_external = match pattern.pattern() {
Some(p) => !p.is_empty() && !p.starts_with('/'),
None => false,
};
// Don't add external resources to the tree
if !is_external {
self.nodes.as_mut().unwrap().push(new_node);
}
} }
} }
pub(crate) fn finish(&self, current: Rc<ResourceMap>) { pub(crate) fn finish(self: &Rc<Self>) {
for (_, nested) in &self.patterns { for node in self.nodes.iter().flatten() {
if let Some(ref nested) = nested { node.parent.replace(Rc::downgrade(self));
*nested.parent.borrow_mut() = Rc::downgrade(&current); ResourceMap::finish(node);
nested.finish(nested.clone());
}
} }
} }
/// Generate url for named resource /// Generate url for named resource
/// ///
/// Check [`HttpRequest::url_for()`](../struct.HttpRequest.html#method. /// Check [`HttpRequest::url_for`] for detailed information.
/// url_for) for detailed information.
pub fn url_for<U, I>( pub fn url_for<U, I>(
&self, &self,
req: &HttpRequest, req: &HttpRequest,
@ -57,10 +89,19 @@ impl ResourceMap {
U: IntoIterator<Item = I>, U: IntoIterator<Item = I>,
I: AsRef<str>, I: AsRef<str>,
{ {
let mut path = String::new();
let mut elements = elements.into_iter(); let mut elements = elements.into_iter();
if self.patterns_for(name, &mut path, &mut elements)?.is_some() { let path = self
.named
.get(name)
.ok_or(UrlGenerationError::ResourceNotFound)?
.root_rmap_fn(String::with_capacity(24), |mut acc, node| {
node.pattern
.resource_path_from_iter(&mut acc, &mut elements)
.then(|| acc)
})
.ok_or(UrlGenerationError::NotEnoughElements)?;
if path.starts_with('/') { if path.starts_with('/') {
let conn = req.connection_info(); let conn = req.connection_info();
Ok(Url::parse(&format!( Ok(Url::parse(&format!(
@ -72,182 +113,73 @@ impl ResourceMap {
} else { } else {
Ok(Url::parse(&path)?) Ok(Url::parse(&path)?)
} }
} else {
Err(UrlGenerationError::ResourceNotFound)
}
} }
pub fn has_resource(&self, path: &str) -> bool { pub fn has_resource(&self, path: &str) -> bool {
let path = if path.is_empty() { "/" } else { path }; self.find_matching_node(path).is_some()
for (pattern, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if let Some(pat_len) = pattern.find_match(path) {
return rmap.has_resource(&path[pat_len..]);
}
} else if pattern.is_match(path) || pattern.pattern() == Some("") && path == "/" {
return true;
}
}
false
} }
/// Returns the name of the route that matches the given path or None if no full match /// Returns the name of the route that matches the given path or None if no full match
/// is possible. /// is possible or the matching resource is not named.
pub fn match_name(&self, path: &str) -> Option<&str> { pub fn match_name(&self, path: &str) -> Option<&str> {
let path = if path.is_empty() { "/" } else { path }; self.find_matching_node(path)?.pattern.name()
for (pattern, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if let Some(plen) = pattern.find_match(path) {
return rmap.match_name(&path[plen..]);
}
} else if pattern.is_match(path) {
return pattern.name();
}
}
None
} }
/// Returns the full resource pattern matched against a path or None if no full match /// Returns the full resource pattern matched against a path or None if no full match
/// is possible. /// is possible.
pub fn match_pattern(&self, path: &str) -> Option<String> { pub fn match_pattern(&self, path: &str) -> Option<String> {
let path = if path.is_empty() { "/" } else { path }; self.find_matching_node(path)?.root_rmap_fn(
String::with_capacity(24),
// ensure a full match exists |mut acc, node| {
if !self.has_resource(path) { acc.push_str(node.pattern.pattern()?);
return None; Some(acc)
},
)
} }
Some(self.traverse_resource_pattern(path)) fn find_matching_node(&self, path: &str) -> Option<&ResourceMap> {
self._find_matching_node(path).flatten()
} }
/// Takes remaining path and tries to match it up against a resource definition within the /// Returns `None` if root pattern doesn't match;
/// current resource map recursively, returning a concatenation of all resource prefixes and /// `Some(None)` if root pattern matches but there is no matching child pattern.
/// patterns matched in the tree. /// Don't search sideways when `Some(none)` is returned.
/// fn _find_matching_node(&self, path: &str) -> Option<Option<&ResourceMap>> {
/// Should only be used after checking the resource exists in the map so that partial match let matched_len = self.pattern.find_match(path)?;
/// patterns are not returned. let path = &path[matched_len..];
fn traverse_resource_pattern(&self, remaining: &str) -> String {
for (pattern, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if let Some(prefix_len) = pattern.find_match(remaining) {
// TODO: think about unwrap_or
let prefix = pattern.pattern().unwrap_or("").to_owned();
return [ Some(match &self.nodes {
prefix, // find first sub-node to match remaining path
rmap.traverse_resource_pattern(&remaining[prefix_len..]), Some(nodes) => nodes
] .iter()
.concat(); .filter_map(|node| node._find_matching_node(path))
} .next()
} else if pattern.is_match(remaining) { .flatten(),
// TODO: think about unwrap_or
return pattern.pattern().unwrap_or("").to_owned(); // only terminate at edge nodes
} None => Some(self),
})
} }
String::new() /// Find `self`'s highest ancestor and then run `F`, providing `B`, in that rmap context.
} fn root_rmap_fn<F, B>(&self, init: B, mut f: F) -> Option<B>
fn patterns_for<U, I>(
&self,
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where where
U: Iterator<Item = I>, F: FnMut(B, &ResourceMap) -> Option<B>,
I: AsRef<str>,
{ {
if self.pattern_for(name, path, elements)?.is_some() { self._root_rmap_fn(init, &mut f)
Ok(Some(()))
} else {
self.parent_pattern_for(name, path, elements)
}
} }
fn pattern_for<U, I>( /// Run `F`, providing `B`, if `self` is top-level resource map, else recurse to parent map.
&self, fn _root_rmap_fn<F, B>(&self, init: B, f: &mut F) -> Option<B>
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where where
U: Iterator<Item = I>, F: FnMut(B, &ResourceMap) -> Option<B>,
I: AsRef<str>,
{ {
if let Some(pattern) = self.named.get(name) { let data = match self.parent.borrow().upgrade() {
if pattern Some(ref parent) => parent._root_rmap_fn(init, f)?,
.pattern() None => init,
.map(|pat| pat.starts_with('/')) };
.unwrap_or(false)
{
self.fill_root(path, elements)?;
}
if pattern.resource_path_from_iter(path, elements) { f(data, self)
Ok(Some(()))
} else {
Err(UrlGenerationError::NotEnoughElements)
}
} else {
for (_, rmap) in &self.patterns {
if let Some(ref rmap) = rmap {
if rmap.pattern_for(name, path, elements)?.is_some() {
return Ok(Some(()));
}
}
}
Ok(None)
}
}
fn fill_root<U, I>(
&self,
path: &mut String,
elements: &mut U,
) -> Result<(), UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
if let Some(ref parent) = self.parent.borrow().upgrade() {
parent.fill_root(path, elements)?;
}
if self.root.resource_path_from_iter(path, elements) {
Ok(())
} else {
Err(UrlGenerationError::NotEnoughElements)
}
}
fn parent_pattern_for<U, I>(
&self,
name: &str,
path: &mut String,
elements: &mut U,
) -> Result<Option<()>, UrlGenerationError>
where
U: Iterator<Item = I>,
I: AsRef<str>,
{
if let Some(ref parent) = self.parent.borrow().upgrade() {
if let Some(pattern) = parent.named.get(name) {
self.fill_root(path, elements)?;
if pattern.resource_path_from_iter(path, elements) {
Ok(Some(()))
} else {
Err(UrlGenerationError::NotEnoughElements)
}
} else {
parent.parent_pattern_for(name, path, elements)
}
} else {
Ok(None)
}
} }
} }
@ -259,7 +191,7 @@ mod tests {
fn extract_matched_pattern() { fn extract_matched_pattern() {
let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None);
@ -275,9 +207,10 @@ mod tests {
&mut ResourceDef::root_prefix("/user/{id}"), &mut ResourceDef::root_prefix("/user/{id}"),
Some(Rc::new(user_map)), Some(Rc::new(user_map)),
); );
root.add(&mut ResourceDef::new("/info"), None);
let root = Rc::new(root); let root = Rc::new(root);
root.finish(Rc::clone(&root)); ResourceMap::finish(&root);
// sanity check resource map setup // sanity check resource map setup
@ -288,7 +221,7 @@ mod tests {
assert!(root.has_resource("/v2")); assert!(root.has_resource("/v2"));
assert!(!root.has_resource("/v33")); assert!(!root.has_resource("/v33"));
assert!(root.has_resource("/user/22")); assert!(!root.has_resource("/user/22"));
assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/"));
assert!(root.has_resource("/user/22/profile")); assert!(root.has_resource("/user/22/profile"));
@ -336,7 +269,7 @@ mod tests {
rdef.set_name("root_info"); rdef.set_name("root_info");
root.add(&mut rdef, None); root.add(&mut rdef, None);
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
let mut rdef = ResourceDef::new("/"); let mut rdef = ResourceDef::new("/");
user_map.add(&mut rdef, None); user_map.add(&mut rdef, None);
@ -350,14 +283,14 @@ mod tests {
); );
let root = Rc::new(root); let root = Rc::new(root);
root.finish(Rc::clone(&root)); ResourceMap::finish(&root);
// sanity check resource map setup // sanity check resource map setup
assert!(root.has_resource("/info")); assert!(root.has_resource("/info"));
assert!(!root.has_resource("/bar")); assert!(!root.has_resource("/bar"));
assert!(root.has_resource("/user/22")); assert!(!root.has_resource("/user/22"));
assert!(root.has_resource("/user/22/")); assert!(root.has_resource("/user/22/"));
assert!(root.has_resource("/user/22/post/55")); assert!(root.has_resource("/user/22/post/55"));
@ -377,7 +310,7 @@ mod tests {
// ref: https://github.com/actix/actix-web/issues/1582 // ref: https://github.com/actix/actix-web/issues/1582
let mut root = ResourceMap::new(ResourceDef::root_prefix("")); let mut root = ResourceMap::new(ResourceDef::root_prefix(""));
let mut user_map = ResourceMap::new(ResourceDef::root_prefix("")); let mut user_map = ResourceMap::new(ResourceDef::root_prefix("/user/{id}"));
user_map.add(&mut ResourceDef::new("/"), None); user_map.add(&mut ResourceDef::new("/"), None);
user_map.add(&mut ResourceDef::new("/profile"), None); user_map.add(&mut ResourceDef::new("/profile"), None);
user_map.add(&mut ResourceDef::new("/article/{id}"), None); user_map.add(&mut ResourceDef::new("/article/{id}"), None);
@ -393,20 +326,119 @@ mod tests {
); );
let root = Rc::new(root); let root = Rc::new(root);
root.finish(Rc::clone(&root)); ResourceMap::finish(&root);
// check root has no parent // check root has no parent
assert!(root.parent.borrow().upgrade().is_none()); assert!(root.parent.borrow().upgrade().is_none());
// check child has parent reference // check child has parent reference
assert!(root.patterns[0].1.is_some()); assert!(root.nodes.as_ref().unwrap()[0]
.parent
.borrow()
.upgrade()
.is_some());
// check child's parent root id matches root's root id // check child's parent root id matches root's root id
assert_eq!( assert!(Rc::ptr_eq(
root.patterns[0].1.as_ref().unwrap().root.id(), &root.nodes.as_ref().unwrap()[0]
root.root.id() .parent
); .borrow()
.upgrade()
.unwrap(),
&root
));
let output = format!("{:?}", root); let output = format!("{:?}", root);
assert!(output.starts_with("ResourceMap {")); assert!(output.starts_with("ResourceMap {"));
assert!(output.ends_with(" }")); assert!(output.ends_with(" }"));
} }
#[test]
fn short_circuit() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut user_root = ResourceDef::prefix("/user");
let mut user_map = ResourceMap::new(user_root.clone());
user_map.add(&mut ResourceDef::new("/u1"), None);
user_map.add(&mut ResourceDef::new("/u2"), None);
root.add(&mut ResourceDef::new("/user/u3"), None);
root.add(&mut user_root, Some(Rc::new(user_map)));
root.add(&mut ResourceDef::new("/user/u4"), None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(rmap.has_resource("/user/u1"));
assert!(rmap.has_resource("/user/u2"));
assert!(rmap.has_resource("/user/u3"));
assert!(!rmap.has_resource("/user/u4"));
}
#[test]
fn url_for() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut user_scope_rdef = ResourceDef::prefix("/user");
let mut user_scope_map = ResourceMap::new(user_scope_rdef.clone());
let mut user_rdef = ResourceDef::new("/{user_id}");
let mut user_map = ResourceMap::new(user_rdef.clone());
let mut post_rdef = ResourceDef::new("/post/{sub_id}");
post_rdef.set_name("post");
user_map.add(&mut post_rdef, None);
user_scope_map.add(&mut user_rdef, Some(Rc::new(user_map)));
root.add(&mut user_scope_rdef, Some(Rc::new(user_scope_map)));
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
let url = rmap
.url_for(&req, "post", &["u123", "foobar"])
.unwrap()
.to_string();
assert_eq!(url, "http://localhost:8888/user/u123/post/foobar");
assert!(rmap.url_for(&req, "missing", &["u123"]).is_err());
}
#[test]
fn external_resource_with_no_name() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef = ResourceDef::new("https://duck.com/{query}");
root.add(&mut rdef, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(!rmap.has_resource("https://duck.com/abc"));
}
#[test]
fn external_resource_with_name() {
let mut root = ResourceMap::new(ResourceDef::prefix(""));
let mut rdef = ResourceDef::new("https://duck.com/{query}");
rdef.set_name("duck");
root.add(&mut rdef, None);
let rmap = Rc::new(root);
ResourceMap::finish(&rmap);
assert!(!rmap.has_resource("https://duck.com/abc"));
let mut req = crate::test::TestRequest::default();
req.set_server_hostname("localhost:8888");
let req = req.to_http_request();
assert_eq!(
rmap.url_for(&req, "duck", &["abcd"]).unwrap().to_string(),
"https://duck.com/abcd"
);
}
} }

View File

@ -41,9 +41,9 @@ type HttpNewService = BoxServiceFactory<(), ServiceRequest, ServiceResponse, Err
/// fn main() { /// fn main() {
/// let app = App::new().service( /// let app = App::new().service(
/// web::scope("/{project_id}/") /// web::scope("/{project_id}/")
/// .service(web::resource("/path1").to(|| async { HttpResponse::Ok() })) /// .service(web::resource("/path1").to(|| async { "OK" }))
/// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok()))) /// .service(web::resource("/path2").route(web::get().to(|| HttpResponse::Ok())))
/// .service(web::resource("/path3").route(web::head().to(|| HttpResponse::MethodNotAllowed()))) /// .service(web::resource("/path3").route(web::head().to(HttpResponse::MethodNotAllowed)))
/// ); /// );
/// } /// }
/// ``` /// ```
@ -1153,4 +1153,70 @@ mod tests {
Bytes::from_static(b"http://localhost:8080/a/b/c/12345") Bytes::from_static(b"http://localhost:8080/a/b/c/12345")
); );
} }
#[actix_rt::test]
async fn dynamic_scopes() {
let srv = init_service(
App::new().service(
web::scope("/{a}/").service(
web::scope("/{b}/")
.route("", web::get().to(|_: HttpRequest| HttpResponse::Created()))
.route(
"/",
web::get().to(|_: HttpRequest| HttpResponse::Accepted()),
)
.route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())),
),
),
)
.await;
// note the unintuitive behavior with trailing slashes on scopes with dynamic segments
let req = TestRequest::with_uri("/a//b//c").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/a//b/").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let req = TestRequest::with_uri("/a//b//").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let req = TestRequest::with_uri("/a//b//c/d").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = init_service(
App::new().service(
web::scope("/{a}").service(
web::scope("/{b}")
.route("", web::get().to(|_: HttpRequest| HttpResponse::Created()))
.route(
"/",
web::get().to(|_: HttpRequest| HttpResponse::Accepted()),
)
.route("/{c}", web::get().to(|_: HttpRequest| HttpResponse::Ok())),
),
),
)
.await;
let req = TestRequest::with_uri("/a/b/c").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let req = TestRequest::with_uri("/a/b").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::CREATED);
let req = TestRequest::with_uri("/a/b/").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::ACCEPTED);
let req = TestRequest::with_uri("/a/b/c/d").to_request();
let resp = call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
} }

View File

@ -117,7 +117,7 @@ impl ServiceRequest {
/// This method returns reference to the request head /// This method returns reference to the request head
#[inline] #[inline]
pub fn head(&self) -> &RequestHead { pub fn head(&self) -> &RequestHead {
&self.req.head() self.req.head()
} }
/// This method returns reference to the request head /// This method returns reference to the request head
@ -476,7 +476,7 @@ impl WebService {
/// Set service name. /// Set service name.
/// ///
/// Name is used for url generation. /// Name is used for URL generation.
pub fn name(mut self, name: &str) -> Self { pub fn name(mut self, name: &str) -> Self {
self.name = Some(name.to_string()); self.name = Some(name.to_string());
self self

View File

@ -1,6 +1,6 @@
//! Various helpers for Actix applications to use during testing. //! Various helpers for Actix applications to use during testing.
use std::{net::SocketAddr, rc::Rc}; use std::{borrow::Cow, net::SocketAddr, rc::Rc};
pub use actix_http::test::TestBuffer; pub use actix_http::test::TestBuffer;
use actix_http::{ use actix_http::{
@ -56,7 +56,7 @@ pub fn default_service(
/// async fn test_init_service() { /// async fn test_init_service() {
/// let app = test::init_service( /// let app = test::init_service(
/// App::new() /// App::new()
/// .service(web::resource("/test").to(|| async { HttpResponse::Ok() })) /// .service(web::resource("/test").to(|| async { "OK" }))
/// ).await; /// ).await;
/// ///
/// // Create request object /// // Create request object
@ -470,19 +470,31 @@ impl TestRequest {
self self
} }
/// Set request path pattern parameter /// Set request path pattern parameter.
pub fn param(mut self, name: &'static str, value: &'static str) -> Self { ///
/// # Examples
/// ```
/// use actix_web::test::TestRequest;
///
/// let req = TestRequest::default().param("foo", "bar");
/// let req = TestRequest::default().param("foo".to_owned(), "bar".to_owned());
/// ```
pub fn param(
mut self,
name: impl Into<Cow<'static, str>>,
value: impl Into<Cow<'static, str>>,
) -> Self {
self.path.add_static(name, value); self.path.add_static(name, value);
self self
} }
/// Set peer addr /// Set peer addr.
pub fn peer_addr(mut self, addr: SocketAddr) -> Self { pub fn peer_addr(mut self, addr: SocketAddr) -> Self {
self.peer_addr = Some(addr); self.peer_addr = Some(addr);
self self
} }
/// Set request payload /// Set request payload.
pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self { pub fn set_payload<B: Into<Bytes>>(mut self, data: B) -> Self {
self.req.set_payload(data); self.req.set_payload(data);
self self

View File

@ -187,7 +187,6 @@ where
{ {
type Error = EitherExtractError<L::Error, R::Error>; type Error = EitherExtractError<L::Error, R::Error>;
type Future = EitherExtractFut<L, R>; type Future = EitherExtractFut<L, R>;
type Config = ();
fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut dev::Payload) -> Self::Future {
EitherExtractFut { EitherExtractFut {
@ -253,7 +252,7 @@ where
Ok(bytes) => { Ok(bytes) => {
let fallback = bytes.clone(); let fallback = bytes.clone();
let left = let left =
L::from_request(&this.req, &mut payload_from_bytes(bytes)); L::from_request(this.req, &mut payload_from_bytes(bytes));
EitherExtractState::Left { left, fallback } EitherExtractState::Left { left, fallback }
} }
Err(err) => break Err(EitherExtractError::Bytes(err)), Err(err) => break Err(EitherExtractError::Bytes(err)),
@ -265,7 +264,7 @@ where
Ok(extracted) => break Ok(Either::Left(extracted)), Ok(extracted) => break Ok(Either::Left(extracted)),
Err(left_err) => { Err(left_err) => {
let right = R::from_request( let right = R::from_request(
&this.req, this.req,
&mut payload_from_bytes(mem::take(fallback)), &mut payload_from_bytes(mem::take(fallback)),
); );
EitherExtractState::Right { EitherExtractState::Right {

View File

@ -30,7 +30,7 @@ use crate::{
/// ///
/// # Extractor /// # Extractor
/// To extract typed data from a request body, the inner type `T` must implement the /// To extract typed data from a request body, the inner type `T` must implement the
/// [`serde::Deserialize`] trait. /// [`DeserializeOwned`] trait.
/// ///
/// Use [`FormConfig`] to configure extraction process. /// Use [`FormConfig`] to configure extraction process.
/// ///
@ -126,20 +126,12 @@ impl<T> FromRequest for Form<T>
where where
T: DeserializeOwned + 'static, T: DeserializeOwned + 'static,
{ {
type Config = FormConfig;
type Error = Error; type Error = Error;
type Future = FormExtractFut<T>; type Future = FormExtractFut<T>;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
let (limit, err_handler) = req let FormConfig { limit, err_handler } = FormConfig::from_req(req).clone();
.app_data::<Self::Config>()
.or_else(|| {
req.app_data::<web::Data<Self::Config>>()
.map(|d| d.as_ref())
})
.map(|c| (c.limit, c.err_handler.clone()))
.unwrap_or((16384, None));
FormExtractFut { FormExtractFut {
fut: UrlEncoded::new(req, payload).limit(limit), fut: UrlEncoded::new(req, payload).limit(limit),
@ -241,14 +233,26 @@ impl FormConfig {
self.err_handler = Some(Rc::new(f)); self.err_handler = Some(Rc::new(f));
self self
} }
/// Extract payload config from app data.
///
/// Checks both `T` and `Data<T>`, in that order, and falls back to the default payload config.
fn from_req(req: &HttpRequest) -> &Self {
req.app_data::<Self>()
.or_else(|| req.app_data::<web::Data<Self>>().map(|d| d.as_ref()))
.unwrap_or(&DEFAULT_CONFIG)
}
} }
/// Allow shared refs used as default.
const DEFAULT_CONFIG: FormConfig = FormConfig {
limit: 16_384, // 2^14 bytes (~16kB)
err_handler: None,
};
impl Default for FormConfig { impl Default for FormConfig {
fn default() -> Self { fn default() -> Self {
FormConfig { DEFAULT_CONFIG
limit: 16_384, // 2^14 bytes (~16kB)
err_handler: None,
}
} }
} }

View File

@ -62,7 +62,6 @@ where
{ {
type Error = ParseError; type Error = ParseError;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
#[inline] #[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {

View File

@ -97,19 +97,13 @@ impl<T> ops::DerefMut for Json<T> {
} }
} }
impl<T> fmt::Display for Json<T> impl<T: fmt::Display> fmt::Display for Json<T> {
where
T: fmt::Display,
{
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f) fmt::Display::fmt(&self.0, f)
} }
} }
impl<T> Serialize for Json<T> impl<T: Serialize> Serialize for Json<T> {
where
T: Serialize,
{
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error> fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where where
S: serde::Serializer, S: serde::Serializer,
@ -133,13 +127,9 @@ impl<T: Serialize> Responder for Json<T> {
} }
/// See [here](#extractor) for example of usage as an extractor. /// See [here](#extractor) for example of usage as an extractor.
impl<T> FromRequest for Json<T> impl<T: DeserializeOwned + 'static> FromRequest for Json<T> {
where
T: DeserializeOwned + 'static,
{
type Error = Error; type Error = Error;
type Future = JsonExtractFut<T>; type Future = JsonExtractFut<T>;
type Config = JsonConfig;
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
@ -167,10 +157,7 @@ pub struct JsonExtractFut<T> {
err_handler: JsonErrorHandler, err_handler: JsonErrorHandler,
} }
impl<T> Future for JsonExtractFut<T> impl<T: DeserializeOwned + 'static> Future for JsonExtractFut<T> {
where
T: DeserializeOwned + 'static,
{
type Output = Result<Json<T>, Error>; type Output = Result<Json<T>, Error>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
@ -320,10 +307,7 @@ pub enum JsonBody<T> {
impl<T> Unpin for JsonBody<T> {} impl<T> Unpin for JsonBody<T> {}
impl<T> JsonBody<T> impl<T: DeserializeOwned> JsonBody<T> {
where
T: DeserializeOwned + 'static,
{
/// Create a new future to decode a JSON request payload. /// Create a new future to decode a JSON request payload.
#[allow(clippy::borrow_interior_mutable_const)] #[allow(clippy::borrow_interior_mutable_const)]
pub fn new( pub fn new(
@ -405,10 +389,7 @@ where
} }
} }
impl<T> Future for JsonBody<T> impl<T: DeserializeOwned + 'static> Future for JsonBody<T> {
where
T: DeserializeOwned + 'static,
{
type Output = Result<T, JsonPayloadError>; type Output = Result<T, JsonPayloadError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
@ -435,7 +416,7 @@ where
} }
} }
None => { None => {
let json = serde_json::from_slice::<T>(&buf) let json = serde_json::from_slice::<T>(buf)
.map_err(JsonPayloadError::Deserialize)?; .map_err(JsonPayloadError::Deserialize)?;
return Poll::Ready(Ok(json)); return Poll::Ready(Ok(json));
} }

View File

@ -97,12 +97,11 @@ where
{ {
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = PathConfig;
#[inline] #[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let error_handler = req let error_handler = req
.app_data::<Self::Config>() .app_data::<PathConfig>()
.and_then(|c| c.ehandler.clone()); .and_then(|c| c.ehandler.clone());
ready( ready(

View File

@ -63,7 +63,6 @@ impl Stream for Payload {
/// See [here](#usage) for example of usage as an extractor. /// See [here](#usage) for example of usage as an extractor.
impl FromRequest for Payload { impl FromRequest for Payload {
type Config = PayloadConfig;
type Error = Error; type Error = Error;
type Future = Ready<Result<Payload, Error>>; type Future = Ready<Result<Payload, Error>>;
@ -90,7 +89,6 @@ impl FromRequest for Payload {
/// } /// }
/// ``` /// ```
impl FromRequest for Bytes { impl FromRequest for Bytes {
type Config = PayloadConfig;
type Error = Error; type Error = Error;
type Future = Either<BytesExtractFut, Ready<Result<Bytes, Error>>>; type Future = Either<BytesExtractFut, Ready<Result<Bytes, Error>>>;
@ -126,8 +124,7 @@ impl<'a> Future for BytesExtractFut {
/// ///
/// Text extractor automatically decode body according to the request's charset. /// Text extractor automatically decode body according to the request's charset.
/// ///
/// [**PayloadConfig**](PayloadConfig) allows to configure /// Use [`PayloadConfig`] to configure extraction process.
/// extraction process.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
@ -139,7 +136,6 @@ impl<'a> Future for BytesExtractFut {
/// format!("Body {}!", text) /// format!("Body {}!", text)
/// } /// }
impl FromRequest for String { impl FromRequest for String {
type Config = PayloadConfig;
type Error = Error; type Error = Error;
type Future = Either<StringExtractFut, Ready<Result<String, Error>>>; type Future = Either<StringExtractFut, Ready<Result<String, Error>>>;
@ -198,14 +194,15 @@ fn bytes_to_string(body: Bytes, encoding: &'static Encoding) -> Result<String, E
/// Configuration for request payloads. /// Configuration for request payloads.
/// ///
/// Applies to the built-in `Bytes` and `String` extractors. Note that the `Payload` extractor does /// Applies to the built-in [`Bytes`] and [`String`] extractors.
/// not automatically check conformance with this configuration to allow more flexibility when /// Note that the [`Payload`] extractor does not automatically check
/// building extractors on top of `Payload`. /// conformance with this configuration to allow more flexibility when
/// building extractors on top of [`Payload`].
/// ///
/// By default, the payload size limit is 256kB and there is no mime type condition. /// By default, the payload size limit is 256kB and there is no mime type condition.
/// ///
/// To use this, add an instance of it to your app or service through one of the /// To use this, add an instance of it to your [`app`](crate::App), [`scope`](crate::Scope)
/// `.app_data()` methods. /// or [`resource`](crate::Resource) through the associated `.app_data()` method.
#[derive(Clone)] #[derive(Clone)]
pub struct PayloadConfig { pub struct PayloadConfig {
limit: usize, limit: usize,

View File

@ -3,14 +3,14 @@
use std::{fmt, ops, sync::Arc}; use std::{fmt, ops, sync::Arc};
use actix_utils::future::{err, ok, Ready}; use actix_utils::future::{err, ok, Ready};
use serde::de; use serde::de::DeserializeOwned;
use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest}; use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequest};
/// Extract typed information from the request's query. /// Extract typed information from the request's query.
/// ///
/// To extract typed data from the URL query string, the inner type `T` must implement the /// To extract typed data from the URL query string, the inner type `T` must implement the
/// [`serde::Deserialize`] trait. /// [`DeserializeOwned`] trait.
/// ///
/// Use [`QueryConfig`] to configure extraction process. /// Use [`QueryConfig`] to configure extraction process.
/// ///
@ -46,18 +46,18 @@ use crate::{dev::Payload, error::QueryPayloadError, Error, FromRequest, HttpRequ
/// // To access the entire underlying query struct, use `.into_inner()`. /// // To access the entire underlying query struct, use `.into_inner()`.
/// #[get("/debug1")] /// #[get("/debug1")]
/// async fn debug1(info: web::Query<AuthRequest>) -> String { /// async fn debug1(info: web::Query<AuthRequest>) -> String {
/// dbg!("Authorization object={:?}", info.into_inner()); /// dbg!("Authorization object = {:?}", info.into_inner());
/// "OK".to_string() /// "OK".to_string()
/// } /// }
/// ///
/// // Or use `.0`, which is equivalent to `.into_inner()`. /// // Or use destructuring, which is equivalent to `.into_inner()`.
/// #[get("/debug2")] /// #[get("/debug2")]
/// async fn debug2(info: web::Query<AuthRequest>) -> String { /// async fn debug2(web::Query(info): web::Query<AuthRequest>) -> String {
/// dbg!("Authorization object={:?}", info.0); /// dbg!("Authorization object = {:?}", info);
/// "OK".to_string() /// "OK".to_string()
/// } /// }
/// ``` /// ```
#[derive(Clone, PartialEq, Eq, PartialOrd, Ord, Debug)] #[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub struct Query<T>(pub T); pub struct Query<T>(pub T);
impl<T> Query<T> { impl<T> Query<T> {
@ -65,8 +65,10 @@ impl<T> Query<T> {
pub fn into_inner(self) -> T { pub fn into_inner(self) -> T {
self.0 self.0
} }
}
/// Deserialize `T` from a URL encoded query parameter string. impl<T: DeserializeOwned> Query<T> {
/// Deserialize a `T` from the URL encoded query parameter string.
/// ///
/// ``` /// ```
/// # use std::collections::HashMap; /// # use std::collections::HashMap;
@ -76,10 +78,7 @@ impl<T> Query<T> {
/// assert_eq!(numbers.get("two"), Some(&2)); /// assert_eq!(numbers.get("two"), Some(&2));
/// assert!(numbers.get("three").is_none()); /// assert!(numbers.get("three").is_none());
/// ``` /// ```
pub fn from_query(query_str: &str) -> Result<Self, QueryPayloadError> pub fn from_query(query_str: &str) -> Result<Self, QueryPayloadError> {
where
T: de::DeserializeOwned,
{
serde_urlencoded::from_str::<T>(query_str) serde_urlencoded::from_str::<T>(query_str)
.map(Self) .map(Self)
.map_err(QueryPayloadError::Deserialize) .map_err(QueryPayloadError::Deserialize)
@ -107,18 +106,14 @@ impl<T: fmt::Display> fmt::Display for Query<T> {
} }
/// See [here](#usage) for example of usage as an extractor. /// See [here](#usage) for example of usage as an extractor.
impl<T> FromRequest for Query<T> impl<T: DeserializeOwned> FromRequest for Query<T> {
where
T: de::DeserializeOwned,
{
type Error = Error; type Error = Error;
type Future = Ready<Result<Self, Error>>; type Future = Ready<Result<Self, Error>>;
type Config = QueryConfig;
#[inline] #[inline]
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let error_handler = req let error_handler = req
.app_data::<Self::Config>() .app_data::<QueryConfig>()
.and_then(|c| c.err_handler.clone()); .and_then(|c| c.err_handler.clone());
serde_urlencoded::from_str::<T>(req.query_string()) serde_urlencoded::from_str::<T>(req.query_string())
@ -165,7 +160,7 @@ where
/// let query_cfg = web::QueryConfig::default() /// let query_cfg = web::QueryConfig::default()
/// // use custom error handler /// // use custom error handler
/// .error_handler(|err, req| { /// .error_handler(|err, req| {
/// error::InternalError::from_response(err, HttpResponse::Conflict().into()).into() /// error::InternalError::from_response(err, HttpResponse::Conflict().finish()).into()
/// }); /// });
/// ///
/// App::new() /// App::new()
@ -213,10 +208,10 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_service_request_extract() { async fn test_service_request_extract() {
let req = TestRequest::with_uri("/name/user1/").to_srv_request(); let req = TestRequest::with_uri("/name/user1/").to_srv_request();
assert!(Query::<Id>::from_query(&req.query_string()).is_err()); assert!(Query::<Id>::from_query(req.query_string()).is_err());
let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request(); let req = TestRequest::with_uri("/name/user1/?id=test").to_srv_request();
let mut s = Query::<Id>::from_query(&req.query_string()).unwrap(); let mut s = Query::<Id>::from_query(req.query_string()).unwrap();
assert_eq!(s.id, "test"); assert_eq!(s.id, "test");
assert_eq!( assert_eq!(

View File

@ -3,44 +3,36 @@
use std::future::Future; use std::future::Future;
use actix_http::http::Method; use actix_http::http::Method;
pub use actix_http::Response as HttpResponse;
use actix_router::IntoPatterns; use actix_router::IntoPatterns;
pub use bytes::{Buf, BufMut, Bytes, BytesMut}; pub use bytes::{Buf, BufMut, Bytes, BytesMut};
use crate::error::BlockingError; use crate::{
use crate::extract::FromRequest; error::BlockingError, extract::FromRequest, handler::Handler, resource::Resource,
use crate::handler::Handler; responder::Responder, route::Route, scope::Scope, service::WebService,
use crate::resource::Resource; };
use crate::responder::Responder;
use crate::route::Route;
use crate::scope::Scope;
use crate::service::WebService;
pub use crate::config::ServiceConfig; pub use crate::config::ServiceConfig;
pub use crate::data::Data; pub use crate::data::Data;
pub use crate::request::HttpRequest; pub use crate::request::HttpRequest;
pub use crate::request_data::ReqData; pub use crate::request_data::ReqData;
pub use crate::response::HttpResponse;
pub use crate::types::*; pub use crate::types::*;
/// Create resource for a specific path. /// Creates a new resource for a specific path.
/// ///
/// Resources may have variable path segments. For example, a /// Resources may have dynamic path segments. For example, a resource with the path `/a/{name}/c`
/// resource with the path `/a/{name}/c` would match all incoming /// would match all incoming requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`.
/// requests with paths such as `/a/b/c`, `/a/1/c`, or `/a/etc/c`.
/// ///
/// A variable segment is specified in the form `{identifier}`, /// A dynamic segment is specified in the form `{identifier}`, where the identifier can be used
/// where the identifier can be used later in a request handler to /// later in a request handler to access the matched value for that segment. This is done by looking
/// access the matched value for that segment. This is done by /// up the identifier in the `Path` object returned by [`HttpRequest.match_info()`] method.
/// looking up the identifier in the `Params` object returned by
/// `HttpRequest.match_info()` method.
/// ///
/// By default, each segment matches the regular expression `[^{}/]+`. /// By default, each segment matches the regular expression `[^{}/]+`.
/// ///
/// You can also specify a custom regex in the form `{identifier:regex}`: /// You can also specify a custom regex in the form `{identifier:regex}`:
/// ///
/// For instance, to route `GET`-requests on any route matching /// For instance, to route `GET`-requests on any route matching `/users/{userid}/{friend}` and store
/// `/users/{userid}/{friend}` and store `userid` and `friend` in /// `userid` and `friend` in the exposed `Path` object:
/// the exposed `Params` object:
/// ///
/// ``` /// ```
/// use actix_web::{web, App, HttpResponse}; /// use actix_web::{web, App, HttpResponse};
@ -55,10 +47,16 @@ pub fn resource<T: IntoPatterns>(path: T) -> Resource {
Resource::new(path) Resource::new(path)
} }
/// Configure scope for common root path. /// Creates scope for common path prefix.
/// ///
/// Scopes collect multiple paths under a common path prefix. /// Scopes collect multiple paths under a common path prefix. The scope's path can contain dynamic
/// Scope path can contain variable path segments as resources. /// path segments.
///
/// # Examples
/// In this example, three routes are set up (and will handle any method):
/// * `/{project_id}/path1`
/// * `/{project_id}/path2`
/// * `/{project_id}/path3`
/// ///
/// ``` /// ```
/// use actix_web::{web, App, HttpResponse}; /// use actix_web::{web, App, HttpResponse};
@ -70,148 +68,50 @@ pub fn resource<T: IntoPatterns>(path: T) -> Resource {
/// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed())) /// .service(web::resource("/path3").to(|| HttpResponse::MethodNotAllowed()))
/// ); /// );
/// ``` /// ```
///
/// In the above example, three routes get added:
/// * /{project_id}/path1
/// * /{project_id}/path2
/// * /{project_id}/path3
///
pub fn scope(path: &str) -> Scope { pub fn scope(path: &str) -> Scope {
Scope::new(path) Scope::new(path)
} }
/// Create *route* without configuration. /// Creates a new un-configured route.
pub fn route() -> Route { pub fn route() -> Route {
Route::new() Route::new()
} }
/// Create *route* with `GET` method guard. macro_rules! method_route {
/// ($method_fn:ident, $method_const:ident) => {
/// ``` paste::paste! {
/// use actix_web::{web, App, HttpResponse}; #[doc = " Creates a new route with `" $method_const "` method guard."]
/// ///
/// let app = App::new().service( /// # Examples
/// web::resource("/{project_id}") #[doc = " In this example, one `" $method_const " /{project_id}` route is set up:"]
/// .route(web::get().to(|| HttpResponse::Ok())) /// ```
/// ); /// use actix_web::{web, App, HttpResponse};
/// ``` ///
/// /// let app = App::new().service(
/// In the above example, one `GET` route gets added: /// web::resource("/{project_id}")
/// * /{project_id} #[doc = " .route(web::" $method_fn "().to(|| HttpResponse::Ok()))"]
/// ///
pub fn get() -> Route { /// );
method(Method::GET) /// ```
pub fn $method_fn() -> Route {
method(Method::$method_const)
}
}
};
} }
/// Create *route* with `POST` method guard. method_route!(get, GET);
/// method_route!(post, POST);
/// ``` method_route!(put, PUT);
/// use actix_web::{web, App, HttpResponse}; method_route!(patch, PATCH);
/// method_route!(delete, DELETE);
/// let app = App::new().service( method_route!(head, HEAD);
/// web::resource("/{project_id}") method_route!(trace, TRACE);
/// .route(web::post().to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// In the above example, one `POST` route gets added:
/// * /{project_id}
///
pub fn post() -> Route {
method(Method::POST)
}
/// Create *route* with `PUT` method guard. /// Creates a new route with specified method guard.
/// ///
/// ``` /// # Examples
/// use actix_web::{web, App, HttpResponse}; /// In this example, one `GET /{project_id}` route is set up:
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
/// .route(web::put().to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// In the above example, one `PUT` route gets added:
/// * /{project_id}
///
pub fn put() -> Route {
method(Method::PUT)
}
/// Create *route* with `PATCH` method guard.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
/// .route(web::patch().to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// In the above example, one `PATCH` route gets added:
/// * /{project_id}
///
pub fn patch() -> Route {
method(Method::PATCH)
}
/// Create *route* with `DELETE` method guard.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
/// .route(web::delete().to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// In the above example, one `DELETE` route gets added:
/// * /{project_id}
///
pub fn delete() -> Route {
method(Method::DELETE)
}
/// Create *route* with `HEAD` method guard.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
/// .route(web::head().to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// In the above example, one `HEAD` route gets added:
/// * /{project_id}
///
pub fn head() -> Route {
method(Method::HEAD)
}
/// Create *route* with `TRACE` method guard.
///
/// ```
/// use actix_web::{web, App, HttpResponse};
///
/// let app = App::new().service(
/// web::resource("/{project_id}")
/// .route(web::trace().to(|| HttpResponse::Ok()))
/// );
/// ```
///
/// In the above example, one `HEAD` route gets added:
/// * /{project_id}
///
pub fn trace() -> Route {
method(Method::TRACE)
}
/// Create *route* and add method guard.
/// ///
/// ``` /// ```
/// use actix_web::{web, http, App, HttpResponse}; /// use actix_web::{web, http, App, HttpResponse};
@ -221,15 +121,11 @@ pub fn trace() -> Route {
/// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok())) /// .route(web::method(http::Method::GET).to(|| HttpResponse::Ok()))
/// ); /// );
/// ``` /// ```
///
/// In the above example, one `GET` route gets added:
/// * /{project_id}
///
pub fn method(method: Method) -> Route { pub fn method(method: Method) -> Route {
Route::new().method(method) Route::new().method(method)
} }
/// Create a new route and add handler. /// Creates a new any-method route with handler.
/// ///
/// ``` /// ```
/// use actix_web::{web, App, HttpResponse, Responder}; /// use actix_web::{web, App, HttpResponse, Responder};
@ -253,7 +149,7 @@ where
Route::new().to(handler) Route::new().to(handler)
} }
/// Create raw service for a specific path. /// Creates a raw service for a specific path.
/// ///
/// ``` /// ```
/// use actix_web::{dev, web, guard, App, Error, HttpResponse}; /// use actix_web::{dev, web, guard, App, Error, HttpResponse};
@ -272,8 +168,8 @@ pub fn service<T: IntoPatterns>(path: T) -> WebService {
WebService::new(path) WebService::new(path)
} }
/// Execute blocking function on a thread pool, returns future that resolves /// Executes blocking function on a thread pool, returns future that resolves to result of the
/// to result of the function execution. /// function execution.
pub fn block<F, R>(f: F) -> impl Future<Output = Result<R, BlockingError>> pub fn block<F, R>(f: F) -> impl Future<Output = Result<R, BlockingError>>
where where
F: FnOnce() -> R + Send + 'static, F: FnOnce() -> R + Send + 'static,

View File

@ -1077,3 +1077,22 @@ async fn test_data_drop() {
assert_eq!(num.load(Ordering::SeqCst), 0); assert_eq!(num.load(Ordering::SeqCst), 0);
} }
#[actix_rt::test]
async fn test_accept_encoding_no_match() {
let srv = actix_test::start_with(actix_test::config().h1(), || {
App::new()
.wrap(Compress::default())
.service(web::resource("/").route(web::to(move || HttpResponse::Ok().finish())))
});
let response = srv
.get("/")
.append_header((ACCEPT_ENCODING, "compress, identity;q=0"))
.no_decompress()
.send()
.await
.unwrap();
assert_eq!(response.status().as_u16(), 406);
}