mirror of https://github.com/fafhrd91/actix-web
Merge branch 'master' into web/fix/url-for-parser
This commit is contained in:
commit
c712a0851f
|
|
@ -1,14 +1,14 @@
|
|||
[alias]
|
||||
chk = "check --workspace --all-features --tests --examples --bins"
|
||||
lint = "clippy --workspace --all-features --tests --examples --bins"
|
||||
ci-min = "hack check --workspace --no-default-features"
|
||||
ci-min-test = "hack check --workspace --no-default-features --tests --examples"
|
||||
ci-default = "check --workspace --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-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
||||
lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
|
||||
lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
|
||||
|
||||
ci-feature-powerset-check-no-tls="hack --workspace --feature-powerset --skip=__compress,rustls,openssl check"
|
||||
ci-feature-powerset-check-rustls="hack --workspace --feature-powerset --features=rustls --skip=__compress,openssl check"
|
||||
ci-feature-powerset-check-openssl="hack --workspace --feature-powerset --features=openssl --skip=__compress,rustls check"
|
||||
ci-feature-powerset-check-all="hack --workspace --feature-powerset --skip=__compress check"
|
||||
# lib checking
|
||||
ci-check-min = "hack --workspace check --no-default-features"
|
||||
ci-check-default = "hack --workspace check"
|
||||
ci-check-default-tests = "check --workspace --tests"
|
||||
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
|
||||
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
|
||||
|
||||
# testing
|
||||
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
|
||||
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"
|
||||
|
|
|
|||
|
|
@ -62,26 +62,34 @@ jobs:
|
|||
|
||||
- name: check minimal
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-min }
|
||||
|
||||
- name: check minimal + tests
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-min-test }
|
||||
with: { command: ci-check-min }
|
||||
|
||||
- name: check default
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-default }
|
||||
|
||||
- name: check full
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-full }
|
||||
with: { command: ci-check-default }
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
timeout-minutes: 40
|
||||
with:
|
||||
command: ci-test
|
||||
args: --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
timeout-minutes: 60
|
||||
run: |
|
||||
cargo test --lib --tests -p=actix-router --all-features
|
||||
cargo test --lib --tests -p=actix-http --all-features
|
||||
cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
|
||||
cargo test --lib --tests -p=actix-web-codegen --all-features
|
||||
cargo test --lib --tests -p=awc --all-features
|
||||
cargo test --lib --tests -p=actix-http-test --all-features
|
||||
cargo test --lib --tests -p=actix-test --all-features
|
||||
cargo test --lib --tests -p=actix-files
|
||||
cargo test --lib --tests -p=actix-multipart --all-features
|
||||
cargo test --lib --tests -p=actix-web-actors --all-features
|
||||
|
||||
- name: tests (io-uring)
|
||||
if: matrix.target.os == 'ubuntu-latest'
|
||||
timeout-minutes: 60
|
||||
run: >
|
||||
sudo bash -c "ulimit -Sl 512
|
||||
&& ulimit -Hl 512
|
||||
&& PATH=$PATH:/usr/share/rust/.cargo/bin
|
||||
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
|
|
@ -114,9 +122,12 @@ jobs:
|
|||
args: cargo-hack
|
||||
|
||||
- name: check feature combinations
|
||||
# if: github.ref == 'refs/heads/master'
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-feature-powerset-check-all }
|
||||
with: { command: ci-check-all-feature-powerset }
|
||||
|
||||
- name: check feature combinations
|
||||
uses: actions-rs/cargo@v1
|
||||
with: { command: ci-check-all-feature-powerset-linux }
|
||||
|
||||
coverage:
|
||||
name: coverage
|
||||
|
|
@ -141,14 +152,14 @@ jobs:
|
|||
if: github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cargo install cargo-tarpaulin --vers "^0.13"
|
||||
cargo tarpaulin --out Xml --verbose
|
||||
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
|
||||
- name: Upload to Codecov
|
||||
if: github.ref == 'refs/heads/master'
|
||||
uses: codecov/codecov-action@v1
|
||||
with: { file: cobertura.xml }
|
||||
|
||||
rustdoc:
|
||||
name: rustdoc
|
||||
name: doc tests
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
|
@ -166,13 +177,7 @@ jobs:
|
|||
- 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
|
||||
timeout-minutes: 60
|
||||
with: { command: ci-doctest }
|
||||
|
|
|
|||
57
CHANGES.md
57
CHANGES.md
|
|
@ -1,8 +1,61 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
### Added
|
||||
* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480]
|
||||
* `AcceptEncoding` typed header. [#2482]
|
||||
* `Range` typed header. [#2485]
|
||||
* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
|
||||
* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
|
||||
|
||||
### Changed
|
||||
* `ContentType::html` now returns `Content-Type: text/html; charset=utf-8` instead of `Content-Type: text/html`.
|
||||
* Rename `Accept::{mime_precedence => ranked}`. [#2480]
|
||||
* Rename `Accept::{mime_preference => preference}`. [#2480]
|
||||
* Un-deprecate `App::data_factory`. [#2484]
|
||||
|
||||
### Fixed
|
||||
* Accept wildcard `*` items in `AcceptLanguage`. [#2480]
|
||||
* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468]
|
||||
* Typed headers containing lists that require one or more items now enforce this minimum. [#2482]
|
||||
|
||||
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||
[#2480]: https://github.com/actix/actix-web/pull/2480
|
||||
[#2482]: https://github.com/actix/actix-web/pull/2482
|
||||
[#2484]: https://github.com/actix/actix-web/pull/2484
|
||||
[#2485]: https://github.com/actix/actix-web/pull/2485
|
||||
|
||||
|
||||
## 4.0.0-beta.13 - 2021-11-30
|
||||
### Changed
|
||||
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 4.0.0-beta.12 - 2021-11-22
|
||||
### Changed
|
||||
* Compress middleware's response type is now `AnyBody<Encoder<B>>`. [#2448]
|
||||
|
||||
### Fixed
|
||||
* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448]
|
||||
|
||||
### Removed
|
||||
* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446]
|
||||
|
||||
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||
|
||||
|
||||
## 4.0.0-beta.11 - 2021-11-15
|
||||
### Added
|
||||
* Re-export `dev::ServerHandle` from `actix-server`. [#2442]
|
||||
|
||||
### Changed
|
||||
* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423]
|
||||
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
|
||||
[#2423]: https://github.com/actix/actix-web/pull/2423
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
|
||||
|
||||
## 4.0.0-beta.10 - 2021-10-20
|
||||
|
|
@ -13,7 +66,7 @@
|
|||
### Changed
|
||||
* Associated type `FromRequest::Config` was removed. [#2233]
|
||||
* Inner field made private on `web::Payload`. [#2384]
|
||||
* `Data::into_inner` and `Data::get_ref` no longer require T: Sized. [#2403]
|
||||
* `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403]
|
||||
* Updated rustls to v0.20. [#2414]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
|
|
|||
34
Cargo.toml
34
Cargo.toml
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.10"
|
||||
version = "4.0.0-beta.13"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
keywords = ["actix", "http", "web", "framework", "async"]
|
||||
|
|
@ -65,16 +65,19 @@ rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
|
|||
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||
__compress = []
|
||||
|
||||
# io-uring feature only avaiable for Linux OSes.
|
||||
experimental-io-uring = ["actix-server/io-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-macros = "0.2.3"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-rt = "2.3"
|
||||
actix-server = "2.0.0-beta.9"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true }
|
||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-http = "3.0.0-beta.14"
|
||||
actix-router = "0.5.0-beta.2"
|
||||
actix-web-codegen = "0.5.0-beta.5"
|
||||
|
||||
|
|
@ -93,7 +96,7 @@ once_cell = "1.5"
|
|||
log = "0.4"
|
||||
mime = "0.3"
|
||||
paste = "1"
|
||||
pin-project = "1.0.0"
|
||||
pin-project-lite = "0.2.7"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
|
|
@ -104,12 +107,12 @@ time = { version = "0.3", default-features = false, features = ["formatting"] }
|
|||
url = "2.1"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.9", features = ["openssl"] }
|
||||
actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
|
||||
awc = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.8"
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
rand = "0.8"
|
||||
|
|
@ -117,7 +120,7 @@ rcgen = "0.8"
|
|||
rustls-pemfile = "0.2"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
zstd = "0.7"
|
||||
zstd = "0.9"
|
||||
|
||||
[profile.dev]
|
||||
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
|
||||
|
|
@ -140,6 +143,15 @@ actix-web-actors = { path = "actix-web-actors" }
|
|||
actix-web-codegen = { path = "actix-web-codegen" }
|
||||
awc = { path = "awc" }
|
||||
|
||||
# uncomment for quick testing against local actix-net repo
|
||||
# actix-service = { path = "../actix-net/actix-service" }
|
||||
# actix-macros = { path = "../actix-net/actix-macros" }
|
||||
# actix-rt = { path = "../actix-net/actix-rt" }
|
||||
# actix-codec = { path = "../actix-net/actix-codec" }
|
||||
# actix-utils = { path = "../actix-net/actix-utils" }
|
||||
# actix-tls = { path = "../actix-net/actix-tls" }
|
||||
# actix-server = { path = "../actix-net/actix-server" }
|
||||
|
||||
[[test]]
|
||||
name = "test_server"
|
||||
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
|
||||
|
|
|
|||
|
|
@ -6,10 +6,10 @@
|
|||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.10)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.13)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.10)
|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.13)
|
||||
<br />
|
||||
[](https://github.com/actix/actix-web/actions)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
|
|
|
|||
|
|
@ -3,6 +3,18 @@
|
|||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.6.0-beta.9 - 2021-11-22
|
||||
* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
|
||||
* Add `NamedFile::open_async`. [#2408]
|
||||
* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
|
||||
* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
|
||||
* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
|
||||
* Add `impl Clone` for `FilesService`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
[#2453]: https://github.com/actix/actix-web/pull/2453
|
||||
|
||||
|
||||
## 0.6.0-beta.8 - 2021-10-20
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,7 +1,11 @@
|
|||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0-beta.8"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
version = "0.6.0-beta.9"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
]
|
||||
description = "Static file serving for Actix Web"
|
||||
keywords = ["actix", "http", "async", "futures"]
|
||||
homepage = "https://actix.rs"
|
||||
|
|
@ -14,11 +18,14 @@ edition = "2018"
|
|||
name = "actix_files"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.0.0-beta.10", default-features = false }
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.11", default-features = false }
|
||||
actix-http = "3.0.0-beta.14"
|
||||
actix-service = "2"
|
||||
actix-utils = "3"
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
|
|
@ -30,8 +37,11 @@ log = "0.4"
|
|||
mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
pin-project-lite = "0.2.7"
|
||||
|
||||
tokio-uring = { version = "0.1", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-web = "4.0.0-beta.10"
|
||||
actix-test = "0.1.0-beta.5"
|
||||
actix-web = "4.0.0-beta.11"
|
||||
actix-test = "0.1.0-beta.7"
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.8)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.9)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.8)
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.9)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -1,98 +1,277 @@
|
|||
use std::{
|
||||
cmp, fmt,
|
||||
fs::File,
|
||||
future::Future,
|
||||
io::{self, Read, Seek},
|
||||
io,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_web::{
|
||||
error::{BlockingError, Error},
|
||||
rt::task::{spawn_blocking, JoinHandle},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use actix_web::{error::Error, web::Bytes};
|
||||
use futures_core::{ready, Stream};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::named::File;
|
||||
|
||||
pin_project! {
|
||||
/// Adapter to read a `std::file::File` in chunks.
|
||||
#[doc(hidden)]
|
||||
/// A helper created from a `std::fs::File` which reads the file
|
||||
/// chunk-by-chunk on a `ThreadPool`.
|
||||
pub struct ChunkedReadFile {
|
||||
pub struct ChunkedReadFile<F, Fut> {
|
||||
size: u64,
|
||||
offset: u64,
|
||||
state: ChunkedReadFileState,
|
||||
#[pin]
|
||||
state: ChunkedReadFileState<Fut>,
|
||||
counter: u64,
|
||||
}
|
||||
|
||||
enum ChunkedReadFileState {
|
||||
File(Option<File>),
|
||||
Future(JoinHandle<Result<(File, Bytes), io::Error>>),
|
||||
}
|
||||
|
||||
impl ChunkedReadFile {
|
||||
pub(crate) fn new(size: u64, offset: u64, file: File) -> Self {
|
||||
Self {
|
||||
size,
|
||||
offset,
|
||||
state: ChunkedReadFileState::File(Some(file)),
|
||||
counter: 0,
|
||||
}
|
||||
callback: F,
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChunkedReadFile {
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pin_project! {
|
||||
#[project = ChunkedReadFileStateProj]
|
||||
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||
enum ChunkedReadFileState<Fut> {
|
||||
File { file: Option<File>, },
|
||||
Future { #[pin] fut: Fut },
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
pin_project! {
|
||||
#[project = ChunkedReadFileStateProj]
|
||||
#[project_replace = ChunkedReadFileStateProjReplace]
|
||||
enum ChunkedReadFileState<Fut> {
|
||||
File { file: Option<(File, BytesMut)> },
|
||||
Future { #[pin] fut: Fut },
|
||||
}
|
||||
}
|
||||
|
||||
impl<F, Fut> fmt::Debug for ChunkedReadFile<F, Fut> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("ChunkedReadFile")
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Result<Bytes, Error>;
|
||||
pub(crate) fn new_chunked_read(
|
||||
size: u64,
|
||||
offset: u64,
|
||||
file: File,
|
||||
) -> impl Stream<Item = Result<Bytes, Error>> {
|
||||
ChunkedReadFile {
|
||||
size,
|
||||
offset,
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
state: ChunkedReadFileState::File { file: Some(file) },
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
state: ChunkedReadFileState::File {
|
||||
file: Some((file, BytesMut::new())),
|
||||
},
|
||||
counter: 0,
|
||||
callback: chunked_read_file_callback,
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.as_mut().get_mut();
|
||||
match this.state {
|
||||
ChunkedReadFileState::File(ref mut file) => {
|
||||
let size = this.size;
|
||||
let offset = this.offset;
|
||||
let counter = this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let mut file = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
let fut = spawn_blocking(move || {
|
||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
async fn chunked_read_file_callback(
|
||||
mut file: File,
|
||||
offset: u64,
|
||||
max_bytes: usize,
|
||||
) -> Result<(File, Bytes), Error> {
|
||||
use io::{Read as _, Seek as _};
|
||||
|
||||
let res = actix_web::rt::task::spawn_blocking(move || {
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
|
||||
let n_bytes =
|
||||
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||
let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
|
||||
|
||||
if n_bytes == 0 {
|
||||
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
|
||||
} else {
|
||||
Ok((file, Bytes::from(buf)))
|
||||
}
|
||||
})
|
||||
.await
|
||||
.map_err(|_| actix_web::error::BlockingError)??;
|
||||
|
||||
Ok(res)
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
async fn chunked_read_file_callback(
|
||||
file: File,
|
||||
offset: u64,
|
||||
max_bytes: usize,
|
||||
mut bytes_mut: BytesMut,
|
||||
) -> io::Result<(File, Bytes, BytesMut)> {
|
||||
bytes_mut.reserve(max_bytes);
|
||||
|
||||
let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await;
|
||||
let n_bytes = res?;
|
||||
|
||||
if n_bytes == 0 {
|
||||
return Err(io::ErrorKind::UnexpectedEof.into());
|
||||
}
|
||||
|
||||
Ok((file, Bytes::from(buf)))
|
||||
});
|
||||
this.state = ChunkedReadFileState::Future(fut);
|
||||
let bytes = bytes_mut.split_to(n_bytes).freeze();
|
||||
|
||||
Ok((file, bytes, bytes_mut))
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||
where
|
||||
F: Fn(File, u64, usize, BytesMut) -> Fut,
|
||||
Fut: Future<Output = io::Result<(File, Bytes, BytesMut)>>,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut().project();
|
||||
match this.state.as_mut().project() {
|
||||
ChunkedReadFileStateProj::File { file } => {
|
||||
let size = *this.size;
|
||||
let offset = *this.offset;
|
||||
let counter = *this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let (file, bytes_mut) = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
let fut = (this.callback)(file, offset, max_bytes, bytes_mut);
|
||||
|
||||
this.state
|
||||
.project_replace(ChunkedReadFileState::Future { fut });
|
||||
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
ChunkedReadFileState::Future(ref mut fut) => {
|
||||
let (file, bytes) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
this.state = ChunkedReadFileState::File(Some(file));
|
||||
ChunkedReadFileStateProj::Future { fut } => {
|
||||
let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?;
|
||||
|
||||
this.offset += bytes.len() as u64;
|
||||
this.counter += bytes.len() as u64;
|
||||
this.state.project_replace(ChunkedReadFileState::File {
|
||||
file: Some((file, bytes_mut)),
|
||||
});
|
||||
|
||||
*this.offset += bytes.len() as u64;
|
||||
*this.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
|
||||
where
|
||||
F: Fn(File, u64, usize) -> Fut,
|
||||
Fut: Future<Output = Result<(File, Bytes), Error>>,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let mut this = self.as_mut().project();
|
||||
match this.state.as_mut().project() {
|
||||
ChunkedReadFileStateProj::File { file } => {
|
||||
let size = *this.size;
|
||||
let offset = *this.offset;
|
||||
let counter = *this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let file = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
let fut = (this.callback)(file, offset, max_bytes);
|
||||
|
||||
this.state
|
||||
.project_replace(ChunkedReadFileState::Future { fut });
|
||||
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
ChunkedReadFileStateProj::Future { fut } => {
|
||||
let (file, bytes) = ready!(fut.poll(cx))?;
|
||||
|
||||
this.state
|
||||
.project_replace(ChunkedReadFileState::File { file: Some(file) });
|
||||
|
||||
*this.offset += bytes.len() as u64;
|
||||
*this.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
use bytes_mut::BytesMut;
|
||||
|
||||
// TODO: remove new type and use bytes::BytesMut directly
|
||||
#[doc(hidden)]
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
mod bytes_mut {
|
||||
use std::ops::{Deref, DerefMut};
|
||||
|
||||
use tokio_uring::buf::{IoBuf, IoBufMut};
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct BytesMut(bytes::BytesMut);
|
||||
|
||||
impl BytesMut {
|
||||
pub(super) fn new() -> Self {
|
||||
Self(bytes::BytesMut::new())
|
||||
}
|
||||
}
|
||||
|
||||
impl Deref for BytesMut {
|
||||
type Target = bytes::BytesMut;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for BytesMut {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.0
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl IoBuf for BytesMut {
|
||||
fn stable_ptr(&self) -> *const u8 {
|
||||
self.0.as_ptr()
|
||||
}
|
||||
|
||||
fn bytes_init(&self) -> usize {
|
||||
self.0.len()
|
||||
}
|
||||
|
||||
fn bytes_total(&self) -> usize {
|
||||
self.0.capacity()
|
||||
}
|
||||
}
|
||||
|
||||
unsafe impl IoBufMut for BytesMut {
|
||||
fn stable_mut_ptr(&mut self) -> *mut u8 {
|
||||
self.0.as_mut_ptr()
|
||||
}
|
||||
|
||||
unsafe fn set_init(&mut self, init_len: usize) {
|
||||
if self.len() < init_len {
|
||||
self.0.set_len(init_len);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ use std::{
|
|||
};
|
||||
|
||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||
use actix_utils::future::ok;
|
||||
use actix_web::{
|
||||
dev::{
|
||||
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
|
||||
|
|
@ -20,8 +19,9 @@ use actix_web::{
|
|||
use futures_core::future::LocalBoxFuture;
|
||||
|
||||
use crate::{
|
||||
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
|
||||
MimeOverride, PathFilter,
|
||||
directory_listing, named,
|
||||
service::{FilesService, FilesServiceInner},
|
||||
Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
|
||||
};
|
||||
|
||||
/// Static files handling service.
|
||||
|
|
@ -283,11 +283,17 @@ impl Files {
|
|||
/// Setting a fallback static file handler:
|
||||
/// ```
|
||||
/// use actix_files::{Files, NamedFile};
|
||||
/// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
|
||||
///
|
||||
/// # fn run() -> Result<(), actix_web::Error> {
|
||||
/// let files = Files::new("/", "./static")
|
||||
/// .index_file("index.html")
|
||||
/// .default_handler(NamedFile::open("./static/404.html")?);
|
||||
/// .default_handler(fn_service(|req: ServiceRequest| async {
|
||||
/// let (req, _) = req.into_parts();
|
||||
/// let file = NamedFile::open_async("./static/404.html").await?;
|
||||
/// let res = file.into_response(&req);
|
||||
/// Ok(ServiceResponse::new(req, res))
|
||||
/// }));
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
|
|
@ -353,7 +359,7 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
let mut srv = FilesService {
|
||||
let mut inner = FilesServiceInner {
|
||||
directory: self.directory.clone(),
|
||||
index: self.index.clone(),
|
||||
show_index: self.show_index,
|
||||
|
|
@ -372,14 +378,14 @@ impl ServiceFactory<ServiceRequest> for Files {
|
|||
Box::pin(async {
|
||||
match fut.await {
|
||||
Ok(default) => {
|
||||
srv.default = Some(default);
|
||||
Ok(srv)
|
||||
inner.default = Some(default);
|
||||
Ok(FilesService(Rc::new(inner)))
|
||||
}
|
||||
Err(_) => Err(()),
|
||||
}
|
||||
})
|
||||
} else {
|
||||
Box::pin(ok(srv))
|
||||
Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -33,12 +33,12 @@ mod path_buf;
|
|||
mod range;
|
||||
mod service;
|
||||
|
||||
pub use crate::chunked::ChunkedReadFile;
|
||||
pub use crate::directory::Directory;
|
||||
pub use crate::files::Files;
|
||||
pub use crate::named::NamedFile;
|
||||
pub use crate::range::HttpRange;
|
||||
pub use crate::service::FilesService;
|
||||
pub use self::chunked::ChunkedReadFile;
|
||||
pub use self::directory::Directory;
|
||||
pub use self::files::Files;
|
||||
pub use self::named::NamedFile;
|
||||
pub use self::range::HttpRange;
|
||||
pub use self::service::FilesService;
|
||||
|
||||
use self::directory::{directory_listing, DirectoryRenderer};
|
||||
use self::error::FilesError;
|
||||
|
|
@ -62,13 +62,12 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
|
|||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::{
|
||||
fs::{self, File},
|
||||
fs::{self},
|
||||
ops::Add,
|
||||
time::{Duration, SystemTime},
|
||||
};
|
||||
|
||||
use actix_service::ServiceFactory;
|
||||
use actix_utils::future::ok;
|
||||
use actix_web::{
|
||||
guard,
|
||||
http::{
|
||||
|
|
@ -82,6 +81,7 @@ mod tests {
|
|||
};
|
||||
|
||||
use super::*;
|
||||
use crate::named::File;
|
||||
|
||||
#[actix_web::test]
|
||||
async fn test_file_extension_to_mime() {
|
||||
|
|
@ -100,7 +100,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_modified_since_without_if_none_match() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
||||
|
||||
let req = TestRequest::default()
|
||||
|
|
@ -112,7 +112,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_modified_since_without_if_none_match_same() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let since = file.last_modified().unwrap();
|
||||
|
||||
let req = TestRequest::default()
|
||||
|
|
@ -124,7 +124,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_modified_since_with_if_none_match() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
|
||||
|
||||
let req = TestRequest::default()
|
||||
|
|
@ -137,7 +137,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_unmodified_since() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let since = file.last_modified().unwrap();
|
||||
|
||||
let req = TestRequest::default()
|
||||
|
|
@ -149,7 +149,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_unmodified_since_failed() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
|
||||
|
||||
let req = TestRequest::default()
|
||||
|
|
@ -161,8 +161,8 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_text() {
|
||||
assert!(NamedFile::open("test--").is_err());
|
||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
||||
assert!(NamedFile::open_async("test--").await.is_err());
|
||||
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
|
|
@ -185,8 +185,8 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_disposition() {
|
||||
assert!(NamedFile::open("test--").is_err());
|
||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
||||
assert!(NamedFile::open_async("test--").await.is_err());
|
||||
let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
|
|
@ -202,7 +202,8 @@ mod tests {
|
|||
"inline; filename=\"Cargo.toml\""
|
||||
);
|
||||
|
||||
let file = NamedFile::open("Cargo.toml")
|
||||
let file = NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.disable_content_disposition();
|
||||
let req = TestRequest::default().to_http_request();
|
||||
|
|
@ -212,8 +213,19 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_non_ascii_file_name() {
|
||||
let mut file =
|
||||
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap();
|
||||
let file = {
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
crate::named::File::open("Cargo.toml").await.unwrap()
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
crate::named::File::open("Cargo.toml").unwrap()
|
||||
}
|
||||
};
|
||||
|
||||
let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
|
|
@ -236,7 +248,8 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_set_content_type() {
|
||||
let mut file = NamedFile::open("Cargo.toml")
|
||||
let mut file = NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.set_content_type(mime::TEXT_XML);
|
||||
{
|
||||
|
|
@ -261,7 +274,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_image() {
|
||||
let mut file = NamedFile::open("tests/test.png").unwrap();
|
||||
let mut file = NamedFile::open_async("tests/test.png").await.unwrap();
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
|
|
@ -284,7 +297,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_javascript() {
|
||||
let file = NamedFile::open("tests/test.js").unwrap();
|
||||
let file = NamedFile::open_async("tests/test.js").await.unwrap();
|
||||
|
||||
let req = TestRequest::default().to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
|
|
@ -304,7 +317,8 @@ mod tests {
|
|||
disposition: DispositionType::Attachment,
|
||||
parameters: vec![DispositionParam::Filename(String::from("test.png"))],
|
||||
};
|
||||
let mut file = NamedFile::open("tests/test.png")
|
||||
let mut file = NamedFile::open_async("tests/test.png")
|
||||
.await
|
||||
.unwrap()
|
||||
.set_content_disposition(cd);
|
||||
{
|
||||
|
|
@ -329,7 +343,7 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_binary() {
|
||||
let mut file = NamedFile::open("tests/test.binary").unwrap();
|
||||
let mut file = NamedFile::open_async("tests/test.binary").await.unwrap();
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
|
|
@ -352,7 +366,8 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_status_code_text() {
|
||||
let mut file = NamedFile::open("Cargo.toml")
|
||||
let mut file = NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.set_status_code(StatusCode::NOT_FOUND);
|
||||
{
|
||||
|
|
@ -568,7 +583,8 @@ mod tests {
|
|||
async fn test_named_file_content_encoding() {
|
||||
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
web::resource("/").to(|| async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.set_content_encoding(header::ContentEncoding::Identity)
|
||||
}),
|
||||
|
|
@ -588,7 +604,8 @@ mod tests {
|
|||
async fn test_named_file_content_encoding_gzip() {
|
||||
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
web::resource("/").to(|| async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
NamedFile::open_async("Cargo.toml")
|
||||
.await
|
||||
.unwrap()
|
||||
.set_content_encoding(header::ContentEncoding::Gzip)
|
||||
}),
|
||||
|
|
@ -614,7 +631,7 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_named_file_allowed_method() {
|
||||
let req = TestRequest::default().method(Method::GET).to_http_request();
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let file = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
|
@ -705,8 +722,8 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_default_handler_file_missing() {
|
||||
let st = Files::new("/", ".")
|
||||
.default_handler(|req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
.default_handler(|req: ServiceRequest| async {
|
||||
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
})
|
||||
.new_service(())
|
||||
.await
|
||||
|
|
@ -789,9 +806,8 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_serve_named_file() {
|
||||
let srv =
|
||||
test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap()))
|
||||
.await;
|
||||
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let srv = test::init_service(App::new().service(factory)).await;
|
||||
|
||||
let req = TestRequest::get().uri("/Cargo.toml").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
|
@ -808,11 +824,9 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_serve_named_file_prefix() {
|
||||
let srv = test::init_service(
|
||||
App::new()
|
||||
.service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())),
|
||||
)
|
||||
.await;
|
||||
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let srv =
|
||||
test::init_service(App::new().service(web::scope("/test").service(factory))).await;
|
||||
|
||||
let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
|
@ -829,10 +843,8 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_default_service() {
|
||||
let srv = test::init_service(
|
||||
App::new().default_service(NamedFile::open("Cargo.toml").unwrap()),
|
||||
)
|
||||
.await;
|
||||
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let srv = test::init_service(App::new().default_service(factory)).await;
|
||||
|
||||
for route in ["/foobar", "/baz", "/"].iter() {
|
||||
let req = TestRequest::get().uri(route).to_request();
|
||||
|
|
@ -847,8 +859,9 @@ mod tests {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_default_handler_named_file() {
|
||||
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
|
||||
let st = Files::new("/", ".")
|
||||
.default_handler(NamedFile::open("Cargo.toml").unwrap())
|
||||
.default_handler(factory)
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
|
@ -926,8 +939,8 @@ mod tests {
|
|||
#[actix_rt::test]
|
||||
async fn test_default_handler_filter() {
|
||||
let st = Files::new("/", ".")
|
||||
.default_handler(|req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
.default_handler(|req: ServiceRequest| async {
|
||||
Ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
})
|
||||
.path_filter(|path, _| path.extension() == Some("png".as_ref()))
|
||||
.new_service(())
|
||||
|
|
|
|||
|
|
@ -1,17 +1,22 @@
|
|||
use actix_service::{Service, ServiceFactory};
|
||||
use actix_utils::future::{ok, ready, Ready};
|
||||
use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef};
|
||||
use std::fs::{File, Metadata};
|
||||
use std::io;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
use std::{
|
||||
fmt,
|
||||
fs::Metadata,
|
||||
io,
|
||||
ops::{Deref, DerefMut},
|
||||
path::{Path, PathBuf},
|
||||
time::{SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use actix_web::{
|
||||
dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream},
|
||||
body::{self, BoxBody, SizedStream},
|
||||
dev::{
|
||||
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
|
||||
ServiceResponse,
|
||||
},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
||||
|
|
@ -21,9 +26,9 @@ use actix_web::{
|
|||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use mime_guess::from_path;
|
||||
|
||||
use crate::ChunkedReadFile;
|
||||
use crate::{encoding::equiv_utf8_text, range::HttpRange};
|
||||
|
||||
bitflags! {
|
||||
|
|
@ -48,9 +53,9 @@ impl Default for Flags {
|
|||
/// use actix_web::App;
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let app = App::new()
|
||||
/// .service(NamedFile::open("./static/index.html")?);
|
||||
/// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
|
||||
/// let file = NamedFile::open_async("./static/index.html").await?;
|
||||
/// let app = App::new().service(file);
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
/// ```
|
||||
|
|
@ -62,10 +67,9 @@ impl Default for Flags {
|
|||
///
|
||||
/// #[get("/")]
|
||||
/// async fn index() -> impl Responder {
|
||||
/// NamedFile::open("./static/index.html")
|
||||
/// NamedFile::open_async("./static/index.html").await
|
||||
/// }
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct NamedFile {
|
||||
path: PathBuf,
|
||||
file: File,
|
||||
|
|
@ -78,6 +82,39 @@ pub struct NamedFile {
|
|||
pub(crate) encoding: Option<ContentEncoding>,
|
||||
}
|
||||
|
||||
impl fmt::Debug for NamedFile {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("NamedFile")
|
||||
.field("path", &self.path)
|
||||
.field(
|
||||
"file",
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
&"tokio_uring::File"
|
||||
},
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
&self.file
|
||||
},
|
||||
)
|
||||
.field("modified", &self.modified)
|
||||
.field("md", &self.md)
|
||||
.field("flags", &self.flags)
|
||||
.field("status_code", &self.status_code)
|
||||
.field("content_type", &self.content_type)
|
||||
.field("content_disposition", &self.content_disposition)
|
||||
.field("encoding", &self.encoding)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
pub(crate) use std::fs::File;
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
pub(crate) use tokio_uring::fs::File;
|
||||
|
||||
use super::chunked;
|
||||
|
||||
impl NamedFile {
|
||||
/// Creates an instance from a previously opened file.
|
||||
///
|
||||
|
|
@ -85,8 +122,7 @@ impl NamedFile {
|
|||
/// `ContentDisposition` headers.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// ```ignore
|
||||
/// use actix_files::NamedFile;
|
||||
/// use std::io::{self, Write};
|
||||
/// use std::env;
|
||||
|
|
@ -147,7 +183,30 @@ impl NamedFile {
|
|||
(ct, cd)
|
||||
};
|
||||
|
||||
let md = file.metadata()?;
|
||||
let md = {
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
file.metadata()?
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
use std::os::unix::prelude::{AsRawFd, FromRawFd};
|
||||
|
||||
let fd = file.as_raw_fd();
|
||||
|
||||
// SAFETY: fd is borrowed and lives longer than the unsafe block
|
||||
unsafe {
|
||||
let file = std::fs::File::from_raw_fd(fd);
|
||||
let md = file.metadata();
|
||||
// SAFETY: forget the fd before exiting block in success or error case but don't
|
||||
// run destructor (that would close file handle)
|
||||
std::mem::forget(file);
|
||||
md?
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
let modified = md.modified().ok();
|
||||
let encoding = None;
|
||||
|
||||
|
|
@ -164,17 +223,45 @@ impl NamedFile {
|
|||
})
|
||||
}
|
||||
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
/// Attempts to open a file in read-only mode.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
Self::from_file(File::open(&path)?, path)
|
||||
let file = File::open(&path)?;
|
||||
Self::from_file(file, path)
|
||||
}
|
||||
|
||||
/// Attempts to open a file asynchronously in read-only mode.
|
||||
///
|
||||
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
|
||||
/// Otherwise, it will be just like [`open`][Self::open].
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_files::NamedFile;
|
||||
/// # async fn open() {
|
||||
/// let file = NamedFile::open_async("foo.txt").await.unwrap();
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = {
|
||||
#[cfg(not(feature = "experimental-io-uring"))]
|
||||
{
|
||||
File::open(&path)?
|
||||
}
|
||||
|
||||
#[cfg(feature = "experimental-io-uring")]
|
||||
{
|
||||
File::open(&path).await?
|
||||
}
|
||||
};
|
||||
|
||||
Self::from_file(file, path)
|
||||
}
|
||||
|
||||
/// Returns reference to the underlying `File` object.
|
||||
|
|
@ -186,13 +273,12 @@ impl NamedFile {
|
|||
/// Retrieve the path of this file.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// # use std::io;
|
||||
/// use actix_files::NamedFile;
|
||||
///
|
||||
/// # fn path() -> io::Result<()> {
|
||||
/// let file = NamedFile::open("test.txt")?;
|
||||
/// # async fn path() -> io::Result<()> {
|
||||
/// let file = NamedFile::open_async("test.txt").await?;
|
||||
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
||||
/// # Ok(())
|
||||
/// # }
|
||||
|
|
@ -310,7 +396,7 @@ impl NamedFile {
|
|||
}
|
||||
|
||||
/// Creates an `HttpResponse` with file as a streaming body.
|
||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
|
||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
|
|
@ -332,7 +418,7 @@ impl NamedFile {
|
|||
res.encoding(current_encoding);
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
|
||||
let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
|
||||
|
||||
return res.streaming(reader);
|
||||
}
|
||||
|
|
@ -443,10 +529,13 @@ impl NamedFile {
|
|||
if precondition_failed {
|
||||
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||
} else if not_modified {
|
||||
return resp.status(StatusCode::NOT_MODIFIED).finish();
|
||||
return resp
|
||||
.status(StatusCode::NOT_MODIFIED)
|
||||
.body(body::None::new())
|
||||
.map_into_boxed_body();
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile::new(length, offset, self.file);
|
||||
let reader = chunked::new_chunked_read(length, offset, self.file);
|
||||
|
||||
if offset != 0 || length != self.md.len() {
|
||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
|
|
@ -456,20 +545,6 @@ impl NamedFile {
|
|||
}
|
||||
}
|
||||
|
||||
impl Deref for NamedFile {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &File {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NamedFile {
|
||||
fn deref_mut(&mut self) -> &mut File {
|
||||
&mut self.file
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfMatch>() {
|
||||
|
|
@ -510,8 +585,24 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
|||
}
|
||||
}
|
||||
|
||||
impl Deref for NamedFile {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NamedFile {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl Responder for NamedFile {
|
||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
|
||||
type Body = BoxBody;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
|
||||
self.into_response(req)
|
||||
}
|
||||
}
|
||||
|
|
@ -520,14 +611,16 @@ impl ServiceFactory<ServiceRequest> for NamedFile {
|
|||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Config = ();
|
||||
type InitError = ();
|
||||
type Service = NamedFileService;
|
||||
type Future = Ready<Result<Self::Service, ()>>;
|
||||
type InitError = ();
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
|
||||
|
||||
fn new_service(&self, _: ()) -> Self::Future {
|
||||
ok(NamedFileService {
|
||||
let service = NamedFileService {
|
||||
path: self.path.clone(),
|
||||
})
|
||||
};
|
||||
|
||||
Box::pin(async move { Ok(service) })
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -540,18 +633,19 @@ pub struct NamedFileService {
|
|||
impl Service<ServiceRequest> for NamedFileService {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let (req, _) = req.into_parts();
|
||||
ready(
|
||||
NamedFile::open(&self.path)
|
||||
.map_err(|e| e.into())
|
||||
.map(|f| f.into_response(&req))
|
||||
.map(|res| ServiceResponse::new(req, res)),
|
||||
)
|
||||
|
||||
let path = self.path.clone();
|
||||
Box::pin(async move {
|
||||
let file = NamedFile::open_async(path).await?;
|
||||
let res = file.into_response(&req);
|
||||
Ok(ServiceResponse::new(req, res))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use actix_web::{dev::Payload, FromRequest, HttpRequest};
|
|||
|
||||
use crate::error::UriSegmentError;
|
||||
|
||||
#[derive(Debug)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub(crate) struct PathBufWrap(PathBuf);
|
||||
|
||||
impl FromStr for PathBufWrap {
|
||||
|
|
@ -21,6 +21,8 @@ impl FromStr for PathBufWrap {
|
|||
|
||||
impl PathBufWrap {
|
||||
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
|
||||
///
|
||||
/// Path traversal is guarded by this method.
|
||||
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
|
||||
let mut buf = PathBuf::new();
|
||||
|
||||
|
|
@ -115,4 +117,24 @@ mod tests {
|
|||
PathBuf::from_iter(vec!["test", ".tt"])
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn path_traversal() {
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/../README.md", false).unwrap().0,
|
||||
PathBuf::from_iter(vec!["README.md"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/../README.md", true).unwrap().0,
|
||||
PathBuf::from_iter(vec!["README.md"])
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
|
||||
.unwrap()
|
||||
.0,
|
||||
PathBuf::from_iter(vec!["etc/passwd"])
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use std::{fmt, io, path::PathBuf, rc::Rc};
|
||||
use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::Service;
|
||||
use actix_utils::future::ok;
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
|
|
@ -17,7 +16,18 @@ use crate::{
|
|||
};
|
||||
|
||||
/// Assembled file serving service.
|
||||
pub struct FilesService {
|
||||
#[derive(Clone)]
|
||||
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
|
||||
|
||||
impl Deref for FilesService {
|
||||
type Target = FilesServiceInner;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&*self.0
|
||||
}
|
||||
}
|
||||
|
||||
pub struct FilesServiceInner {
|
||||
pub(crate) directory: PathBuf,
|
||||
pub(crate) index: Option<String>,
|
||||
pub(crate) show_index: bool,
|
||||
|
|
@ -31,20 +41,50 @@ pub struct FilesService {
|
|||
pub(crate) hidden_files: bool,
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesServiceInner {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("FilesServiceInner")
|
||||
}
|
||||
}
|
||||
|
||||
impl FilesService {
|
||||
fn handle_err(
|
||||
async fn handle_err(
|
||||
&self,
|
||||
err: io::Error,
|
||||
req: ServiceRequest,
|
||||
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> {
|
||||
) -> Result<ServiceResponse, Error> {
|
||||
log::debug!("error handling {}: {}", req.path(), err);
|
||||
|
||||
if let Some(ref default) = self.default {
|
||||
Box::pin(default.call(req))
|
||||
default.call(req).await
|
||||
} else {
|
||||
Box::pin(ok(req.error_response(err)))
|
||||
Ok(req.error_response(err))
|
||||
}
|
||||
}
|
||||
|
||||
fn serve_named_file(
|
||||
&self,
|
||||
req: ServiceRequest,
|
||||
mut named_file: NamedFile,
|
||||
) -> ServiceResponse {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
ServiceResponse::new(req, res)
|
||||
}
|
||||
|
||||
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
|
||||
let dir = Directory::new(self.directory.clone(), path);
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
|
||||
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for FilesService {
|
||||
|
|
@ -56,7 +96,7 @@ impl fmt::Debug for FilesService {
|
|||
impl Service<ServiceRequest> for FilesService {
|
||||
type Response = ServiceResponse;
|
||||
type Error = Error;
|
||||
type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
|
||||
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
|
||||
|
|
@ -69,103 +109,87 @@ impl Service<ServiceRequest> for FilesService {
|
|||
matches!(*req.method(), Method::HEAD | Method::GET)
|
||||
};
|
||||
|
||||
let this = self.clone();
|
||||
|
||||
Box::pin(async move {
|
||||
if !is_method_valid {
|
||||
return Box::pin(ok(req.into_response(
|
||||
return Ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
)));
|
||||
));
|
||||
}
|
||||
|
||||
let real_path =
|
||||
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
|
||||
match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Box::pin(ok(req.error_response(e))),
|
||||
Err(e) => return Ok(req.error_response(e)),
|
||||
};
|
||||
|
||||
if let Some(filter) = &self.path_filter {
|
||||
if let Some(filter) = &this.path_filter {
|
||||
if !filter(real_path.as_ref(), req.head()) {
|
||||
if let Some(ref default) = self.default {
|
||||
return Box::pin(default.call(req));
|
||||
if let Some(ref default) = this.default {
|
||||
return default.call(req).await;
|
||||
} else {
|
||||
return Box::pin(ok(
|
||||
return Ok(
|
||||
req.into_response(actix_web::HttpResponse::NotFound().finish())
|
||||
));
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// full file path
|
||||
let path = self.directory.join(&real_path);
|
||||
let path = this.directory.join(&real_path);
|
||||
if let Err(err) = path.canonicalize() {
|
||||
return Box::pin(self.handle_err(err, req));
|
||||
return this.handle_err(err, req).await;
|
||||
}
|
||||
|
||||
if path.is_dir() {
|
||||
if self.redirect_to_slash
|
||||
if this.redirect_to_slash
|
||||
&& !req.path().ends_with('/')
|
||||
&& (self.index.is_some() || self.show_index)
|
||||
&& (this.index.is_some() || this.show_index)
|
||||
{
|
||||
let redirect_to = format!("{}/", req.path());
|
||||
|
||||
return Box::pin(ok(req.into_response(
|
||||
return Ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.finish(),
|
||||
)));
|
||||
));
|
||||
}
|
||||
|
||||
let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
match this.index {
|
||||
Some(ref index) => {
|
||||
let named_path = path.join(index);
|
||||
match NamedFile::open_async(named_path).await {
|
||||
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
|
||||
Err(_) if this.show_index => Ok(this.show_index(req, path)),
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
Box::pin(ok(ServiceResponse::new(req, res)))
|
||||
};
|
||||
|
||||
let show_index = |req: ServiceRequest| {
|
||||
let dir = Directory::new(self.directory.clone(), path.clone());
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let x = (self.renderer)(&dir, &req);
|
||||
|
||||
Box::pin(match x {
|
||||
Ok(resp) => ok(resp),
|
||||
Err(err) => ok(ServiceResponse::from_err(err, req)),
|
||||
})
|
||||
};
|
||||
|
||||
match self.index {
|
||||
Some(ref index) => match NamedFile::open(path.join(index)) {
|
||||
Ok(named_file) => serve_named_file(req, named_file),
|
||||
Err(_) if self.show_index => show_index(req),
|
||||
Err(err) => self.handle_err(err, req),
|
||||
},
|
||||
None if self.show_index => show_index(req),
|
||||
_ => Box::pin(ok(ServiceResponse::from_err(
|
||||
}
|
||||
None if this.show_index => Ok(this.show_index(req, path)),
|
||||
_ => Ok(ServiceResponse::from_err(
|
||||
FilesError::IsDirectory,
|
||||
req.into_parts().0,
|
||||
))),
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
match NamedFile::open(path) {
|
||||
match NamedFile::open_async(&path).await {
|
||||
Ok(mut named_file) => {
|
||||
if let Some(ref mime_override) = self.mime_override {
|
||||
let new_disposition = mime_override(&named_file.content_type.type_());
|
||||
if let Some(ref mime_override) = this.mime_override {
|
||||
let new_disposition =
|
||||
mime_override(&named_file.content_type.type_());
|
||||
named_file.content_disposition.disposition = new_disposition;
|
||||
}
|
||||
named_file.flags = self.file_flags;
|
||||
named_file.flags = this.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
let res = named_file.into_response(&req);
|
||||
Box::pin(ok(ServiceResponse::new(req, res)))
|
||||
Ok(ServiceResponse::new(req, res))
|
||||
}
|
||||
Err(err) => self.handle_err(err, req),
|
||||
Err(err) => this.handle_err(err, req).await,
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,27 @@
|
|||
use actix_files::Files;
|
||||
use actix_web::{
|
||||
http::StatusCode,
|
||||
test::{self, TestRequest},
|
||||
App,
|
||||
};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_directory_traversal_prevention() {
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||
|
||||
let req =
|
||||
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri(
|
||||
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
|
||||
)
|
||||
.to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
|
@ -1,8 +1,27 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.8 - 2021-11-30
|
||||
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 3.0.0-beta.7 - 2021-11-22
|
||||
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
|
||||
|
||||
## 3.0.0-beta.6 - 2021-11-15
|
||||
* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
|
||||
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
|
||||
|
||||
## 3.0.0-beta.5 - 2021-09-09
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.5"
|
||||
version = "3.0.0-beta.8"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
|
|
@ -30,17 +30,17 @@ openssl = ["tls-openssl", "awc/openssl"]
|
|||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.0"
|
||||
actix-tls = "3.0.0-beta.7"
|
||||
actix-codec = "0.4.1"
|
||||
actix-tls = "3.0.0-rc.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
awc = { version = "3.0.0-beta.9", default-features = false }
|
||||
actix-server = "2.0.0-beta.9"
|
||||
awc = { version = "3.0.0-beta.11", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
http = "0.2.2"
|
||||
http = "0.2.5"
|
||||
log = "0.4"
|
||||
socket2 = "0.4"
|
||||
serde = "1.0"
|
||||
|
|
@ -48,7 +48,8 @@ serde_json = "1.0"
|
|||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.14"
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.5)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.8)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
<br>
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.5)
|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.8)
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
use std::{net, sync::mpsc, thread, time::Duration};
|
||||
use std::{net, thread, time::Duration};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_rt::{net::TcpStream, System};
|
||||
|
|
@ -19,17 +19,17 @@ use bytes::Bytes;
|
|||
use futures_core::stream::Stream;
|
||||
use http::Method;
|
||||
use socket2::{Domain, Protocol, Socket, Type};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Start test server
|
||||
/// Start test server.
|
||||
///
|
||||
/// `TestServer` is very simple test server that simplify process of writing
|
||||
/// integration tests cases for actix web applications.
|
||||
/// `TestServer` is very simple test server that simplify process of writing integration tests cases
|
||||
/// for HTTP applications.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```
|
||||
/// ```no_run
|
||||
/// use actix_http::HttpService;
|
||||
/// use actix_http_test::TestServer;
|
||||
/// use actix_http_test::test_server;
|
||||
/// use actix_web::{web, App, HttpResponse, Error};
|
||||
///
|
||||
/// async fn my_handler() -> Result<HttpResponse, Error> {
|
||||
|
|
@ -38,10 +38,9 @@ use socket2::{Domain, Protocol, Socket, Type};
|
|||
///
|
||||
/// #[actix_web::test]
|
||||
/// async fn test_example() {
|
||||
/// let mut srv = TestServer::start(
|
||||
/// || HttpService::new(
|
||||
/// App::new().service(
|
||||
/// web::resource("/").to(my_handler))
|
||||
/// let mut srv = TestServer::start(||
|
||||
/// HttpService::new(
|
||||
/// App::new().service(web::resource("/").to(my_handler))
|
||||
/// )
|
||||
/// );
|
||||
///
|
||||
|
|
@ -55,72 +54,86 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
|
|||
test_server_with_addr(tcp, factory).await
|
||||
}
|
||||
|
||||
/// Start [`test server`](test_server()) on a concrete Address
|
||||
/// Start [`test server`](test_server()) on an existing address binding.
|
||||
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
tcp: net::TcpListener,
|
||||
factory: F,
|
||||
) -> TestServer {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
let (started_tx, started_rx) = std::sync::mpsc::channel();
|
||||
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
|
||||
|
||||
// run server in separate thread
|
||||
thread::spawn(move || {
|
||||
let sys = System::new();
|
||||
System::new().block_on(async move {
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
|
||||
let srv = Server::build()
|
||||
.listen("test", tcp, factory)?
|
||||
.workers(1)
|
||||
.disable_signals();
|
||||
.disable_signals()
|
||||
.system_exit()
|
||||
.listen("test", tcp, factory)
|
||||
.expect("test server could not be created");
|
||||
|
||||
sys.block_on(async {
|
||||
srv.run();
|
||||
tx.send((System::current(), local_addr)).unwrap();
|
||||
let srv = srv.run();
|
||||
started_tx
|
||||
.send((System::current(), srv.handle(), local_addr))
|
||||
.unwrap();
|
||||
|
||||
// drive server loop
|
||||
srv.await.unwrap();
|
||||
});
|
||||
|
||||
sys.run()
|
||||
// notify TestServer that server and system have shut down
|
||||
// all thread managed resources should be dropped at this point
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
let (system, addr) = rx.recv().unwrap();
|
||||
let (system, server, addr) = started_rx.recv().unwrap();
|
||||
|
||||
let client = {
|
||||
let connector = {
|
||||
#[cfg(feature = "openssl")]
|
||||
{
|
||||
let connector = {
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
|
||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
|
||||
builder.set_verify(SslVerifyMode::NONE);
|
||||
let _ = builder
|
||||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||
|
||||
Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
.ssl(builder.build())
|
||||
}
|
||||
};
|
||||
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
{
|
||||
let connector = {
|
||||
Connector::new()
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
}
|
||||
};
|
||||
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
|
||||
TestServer {
|
||||
addr,
|
||||
server,
|
||||
client,
|
||||
system,
|
||||
addr,
|
||||
thread_stop_rx,
|
||||
}
|
||||
}
|
||||
|
||||
/// Test server controller
|
||||
pub struct TestServer {
|
||||
server: actix_server::ServerHandle,
|
||||
client: awc::Client,
|
||||
system: actix_rt::System,
|
||||
addr: net::SocketAddr,
|
||||
client: Client,
|
||||
system: System,
|
||||
thread_stop_rx: mpsc::Receiver<()>,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
|
|
@ -257,15 +270,32 @@ impl TestServer {
|
|||
self.client.headers()
|
||||
}
|
||||
|
||||
/// Stop HTTP server
|
||||
fn stop(&mut self) {
|
||||
/// Stop HTTP server.
|
||||
///
|
||||
/// Waits for spawned `Server` and `System` to (force) shutdown.
|
||||
pub async fn stop(&mut self) {
|
||||
// signal server to stop
|
||||
self.server.stop(false).await;
|
||||
|
||||
// also signal system to stop
|
||||
// though this is handled by `ServerBuilder::exit_system` too
|
||||
self.system.stop();
|
||||
|
||||
// wait for thread to be stopped but don't care about result
|
||||
let _ = self.thread_stop_rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
self.stop()
|
||||
// calls in this Drop impl should be enough to shut down the server, system, and thread
|
||||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
self.system.stop();
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -1,11 +1,92 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
### Added
|
||||
* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
|
||||
* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
|
||||
* `Response::map_into_boxed_body`. [#2468]
|
||||
* `body::EitherBody` enum. [#2468]
|
||||
* `body::None` struct. [#2468]
|
||||
* Impl `MessageBody` for `bytestring::ByteString`. [#2468]
|
||||
* `impl Clone for ws::HandshakeError`. [#2468]
|
||||
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
|
||||
* `impl Default ` for `ws::Codec`. [#1920]
|
||||
* `header::QualityItem::{max, min}`. [#2486]
|
||||
* `header::Quality::{MAX, MIN}`. [#2486]
|
||||
* `impl Display` for `header::Quality`. [#2486]
|
||||
|
||||
### Changed
|
||||
* Rename `body::BoxBody::{from_body => new}`. [#2468]
|
||||
* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
|
||||
* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
|
||||
* Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
|
||||
* `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
|
||||
* `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
|
||||
* `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
|
||||
|
||||
### Removed
|
||||
* `ResponseBuilder::streaming`. [#2468]
|
||||
* `impl Future` for `ResponseBuilder`. [#2468]
|
||||
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
|
||||
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
|
||||
* `impl Copy` for `ws::Codec`. [#1920]
|
||||
* `header::qitem` helper. Replaced with `header::QualityItem::max` [#2486]
|
||||
* `impl TryFrom<u16>` for `header::Quality` [#2486]
|
||||
|
||||
[#2483]: https://github.com/actix/actix-web/pull/2483
|
||||
[#2468]: https://github.com/actix/actix-web/pull/2468
|
||||
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||
[#2486]: https://github.com/actix/actix-web/pull/2486
|
||||
|
||||
|
||||
## 3.0.0-beta.14 - 2021-11-30
|
||||
### Changed
|
||||
* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
|
||||
* Expose `header::map` module. [#2467]
|
||||
* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
|
||||
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2467]: https://github.com/actix/actix-web/pull/2467
|
||||
[#2470]: https://github.com/actix/actix-web/pull/2470
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 3.0.0-beta.13 - 2021-11-22
|
||||
### Added
|
||||
* `body::AnyBody::empty` for quickly creating an empty body. [#2446]
|
||||
* `body::AnyBody::none` for quickly creating a "none" body. [#2456]
|
||||
* `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
|
||||
* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
|
||||
|
||||
### Changed
|
||||
* Rename `body::AnyBody::{Message => Body}`. [#2446]
|
||||
* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
|
||||
* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
|
||||
* Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
|
||||
* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
|
||||
* `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
|
||||
|
||||
### Removed
|
||||
* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
|
||||
* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
|
||||
* `EncoderError::Boxed`; it is no longer required. [#2446]
|
||||
* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
|
||||
|
||||
[#2446]: https://github.com/actix/actix-web/pull/2446
|
||||
[#2448]: https://github.com/actix/actix-web/pull/2448
|
||||
[#2456]: https://github.com/actix/actix-web/pull/2456
|
||||
|
||||
|
||||
## 3.0.0-beta.12 - 2021-11-15
|
||||
### Changed
|
||||
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
|
||||
|
||||
### Removed
|
||||
* `client` module. [#2425]
|
||||
* `trust-dns` feature. [#2425]
|
||||
|
||||
[#2425]: https://github.com/actix/actix-web/pull/2425
|
||||
[#2442]: https://github.com/actix/actix-web/pull/2442
|
||||
|
||||
|
||||
## 3.0.0-beta.11 - 2021-10-20
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.11"
|
||||
version = "3.0.0-beta.14"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
keywords = ["actix", "http", "framework", "async", "futures"]
|
||||
|
|
@ -43,7 +43,7 @@ __compress = []
|
|||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0"
|
||||
actix-codec = "0.4.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-utils = "3.0.0"
|
||||
actix-rt = "2.2"
|
||||
|
||||
|
|
@ -57,7 +57,7 @@ encoding_rs = "0.8"
|
|||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.1"
|
||||
http = "0.2.2"
|
||||
http = "0.2.5"
|
||||
httparse = "1.5.1"
|
||||
httpdate = "1.0.1"
|
||||
itoa = "0.4"
|
||||
|
|
@ -71,30 +71,31 @@ pin-project-lite = "0.2"
|
|||
rand = "0.8"
|
||||
sha-1 = "0.9"
|
||||
smallvec = "1.6.1"
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
|
||||
# tls
|
||||
actix-tls = { version = "3.0.0-beta.7", default-features = false, optional = true }
|
||||
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
|
||||
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
zstd = { version = "0.7", optional = true }
|
||||
zstd = { version = "0.9", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] }
|
||||
actix-tls = { version = "3.0.0-beta.7", features = ["openssl"] }
|
||||
actix-server = "2.0.0-beta.9"
|
||||
actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
|
||||
async-stream = "0.3"
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
env_logger = "0.8"
|
||||
env_logger = "0.9"
|
||||
rcgen = "0.8"
|
||||
regex = "1.3"
|
||||
rustls-pemfile = "0.2"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
static_assertions = "1"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9" }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0" }
|
||||
tokio = { version = "1.2", features = ["net", "rt"] }
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
|
|
@ -111,3 +112,7 @@ harness = false
|
|||
[[bench]]
|
||||
name = "uninit-headers"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "quality-value"
|
||||
harness = false
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.11)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.14)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.11)
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.14)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -0,0 +1,90 @@
|
|||
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
|
||||
|
||||
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
|
||||
|
||||
fn bench_quality_display_impls(c: &mut Criterion) {
|
||||
let mut group = c.benchmark_group("quality value display impls");
|
||||
|
||||
for i in CODES.iter() {
|
||||
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
|
||||
b.iter(|| _new::Quality(i).to_string())
|
||||
});
|
||||
|
||||
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
|
||||
b.iter(|| _naive::Quality(i).to_string())
|
||||
});
|
||||
}
|
||||
|
||||
group.finish();
|
||||
}
|
||||
|
||||
criterion_group!(benches, bench_quality_display_impls);
|
||||
criterion_main!(benches);
|
||||
|
||||
mod _new {
|
||||
use std::fmt;
|
||||
|
||||
pub struct Quality(pub(crate) u16);
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
1000 => f.write_str("1"),
|
||||
|
||||
// some number in the range 1–999
|
||||
x => {
|
||||
f.write_str("0.")?;
|
||||
|
||||
// this implementation avoids string allocation otherwise required
|
||||
// for `.trim_end_matches('0')`
|
||||
|
||||
if x < 10 {
|
||||
f.write_str("00")?;
|
||||
// 0 is handled so it's not possible to have a trailing 0, we can just return
|
||||
itoa::fmt(f, x)
|
||||
} else if x < 100 {
|
||||
f.write_str("0")?;
|
||||
if x % 10 == 0 {
|
||||
// trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
}
|
||||
} else {
|
||||
// x is in range 101–999
|
||||
|
||||
if x % 100 == 0 {
|
||||
// two trailing 0s, divide by 100 and write
|
||||
itoa::fmt(f, x / 100)
|
||||
} else if x % 10 == 0 {
|
||||
// one trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mod _naive {
|
||||
use std::fmt;
|
||||
|
||||
pub struct Quality(pub(crate) u16);
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
1000 => f.write_str("1"),
|
||||
|
||||
x => {
|
||||
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -1,12 +1,14 @@
|
|||
use std::io;
|
||||
|
||||
use actix_http::{body::Body, http::HeaderValue, http::StatusCode};
|
||||
use actix_http::{Error, HttpService, Request, Response};
|
||||
use actix_http::{
|
||||
body::MessageBody, http::HeaderValue, http::StatusCode, Error, HttpService, Request,
|
||||
Response,
|
||||
};
|
||||
use actix_server::Server;
|
||||
use bytes::BytesMut;
|
||||
use futures_util::StreamExt as _;
|
||||
|
||||
async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> {
|
||||
async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
|
||||
let mut body = BytesMut::new();
|
||||
while let Some(item) = req.payload().next().await {
|
||||
body.extend_from_slice(&item?)
|
||||
|
|
|
|||
|
|
@ -1,227 +0,0 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
error::Error as StdError,
|
||||
fmt, mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream};
|
||||
|
||||
pub type Body = AnyBody;
|
||||
|
||||
/// Represents various types of HTTP message body.
|
||||
pub enum AnyBody {
|
||||
/// Empty response. `Content-Length` header is not set.
|
||||
None,
|
||||
|
||||
/// Zero sized response body. `Content-Length` header is set to `0`.
|
||||
Empty,
|
||||
|
||||
/// Specific response body.
|
||||
Bytes(Bytes),
|
||||
|
||||
/// Generic message body.
|
||||
Message(BoxAnyBody),
|
||||
}
|
||||
|
||||
impl AnyBody {
|
||||
/// Create body from slice (copy)
|
||||
pub fn from_slice(s: &[u8]) -> Self {
|
||||
Self::Bytes(Bytes::copy_from_slice(s))
|
||||
}
|
||||
|
||||
/// Create body from generic message body.
|
||||
pub fn from_message<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError + 'static>>,
|
||||
{
|
||||
Self::Message(BoxAnyBody::from_body(body))
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for AnyBody {
|
||||
type Error = Error;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
AnyBody::None => BodySize::None,
|
||||
AnyBody::Empty => BodySize::Empty,
|
||||
AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
|
||||
AnyBody::Message(ref body) => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.get_mut() {
|
||||
AnyBody::None => Poll::Ready(None),
|
||||
AnyBody::Empty => Poll::Ready(None),
|
||||
AnyBody::Bytes(ref mut bin) => {
|
||||
let len = bin.len();
|
||||
if len == 0 {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(bin))))
|
||||
}
|
||||
}
|
||||
|
||||
AnyBody::Message(body) => body
|
||||
.as_pin_mut()
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyBody {
|
||||
fn eq(&self, other: &Body) -> bool {
|
||||
match *self {
|
||||
AnyBody::None => matches!(*other, AnyBody::None),
|
||||
AnyBody::Empty => matches!(*other, AnyBody::Empty),
|
||||
AnyBody::Bytes(ref b) => match *other {
|
||||
AnyBody::Bytes(ref b2) => b == b2,
|
||||
_ => false,
|
||||
},
|
||||
AnyBody::Message(_) => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for AnyBody {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
AnyBody::None => write!(f, "AnyBody::None"),
|
||||
AnyBody::Empty => write!(f, "AnyBody::Empty"),
|
||||
AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b),
|
||||
AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for AnyBody {
|
||||
fn from(s: &'static str) -> Body {
|
||||
AnyBody::Bytes(Bytes::from_static(s.as_ref()))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for AnyBody {
|
||||
fn from(s: &'static [u8]) -> Body {
|
||||
AnyBody::Bytes(Bytes::from_static(s))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Vec<u8>> for AnyBody {
|
||||
fn from(vec: Vec<u8>) -> Body {
|
||||
AnyBody::Bytes(Bytes::from(vec))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for AnyBody {
|
||||
fn from(s: String) -> Body {
|
||||
s.into_bytes().into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'_ String> for AnyBody {
|
||||
fn from(s: &String) -> Body {
|
||||
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Cow<'_, str>> for AnyBody {
|
||||
fn from(s: Cow<'_, str>) -> Body {
|
||||
match s {
|
||||
Cow::Owned(s) => AnyBody::from(s),
|
||||
Cow::Borrowed(s) => {
|
||||
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for AnyBody {
|
||||
fn from(s: Bytes) -> Body {
|
||||
AnyBody::Bytes(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BytesMut> for AnyBody {
|
||||
fn from(s: BytesMut) -> Body {
|
||||
AnyBody::Bytes(s.freeze())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<SizedStream<S>> for AnyBody
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
fn from(s: SizedStream<S>) -> Body {
|
||||
AnyBody::from_message(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<BodyStream<S>> for AnyBody
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
fn from(s: BodyStream<S>) -> Body {
|
||||
AnyBody::from_message(s)
|
||||
}
|
||||
}
|
||||
|
||||
/// A boxed message body with boxed errors.
|
||||
pub struct BoxAnyBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError + 'static>>>>);
|
||||
|
||||
impl BoxAnyBody {
|
||||
/// Boxes a `MessageBody` and any errors it generates.
|
||||
pub fn from_body<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError + 'static>>,
|
||||
{
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(Box::pin(body))
|
||||
}
|
||||
|
||||
/// Returns a mutable pinned reference to the inner message body type.
|
||||
pub fn as_pin_mut(
|
||||
&mut self,
|
||||
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError + 'static>>)> {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BoxAnyBody {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("BoxAnyBody(dyn MessageBody)")
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BoxAnyBody {
|
||||
type Error = Error;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
self.0.size()
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
self.0
|
||||
.as_mut()
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err))
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,8 @@ pin_project! {
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: from_infallible method
|
||||
|
||||
impl<S, E> BodyStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
|
|
@ -75,10 +77,23 @@ mod tests {
|
|||
use derive_more::{Display, Error};
|
||||
use futures_core::ready;
|
||||
use futures_util::{stream, FutureExt as _};
|
||||
use pin_project_lite::pin_project;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||
|
||||
assert_not_impl_all!(BodyStream<stream::Empty<Bytes>>: MessageBody);
|
||||
assert_not_impl_all!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
|
||||
// crate::Error is not Clone
|
||||
assert_not_impl_all!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn skips_empty_chunks() {
|
||||
let body = BodyStream::new(stream::iter(
|
||||
|
|
@ -124,19 +139,45 @@ mod tests {
|
|||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_string_error() {
|
||||
// `&'static str` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = BodyStream::new(stream::once(async { Err("stringy error") }));
|
||||
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_boxed_error() {
|
||||
// `Box<dyn Error>` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = BodyStream::new(stream::once(async {
|
||||
Err(Box::<dyn StdError>::from("stringy error"))
|
||||
}));
|
||||
|
||||
assert_eq!(
|
||||
to_bytes(body).await.unwrap_err().to_string(),
|
||||
"stringy error"
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_delayed_error() {
|
||||
let body =
|
||||
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
|
||||
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
|
||||
|
||||
#[pin_project::pin_project(project = TimeDelayStreamProj)]
|
||||
pin_project! {
|
||||
#[derive(Debug)]
|
||||
#[project = TimeDelayStreamProj]
|
||||
enum TimeDelayStream {
|
||||
Start,
|
||||
Sleep(Pin<Box<Sleep>>),
|
||||
Sleep { delay: Pin<Box<Sleep>> },
|
||||
Done,
|
||||
}
|
||||
}
|
||||
|
||||
impl Stream for TimeDelayStream {
|
||||
type Item = Result<Bytes, StreamErr>;
|
||||
|
|
@ -148,12 +189,14 @@ mod tests {
|
|||
match self.as_mut().get_mut() {
|
||||
TimeDelayStream::Start => {
|
||||
let sleep = sleep(Duration::from_millis(1));
|
||||
self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep)));
|
||||
self.as_mut().set(TimeDelayStream::Sleep {
|
||||
delay: Box::pin(sleep),
|
||||
});
|
||||
cx.waker().wake_by_ref();
|
||||
Poll::Pending
|
||||
}
|
||||
|
||||
TimeDelayStream::Sleep(ref mut delay) => {
|
||||
TimeDelayStream::Sleep { ref mut delay } => {
|
||||
ready!(delay.poll_unpin(cx));
|
||||
self.set(TimeDelayStream::Done);
|
||||
cx.waker().wake_by_ref();
|
||||
|
|
|
|||
|
|
@ -0,0 +1,80 @@
|
|||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::{BodySize, MessageBody, MessageBodyMapErr};
|
||||
use crate::Error;
|
||||
|
||||
/// A boxed message body with boxed errors.
|
||||
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
|
||||
|
||||
impl BoxBody {
|
||||
/// Boxes a `MessageBody` and any errors it generates.
|
||||
pub fn new<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
let body = MessageBodyMapErr::new(body, Into::into);
|
||||
Self(Box::pin(body))
|
||||
}
|
||||
|
||||
/// Returns a mutable pinned reference to the inner message body type.
|
||||
pub fn as_pin_mut(
|
||||
&mut self,
|
||||
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
|
||||
self.0.as_mut()
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for BoxBody {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str("BoxBody(dyn MessageBody)")
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for BoxBody {
|
||||
type Error = Error;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
self.0.size()
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
self.0
|
||||
.as_mut()
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
|
||||
|
||||
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn nested_boxed_body() {
|
||||
let body = Bytes::from_static(&[1, 2, 3]);
|
||||
let boxed_body = BoxBody::new(BoxBody::new(body));
|
||||
|
||||
assert_eq!(
|
||||
to_bytes(boxed_body).await.unwrap(),
|
||||
Bytes::from(vec![1, 2, 3]),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,83 @@
|
|||
use std::{
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{BodySize, BoxBody, MessageBody};
|
||||
use crate::Error;
|
||||
|
||||
pin_project! {
|
||||
#[project = EitherBodyProj]
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum EitherBody<L, R = BoxBody> {
|
||||
/// A body of type `L`.
|
||||
Left { #[pin] body: L },
|
||||
|
||||
/// A body of type `R`.
|
||||
Right { #[pin] body: R },
|
||||
}
|
||||
}
|
||||
|
||||
impl<L> EitherBody<L, BoxBody> {
|
||||
/// Creates new `EitherBody` using left variant and boxed right variant.
|
||||
pub fn new(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, R> EitherBody<L, R> {
|
||||
/// Creates new `EitherBody` using left variant.
|
||||
pub fn left(body: L) -> Self {
|
||||
Self::Left { body }
|
||||
}
|
||||
|
||||
/// Creates new `EitherBody` using right variant.
|
||||
pub fn right(body: R) -> Self {
|
||||
Self::Right { body }
|
||||
}
|
||||
}
|
||||
|
||||
impl<L, R> MessageBody for EitherBody<L, R>
|
||||
where
|
||||
L: MessageBody + 'static,
|
||||
R: MessageBody + 'static,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
EitherBody::Left { body } => body.size(),
|
||||
EitherBody::Right { body } => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.project() {
|
||||
EitherBodyProj::Left { body } => body
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err)),
|
||||
EitherBodyProj::Right { body } => body
|
||||
.poll_next(cx)
|
||||
.map_err(|err| Error::new_body().with_cause(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn type_parameter_inference() {
|
||||
let _body: EitherBody<(), _> = EitherBody::new(());
|
||||
|
||||
let _body: EitherBody<_, ()> = EitherBody::left(());
|
||||
let _body: EitherBody<(), _> = EitherBody::right(());
|
||||
}
|
||||
}
|
||||
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
use std::{
|
||||
convert::Infallible,
|
||||
error::Error as StdError,
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
|
|
@ -13,9 +14,12 @@ use pin_project_lite::pin_project;
|
|||
|
||||
use super::BodySize;
|
||||
|
||||
/// An interface for response bodies.
|
||||
/// An interface types that can converted to bytes and used as response bodies.
|
||||
// TODO: examples
|
||||
pub trait MessageBody {
|
||||
type Error;
|
||||
// TODO: consider this bound to only fmt::Display since the error type is not really used
|
||||
// and there is an impl for Into<Box<StdError>> on String
|
||||
type Error: Into<Box<dyn StdError>>;
|
||||
|
||||
/// Body size hint.
|
||||
fn size(&self) -> BodySize;
|
||||
|
|
@ -27,16 +31,38 @@ pub trait MessageBody {
|
|||
) -> Poll<Option<Result<Bytes, Self::Error>>>;
|
||||
}
|
||||
|
||||
mod foreign_impls {
|
||||
use super::*;
|
||||
|
||||
impl MessageBody for Infallible {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
match *self {}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match *self {}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for () {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Empty
|
||||
BodySize::Sized(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Poll::Ready(None)
|
||||
}
|
||||
|
|
@ -48,10 +74,12 @@ where
|
|||
{
|
||||
type Error = B::Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.as_ref().size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
|
|
@ -66,10 +94,12 @@ where
|
|||
{
|
||||
type Error = B::Error;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
self.as_ref().size()
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
|
|
@ -78,6 +108,27 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static [u8] {
|
||||
type Error = Infallible;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let bytes = mem::take(self.get_mut());
|
||||
let bytes = Bytes::from_static(bytes);
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for Bytes {
|
||||
type Error = Infallible;
|
||||
|
||||
|
|
@ -87,12 +138,13 @@ impl MessageBody for Bytes {
|
|||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
|
||||
let bytes = mem::take(self.get_mut());
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -106,33 +158,13 @@ impl MessageBody for BytesMut {
|
|||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static str {
|
||||
type Error = Infallible;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(Bytes::from_static(
|
||||
mem::take(self.get_mut()).as_ref(),
|
||||
))))
|
||||
let bytes = mem::take(self.get_mut()).freeze();
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -146,12 +178,34 @@ impl MessageBody for Vec<u8> {
|
|||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
|
||||
let bytes = mem::take(self.get_mut());
|
||||
Poll::Ready(Some(Ok(Bytes::from(bytes))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for &'static str {
|
||||
type Error = Infallible;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let string = mem::take(self.get_mut());
|
||||
let bytes = Bytes::from_static(string.as_bytes());
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -165,14 +219,30 @@ impl MessageBody for String {
|
|||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_: &mut Context<'_>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
if self.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(Bytes::from(
|
||||
mem::take(self.get_mut()).into_bytes(),
|
||||
))))
|
||||
let string = mem::take(self.get_mut());
|
||||
Poll::Ready(Some(Ok(Bytes::from(string))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for bytestring::ByteString {
|
||||
type Error = Infallible;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::Sized(self.len() as u64)
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
let string = mem::take(self.get_mut());
|
||||
Poll::Ready(Some(Ok(string.into_bytes())))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -202,6 +272,7 @@ impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
|
|||
where
|
||||
B: MessageBody,
|
||||
F: FnOnce(B::Error) -> E,
|
||||
E: Into<Box<dyn StdError>>,
|
||||
{
|
||||
type Error = E;
|
||||
|
||||
|
|
@ -226,3 +297,129 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::*;
|
||||
|
||||
macro_rules! assert_poll_next {
|
||||
($pin:expr, $exp:expr) => {
|
||||
assert_eq!(
|
||||
poll_fn(|cx| $pin.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap() // unwrap option
|
||||
.unwrap(), // unwrap result
|
||||
$exp
|
||||
);
|
||||
};
|
||||
}
|
||||
|
||||
macro_rules! assert_poll_next_none {
|
||||
($pin:expr) => {
|
||||
assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none());
|
||||
};
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn boxing_equivalence() {
|
||||
assert_eq!(().size(), BodySize::Sized(0));
|
||||
assert_eq!(().size(), Box::new(()).size());
|
||||
assert_eq!(().size(), Box::pin(()).size());
|
||||
|
||||
let pl = Box::new(());
|
||||
pin!(pl);
|
||||
assert_poll_next_none!(pl);
|
||||
|
||||
let mut pl = Box::pin(());
|
||||
assert_poll_next_none!(pl);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_unit() {
|
||||
let pl = ();
|
||||
assert_eq!(pl.size(), BodySize::Sized(0));
|
||||
pin!(pl);
|
||||
assert_poll_next_none!(pl);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_str() {
|
||||
assert_eq!("".size(), BodySize::Sized(0));
|
||||
assert_eq!("test".size(), BodySize::Sized(4));
|
||||
|
||||
let pl = "test";
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_bytes() {
|
||||
assert_eq!(b"".as_ref().size(), BodySize::Sized(0));
|
||||
assert_eq!(b"test".as_ref().size(), BodySize::Sized(4));
|
||||
|
||||
let pl = b"test".as_ref();
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vec() {
|
||||
assert_eq!(vec![0; 0].size(), BodySize::Sized(0));
|
||||
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
|
||||
|
||||
let pl = Vec::from("test");
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bytes() {
|
||||
assert_eq!(Bytes::new().size(), BodySize::Sized(0));
|
||||
assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4));
|
||||
|
||||
let pl = Bytes::from_static(b"test");
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bytes_mut() {
|
||||
assert_eq!(BytesMut::new().size(), BodySize::Sized(0));
|
||||
assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||
|
||||
let pl = BytesMut::from("test");
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_string() {
|
||||
assert_eq!(String::new().size(), BodySize::Sized(0));
|
||||
assert_eq!("test".to_owned().size(), BodySize::Sized(4));
|
||||
|
||||
let pl = "test".to_owned();
|
||||
pin!(pl);
|
||||
assert_poll_next!(pl, Bytes::from("test"));
|
||||
}
|
||||
|
||||
// down-casting used to be done with a method on MessageBody trait
|
||||
// test is kept to demonstrate equivalence of Any trait
|
||||
#[actix_rt::test]
|
||||
async fn test_body_casting() {
|
||||
let mut body = String::from("hello cast");
|
||||
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
|
||||
let resp_body: &mut dyn std::any::Any = &mut body;
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
assert!(not_body.is_none());
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,263 +1,20 @@
|
|||
//! Traits and structures to aid consuming and writing HTTP payloads.
|
||||
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
|
||||
#[allow(clippy::module_inception)]
|
||||
mod body;
|
||||
mod body_stream;
|
||||
mod boxed;
|
||||
mod either;
|
||||
mod message_body;
|
||||
mod response_body;
|
||||
mod none;
|
||||
mod size;
|
||||
mod sized_stream;
|
||||
mod utils;
|
||||
|
||||
pub use self::body::{AnyBody, Body, BoxAnyBody};
|
||||
pub use self::body_stream::BodyStream;
|
||||
pub use self::boxed::BoxBody;
|
||||
pub use self::either::EitherBody;
|
||||
pub use self::message_body::MessageBody;
|
||||
pub(crate) use self::message_body::MessageBodyMapErr;
|
||||
pub use self::response_body::ResponseBody;
|
||||
pub use self::none::None;
|
||||
pub use self::size::BodySize;
|
||||
pub use self::sized_stream::SizedStream;
|
||||
|
||||
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
||||
///
|
||||
/// Any errors produced by the body stream are returned immediately.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_http::body::{Body, to_bytes};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// # async fn test_to_bytes() {
|
||||
/// let body = Body::Empty;
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert!(bytes.is_empty());
|
||||
///
|
||||
/// let body = Body::Bytes(Bytes::from_static(b"123"));
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert_eq!(bytes, b"123"[..]);
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||
let cap = match body.size() {
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
|
||||
BodySize::Sized(size) => size as usize,
|
||||
BodySize::Stream => 32_768,
|
||||
};
|
||||
|
||||
let mut buf = BytesMut::with_capacity(cap);
|
||||
|
||||
pin!(body);
|
||||
|
||||
poll_fn(|cx| loop {
|
||||
let body = body.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(buf.freeze())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::pin::Pin;
|
||||
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl Body {
|
||||
pub(crate) fn get_ref(&self) -> &[u8] {
|
||||
match *self {
|
||||
Body::Bytes(ref bin) => bin,
|
||||
_ => panic!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_str() {
|
||||
assert_eq!(Body::from("").size(), BodySize::Sized(0));
|
||||
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from("test").get_ref(), b"test");
|
||||
|
||||
assert_eq!("test".size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok(),
|
||||
Some(Bytes::from("test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_bytes() {
|
||||
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
|
||||
assert_eq!(
|
||||
Body::from_slice(b"test".as_ref()).size(),
|
||||
BodySize::Sized(4)
|
||||
);
|
||||
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
|
||||
let sb = Bytes::from(&b"test"[..]);
|
||||
pin!(sb);
|
||||
|
||||
assert_eq!(sb.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||
Some(Bytes::from("test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_vec() {
|
||||
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
|
||||
let test_vec = Vec::from("test");
|
||||
pin!(test_vec);
|
||||
|
||||
assert_eq!(test_vec.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
|
||||
.await
|
||||
.unwrap()
|
||||
.ok(),
|
||||
Some(Bytes::from("test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bytes() {
|
||||
let b = Bytes::from("test");
|
||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||
pin!(b);
|
||||
|
||||
assert_eq!(b.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||
Some(Bytes::from("test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_bytes_mut() {
|
||||
let b = BytesMut::from("test");
|
||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||
pin!(b);
|
||||
|
||||
assert_eq!(b.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||
Some(Bytes::from("test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_string() {
|
||||
let b = "test".to_owned();
|
||||
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
|
||||
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
|
||||
assert_eq!(Body::from(&b).get_ref(), b"test");
|
||||
pin!(b);
|
||||
|
||||
assert_eq!(b.size(), BodySize::Sized(4));
|
||||
assert_eq!(
|
||||
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
|
||||
Some(Bytes::from("test"))
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_unit() {
|
||||
assert_eq!(().size(), BodySize::Empty);
|
||||
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
|
||||
.await
|
||||
.is_none());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_box_and_pin() {
|
||||
let val = Box::new(());
|
||||
pin!(val);
|
||||
assert_eq!(val.size(), BodySize::Empty);
|
||||
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
||||
|
||||
let mut val = Box::pin(());
|
||||
assert_eq!(val.size(), BodySize::Empty);
|
||||
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_body_eq() {
|
||||
assert!(
|
||||
Body::Bytes(Bytes::from_static(b"1"))
|
||||
== Body::Bytes(Bytes::from_static(b"1"))
|
||||
);
|
||||
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_body_debug() {
|
||||
assert!(format!("{:?}", Body::None).contains("Body::None"));
|
||||
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
|
||||
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_serde_json() {
|
||||
use serde_json::{json, Value};
|
||||
assert_eq!(
|
||||
Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap())
|
||||
.size(),
|
||||
BodySize::Sized(6)
|
||||
);
|
||||
assert_eq!(
|
||||
Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap())
|
||||
.size(),
|
||||
BodySize::Sized(25)
|
||||
);
|
||||
}
|
||||
|
||||
// down-casting used to be done with a method on MessageBody trait
|
||||
// test is kept to demonstrate equivalence of Any trait
|
||||
#[actix_rt::test]
|
||||
async fn test_body_casting() {
|
||||
let mut body = String::from("hello cast");
|
||||
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
|
||||
let resp_body: &mut dyn std::any::Any = &mut body;
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast");
|
||||
let body = &mut resp_body.downcast_mut::<String>().unwrap();
|
||||
body.push('!');
|
||||
let body = resp_body.downcast_ref::<String>().unwrap();
|
||||
assert_eq!(body, "hello cast!");
|
||||
let not_body = resp_body.downcast_ref::<()>();
|
||||
assert!(not_body.is_none());
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_to_bytes() {
|
||||
let body = Body::Empty;
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
let body = Body::Bytes(Bytes::from_static(b"123"));
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123"[..]);
|
||||
}
|
||||
}
|
||||
pub use self::utils::to_bytes;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
use std::{
|
||||
convert::Infallible,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
/// Body type for responses that forbid payloads.
|
||||
///
|
||||
/// Distinct from an empty response which would contain a Content-Length header.
|
||||
///
|
||||
/// For an "empty" body, use `()` or `Bytes::new()`.
|
||||
#[derive(Debug, Clone, Copy, Default)]
|
||||
#[non_exhaustive]
|
||||
pub struct None;
|
||||
|
||||
impl None {
|
||||
/// Constructs new "none" body.
|
||||
#[inline]
|
||||
pub fn new() -> Self {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageBody for None {
|
||||
type Error = Infallible;
|
||||
|
||||
#[inline]
|
||||
fn size(&self) -> BodySize {
|
||||
BodySize::None
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Poll::Ready(Option::None)
|
||||
}
|
||||
}
|
||||
|
|
@ -1,84 +0,0 @@
|
|||
use std::{
|
||||
mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::error::Error;
|
||||
|
||||
use super::{Body, BodySize, MessageBody};
|
||||
|
||||
#[pin_project(project = ResponseBodyProj)]
|
||||
pub enum ResponseBody<B> {
|
||||
Body(#[pin] B),
|
||||
Other(Body),
|
||||
}
|
||||
|
||||
impl ResponseBody<Body> {
|
||||
pub fn into_body<B>(self) -> ResponseBody<B> {
|
||||
match self {
|
||||
ResponseBody::Body(b) => ResponseBody::Other(b),
|
||||
ResponseBody::Other(b) => ResponseBody::Other(b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> ResponseBody<B> {
|
||||
pub fn take_body(&mut self) -> ResponseBody<B> {
|
||||
mem::replace(self, ResponseBody::Other(Body::None))
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> ResponseBody<B> {
|
||||
pub fn as_ref(&self) -> Option<&B> {
|
||||
if let ResponseBody::Body(ref b) = self {
|
||||
Some(b)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for ResponseBody<B>
|
||||
where
|
||||
B: MessageBody,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
type Error = Error;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
ResponseBody::Body(ref body) => body.size(),
|
||||
ResponseBody::Other(ref body) => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
Stream::poll_next(self, cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> Stream for ResponseBody<B>
|
||||
where
|
||||
B: MessageBody,
|
||||
B::Error: Into<Error>,
|
||||
{
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
match self.project() {
|
||||
ResponseBodyProj::Body(body) => body.poll_next(cx).map_err(Into::into),
|
||||
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -6,14 +6,9 @@ pub enum BodySize {
|
|||
/// Will skip writing Content-Length header.
|
||||
None,
|
||||
|
||||
/// Zero size body.
|
||||
///
|
||||
/// Will write `Content-Length: 0` header.
|
||||
Empty,
|
||||
|
||||
/// Known size body.
|
||||
///
|
||||
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`.
|
||||
/// Will write `Content-Length: N` header.
|
||||
Sized(u64),
|
||||
|
||||
/// Unknown size body.
|
||||
|
|
@ -23,18 +18,19 @@ pub enum BodySize {
|
|||
}
|
||||
|
||||
impl BodySize {
|
||||
/// Returns true if size hint indicates no or empty body.
|
||||
/// Returns true if size hint indicates omitted or empty body.
|
||||
///
|
||||
/// Streams will return false because it cannot be known without reading the stream.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::body::BodySize;
|
||||
/// assert!(BodySize::None.is_eof());
|
||||
/// assert!(BodySize::Empty.is_eof());
|
||||
/// assert!(BodySize::Sized(0).is_eof());
|
||||
///
|
||||
/// assert!(!BodySize::Sized(64).is_eof());
|
||||
/// assert!(!BodySize::Stream.is_eof());
|
||||
/// ```
|
||||
pub fn is_eof(&self) -> bool {
|
||||
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
|
||||
matches!(self, BodySize::None | BodySize::Sized(0))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -32,6 +32,8 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
// TODO: from_infallible method
|
||||
|
||||
impl<S, E> MessageBody for SizedStream<S>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>>,
|
||||
|
|
@ -72,10 +74,22 @@ mod tests {
|
|||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use futures_util::stream;
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
|
||||
use super::*;
|
||||
use crate::body::to_bytes;
|
||||
|
||||
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
|
||||
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
|
||||
|
||||
assert_not_impl_all!(SizedStream<stream::Empty<Bytes>>: MessageBody);
|
||||
assert_not_impl_all!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
|
||||
// crate::Error is not Clone
|
||||
assert_not_impl_all!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn skips_empty_chunks() {
|
||||
let body = SizedStream::new(
|
||||
|
|
@ -119,4 +133,37 @@ mod tests {
|
|||
|
||||
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_string_error() {
|
||||
// `&'static str` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
|
||||
assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
|
||||
|
||||
let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
|
||||
assert!(matches!(to_bytes(body).await, Err("stringy error")));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn stream_boxed_error() {
|
||||
// `Box<dyn Error>` does not impl `Error`
|
||||
// but it does impl `Into<Box<dyn Error>>`
|
||||
|
||||
let body = SizedStream::new(
|
||||
0,
|
||||
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||
);
|
||||
assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
|
||||
|
||||
let body = SizedStream::new(
|
||||
1,
|
||||
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
|
||||
);
|
||||
assert_eq!(
|
||||
to_bytes(body).await.unwrap_err().to_string(),
|
||||
"stringy error"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
use std::task::Poll;
|
||||
|
||||
use actix_rt::pin;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
|
||||
use super::{BodySize, MessageBody};
|
||||
|
||||
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
|
||||
///
|
||||
/// Any errors produced by the body stream are returned immediately.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_http::body::{self, to_bytes};
|
||||
/// use bytes::Bytes;
|
||||
///
|
||||
/// # async fn test_to_bytes() {
|
||||
/// let body = body::None::new();
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert!(bytes.is_empty());
|
||||
///
|
||||
/// let body = Bytes::from_static(b"123");
|
||||
/// let bytes = to_bytes(body).await.unwrap();
|
||||
/// assert_eq!(bytes, b"123"[..]);
|
||||
/// # }
|
||||
/// ```
|
||||
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
|
||||
let cap = match body.size() {
|
||||
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
|
||||
BodySize::Sized(size) => size as usize,
|
||||
// good enough first guess for chunk size
|
||||
BodySize::Stream => 32_768,
|
||||
};
|
||||
|
||||
let mut buf = BytesMut::with_capacity(cap);
|
||||
|
||||
pin!(body);
|
||||
|
||||
poll_fn(|cx| loop {
|
||||
let body = body.as_mut();
|
||||
|
||||
match ready!(body.poll_next(cx)) {
|
||||
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
|
||||
None => return Poll::Ready(Ok(())),
|
||||
Some(Err(err)) => return Poll::Ready(Err(err)),
|
||||
}
|
||||
})
|
||||
.await?;
|
||||
|
||||
Ok(buf.freeze())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use futures_util::{stream, StreamExt as _};
|
||||
|
||||
use super::*;
|
||||
use crate::{body::BodyStream, Error};
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_to_bytes() {
|
||||
let bytes = to_bytes(()).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
let body = Bytes::from_static(b"123");
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123"[..]);
|
||||
|
||||
let stream =
|
||||
stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
|
||||
.map(Ok::<_, Error>);
|
||||
let body = BodyStream::new(stream);
|
||||
let bytes = to_bytes(body).await.unwrap();
|
||||
assert_eq!(bytes, b"123abc"[..]);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,10 +1,10 @@
|
|||
use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc};
|
||||
use std::{fmt, marker::PhantomData, net, rc::Rc};
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::{IntoServiceFactory, Service, ServiceFactory};
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, MessageBody},
|
||||
body::{BoxBody, MessageBody},
|
||||
config::{KeepAlive, ServiceConfig},
|
||||
h1::{self, ExpectHandler, H1Service, UpgradeHandler},
|
||||
h2::H2Service,
|
||||
|
|
@ -31,7 +31,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
|
|||
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
{
|
||||
|
|
@ -54,11 +54,11 @@ where
|
|||
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -120,7 +120,7 @@ where
|
|||
where
|
||||
F: IntoServiceFactory<X1, Request>,
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X1::Error: Into<Response<AnyBody>>,
|
||||
X1::Error: Into<Response<BoxBody>>,
|
||||
X1::InitError: fmt::Debug,
|
||||
{
|
||||
HttpServiceBuilder {
|
||||
|
|
@ -178,7 +178,7 @@ where
|
|||
where
|
||||
B: MessageBody,
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
{
|
||||
|
|
@ -200,12 +200,11 @@ where
|
|||
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
|
|
@ -223,12 +222,11 @@ where
|
|||
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
|
||||
where
|
||||
F: IntoServiceFactory<S, Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
let cfg = ServiceConfig::new(
|
||||
self.keep_alive,
|
||||
|
|
|
|||
|
|
@ -20,8 +20,10 @@ pub(crate) const DATE_VALUE_LENGTH: usize = 29;
|
|||
pub enum KeepAlive {
|
||||
/// Keep alive in seconds
|
||||
Timeout(usize),
|
||||
|
||||
/// Rely on OS to shutdown tcp connection
|
||||
Os,
|
||||
|
||||
/// Disabled
|
||||
Disabled,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -12,7 +12,7 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
|
|||
use bytes::Bytes;
|
||||
use derive_more::Display;
|
||||
use futures_core::ready;
|
||||
use pin_project::pin_project;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
use brotli2::write::BrotliEncoder;
|
||||
|
|
@ -23,8 +23,10 @@ use flate2::write::{GzEncoder, ZlibEncoder};
|
|||
#[cfg(feature = "compress-zstd")]
|
||||
use zstd::stream::write::Encoder as ZstdEncoder;
|
||||
|
||||
use super::Writer;
|
||||
use crate::{
|
||||
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
|
||||
body::{BodySize, MessageBody},
|
||||
error::BlockingError,
|
||||
http::{
|
||||
header::{ContentEncoding, CONTENT_ENCODING},
|
||||
HeaderValue, StatusCode,
|
||||
|
|
@ -32,89 +34,92 @@ use crate::{
|
|||
ResponseHead,
|
||||
};
|
||||
|
||||
use super::Writer;
|
||||
use crate::error::BlockingError;
|
||||
|
||||
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
|
||||
|
||||
#[pin_project]
|
||||
pin_project! {
|
||||
pub struct Encoder<B> {
|
||||
eof: bool,
|
||||
#[pin]
|
||||
body: EncoderBody<B>,
|
||||
encoder: Option<ContentEncoder>,
|
||||
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
|
||||
eof: bool,
|
||||
}
|
||||
}
|
||||
|
||||
impl<B: MessageBody> Encoder<B> {
|
||||
fn none() -> Self {
|
||||
Encoder {
|
||||
body: EncoderBody::None,
|
||||
encoder: None,
|
||||
fut: None,
|
||||
eof: true,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn response(
|
||||
encoding: ContentEncoding,
|
||||
head: &mut ResponseHead,
|
||||
body: ResponseBody<B>,
|
||||
) -> ResponseBody<Encoder<B>> {
|
||||
body: B,
|
||||
) -> Self {
|
||||
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|
||||
|| head.status == StatusCode::SWITCHING_PROTOCOLS
|
||||
|| head.status == StatusCode::NO_CONTENT
|
||||
|| encoding == ContentEncoding::Identity
|
||||
|| encoding == ContentEncoding::Auto);
|
||||
|
||||
let body = match body {
|
||||
ResponseBody::Other(b) => match b {
|
||||
Body::None => return ResponseBody::Other(Body::None),
|
||||
Body::Empty => return ResponseBody::Other(Body::Empty),
|
||||
Body::Bytes(buf) => {
|
||||
if can_encode {
|
||||
EncoderBody::Bytes(buf)
|
||||
} else {
|
||||
return ResponseBody::Other(Body::Bytes(buf));
|
||||
match body.size() {
|
||||
// no need to compress an empty body
|
||||
BodySize::None => return Self::none(),
|
||||
|
||||
// we cannot assume that Sized is not a stream
|
||||
BodySize::Sized(_) | BodySize::Stream => {}
|
||||
}
|
||||
}
|
||||
Body::Message(stream) => EncoderBody::BoxedStream(stream),
|
||||
},
|
||||
ResponseBody::Body(stream) => EncoderBody::Stream(stream),
|
||||
};
|
||||
|
||||
// TODO potentially some optimisation for single-chunk responses here by trying to read the
|
||||
// payload eagerly, stopping after 2 polls if the first is a chunk and the second is None
|
||||
|
||||
if can_encode {
|
||||
// Modify response body only if encoder is not None
|
||||
// Modify response body only if encoder is set
|
||||
if let Some(enc) = ContentEncoder::encoder(encoding) {
|
||||
update_head(encoding, head);
|
||||
head.no_chunking(false);
|
||||
return ResponseBody::Body(Encoder {
|
||||
body,
|
||||
eof: false,
|
||||
fut: None,
|
||||
|
||||
return Encoder {
|
||||
body: EncoderBody::Stream { body },
|
||||
encoder: Some(enc),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ResponseBody::Body(Encoder {
|
||||
body,
|
||||
eof: false,
|
||||
fut: None,
|
||||
encoder: None,
|
||||
})
|
||||
eof: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = EncoderBodyProj)]
|
||||
Encoder {
|
||||
body: EncoderBody::Stream { body },
|
||||
encoder: None,
|
||||
fut: None,
|
||||
eof: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pin_project! {
|
||||
#[project = EncoderBodyProj]
|
||||
enum EncoderBody<B> {
|
||||
Bytes(Bytes),
|
||||
Stream(#[pin] B),
|
||||
BoxedStream(BoxAnyBody),
|
||||
None,
|
||||
Stream { #[pin] body: B },
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for EncoderBody<B>
|
||||
where
|
||||
B: MessageBody,
|
||||
{
|
||||
type Error = EncoderError<B::Error>;
|
||||
type Error = EncoderError;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
EncoderBody::Bytes(ref b) => b.size(),
|
||||
EncoderBody::Stream(ref b) => b.size(),
|
||||
EncoderBody::BoxedStream(ref b) => b.size(),
|
||||
EncoderBody::None => BodySize::None,
|
||||
EncoderBody::Stream { body } => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,17 +128,11 @@ where
|
|||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.project() {
|
||||
EncoderBodyProj::Bytes(b) => {
|
||||
if b.is_empty() {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(std::mem::take(b))))
|
||||
}
|
||||
}
|
||||
EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body),
|
||||
EncoderBodyProj::BoxedStream(ref mut b) => {
|
||||
b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed)
|
||||
}
|
||||
EncoderBodyProj::None => Poll::Ready(None),
|
||||
|
||||
EncoderBodyProj::Stream { body } => body
|
||||
.poll_next(cx)
|
||||
.map_err(|err| EncoderError::Body(err.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -142,7 +141,7 @@ impl<B> MessageBody for Encoder<B>
|
|||
where
|
||||
B: MessageBody,
|
||||
{
|
||||
type Error = EncoderError<B::Error>;
|
||||
type Error = EncoderError;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
if self.encoder.is_none() {
|
||||
|
|
@ -205,6 +204,7 @@ where
|
|||
None => {
|
||||
if let Some(encoder) = this.encoder.take() {
|
||||
let chunk = encoder.finish().map_err(EncoderError::Io)?;
|
||||
|
||||
if chunk.is_empty() {
|
||||
return Poll::Ready(None);
|
||||
} else {
|
||||
|
|
@ -230,12 +230,15 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
|
|||
enum ContentEncoder {
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
Deflate(ZlibEncoder<Writer>),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
Gzip(GzEncoder<Writer>),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
Br(BrotliEncoder<Writer>),
|
||||
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
|
||||
// argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static`
|
||||
|
||||
// Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
|
||||
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
Zstd(ZstdEncoder<'static, Writer>),
|
||||
}
|
||||
|
|
@ -248,20 +251,24 @@ impl ContentEncoder {
|
|||
Writer::new(),
|
||||
flate2::Compression::fast(),
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
|
||||
Writer::new(),
|
||||
flate2::Compression::fast(),
|
||||
))),
|
||||
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoding::Br => {
|
||||
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
|
||||
}
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoding::Zstd => {
|
||||
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
|
||||
Some(ContentEncoder::Zstd(encoder))
|
||||
}
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
|
@ -271,10 +278,13 @@ impl ContentEncoder {
|
|||
match *self {
|
||||
#[cfg(feature = "compress-brotli")]
|
||||
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
|
||||
}
|
||||
|
|
@ -287,16 +297,19 @@ impl ContentEncoder {
|
|||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Gzip(encoder) => match encoder.finish() {
|
||||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Deflate(encoder) => match encoder.finish() {
|
||||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
Err(err) => Err(err),
|
||||
},
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoder::Zstd(encoder) => match encoder.finish() {
|
||||
Ok(writer) => Ok(writer.buf.freeze()),
|
||||
|
|
@ -315,6 +328,7 @@ impl ContentEncoder {
|
|||
Err(err)
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
|
|
@ -323,6 +337,7 @@ impl ContentEncoder {
|
|||
Err(err)
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "compress-gzip")]
|
||||
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
|
|
@ -331,6 +346,7 @@ impl ContentEncoder {
|
|||
Err(err)
|
||||
}
|
||||
},
|
||||
|
||||
#[cfg(feature = "compress-zstd")]
|
||||
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
|
||||
Ok(_) => Ok(()),
|
||||
|
|
@ -345,12 +361,9 @@ impl ContentEncoder {
|
|||
|
||||
#[derive(Debug, Display)]
|
||||
#[non_exhaustive]
|
||||
pub enum EncoderError<E> {
|
||||
pub enum EncoderError {
|
||||
#[display(fmt = "body")]
|
||||
Body(E),
|
||||
|
||||
#[display(fmt = "boxed")]
|
||||
Boxed(Box<dyn StdError>),
|
||||
Body(Box<dyn StdError>),
|
||||
|
||||
#[display(fmt = "blocking")]
|
||||
Blocking(BlockingError),
|
||||
|
|
@ -359,19 +372,18 @@ pub enum EncoderError<E> {
|
|||
Io(io::Error),
|
||||
}
|
||||
|
||||
impl<E: StdError + 'static> StdError for EncoderError<E> {
|
||||
impl StdError for EncoderError {
|
||||
fn source(&self) -> Option<&(dyn StdError + 'static)> {
|
||||
match self {
|
||||
EncoderError::Body(err) => Some(err),
|
||||
EncoderError::Boxed(err) => Some(&**err),
|
||||
EncoderError::Body(err) => Some(&**err),
|
||||
EncoderError::Blocking(err) => Some(err),
|
||||
EncoderError::Io(err) => Some(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error {
|
||||
fn from(err: EncoderError<E>) -> Self {
|
||||
impl From<EncoderError> for crate::Error {
|
||||
fn from(err: EncoderError) -> Self {
|
||||
crate::Error::new_encoder().with_cause(err)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,6 +10,9 @@ mod encoder;
|
|||
pub use self::decoder::Decoder;
|
||||
pub use self::encoder::Encoder;
|
||||
|
||||
/// Special-purpose writer for streaming (de-)compression.
|
||||
///
|
||||
/// Pre-allocates 8KiB of capacity.
|
||||
pub(self) struct Writer {
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,10 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
|
|||
use derive_more::{Display, Error, From};
|
||||
use http::{uri::InvalidUri, StatusCode};
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, Body},
|
||||
ws, Response,
|
||||
};
|
||||
use crate::{body::BoxBody, ws, Response};
|
||||
|
||||
pub use http::Error as HttpError;
|
||||
|
||||
|
|
@ -29,6 +26,11 @@ impl Error {
|
|||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
|
||||
self.inner.cause = Some(cause.into());
|
||||
self
|
||||
}
|
||||
|
||||
pub(crate) fn new_http() -> Self {
|
||||
Self::new(Kind::Http)
|
||||
}
|
||||
|
|
@ -49,14 +51,12 @@ impl Error {
|
|||
Self::new(Kind::SendResponse)
|
||||
}
|
||||
|
||||
// TODO: remove allow
|
||||
#[allow(dead_code)]
|
||||
#[allow(unused)] // reserved for future use (TODO: remove allow when being used)
|
||||
pub(crate) fn new_io() -> Self {
|
||||
Self::new(Kind::Io)
|
||||
}
|
||||
|
||||
// used in encoder behind feature flag so ignore unused warning
|
||||
#[allow(unused)]
|
||||
#[allow(unused)] // used in encoder behind feature flag so ignore unused warning
|
||||
pub(crate) fn new_encoder() -> Self {
|
||||
Self::new(Kind::Encoder)
|
||||
}
|
||||
|
|
@ -64,26 +64,22 @@ impl Error {
|
|||
pub(crate) fn new_ws() -> Self {
|
||||
Self::new(Kind::Ws)
|
||||
}
|
||||
|
||||
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
|
||||
self.inner.cause = Some(cause.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Error> for Response<AnyBody> {
|
||||
impl From<Error> for Response<BoxBody> {
|
||||
fn from(err: Error) -> Self {
|
||||
// TODO: more appropriate error status codes, usage assessment needed
|
||||
let status_code = match err.inner.kind {
|
||||
Kind::Parse => StatusCode::BAD_REQUEST,
|
||||
_ => StatusCode::INTERNAL_SERVER_ERROR,
|
||||
};
|
||||
|
||||
Response::new(status_code).set_body(Body::from(err.to_string()))
|
||||
Response::new(status_code).set_body(BoxBody::new(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
|
||||
pub enum Kind {
|
||||
pub(crate) enum Kind {
|
||||
#[display(fmt = "error processing HTTP")]
|
||||
Http,
|
||||
|
||||
|
|
@ -137,12 +133,6 @@ impl From<std::convert::Infallible> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ws::ProtocolError> for Error {
|
||||
fn from(err: ws::ProtocolError) -> Self {
|
||||
Self::new_ws().with_cause(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<HttpError> for Error {
|
||||
fn from(err: HttpError) -> Self {
|
||||
Self::new_http().with_cause(err)
|
||||
|
|
@ -155,6 +145,12 @@ impl From<ws::HandshakeError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ws::ProtocolError> for Error {
|
||||
fn from(err: ws::ProtocolError) -> Self {
|
||||
Self::new_ws().with_cause(err)
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of errors that can occur during parsing HTTP streams.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[non_exhaustive]
|
||||
|
|
@ -245,7 +241,7 @@ impl From<ParseError> for Error {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<ParseError> for Response<AnyBody> {
|
||||
impl From<ParseError> for Response<BoxBody> {
|
||||
fn from(err: ParseError) -> Self {
|
||||
Error::from(err).into()
|
||||
}
|
||||
|
|
@ -342,7 +338,7 @@ pub enum DispatchError {
|
|||
/// Service error
|
||||
// FIXME: display and error type
|
||||
#[display(fmt = "Service Error")]
|
||||
Service(#[error(not(source))] Response<AnyBody>),
|
||||
Service(#[error(not(source))] Response<BoxBody>),
|
||||
|
||||
/// Body error
|
||||
// FIXME: display and error type
|
||||
|
|
@ -426,11 +422,11 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_into_response() {
|
||||
let resp: Response<AnyBody> = ParseError::Incomplete.into();
|
||||
let resp: Response<BoxBody> = ParseError::Incomplete.into();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
|
||||
let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into();
|
||||
let resp: Response<AnyBody> = Error::new_http().with_cause(err).into();
|
||||
let resp: Response<BoxBody> = Error::new_http().with_cause(err).into();
|
||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
|
|
@ -455,7 +451,7 @@ mod tests {
|
|||
fn test_error_http_response() {
|
||||
let orig = io::Error::new(io::ErrorKind::Other, "other");
|
||||
let err = Error::new_io().with_cause(orig);
|
||||
let resp: Response<AnyBody> = err.into();
|
||||
let resp: Response<BoxBody> = err.into();
|
||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -120,7 +120,7 @@ impl Decoder for ClientCodec {
|
|||
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
|
||||
|
||||
if let Some((req, payload)) = self.inner.decoder.decode(src)? {
|
||||
if let Some(ctype) = req.ctype() {
|
||||
if let Some(ctype) = req.conn_type() {
|
||||
// do not use peer's keep-alive
|
||||
self.inner.ctype = if ctype == ConnectionType::KeepAlive {
|
||||
self.inner.ctype
|
||||
|
|
|
|||
|
|
@ -29,7 +29,7 @@ pub struct Codec {
|
|||
decoder: decoder::MessageDecoder<Request>,
|
||||
payload: Option<PayloadDecoder>,
|
||||
version: Version,
|
||||
ctype: ConnectionType,
|
||||
conn_type: ConnectionType,
|
||||
|
||||
// encoder part
|
||||
flags: Flags,
|
||||
|
|
@ -65,7 +65,7 @@ impl Codec {
|
|||
decoder: decoder::MessageDecoder::default(),
|
||||
payload: None,
|
||||
version: Version::HTTP_11,
|
||||
ctype: ConnectionType::Close,
|
||||
conn_type: ConnectionType::Close,
|
||||
encoder: encoder::MessageEncoder::default(),
|
||||
}
|
||||
}
|
||||
|
|
@ -73,13 +73,13 @@ impl Codec {
|
|||
/// Check if request is upgrade.
|
||||
#[inline]
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.ctype == ConnectionType::Upgrade
|
||||
self.conn_type == ConnectionType::Upgrade
|
||||
}
|
||||
|
||||
/// Check if last response is keep-alive.
|
||||
#[inline]
|
||||
pub fn keepalive(&self) -> bool {
|
||||
self.ctype == ConnectionType::KeepAlive
|
||||
self.conn_type == ConnectionType::KeepAlive
|
||||
}
|
||||
|
||||
/// Check if keep-alive enabled on server level.
|
||||
|
|
@ -124,11 +124,11 @@ impl Decoder for Codec {
|
|||
let head = req.head();
|
||||
self.flags.set(Flags::HEAD, head.method == Method::HEAD);
|
||||
self.version = head.version;
|
||||
self.ctype = head.connection_type();
|
||||
if self.ctype == ConnectionType::KeepAlive
|
||||
self.conn_type = head.connection_type();
|
||||
if self.conn_type == ConnectionType::KeepAlive
|
||||
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED)
|
||||
{
|
||||
self.ctype = ConnectionType::Close
|
||||
self.conn_type = ConnectionType::Close
|
||||
}
|
||||
match payload {
|
||||
PayloadType::None => self.payload = None,
|
||||
|
|
@ -159,14 +159,14 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||
res.head_mut().version = self.version;
|
||||
|
||||
// connection status
|
||||
self.ctype = if let Some(ct) = res.head().ctype() {
|
||||
self.conn_type = if let Some(ct) = res.head().conn_type() {
|
||||
if ct == ConnectionType::KeepAlive {
|
||||
self.ctype
|
||||
self.conn_type
|
||||
} else {
|
||||
ct
|
||||
}
|
||||
} else {
|
||||
self.ctype
|
||||
self.conn_type
|
||||
};
|
||||
|
||||
// encode message
|
||||
|
|
@ -177,10 +177,9 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||
self.flags.contains(Flags::STREAM),
|
||||
self.version,
|
||||
length,
|
||||
self.ctype,
|
||||
self.conn_type,
|
||||
&self.config,
|
||||
)?;
|
||||
// self.headers_size = (dst.len() - len) as u32;
|
||||
}
|
||||
Message::Chunk(Some(bytes)) => {
|
||||
self.encoder.encode_chunk(bytes.as_ref(), dst)?;
|
||||
|
|
@ -189,6 +188,7 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
|
|||
self.encoder.encode_eof(dst)?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -174,7 +174,7 @@ pub(crate) trait MessageType: Sized {
|
|||
self.set_expect()
|
||||
}
|
||||
|
||||
// https://tools.ietf.org/html/rfc7230#section-3.3.3
|
||||
// https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
|
||||
if chunked {
|
||||
// Chunked encoding
|
||||
Ok(PayloadLength::Payload(PayloadType::Payload(
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
use std::{
|
||||
collections::VecDeque,
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
future::Future,
|
||||
io, mem, net,
|
||||
|
|
@ -19,7 +18,7 @@ use log::{error, trace};
|
|||
use pin_project::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, BodySize, MessageBody},
|
||||
body::{BodySize, BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, ParseError, PayloadError},
|
||||
service::HttpFlow,
|
||||
|
|
@ -51,13 +50,12 @@ bitflags! {
|
|||
pub struct Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -73,13 +71,12 @@ where
|
|||
enum DispatcherState<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -92,13 +89,12 @@ where
|
|||
struct InnerDispatcher<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -137,13 +133,12 @@ where
|
|||
X: Service<Request, Response = Request>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
None,
|
||||
ExpectCall(#[pin] X::Future),
|
||||
ServiceCall(#[pin] S::Future),
|
||||
SendPayload(#[pin] B),
|
||||
SendErrorPayload(#[pin] AnyBody),
|
||||
SendErrorPayload(#[pin] BoxBody),
|
||||
}
|
||||
|
||||
impl<S, B, X> State<S, B, X>
|
||||
|
|
@ -153,7 +148,6 @@ where
|
|||
X: Service<Request, Response = Request>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
fn is_empty(&self) -> bool {
|
||||
matches!(self, State::None)
|
||||
|
|
@ -171,14 +165,13 @@ where
|
|||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -232,14 +225,13 @@ where
|
|||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -325,7 +317,7 @@ where
|
|||
) -> Result<(), DispatchError> {
|
||||
let size = self.as_mut().send_response_inner(message, &body)?;
|
||||
let state = match size {
|
||||
BodySize::None | BodySize::Empty => State::None,
|
||||
BodySize::None | BodySize::Sized(0) => State::None,
|
||||
_ => State::SendPayload(body),
|
||||
};
|
||||
self.project().state.set(state);
|
||||
|
|
@ -335,11 +327,11 @@ where
|
|||
fn send_error_response(
|
||||
mut self: Pin<&mut Self>,
|
||||
message: Response<()>,
|
||||
body: AnyBody,
|
||||
body: BoxBody,
|
||||
) -> Result<(), DispatchError> {
|
||||
let size = self.as_mut().send_response_inner(message, &body)?;
|
||||
let state = match size {
|
||||
BodySize::None | BodySize::Empty => State::None,
|
||||
BodySize::None | BodySize::Sized(0) => State::None,
|
||||
_ => State::SendErrorPayload(body),
|
||||
};
|
||||
self.project().state.set(state);
|
||||
|
|
@ -380,7 +372,7 @@ where
|
|||
// send_response would update InnerDispatcher state to SendPayload or
|
||||
// None(If response body is empty).
|
||||
// continue loop to poll it.
|
||||
self.as_mut().send_error_response(res, AnyBody::Empty)?;
|
||||
self.as_mut().send_error_response(res, BoxBody::new(()))?;
|
||||
}
|
||||
|
||||
// return with upgrade request and poll it exclusively.
|
||||
|
|
@ -400,7 +392,7 @@ where
|
|||
|
||||
// send service call error as response
|
||||
Poll::Ready(Err(err)) => {
|
||||
let res: Response<AnyBody> = err.into();
|
||||
let res: Response<BoxBody> = err.into();
|
||||
let (res, body) = res.replace_body(());
|
||||
self.as_mut().send_error_response(res, body)?;
|
||||
}
|
||||
|
|
@ -497,7 +489,7 @@ where
|
|||
|
||||
// send expect error as response
|
||||
Poll::Ready(Err(err)) => {
|
||||
let res: Response<AnyBody> = err.into();
|
||||
let res: Response<BoxBody> = err.into();
|
||||
let (res, body) = res.replace_body(());
|
||||
self.as_mut().send_error_response(res, body)?;
|
||||
}
|
||||
|
|
@ -546,7 +538,7 @@ where
|
|||
// to notify the dispatcher a new state is set and the outer loop
|
||||
// should be continue.
|
||||
Poll::Ready(Err(err)) => {
|
||||
let res: Response<AnyBody> = err.into();
|
||||
let res: Response<BoxBody> = err.into();
|
||||
let (res, body) = res.replace_body(());
|
||||
return self.send_error_response(res, body);
|
||||
}
|
||||
|
|
@ -566,7 +558,7 @@ where
|
|||
Poll::Pending => Ok(()),
|
||||
// see the comment on ExpectCall state branch's Ready(Err(err)).
|
||||
Poll::Ready(Err(err)) => {
|
||||
let res: Response<AnyBody> = err.into();
|
||||
let res: Response<BoxBody> = err.into();
|
||||
let (res, body) = res.replace_body(());
|
||||
self.send_error_response(res, body)
|
||||
}
|
||||
|
|
@ -772,7 +764,7 @@ where
|
|||
trace!("Slow request timeout");
|
||||
let _ = self.as_mut().send_error_response(
|
||||
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
|
||||
AnyBody::Empty,
|
||||
BoxBody::new(()),
|
||||
);
|
||||
this = self.project();
|
||||
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
|
||||
|
|
@ -909,14 +901,13 @@ where
|
|||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -1067,17 +1058,19 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
fn ok_service() -> impl Service<Request, Response = Response<AnyBody>, Error = Error>
|
||||
fn ok_service(
|
||||
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error>
|
||||
{
|
||||
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok())))
|
||||
}
|
||||
|
||||
fn echo_path_service(
|
||||
) -> impl Service<Request, Response = Response<AnyBody>, Error = Error> {
|
||||
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error>
|
||||
{
|
||||
fn_service(|req: Request| {
|
||||
let path = req.path().as_bytes();
|
||||
ready(Ok::<_, Error>(
|
||||
Response::ok().set_body(AnyBody::from_slice(path)),
|
||||
Response::ok().set_body(Bytes::copy_from_slice(path)),
|
||||
))
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -56,7 +56,7 @@ pub(crate) trait MessageType: Sized {
|
|||
dst: &mut BytesMut,
|
||||
version: Version,
|
||||
mut length: BodySize,
|
||||
ctype: ConnectionType,
|
||||
conn_type: ConnectionType,
|
||||
config: &ServiceConfig,
|
||||
) -> io::Result<()> {
|
||||
let chunked = self.chunked();
|
||||
|
|
@ -71,14 +71,24 @@ pub(crate) trait MessageType: Sized {
|
|||
| StatusCode::PROCESSING
|
||||
| StatusCode::NO_CONTENT => {
|
||||
// skip content-length and transfer-encoding headers
|
||||
// See https://tools.ietf.org/html/rfc7230#section-3.3.1
|
||||
// and https://tools.ietf.org/html/rfc7230#section-3.3.2
|
||||
// see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
|
||||
// and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
|
||||
skip_len = true;
|
||||
length = BodySize::None
|
||||
}
|
||||
|
||||
StatusCode::NOT_MODIFIED => {
|
||||
// 304 responses should never have a body but should retain a manually set
|
||||
// content-length header
|
||||
// see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
|
||||
skip_len = false;
|
||||
length = BodySize::None;
|
||||
}
|
||||
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
match length {
|
||||
BodySize::Stream => {
|
||||
if chunked {
|
||||
|
|
@ -93,19 +103,16 @@ pub(crate) trait MessageType: Sized {
|
|||
dst.put_slice(b"\r\n");
|
||||
}
|
||||
}
|
||||
BodySize::Empty => {
|
||||
if camel_case {
|
||||
dst.put_slice(b"\r\nContent-Length: 0\r\n");
|
||||
} else {
|
||||
dst.put_slice(b"\r\ncontent-length: 0\r\n");
|
||||
}
|
||||
BodySize::Sized(0) if camel_case => {
|
||||
dst.put_slice(b"\r\nContent-Length: 0\r\n")
|
||||
}
|
||||
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
|
||||
BodySize::Sized(len) => helpers::write_content_length(len, dst),
|
||||
BodySize::None => dst.put_slice(b"\r\n"),
|
||||
}
|
||||
|
||||
// Connection
|
||||
match ctype {
|
||||
match conn_type {
|
||||
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
|
||||
ConnectionType::KeepAlive if version < Version::HTTP_11 => {
|
||||
if camel_case {
|
||||
|
|
@ -330,13 +337,13 @@ impl<T: MessageType> MessageEncoder<T> {
|
|||
stream: bool,
|
||||
version: Version,
|
||||
length: BodySize,
|
||||
ctype: ConnectionType,
|
||||
conn_type: ConnectionType,
|
||||
config: &ServiceConfig,
|
||||
) -> io::Result<()> {
|
||||
// transfer encoding
|
||||
if !head {
|
||||
self.te = match length {
|
||||
BodySize::Empty => TransferEncoding::empty(),
|
||||
BodySize::Sized(0) => TransferEncoding::empty(),
|
||||
BodySize::Sized(len) => TransferEncoding::length(len),
|
||||
BodySize::Stream => {
|
||||
if message.chunked() && !stream {
|
||||
|
|
@ -352,7 +359,7 @@ impl<T: MessageType> MessageEncoder<T> {
|
|||
}
|
||||
|
||||
message.encode_status(dst)?;
|
||||
message.encode_headers(dst, version, length, ctype, config)
|
||||
message.encode_headers(dst, version, length, conn_type, config)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -366,10 +373,12 @@ pub(crate) struct TransferEncoding {
|
|||
enum TransferEncodingKind {
|
||||
/// An Encoder for when Transfer-Encoding includes `chunked`.
|
||||
Chunked(bool),
|
||||
|
||||
/// An Encoder for when Content-Length is set.
|
||||
///
|
||||
/// Enforces that the body is not longer than the Content-Length header.
|
||||
Length(u64),
|
||||
|
||||
/// An Encoder for when Content-Length is not known.
|
||||
///
|
||||
/// Application decides when to stop writing.
|
||||
|
|
@ -553,7 +562,7 @@ mod tests {
|
|||
let _ = head.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
BodySize::Empty,
|
||||
BodySize::Sized(0),
|
||||
ConnectionType::Close,
|
||||
&ServiceConfig::default(),
|
||||
);
|
||||
|
|
@ -624,7 +633,7 @@ mod tests {
|
|||
let _ = head.encode_headers(
|
||||
&mut bytes,
|
||||
Version::HTTP_11,
|
||||
BodySize::Empty,
|
||||
BodySize::Sized(0),
|
||||
ConnectionType::Close,
|
||||
&ServiceConfig::default(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
marker::PhantomData,
|
||||
net,
|
||||
|
|
@ -16,7 +15,7 @@ use actix_utils::future::ready;
|
|||
use futures_core::future::LocalBoxFuture;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, MessageBody},
|
||||
body::{BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
error::DispatchError,
|
||||
service::HttpServiceHandler,
|
||||
|
|
@ -38,7 +37,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
|
|||
impl<T, S, B> H1Service<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
B: MessageBody,
|
||||
|
|
@ -63,21 +62,20 @@ impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
|
|||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create simple tcp stream service
|
||||
|
|
@ -102,9 +100,11 @@ where
|
|||
mod openssl {
|
||||
use super::*;
|
||||
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::{
|
||||
openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
|
||||
openssl::{
|
||||
reexports::{Error as SslError, SslAcceptor},
|
||||
Acceptor, TlsStream,
|
||||
},
|
||||
TlsError,
|
||||
};
|
||||
|
||||
|
|
@ -112,16 +112,15 @@ mod openssl {
|
|||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<
|
||||
|
|
@ -130,10 +129,10 @@ mod openssl {
|
|||
Response = (),
|
||||
>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create openssl based service
|
||||
/// Create OpenSSL based service.
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
|
|
@ -145,11 +144,13 @@ mod openssl {
|
|||
InitError = (),
|
||||
> {
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!())
|
||||
.and_then(|io: TlsStream<TcpStream>| {
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
.map_err(TlsError::into_service_error)
|
||||
.map(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ready(Ok((io, peer_addr)))
|
||||
(io, peer_addr)
|
||||
})
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
|
|
@ -158,30 +159,30 @@ mod openssl {
|
|||
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls {
|
||||
use super::*;
|
||||
|
||||
use std::io;
|
||||
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
use actix_tls::accept::{
|
||||
rustls::{Acceptor, ServerConfig, TlsStream},
|
||||
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||
TlsError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<
|
||||
|
|
@ -190,10 +191,10 @@ mod rustls {
|
|||
Response = (),
|
||||
>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create rustls based service
|
||||
/// Create Rustls based service.
|
||||
pub fn rustls(
|
||||
self,
|
||||
config: ServerConfig,
|
||||
|
|
@ -205,11 +206,13 @@ mod rustls {
|
|||
InitError = (),
|
||||
> {
|
||||
Acceptor::new(config)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!())
|
||||
.and_then(|io: TlsStream<TcpStream>| {
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
.map_err(TlsError::into_service_error)
|
||||
.map(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||
ready(Ok((io, peer_addr)))
|
||||
(io, peer_addr)
|
||||
})
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
|
|
@ -219,7 +222,7 @@ mod rustls {
|
|||
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Response: Into<Response<B>>,
|
||||
S::InitError: fmt::Debug,
|
||||
B: MessageBody,
|
||||
|
|
@ -227,7 +230,7 @@ where
|
|||
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
|
||||
where
|
||||
X1: ServiceFactory<Request, Response = Request>,
|
||||
X1::Error: Into<Response<AnyBody>>,
|
||||
X1::Error: Into<Response<BoxBody>>,
|
||||
X1::InitError: fmt::Debug,
|
||||
{
|
||||
H1Service {
|
||||
|
|
@ -270,21 +273,20 @@ where
|
|||
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Response: Into<Response<B>>,
|
||||
S::InitError: fmt::Debug,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
type Response = ();
|
||||
|
|
@ -340,17 +342,16 @@ where
|
|||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, Codec>), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
{
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
|
|
|
|||
|
|
@ -1,23 +1,31 @@
|
|||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::body::{BodySize, MessageBody};
|
||||
use crate::error::Error;
|
||||
use crate::h1::{Codec, Message};
|
||||
use crate::response::Response;
|
||||
use crate::{
|
||||
body::{BodySize, MessageBody},
|
||||
error::Error,
|
||||
h1::{Codec, Message},
|
||||
response::Response,
|
||||
};
|
||||
|
||||
pin_project! {
|
||||
/// Send HTTP/1 response
|
||||
#[pin_project::pin_project]
|
||||
pub struct SendResponse<T, B> {
|
||||
res: Option<Message<(Response<()>, BodySize)>>,
|
||||
|
||||
#[pin]
|
||||
body: Option<B>,
|
||||
|
||||
#[pin]
|
||||
framed: Option<Framed<T, Codec>>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, B> SendResponse<T, B>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -10,17 +10,21 @@ use std::{
|
|||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::{sleep, Sleep};
|
||||
use actix_service::Service;
|
||||
use actix_utils::future::poll_fn;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
use h2::server::{Connection, SendResponse};
|
||||
use h2::{
|
||||
server::{Connection, SendResponse},
|
||||
Ping, PingPong,
|
||||
};
|
||||
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
||||
use log::{error, trace};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, BodySize, MessageBody},
|
||||
body::{BodySize, BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
service::HttpFlow,
|
||||
OnConnectData, Payload, Request, Response, ResponseHead,
|
||||
|
|
@ -36,40 +40,63 @@ pin_project! {
|
|||
on_connect_data: OnConnectData,
|
||||
config: ServiceConfig,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
_phantom: PhantomData<B>,
|
||||
ping_pong: Option<H2PingPong>,
|
||||
_phantom: PhantomData<B>
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U> {
|
||||
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
connection: Connection<T, Bytes>,
|
||||
mut conn: Connection<T, Bytes>,
|
||||
on_connect_data: OnConnectData,
|
||||
config: ServiceConfig,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
timer: Option<Pin<Box<Sleep>>>,
|
||||
) -> Self {
|
||||
let ping_pong = config.keep_alive().map(|dur| H2PingPong {
|
||||
timer: timer
|
||||
.map(|mut timer| {
|
||||
// reset timer if it's received from new function.
|
||||
timer.as_mut().reset(config.now() + dur);
|
||||
timer
|
||||
})
|
||||
.unwrap_or_else(|| Box::pin(sleep(dur))),
|
||||
on_flight: false,
|
||||
ping_pong: conn.ping_pong().unwrap(),
|
||||
});
|
||||
|
||||
Self {
|
||||
flow,
|
||||
config,
|
||||
peer_addr,
|
||||
connection,
|
||||
connection: conn,
|
||||
on_connect_data,
|
||||
ping_pong,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct H2PingPong {
|
||||
timer: Pin<Box<Sleep>>,
|
||||
on_flight: bool,
|
||||
ping_pong: PingPong,
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
type Output = Result<(), crate::error::DispatchError>;
|
||||
|
||||
|
|
@ -77,9 +104,9 @@ where
|
|||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
while let Some((req, tx)) =
|
||||
ready!(Pin::new(&mut this.connection).poll_accept(cx)?)
|
||||
{
|
||||
loop {
|
||||
match Pin::new(&mut this.connection).poll_accept(cx)? {
|
||||
Poll::Ready(Some((req, tx))) => {
|
||||
let (parts, body) = req.into_parts();
|
||||
let pl = crate::h2::Payload::new(body);
|
||||
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
|
||||
|
|
@ -104,7 +131,7 @@ where
|
|||
let res = match fut.await {
|
||||
Ok(res) => handle_response(res.into(), tx, config).await,
|
||||
Err(err) => {
|
||||
let res: Response<AnyBody> = err.into();
|
||||
let res: Response<BoxBody> = err.into();
|
||||
handle_response(res, tx, config).await
|
||||
}
|
||||
};
|
||||
|
|
@ -123,8 +150,46 @@ where
|
|||
}
|
||||
});
|
||||
}
|
||||
Poll::Ready(None) => return Poll::Ready(Ok(())),
|
||||
Poll::Pending => match this.ping_pong.as_mut() {
|
||||
Some(ping_pong) => loop {
|
||||
if ping_pong.on_flight {
|
||||
// When have on flight ping pong. poll pong and and keep alive timer.
|
||||
// on success pong received update keep alive timer to determine the next timing of
|
||||
// ping pong.
|
||||
match ping_pong.ping_pong.poll_pong(cx)? {
|
||||
Poll::Ready(_) => {
|
||||
ping_pong.on_flight = false;
|
||||
|
||||
Poll::Ready(Ok(()))
|
||||
let dead_line =
|
||||
this.config.keep_alive_expire().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line);
|
||||
}
|
||||
Poll::Pending => {
|
||||
return ping_pong
|
||||
.timer
|
||||
.as_mut()
|
||||
.poll(cx)
|
||||
.map(|_| Ok(()))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// When there is no on flight ping pong. keep alive timer is used to wait for next
|
||||
// timing of ping pong. Therefore at this point it serves as an interval instead.
|
||||
ready!(ping_pong.timer.as_mut().poll(cx));
|
||||
|
||||
ping_pong.ping_pong.send_ping(Ping::opaque())?;
|
||||
|
||||
let dead_line = this.config.keep_alive_expire().unwrap();
|
||||
ping_pong.timer.as_mut().reset(dead_line);
|
||||
|
||||
ping_pong.on_flight = true;
|
||||
}
|
||||
},
|
||||
None => return Poll::Pending,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -141,7 +206,6 @@ async fn handle_response<B>(
|
|||
) -> Result<(), DispatchError>
|
||||
where
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
let (res, body) = res.replace_body(());
|
||||
|
||||
|
|
@ -226,9 +290,11 @@ fn prepare_response(
|
|||
|
||||
let _ = match size {
|
||||
BodySize::None | BodySize::Stream => None,
|
||||
BodySize::Empty => res
|
||||
|
||||
BodySize::Sized(0) => res
|
||||
.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
||||
|
||||
BodySize::Sized(len) => {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
|
||||
|
|
@ -243,7 +309,7 @@ fn prepare_response(
|
|||
for (key, value) in head.headers.iter() {
|
||||
match *key {
|
||||
// TODO: consider skipping other headers according to:
|
||||
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
|
||||
// https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
|
||||
// omit HTTP/1.x only headers
|
||||
CONNECTION | TRANSFER_ENCODING => continue,
|
||||
CONTENT_LENGTH if skip_len => continue,
|
||||
|
|
|
|||
|
|
@ -1,20 +1,30 @@
|
|||
//! HTTP/2 protocol.
|
||||
|
||||
use std::{
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::Sleep;
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use h2::RecvStream;
|
||||
use h2::{
|
||||
server::{handshake, Connection, Handshake},
|
||||
RecvStream,
|
||||
};
|
||||
|
||||
mod dispatcher;
|
||||
mod service;
|
||||
|
||||
pub use self::dispatcher::Dispatcher;
|
||||
pub use self::service::H2Service;
|
||||
use crate::error::PayloadError;
|
||||
|
||||
use crate::{
|
||||
config::ServiceConfig,
|
||||
error::{DispatchError, PayloadError},
|
||||
};
|
||||
|
||||
/// HTTP/2 peer stream.
|
||||
pub struct Payload {
|
||||
|
|
@ -50,3 +60,44 @@ impl Stream for Payload {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn handshake_with_timeout<T>(
|
||||
io: T,
|
||||
config: &ServiceConfig,
|
||||
) -> HandshakeWithTimeout<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
HandshakeWithTimeout {
|
||||
handshake: handshake(io),
|
||||
timer: config.client_timer().map(Box::pin),
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HandshakeWithTimeout<T: AsyncRead + AsyncWrite + Unpin> {
|
||||
handshake: Handshake<T>,
|
||||
timer: Option<Pin<Box<Sleep>>>,
|
||||
}
|
||||
|
||||
impl<T> Future for HandshakeWithTimeout<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
{
|
||||
type Output = Result<(Connection<T, Bytes>, Option<Pin<Box<Sleep>>>), DispatchError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
let this = self.get_mut();
|
||||
|
||||
match Pin::new(&mut this.handshake).poll(cx)? {
|
||||
// return the timer on success handshake. It can be re-used for h2 ping-pong.
|
||||
Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
|
||||
Poll::Pending => match this.timer.as_mut() {
|
||||
Some(timer) => {
|
||||
ready!(timer.as_mut().poll(cx));
|
||||
Poll::Ready(Err(DispatchError::SlowRequestTimeout))
|
||||
}
|
||||
None => Poll::Pending,
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::{
|
||||
error::Error as StdError,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
net,
|
||||
|
|
@ -15,20 +14,18 @@ use actix_service::{
|
|||
ServiceFactoryExt as _,
|
||||
};
|
||||
use actix_utils::future::ready;
|
||||
use bytes::Bytes;
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
|
||||
use log::error;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, MessageBody},
|
||||
body::{BoxBody, MessageBody},
|
||||
config::ServiceConfig,
|
||||
error::DispatchError,
|
||||
service::HttpFlow,
|
||||
ConnectCallback, OnConnectData, Request, Response,
|
||||
};
|
||||
|
||||
use super::dispatcher::Dispatcher;
|
||||
use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
|
||||
|
||||
/// `ServiceFactory` implementation for HTTP/2 transport
|
||||
pub struct H2Service<T, S, B> {
|
||||
|
|
@ -41,12 +38,11 @@ pub struct H2Service<T, S, B> {
|
|||
impl<T, S, B> H2Service<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
/// Create new `H2Service` instance with config.
|
||||
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
|
||||
|
|
@ -72,12 +68,11 @@ impl<S, B> H2Service<TcpStream, S, B>
|
|||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
/// Create plain TCP based service
|
||||
pub fn tcp(
|
||||
|
|
@ -101,9 +96,14 @@ where
|
|||
|
||||
#[cfg(feature = "openssl")]
|
||||
mod openssl {
|
||||
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
use actix_tls::accept::{
|
||||
openssl::{
|
||||
reexports::{Error as SslError, SslAcceptor},
|
||||
Acceptor, TlsStream,
|
||||
},
|
||||
TlsError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -111,14 +111,13 @@ mod openssl {
|
|||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
/// Create OpenSSL based service
|
||||
/// Create OpenSSL based service.
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
|
|
@ -130,16 +129,14 @@ mod openssl {
|
|||
InitError = S::InitError,
|
||||
> {
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!())
|
||||
.and_then(fn_factory(|| {
|
||||
ready(Ok::<_, S::InitError>(fn_service(
|
||||
|io: TlsStream<TcpStream>| {
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
.map_err(TlsError::into_service_error)
|
||||
.map(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ready(Ok((io, peer_addr)))
|
||||
},
|
||||
)))
|
||||
}))
|
||||
(io, peer_addr)
|
||||
})
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
|
|
@ -147,24 +144,27 @@ mod openssl {
|
|||
|
||||
#[cfg(feature = "rustls")]
|
||||
mod rustls {
|
||||
use super::*;
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
use std::io;
|
||||
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
use actix_tls::accept::{
|
||||
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||
TlsError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
/// Create Rustls based service
|
||||
/// Create Rustls based service.
|
||||
pub fn rustls(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
|
|
@ -180,16 +180,14 @@ mod rustls {
|
|||
config.alpn_protocols = protos;
|
||||
|
||||
Acceptor::new(config)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!())
|
||||
.and_then(fn_factory(|| {
|
||||
ready(Ok::<_, S::InitError>(fn_service(
|
||||
|io: TlsStream<TcpStream>| {
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
.map_err(TlsError::into_service_error)
|
||||
.map(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().0.peer_addr().ok();
|
||||
ready(Ok((io, peer_addr)))
|
||||
},
|
||||
)))
|
||||
}))
|
||||
(io, peer_addr)
|
||||
})
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
}
|
||||
|
|
@ -201,12 +199,11 @@ where
|
|||
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
|
|
@ -241,7 +238,7 @@ where
|
|||
impl<T, S, B> H2ServiceHandler<T, S, B>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
|
|
@ -264,11 +261,10 @@ impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B
|
|||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
|
|
@ -292,7 +288,7 @@ where
|
|||
Some(self.cfg.clone()),
|
||||
addr,
|
||||
on_connect_data,
|
||||
h2_handshake(io),
|
||||
handshake_with_timeout(io, &self.cfg),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
|
@ -309,7 +305,7 @@ where
|
|||
Option<ServiceConfig>,
|
||||
Option<net::SocketAddr>,
|
||||
OnConnectData,
|
||||
H2Handshake<T, Bytes>,
|
||||
HandshakeWithTimeout<T>,
|
||||
),
|
||||
}
|
||||
|
||||
|
|
@ -317,7 +313,7 @@ pub struct H2ServiceHandlerResponse<T, S, B>
|
|||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody + 'static,
|
||||
|
|
@ -329,11 +325,10 @@ impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
|
|||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
type Output = Result<(), DispatchError>;
|
||||
|
||||
|
|
@ -347,7 +342,7 @@ where
|
|||
ref mut on_connect_data,
|
||||
ref mut handshake,
|
||||
) => match ready!(Pin::new(handshake).poll(cx)) {
|
||||
Ok(conn) => {
|
||||
Ok((conn, timer)) => {
|
||||
let on_connect_data = std::mem::take(on_connect_data);
|
||||
self.state = State::Incoming(Dispatcher::new(
|
||||
srv.take().unwrap(),
|
||||
|
|
@ -355,12 +350,13 @@ where
|
|||
on_connect_data,
|
||||
config.take().unwrap(),
|
||||
*peer_addr,
|
||||
timer,
|
||||
));
|
||||
self.poll(cx)
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err.into()))
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
},
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
//! Helper trait for types that can be effectively borrowed as a [HeaderValue].
|
||||
//!
|
||||
//! [HeaderValue]: crate::http::HeaderValue
|
||||
//! Sealed [`AsHeaderName`] trait and implementations.
|
||||
|
||||
use std::{borrow::Cow, str::FromStr};
|
||||
use std::{borrow::Cow, str::FromStr as _};
|
||||
|
||||
use http::header::{HeaderName, InvalidHeaderName};
|
||||
|
||||
/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`].
|
||||
///
|
||||
/// [`HeaderValue`]: crate::http::HeaderValue
|
||||
pub trait AsHeaderName: Sealed {}
|
||||
|
||||
pub struct Seal;
|
||||
|
|
|
|||
|
|
@ -1,4 +1,6 @@
|
|||
use std::convert::TryFrom;
|
||||
//! [`IntoHeaderPair`] trait and implementations.
|
||||
|
||||
use std::convert::TryFrom as _;
|
||||
|
||||
use http::{
|
||||
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
|
||||
|
|
@ -7,7 +9,10 @@ use http::{
|
|||
|
||||
use super::{Header, IntoHeaderValue};
|
||||
|
||||
/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s.
|
||||
/// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for
|
||||
/// insertion into a [`HeaderMap`].
|
||||
///
|
||||
/// [`HeaderMap`]: crate::http::HeaderMap
|
||||
pub trait IntoHeaderPair: Sized {
|
||||
type Error: Into<HttpError>;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,10 +1,12 @@
|
|||
use std::convert::TryFrom;
|
||||
//! [`IntoHeaderValue`] trait and implementations.
|
||||
|
||||
use std::convert::TryFrom as _;
|
||||
|
||||
use bytes::Bytes;
|
||||
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
|
||||
use mime::Mime;
|
||||
|
||||
/// A trait for any object that can be Converted to a `HeaderValue`
|
||||
/// An interface for types that can be converted into a [`HeaderValue`].
|
||||
pub trait IntoHeaderValue: Sized {
|
||||
/// The type returned in the event of a conversion error.
|
||||
type Error: Into<HttpError>;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
//! A multi-value [`HeaderMap`] and its iterators.
|
||||
|
||||
use std::{borrow::Cow, collections::hash_map, ops};
|
||||
use std::{borrow::Cow, collections::hash_map, iter, ops};
|
||||
|
||||
use ahash::AHashMap;
|
||||
use http::header::{HeaderName, HeaderValue};
|
||||
|
|
@ -288,7 +288,7 @@ impl HeaderMap {
|
|||
/// Returns an iterator over all values associated with a header name.
|
||||
///
|
||||
/// The returned iterator does not incur any allocations and will yield no items if there are no
|
||||
/// values associated with the key. Iteration order is **not** guaranteed to be the same as
|
||||
/// values associated with the key. Iteration order is guaranteed to be the same as
|
||||
/// insertion order.
|
||||
///
|
||||
/// # Examples
|
||||
|
|
@ -355,6 +355,19 @@ impl HeaderMap {
|
|||
///
|
||||
/// assert_eq!(map.len(), 1);
|
||||
/// ```
|
||||
///
|
||||
/// A convenience method is provided on the returned iterator to check if the insertion replaced
|
||||
/// any values.
|
||||
/// ```
|
||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
||||
/// let mut map = HeaderMap::new();
|
||||
///
|
||||
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
|
||||
/// assert!(removed.is_empty());
|
||||
///
|
||||
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
|
||||
/// assert!(!removed.is_empty());
|
||||
/// ```
|
||||
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed {
|
||||
let value = self.inner.insert(key, Value::one(val));
|
||||
Removed::new(value)
|
||||
|
|
@ -393,6 +406,9 @@ impl HeaderMap {
|
|||
|
||||
/// Removes all headers for a particular header name from the map.
|
||||
///
|
||||
/// Providing an invalid header names (as a string argument) will have no effect and return
|
||||
/// without error.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
||||
|
|
@ -409,6 +425,21 @@ impl HeaderMap {
|
|||
/// assert!(removed.next().is_none());
|
||||
///
|
||||
/// assert!(map.is_empty());
|
||||
/// ```
|
||||
///
|
||||
/// A convenience method is provided on the returned iterator to check if the `remove` call
|
||||
/// actually removed any values.
|
||||
/// ```
|
||||
/// # use actix_http::http::{header, HeaderMap, HeaderValue};
|
||||
/// let mut map = HeaderMap::new();
|
||||
///
|
||||
/// let removed = map.remove("accept");
|
||||
/// assert!(removed.is_empty());
|
||||
///
|
||||
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
|
||||
/// let removed = map.remove("accept");
|
||||
/// assert!(!removed.is_empty());
|
||||
/// ```
|
||||
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
|
||||
let value = match key.try_as_name(super::as_name::Seal) {
|
||||
Ok(Cow::Borrowed(name)) => self.inner.remove(name),
|
||||
|
|
@ -550,7 +581,8 @@ impl HeaderMap {
|
|||
}
|
||||
}
|
||||
|
||||
/// Note that this implementation will clone a [HeaderName] for each value.
|
||||
/// Note that this implementation will clone a [HeaderName] for each value. Consider using
|
||||
/// [`drain`](Self::drain) to control header name cloning.
|
||||
impl IntoIterator for HeaderMap {
|
||||
type Item = (HeaderName, HeaderValue);
|
||||
type IntoIter = IntoIter;
|
||||
|
|
@ -571,7 +603,7 @@ impl<'a> IntoIterator for &'a HeaderMap {
|
|||
}
|
||||
}
|
||||
|
||||
/// Iterator for all values with the same header name.
|
||||
/// Iterator over borrowed values with the same associated name.
|
||||
///
|
||||
/// See [`HeaderMap::get_all`].
|
||||
#[derive(Debug)]
|
||||
|
|
@ -613,18 +645,36 @@ impl<'a> Iterator for GetAll<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from methods
|
||||
/// on [`HeaderMap`] that remove or replace items.
|
||||
impl ExactSizeIterator for GetAll<'_> {}
|
||||
|
||||
impl iter::FusedIterator for GetAll<'_> {}
|
||||
|
||||
/// Iterator over removed, owned values with the same associated name.
|
||||
///
|
||||
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
|
||||
/// and [`HeaderMap::remove`].
|
||||
#[derive(Debug)]
|
||||
pub struct Removed {
|
||||
inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>,
|
||||
}
|
||||
|
||||
impl<'a> Removed {
|
||||
impl Removed {
|
||||
fn new(value: Option<Value>) -> Self {
|
||||
let inner = value.map(|value| value.inner.into_iter());
|
||||
Self { inner }
|
||||
}
|
||||
|
||||
/// Returns true if iterator contains no elements, without consuming it.
|
||||
///
|
||||
/// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate
|
||||
/// wether any items were actually replaced or removed, respectively.
|
||||
pub fn is_empty(&self) -> bool {
|
||||
match self.inner {
|
||||
// size hint lower bound of smallvec is the correct length
|
||||
Some(ref iter) => iter.size_hint().0 == 0,
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Iterator for Removed {
|
||||
|
|
@ -644,7 +694,11 @@ impl Iterator for Removed {
|
|||
}
|
||||
}
|
||||
|
||||
/// Iterator over all [`HeaderName`]s in the map.
|
||||
impl ExactSizeIterator for Removed {}
|
||||
|
||||
impl iter::FusedIterator for Removed {}
|
||||
|
||||
/// Iterator over all names in the map.
|
||||
#[derive(Debug)]
|
||||
pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>);
|
||||
|
||||
|
|
@ -662,6 +716,11 @@ impl<'a> Iterator for Keys<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for Keys<'_> {}
|
||||
|
||||
impl iter::FusedIterator for Keys<'_> {}
|
||||
|
||||
/// Iterator over borrowed name-value pairs.
|
||||
#[derive(Debug)]
|
||||
pub struct Iter<'a> {
|
||||
inner: hash_map::Iter<'a, HeaderName, Value>,
|
||||
|
|
@ -713,6 +772,10 @@ impl<'a> Iterator for Iter<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for Iter<'_> {}
|
||||
|
||||
impl iter::FusedIterator for Iter<'_> {}
|
||||
|
||||
/// Iterator over drained name-value pairs.
|
||||
///
|
||||
/// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning.
|
||||
|
|
@ -764,6 +827,10 @@ impl<'a> Iterator for Drain<'a> {
|
|||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for Drain<'_> {}
|
||||
|
||||
impl iter::FusedIterator for Drain<'_> {}
|
||||
|
||||
/// Iterator over owned name-value pairs.
|
||||
///
|
||||
/// Implementation necessarily clones header names for each value.
|
||||
|
|
@ -814,12 +881,27 @@ impl Iterator for IntoIter {
|
|||
}
|
||||
}
|
||||
|
||||
impl ExactSizeIterator for IntoIter {}
|
||||
|
||||
impl iter::FusedIterator for IntoIter {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::iter::FusedIterator;
|
||||
|
||||
use http::header;
|
||||
use static_assertions::assert_impl_all;
|
||||
|
||||
use super::*;
|
||||
|
||||
assert_impl_all!(HeaderMap: IntoIterator);
|
||||
assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);
|
||||
assert_impl_all!(Drain<'_>: Iterator, ExactSizeIterator, FusedIterator);
|
||||
|
||||
#[test]
|
||||
fn create() {
|
||||
let map = HeaderMap::new();
|
||||
|
|
@ -945,6 +1027,56 @@ mod tests {
|
|||
assert_eq!(vals.next(), removed.next().as_ref());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn get_all_iteration_order_matches_insertion_order() {
|
||||
let mut map = HeaderMap::new();
|
||||
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
map.append(header::COOKIE, HeaderValue::from_static("1"));
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
map.append(header::COOKIE, HeaderValue::from_static("2"));
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"2");
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
map.append(header::COOKIE, HeaderValue::from_static("3"));
|
||||
map.append(header::COOKIE, HeaderValue::from_static("4"));
|
||||
map.append(header::COOKIE, HeaderValue::from_static("5"));
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"2");
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"3");
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"4");
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"5");
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
let _ = map.insert(header::COOKIE, HeaderValue::from_static("6"));
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"6");
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
let _ = map.insert(header::COOKIE, HeaderValue::from_static("7"));
|
||||
let _ = map.insert(header::COOKIE, HeaderValue::from_static("8"));
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"8");
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
map.append(header::COOKIE, HeaderValue::from_static("9"));
|
||||
let mut vals = map.get_all(header::COOKIE);
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"8");
|
||||
assert_eq!(vals.next().unwrap().as_bytes(), b"9");
|
||||
assert!(vals.next().is_none());
|
||||
|
||||
// check for fused-ness
|
||||
assert!(vals.next().is_none());
|
||||
}
|
||||
|
||||
fn owned_pair<'a>(
|
||||
(name, val): (&'a HeaderName, &'a HeaderValue),
|
||||
) -> (HeaderName, HeaderValue) {
|
||||
|
|
|
|||
|
|
@ -29,35 +29,34 @@ pub use http::header::{
|
|||
X_FRAME_OPTIONS, X_XSS_PROTECTION,
|
||||
};
|
||||
|
||||
use crate::error::ParseError;
|
||||
use crate::HttpMessage;
|
||||
use crate::{error::ParseError, HttpMessage};
|
||||
|
||||
mod as_name;
|
||||
mod into_pair;
|
||||
mod into_value;
|
||||
mod utils;
|
||||
|
||||
pub(crate) mod map;
|
||||
pub mod map;
|
||||
mod shared;
|
||||
|
||||
#[doc(hidden)]
|
||||
pub use self::shared::*;
|
||||
mod utils;
|
||||
|
||||
pub use self::as_name::AsHeaderName;
|
||||
pub use self::into_pair::IntoHeaderPair;
|
||||
pub use self::into_value::IntoHeaderValue;
|
||||
#[doc(hidden)]
|
||||
pub use self::map::GetAll;
|
||||
pub use self::map::HeaderMap;
|
||||
pub use self::utils::*;
|
||||
pub use self::shared::{
|
||||
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
|
||||
LanguageTag, Quality, QualityItem,
|
||||
};
|
||||
pub use self::utils::{
|
||||
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
|
||||
};
|
||||
|
||||
/// A trait for any object that already represents a valid header field and value.
|
||||
/// An interface for types that already represent a valid header.
|
||||
pub trait Header: IntoHeaderValue {
|
||||
/// Returns the name of the header field
|
||||
fn name() -> HeaderName;
|
||||
|
||||
/// Parse a header
|
||||
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
|
||||
fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
|
||||
}
|
||||
|
||||
/// Convert `http::HeaderMap` to our `HeaderMap`.
|
||||
|
|
@ -68,7 +67,7 @@ impl From<http::HeaderMap> for HeaderMap {
|
|||
}
|
||||
|
||||
/// This encode set is used for HTTP header values and is defined at
|
||||
/// https://tools.ietf.org/html/rfc5987#section-3.2.
|
||||
/// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
|
||||
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
|
|
|
|||
|
|
@ -1,14 +1,13 @@
|
|||
use std::fmt::{self, Display};
|
||||
use std::str::FromStr;
|
||||
use std::{fmt, str};
|
||||
|
||||
use self::Charset::*;
|
||||
|
||||
/// A Mime charset.
|
||||
/// A MIME character set.
|
||||
///
|
||||
/// The string representation is normalized to upper case.
|
||||
///
|
||||
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
#[allow(non_camel_case_types)]
|
||||
pub enum Charset {
|
||||
/// US ASCII
|
||||
|
|
@ -88,20 +87,20 @@ impl Charset {
|
|||
Iso_8859_8_E => "ISO-8859-8-E",
|
||||
Iso_8859_8_I => "ISO-8859-8-I",
|
||||
Gb2312 => "GB2312",
|
||||
Big5 => "big5",
|
||||
Big5 => "Big5",
|
||||
Koi8_R => "KOI8-R",
|
||||
Ext(ref s) => s,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Charset {
|
||||
impl fmt::Display for Charset {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.write_str(self.label())
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for Charset {
|
||||
impl str::FromStr for Charset {
|
||||
type Err = crate::Error;
|
||||
|
||||
fn from_str(s: &str) -> Result<Charset, crate::Error> {
|
||||
|
|
@ -128,7 +127,7 @@ impl FromStr for Charset {
|
|||
"ISO-8859-8-E" => Iso_8859_8_E,
|
||||
"ISO-8859-8-I" => Iso_8859_8_I,
|
||||
"GB2312" => Gb2312,
|
||||
"big5" => Big5,
|
||||
"BIG5" => Big5,
|
||||
"KOI8-R" => Koi8_R,
|
||||
s => Ext(s.to_owned()),
|
||||
})
|
||||
|
|
|
|||
|
|
@ -9,14 +9,17 @@ use crate::{
|
|||
HttpMessage,
|
||||
};
|
||||
|
||||
/// Error return when a content encoding is unknown.
|
||||
///
|
||||
/// Example: 'compress'
|
||||
/// Error returned when a content encoding is unknown.
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "unsupported content encoding")]
|
||||
pub struct ContentEncodingParseError;
|
||||
|
||||
/// Represents a supported content encoding.
|
||||
///
|
||||
/// Includes a commonly-used subset of media types appropriate for use as HTTP content encodings.
|
||||
/// See [IANA HTTP Content Coding Registry].
|
||||
///
|
||||
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
#[non_exhaustive]
|
||||
pub enum ContentEncoding {
|
||||
|
|
@ -32,7 +35,7 @@ pub enum ContentEncoding {
|
|||
/// Gzip algorithm.
|
||||
Gzip,
|
||||
|
||||
// Zstd algorithm.
|
||||
/// Zstd algorithm.
|
||||
Zstd,
|
||||
|
||||
/// Indicates the identity function (i.e. no compression, nor modification).
|
||||
|
|
|
|||
|
|
@ -1,17 +1,17 @@
|
|||
//! Originally taken from `hyper::header::parsing`.
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
|
||||
use crate::header::{Charset, HTTP_VALUE};
|
||||
|
||||
// From hyper v0.11.27 src/header/parsing.rs
|
||||
|
||||
/// The value part of an extended parameter consisting of three parts:
|
||||
/// - The REQUIRED character set name (`charset`).
|
||||
/// - The OPTIONAL language information (`language_tag`).
|
||||
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||
///
|
||||
/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
||||
/// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ExtendedValue {
|
||||
/// The character set that is used to encode the `value` to a string.
|
||||
|
|
@ -24,17 +24,17 @@ pub struct ExtendedValue {
|
|||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parses extended header parameter values (`ext-value`), as defined in
|
||||
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
||||
/// Parses extended header parameter values (`ext-value`), as defined
|
||||
/// in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
|
||||
///
|
||||
/// Extended values are denoted by parameter names that end with `*`.
|
||||
///
|
||||
/// ## ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// ```plain
|
||||
/// ext-value = charset "'" [ language ] "'" value-chars
|
||||
/// ; like RFC 2231's <extended-initial-value>
|
||||
/// ; (see [RFC2231], Section 7)
|
||||
/// ; (see [RFC 2231 §7])
|
||||
///
|
||||
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
||||
///
|
||||
|
|
@ -43,22 +43,26 @@ pub struct ExtendedValue {
|
|||
/// / "!" / "#" / "$" / "%" / "&"
|
||||
/// / "+" / "-" / "^" / "_" / "`"
|
||||
/// / "{" / "}" / "~"
|
||||
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
|
||||
/// ; as <mime-charset> in [RFC 2978 §2.3]
|
||||
/// ; except that the single quote is not included
|
||||
/// ; SHOULD be registered in the IANA charset registry
|
||||
///
|
||||
/// language = <Language-Tag, defined in [RFC5646], Section 2.1>
|
||||
/// language = <Language-Tag, defined in [RFC 5646 §2.1]>
|
||||
///
|
||||
/// value-chars = *( pct-encoded / attr-char )
|
||||
///
|
||||
/// pct-encoded = "%" HEXDIG HEXDIG
|
||||
/// ; see [RFC3986], Section 2.1
|
||||
/// ; see [RFC 3986 §2.1]
|
||||
///
|
||||
/// attr-char = ALPHA / DIGIT
|
||||
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
||||
/// / "^" / "_" / "`" / "|" / "~"
|
||||
/// ; token except ( "*" / "'" / "%" )
|
||||
/// ```
|
||||
///
|
||||
/// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7
|
||||
/// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3
|
||||
/// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
|
||||
pub fn parse_extended_value(
|
||||
val: &str,
|
||||
) -> Result<ExtendedValue, crate::error::ParseError> {
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@ use crate::{
|
|||
helpers::MutWriter,
|
||||
};
|
||||
|
||||
/// A timestamp with HTTP formatting and parsing.
|
||||
/// A timestamp with HTTP-style formatting and parsing.
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct HttpDate(SystemTime);
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@ mod charset;
|
|||
mod content_encoding;
|
||||
mod extended;
|
||||
mod http_date;
|
||||
mod quality;
|
||||
mod quality_item;
|
||||
|
||||
pub use self::charset::Charset;
|
||||
pub use self::content_encoding::ContentEncoding;
|
||||
pub use self::extended::{parse_extended_value, ExtendedValue};
|
||||
pub use self::http_date::HttpDate;
|
||||
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
||||
pub use self::quality::{q, Quality};
|
||||
pub use self::quality_item::QualityItem;
|
||||
pub use language_tags::LanguageTag;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,208 @@
|
|||
use std::{
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
};
|
||||
|
||||
use derive_more::{Display, Error};
|
||||
|
||||
const MAX_QUALITY_INT: u16 = 1000;
|
||||
const MAX_QUALITY_FLOAT: f32 = 1.0;
|
||||
|
||||
/// Represents a quality used in q-factor values.
|
||||
///
|
||||
/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
|
||||
///
|
||||
/// # Implementation notes
|
||||
/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
|
||||
/// This means there are 1001 possible values. Since floating point numbers are not exact and the
|
||||
/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
|
||||
/// the quality internally.
|
||||
///
|
||||
/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use actix_http::header::{Quality, q};
|
||||
/// assert_eq!(q(1.0), Quality::MAX);
|
||||
///
|
||||
/// assert_eq!(q(0.42).to_string(), "0.42");
|
||||
/// assert_eq!(q(1.0).to_string(), "1");
|
||||
/// assert_eq!(Quality::MIN.to_string(), "0");
|
||||
/// ```
|
||||
///
|
||||
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Quality(pub(super) u16);
|
||||
|
||||
impl Quality {
|
||||
/// The maximum quality value, equivalent to `q=1.0`.
|
||||
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
|
||||
|
||||
/// The minimum quality value, equivalent to `q=0.0`.
|
||||
pub const MIN: Quality = Quality(0);
|
||||
|
||||
/// Converts a float in the range 0.0–1.0 to a `Quality`.
|
||||
///
|
||||
/// Intentionally private. External uses should rely on the `TryFrom` impl.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
||||
fn from_f32(value: f32) -> Self {
|
||||
// Check that `value` is within range should be done before calling this method.
|
||||
// Just in case, this debug_assert should catch if we were forgetful.
|
||||
debug_assert!(
|
||||
(0.0f32..=1.0f32).contains(&value),
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
|
||||
Quality((value * MAX_QUALITY_INT as f32) as u16)
|
||||
}
|
||||
}
|
||||
|
||||
/// The default value is [`Quality::MAX`].
|
||||
impl Default for Quality {
|
||||
fn default() -> Quality {
|
||||
Quality::MAX
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for Quality {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.0 {
|
||||
0 => f.write_str("0"),
|
||||
MAX_QUALITY_INT => f.write_str("1"),
|
||||
|
||||
// some number in the range 1–999
|
||||
x => {
|
||||
f.write_str("0.")?;
|
||||
|
||||
// This implementation avoids string allocation for removing trailing zeroes.
|
||||
// In benchmarks it is twice as fast as approach using something like
|
||||
// `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
|
||||
|
||||
if x < 10 {
|
||||
// x in is range 1–9
|
||||
|
||||
f.write_str("00")?;
|
||||
|
||||
// 0 is already handled so it's not possible to have a trailing 0 in this range
|
||||
// we can just write the integer
|
||||
itoa::fmt(f, x)
|
||||
} else if x < 100 {
|
||||
// x in is range 10–99
|
||||
|
||||
f.write_str("0")?;
|
||||
|
||||
if x % 10 == 0 {
|
||||
// trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
}
|
||||
} else {
|
||||
// x is in range 100–999
|
||||
|
||||
if x % 100 == 0 {
|
||||
// two trailing 0s, divide by 100 and write
|
||||
itoa::fmt(f, x / 100)
|
||||
} else if x % 10 == 0 {
|
||||
// one trailing 0, divide by 10 and write
|
||||
itoa::fmt(f, x / 10)
|
||||
} else {
|
||||
itoa::fmt(f, x)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "quality out of bounds")]
|
||||
#[non_exhaustive]
|
||||
pub struct QualityOutOfBounds;
|
||||
|
||||
impl TryFrom<f32> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
#[inline]
|
||||
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
||||
if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
|
||||
Ok(Quality::from_f32(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to create a [`Quality`] from an `f32` (0.0–1.0).
|
||||
///
|
||||
/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
|
||||
///
|
||||
/// # Panics
|
||||
/// Panics if value is out of range.
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_http::header::{q, Quality};
|
||||
/// let q1 = q(1.0);
|
||||
/// assert_eq!(q1, Quality::MAX);
|
||||
///
|
||||
/// let q2 = q(0.0);
|
||||
/// assert_eq!(q2, Quality::MIN);
|
||||
///
|
||||
/// let q3 = q(0.42);
|
||||
/// ```
|
||||
///
|
||||
/// An out-of-range `f32` quality will panic.
|
||||
/// ```should_panic
|
||||
/// # use actix_http::header::q;
|
||||
/// let _q2 = q(1.42);
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn q<T>(quality: T) -> Quality
|
||||
where
|
||||
T: TryInto<Quality>,
|
||||
T::Error: fmt::Debug,
|
||||
{
|
||||
quality.try_into().expect("quality value was out of bounds")
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn q_helper() {
|
||||
assert_eq!(q(0.5), Quality(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn display_output() {
|
||||
assert_eq!(q(0.0).to_string(), "0");
|
||||
assert_eq!(q(1.0).to_string(), "1");
|
||||
assert_eq!(q(0.001).to_string(), "0.001");
|
||||
assert_eq!(q(0.5).to_string(), "0.5");
|
||||
assert_eq!(q(0.22).to_string(), "0.22");
|
||||
assert_eq!(q(0.123).to_string(), "0.123");
|
||||
assert_eq!(q(0.999).to_string(), "0.999");
|
||||
|
||||
for x in 0..=1000 {
|
||||
// if trailing zeroes are handled correctly, we would not expect the serialized length
|
||||
// to ever exceed "0." + 3 decimal places = 5 in length
|
||||
assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn negative_quality() {
|
||||
q(-1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn quality_out_of_bounds() {
|
||||
q(2.0);
|
||||
}
|
||||
}
|
||||
|
|
@ -1,104 +1,65 @@
|
|||
use std::{
|
||||
cmp,
|
||||
convert::{TryFrom, TryInto},
|
||||
fmt,
|
||||
str::{self, FromStr},
|
||||
};
|
||||
|
||||
use derive_more::{Display, Error};
|
||||
use std::{cmp, convert::TryFrom as _, fmt, str};
|
||||
|
||||
use crate::error::ParseError;
|
||||
|
||||
const MAX_QUALITY: u16 = 1000;
|
||||
const MAX_FLOAT_QUALITY: f32 = 1.0;
|
||||
use super::Quality;
|
||||
|
||||
/// Represents a quality used in quality values.
|
||||
/// Represents an item with a quality value as defined
|
||||
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).
|
||||
///
|
||||
/// Can be created with the [`q`] function.
|
||||
/// # Parsing and Formatting
|
||||
/// This wrapper be used to parse header value items that have a q-factor annotation as well as
|
||||
/// serialize items with a their q-factor.
|
||||
///
|
||||
/// # Implementation notes
|
||||
/// # Ordering
|
||||
/// Since this context of use for this type is header value items, ordering is defined for
|
||||
/// `QualityItem`s but _only_ considers the item's quality. Order of appearance should be used as
|
||||
/// the secondary sorting parameter; i.e., a stable sort over the quality values will produce a
|
||||
/// correctly sorted sequence.
|
||||
///
|
||||
/// The quality value is defined as a number between 0 and 1 with three decimal
|
||||
/// places. This means there are 1001 possible values. Since floating point
|
||||
/// numbers are not exact and the smallest floating point data type (`f32`)
|
||||
/// consumes four bytes, hyper uses an `u16` value to store the
|
||||
/// quality internally. For performance reasons you may set quality directly to
|
||||
/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality
|
||||
/// `q=0.532`.
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// # use actix_http::header::{QualityItem, q};
|
||||
/// let q_item: QualityItem<String> = "hello;q=0.3".parse().unwrap();
|
||||
/// assert_eq!(&q_item.item, "hello");
|
||||
/// assert_eq!(q_item.quality, q(0.3));
|
||||
///
|
||||
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1)
|
||||
/// gives more information on quality values in HTTP header fields.
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub struct Quality(u16);
|
||||
|
||||
impl Quality {
|
||||
/// # Panics
|
||||
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
|
||||
fn from_f32(value: f32) -> Self {
|
||||
// Check that `value` is within range should be done before calling this method.
|
||||
// Just in case, this debug_assert should catch if we were forgetful.
|
||||
debug_assert!(
|
||||
(0.0f32..=1.0f32).contains(&value),
|
||||
"q value must be between 0.0 and 1.0"
|
||||
);
|
||||
|
||||
Quality((value * MAX_QUALITY as f32) as u16)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Quality {
|
||||
fn default() -> Quality {
|
||||
Quality(MAX_QUALITY)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
pub struct QualityOutOfBounds;
|
||||
|
||||
impl TryFrom<u16> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
if (0..=MAX_QUALITY).contains(&value) {
|
||||
Ok(Quality(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl TryFrom<f32> for Quality {
|
||||
type Error = QualityOutOfBounds;
|
||||
|
||||
fn try_from(value: f32) -> Result<Self, Self::Error> {
|
||||
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
|
||||
Ok(Quality::from_f32(value))
|
||||
} else {
|
||||
Err(QualityOutOfBounds)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents an item with a quality value as defined in
|
||||
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1).
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
/// // note that format is normalized compared to parsed item
|
||||
/// assert_eq!(q_item.to_string(), "hello; q=0.3");
|
||||
///
|
||||
/// // item with q=0.3 is greater than item with q=0.1
|
||||
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
|
||||
/// assert!(q_item > q_item_fallback);
|
||||
/// ```
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct QualityItem<T> {
|
||||
/// The actual contents of the field.
|
||||
/// The wrapped contents of the field.
|
||||
pub item: T,
|
||||
|
||||
/// The quality (client or server preference) for the value.
|
||||
pub quality: Quality,
|
||||
}
|
||||
|
||||
impl<T> QualityItem<T> {
|
||||
/// Creates a new `QualityItem` from an item and a quality.
|
||||
/// The item can be of any type.
|
||||
/// The quality should be a value in the range [0, 1].
|
||||
pub fn new(item: T, quality: Quality) -> QualityItem<T> {
|
||||
/// Constructs a new `QualityItem` from an item and a quality value.
|
||||
///
|
||||
/// The item can be of any type. The quality should be a value in the range [0, 1].
|
||||
pub fn new(item: T, quality: Quality) -> Self {
|
||||
QualityItem { item, quality }
|
||||
}
|
||||
|
||||
/// Constructs a new `QualityItem` from an item, using the maximum q-value.
|
||||
pub fn max(item: T) -> Self {
|
||||
Self::new(item, Quality::MAX)
|
||||
}
|
||||
|
||||
/// Constructs a new `QualityItem` from an item, using the minimum q-value.
|
||||
pub fn min(item: T) -> Self {
|
||||
Self::new(item, Quality::MIN)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> {
|
||||
impl<T: PartialEq> PartialOrd for QualityItem<T> {
|
||||
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
|
||||
self.quality.partial_cmp(&other.quality)
|
||||
}
|
||||
|
|
@ -108,93 +69,77 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
|
|||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
fmt::Display::fmt(&self.item, f)?;
|
||||
|
||||
match self.quality.0 {
|
||||
MAX_QUALITY => Ok(()),
|
||||
0 => f.write_str("; q=0"),
|
||||
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
|
||||
match self.quality {
|
||||
// q-factor value is implied for max value
|
||||
Quality::MAX => Ok(()),
|
||||
|
||||
Quality::MIN => f.write_str("; q=0"),
|
||||
q => write!(f, "; q={}", q),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: FromStr> FromStr for QualityItem<T> {
|
||||
impl<T: str::FromStr> str::FromStr for QualityItem<T> {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(qitem_str: &str) -> Result<Self, Self::Err> {
|
||||
if !qitem_str.is_ascii() {
|
||||
fn from_str(q_item_str: &str) -> Result<Self, Self::Err> {
|
||||
if !q_item_str.is_ascii() {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
// Set defaults used if parsing fails.
|
||||
let mut raw_item = qitem_str;
|
||||
let mut quality = 1f32;
|
||||
// set defaults used if quality-item parsing fails, i.e., item has no q attribute
|
||||
let mut raw_item = q_item_str;
|
||||
let mut quality = Quality::MAX;
|
||||
|
||||
let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect();
|
||||
let parts = q_item_str
|
||||
.rsplit_once(';')
|
||||
.map(|(item, q_attr)| (item.trim(), q_attr.trim()));
|
||||
|
||||
if parts.len() == 2 {
|
||||
if let Some((val, q_attr)) = parts {
|
||||
// example for item with q-factor:
|
||||
//
|
||||
// gzip;q=0.65
|
||||
// ^^^^^^ parts[0]
|
||||
// ^^ start
|
||||
// ^^^^ val
|
||||
// ^^^^^^ q_attr
|
||||
// ^^ q
|
||||
// ^^^^ q_val
|
||||
// ^^^^ parts[1]
|
||||
|
||||
if parts[0].len() < 2 {
|
||||
if q_attr.len() < 2 {
|
||||
// Can't possibly be an attribute since an attribute needs at least a name followed
|
||||
// by an equals sign. And bare identifiers are forbidden.
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
let start = &parts[0][0..2];
|
||||
let q = &q_attr[0..2];
|
||||
|
||||
if start == "q=" || start == "Q=" {
|
||||
let q_val = &parts[0][2..];
|
||||
if q == "q=" || q == "Q=" {
|
||||
let q_val = &q_attr[2..];
|
||||
if q_val.len() > 5 {
|
||||
// longer than 5 indicates an over-precise q-factor
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
|
||||
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
|
||||
let q_value =
|
||||
Quality::try_from(q_value).map_err(|_| ParseError::Header)?;
|
||||
|
||||
if (0f32..=1f32).contains(&q_value) {
|
||||
quality = q_value;
|
||||
raw_item = parts[1];
|
||||
} else {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
raw_item = val;
|
||||
}
|
||||
}
|
||||
|
||||
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
|
||||
|
||||
// we already checked above that the quality is within range
|
||||
Ok(QualityItem::new(item, Quality::from_f32(quality)))
|
||||
Ok(QualityItem::new(item, quality))
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience function to wrap a value in a `QualityItem`
|
||||
/// Sets `q` to the default 1.0
|
||||
pub fn qitem<T>(item: T) -> QualityItem<T> {
|
||||
QualityItem::new(item, Quality::default())
|
||||
}
|
||||
|
||||
/// Convenience function to create a `Quality` from a float or integer.
|
||||
///
|
||||
/// Implemented for `u16` and `f32`. Panics if value is out of range.
|
||||
pub fn q<T>(val: T) -> Quality
|
||||
where
|
||||
T: TryInto<Quality>,
|
||||
T::Error: fmt::Debug,
|
||||
{
|
||||
// TODO: on next breaking change, handle unwrap differently
|
||||
val.try_into().unwrap()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// copy of encoding from actix-web headers
|
||||
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
|
||||
#[derive(Clone, PartialEq, Debug)]
|
||||
pub enum Encoding {
|
||||
Chunked,
|
||||
|
|
@ -223,7 +168,7 @@ mod tests {
|
|||
}
|
||||
}
|
||||
|
||||
impl FromStr for Encoding {
|
||||
impl str::FromStr for Encoding {
|
||||
type Err = crate::error::ParseError;
|
||||
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
|
||||
use Encoding::*;
|
||||
|
|
@ -243,7 +188,7 @@ mod tests {
|
|||
#[test]
|
||||
fn test_quality_item_fmt_q_1() {
|
||||
use Encoding::*;
|
||||
let x = qitem(Chunked);
|
||||
let x = QualityItem::max(Chunked);
|
||||
assert_eq!(format!("{}", x), "chunked");
|
||||
}
|
||||
#[test]
|
||||
|
|
@ -342,25 +287,8 @@ mod tests {
|
|||
fn test_quality_item_ordering() {
|
||||
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
|
||||
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
|
||||
let comparision_result: bool = x.gt(&y);
|
||||
assert!(comparision_result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_quality() {
|
||||
assert_eq!(q(0.5), Quality(500));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_quality_invalid() {
|
||||
q(-1.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_quality_invalid2() {
|
||||
q(2.0);
|
||||
let comparison_result: bool = x.gt(&y);
|
||||
assert!(comparison_result)
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! Header parsing utilities.
|
||||
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use super::HeaderValue;
|
||||
|
|
@ -10,9 +12,12 @@ where
|
|||
I: Iterator<Item = &'a HeaderValue> + 'a,
|
||||
T: FromStr,
|
||||
{
|
||||
let mut result = Vec::new();
|
||||
let size_guess = all.size_hint().1.unwrap_or(2);
|
||||
let mut result = Vec::with_capacity(size_guess);
|
||||
|
||||
for h in all {
|
||||
let s = h.to_str().map_err(|_| ParseError::Header)?;
|
||||
|
||||
result.extend(
|
||||
s.split(',')
|
||||
.filter_map(|x| match x.trim() {
|
||||
|
|
@ -22,6 +27,7 @@ where
|
|||
.filter_map(|x| x.trim().parse().ok()),
|
||||
)
|
||||
}
|
||||
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
|
|
@ -30,10 +36,12 @@ where
|
|||
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
|
||||
if let Some(line) = val {
|
||||
let line = line.to_str().map_err(|_| ParseError::Header)?;
|
||||
|
||||
if !line.is_empty() {
|
||||
return T::from_str(line).or(Err(ParseError::Header));
|
||||
}
|
||||
}
|
||||
|
||||
Err(ParseError::Header)
|
||||
}
|
||||
|
||||
|
|
@ -44,19 +52,53 @@ where
|
|||
T: fmt::Display,
|
||||
{
|
||||
let mut iter = parts.iter();
|
||||
|
||||
if let Some(part) = iter.next() {
|
||||
fmt::Display::fmt(part, f)?;
|
||||
}
|
||||
|
||||
for part in iter {
|
||||
f.write_str(", ")?;
|
||||
fmt::Display::fmt(part, f)?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Percent encode a sequence of bytes with a character set defined in
|
||||
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
|
||||
/// Percent encode a sequence of bytes with a character set defined in [RFC 5987 §3.2].
|
||||
///
|
||||
/// [RFC 5987 §3.2]: https://datatracker.ietf.org/doc/html/rfc5987#section-3.2
|
||||
#[inline]
|
||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
||||
fmt::Display::fmt(&encoded, f)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn comma_delimited_parsing() {
|
||||
let headers = vec![];
|
||||
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
|
||||
assert_eq!(res, vec![0; 0]);
|
||||
|
||||
let headers = vec![
|
||||
HeaderValue::from_static("1, 2"),
|
||||
HeaderValue::from_static("3,4"),
|
||||
];
|
||||
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
|
||||
assert_eq!(res, vec![1, 2, 3, 4]);
|
||||
|
||||
let headers = vec![
|
||||
HeaderValue::from_static(""),
|
||||
HeaderValue::from_static(","),
|
||||
HeaderValue::from_static(" "),
|
||||
HeaderValue::from_static("1 ,"),
|
||||
HeaderValue::from_static(""),
|
||||
];
|
||||
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
|
||||
assert_eq!(res, vec![1]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -46,8 +46,8 @@ pub trait Head: Default + 'static {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct RequestHead {
|
||||
pub uri: Uri,
|
||||
pub method: Method,
|
||||
pub uri: Uri,
|
||||
pub version: Version,
|
||||
pub headers: HeaderMap,
|
||||
pub extensions: RefCell<Extensions>,
|
||||
|
|
@ -58,13 +58,13 @@ pub struct RequestHead {
|
|||
impl Default for RequestHead {
|
||||
fn default() -> RequestHead {
|
||||
RequestHead {
|
||||
uri: Uri::default(),
|
||||
method: Method::default(),
|
||||
uri: Uri::default(),
|
||||
version: Version::HTTP_11,
|
||||
headers: HeaderMap::with_capacity(16),
|
||||
flags: Flags::empty(),
|
||||
peer_addr: None,
|
||||
extensions: RefCell::new(Extensions::new()),
|
||||
peer_addr: None,
|
||||
flags: Flags::empty(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -192,6 +192,7 @@ impl RequestHead {
|
|||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
#[allow(clippy::large_enum_variant)]
|
||||
pub enum RequestHeadType {
|
||||
Owned(RequestHead),
|
||||
Rc(Rc<RequestHead>, Option<HeaderMap>),
|
||||
|
|
@ -317,7 +318,7 @@ impl ResponseHead {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn ctype(&self) -> Option<ConnectionType> {
|
||||
pub(crate) fn conn_type(&self) -> Option<ConnectionType> {
|
||||
if self.flags.contains(Flags::CLOSE) {
|
||||
Some(ConnectionType::Close)
|
||||
} else if self.flags.contains(Flags::KEEP_ALIVE) {
|
||||
|
|
|
|||
|
|
@ -6,14 +6,15 @@ use std::{
|
|||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, MessageBody},
|
||||
error::Error,
|
||||
body::{BoxBody, MessageBody},
|
||||
extensions::Extensions,
|
||||
header::{self, IntoHeaderValue},
|
||||
http::{HeaderMap, StatusCode},
|
||||
message::{BoxedResponseHead, ResponseHead},
|
||||
ResponseBuilder,
|
||||
Error, ResponseBuilder,
|
||||
};
|
||||
|
||||
/// An HTTP response.
|
||||
|
|
@ -22,13 +23,13 @@ pub struct Response<B> {
|
|||
pub(crate) body: B,
|
||||
}
|
||||
|
||||
impl Response<AnyBody> {
|
||||
impl Response<BoxBody> {
|
||||
/// Constructs a new response with default body.
|
||||
#[inline]
|
||||
pub fn new(status: StatusCode) -> Self {
|
||||
Response {
|
||||
head: BoxedResponseHead::new(status),
|
||||
body: AnyBody::Empty,
|
||||
body: BoxBody::new(()),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -189,6 +190,14 @@ impl<B> Response<B> {
|
|||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn map_into_boxed_body(self) -> Response<BoxBody>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
self.map_body(|_, body| BoxBody::new(body))
|
||||
}
|
||||
|
||||
/// Returns body, consuming this response.
|
||||
pub fn into_body(self) -> B {
|
||||
self.body
|
||||
|
|
@ -223,81 +232,99 @@ impl<B: Default> Default for Response<B> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<I: Into<Response<AnyBody>>, E: Into<Error>> From<Result<I, E>>
|
||||
for Response<AnyBody>
|
||||
impl<I: Into<Response<BoxBody>>, E: Into<Error>> From<Result<I, E>>
|
||||
for Response<BoxBody>
|
||||
{
|
||||
fn from(res: Result<I, E>) -> Self {
|
||||
match res {
|
||||
Ok(val) => val.into(),
|
||||
Err(err) => err.into().into(),
|
||||
Err(err) => Response::from(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ResponseBuilder> for Response<AnyBody> {
|
||||
impl From<ResponseBuilder> for Response<BoxBody> {
|
||||
fn from(mut builder: ResponseBuilder) -> Self {
|
||||
builder.finish()
|
||||
builder.finish().map_into_boxed_body()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<std::convert::Infallible> for Response<AnyBody> {
|
||||
impl From<std::convert::Infallible> for Response<BoxBody> {
|
||||
fn from(val: std::convert::Infallible) -> Self {
|
||||
match val {}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static str> for Response<AnyBody> {
|
||||
impl From<&'static str> for Response<&'static str> {
|
||||
fn from(val: &'static str) -> Self {
|
||||
Response::build(StatusCode::OK)
|
||||
.content_type(mime::TEXT_PLAIN_UTF_8)
|
||||
.body(val)
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&'static [u8]> for Response<AnyBody> {
|
||||
impl From<&'static [u8]> for Response<&'static [u8]> {
|
||||
fn from(val: &'static [u8]) -> Self {
|
||||
Response::build(StatusCode::OK)
|
||||
.content_type(mime::APPLICATION_OCTET_STREAM)
|
||||
.body(val)
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<String> for Response<AnyBody> {
|
||||
impl From<String> for Response<String> {
|
||||
fn from(val: String) -> Self {
|
||||
Response::build(StatusCode::OK)
|
||||
.content_type(mime::TEXT_PLAIN_UTF_8)
|
||||
.body(val)
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a String> for Response<AnyBody> {
|
||||
fn from(val: &'a String) -> Self {
|
||||
Response::build(StatusCode::OK)
|
||||
.content_type(mime::TEXT_PLAIN_UTF_8)
|
||||
.body(val)
|
||||
impl From<&String> for Response<String> {
|
||||
fn from(val: &String) -> Self {
|
||||
let mut res = Response::with_body(StatusCode::OK, val.clone());
|
||||
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Bytes> for Response<AnyBody> {
|
||||
impl From<Bytes> for Response<Bytes> {
|
||||
fn from(val: Bytes) -> Self {
|
||||
Response::build(StatusCode::OK)
|
||||
.content_type(mime::APPLICATION_OCTET_STREAM)
|
||||
.body(val)
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BytesMut> for Response<AnyBody> {
|
||||
impl From<BytesMut> for Response<BytesMut> {
|
||||
fn from(val: BytesMut) -> Self {
|
||||
Response::build(StatusCode::OK)
|
||||
.content_type(mime::APPLICATION_OCTET_STREAM)
|
||||
.body(val)
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ByteString> for Response<ByteString> {
|
||||
fn from(val: ByteString) -> Self {
|
||||
let mut res = Response::with_body(StatusCode::OK, val);
|
||||
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
|
||||
res.headers_mut().insert(header::CONTENT_TYPE, mime);
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE};
|
||||
use crate::{
|
||||
body::to_bytes,
|
||||
http::header::{HeaderValue, CONTENT_TYPE, COOKIE},
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_debug() {
|
||||
|
|
@ -309,73 +336,73 @@ mod tests {
|
|||
assert!(dbg.contains("Response"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_response() {
|
||||
let resp: Response<AnyBody> = "test".into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
#[actix_rt::test]
|
||||
async fn test_into_response() {
|
||||
let res = Response::from("test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain; charset=utf-8")
|
||||
);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
|
||||
let resp: Response<AnyBody> = b"test".as_ref().into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let res = Response::from(b"test".as_ref());
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("application/octet-stream")
|
||||
);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
|
||||
let resp: Response<AnyBody> = "test".to_owned().into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let res = Response::from("test".to_owned());
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain; charset=utf-8")
|
||||
);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
|
||||
let resp: Response<AnyBody> = (&"test".to_owned()).into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let res = Response::from("test".to_owned());
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("text/plain; charset=utf-8")
|
||||
);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
|
||||
let b = Bytes::from_static(b"test");
|
||||
let resp: Response<AnyBody> = b.into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let res = Response::from(b);
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("application/octet-stream")
|
||||
);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
|
||||
let b = Bytes::from_static(b"test");
|
||||
let resp: Response<AnyBody> = b.into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let res = Response::from(b);
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("application/octet-stream")
|
||||
);
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
|
||||
let b = BytesMut::from("test");
|
||||
let resp: Response<AnyBody> = b.into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let res = Response::from(b);
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
res.headers().get(CONTENT_TYPE).unwrap(),
|
||||
HeaderValue::from_static("application/octet-stream")
|
||||
);
|
||||
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
assert_eq!(resp.body().get_ref(), b"test");
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,19 +2,11 @@
|
|||
|
||||
use std::{
|
||||
cell::{Ref, RefMut},
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
future::Future,
|
||||
pin::Pin,
|
||||
str,
|
||||
task::{Context, Poll},
|
||||
fmt, str,
|
||||
};
|
||||
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, BodyStream},
|
||||
body::{EitherBody, MessageBody},
|
||||
error::{Error, HttpError},
|
||||
header::{self, IntoHeaderPair, IntoHeaderValue},
|
||||
message::{BoxedResponseHead, ConnectionType, ResponseHead},
|
||||
|
|
@ -235,10 +227,14 @@ impl ResponseBuilder {
|
|||
/// Generate response with a wrapped body.
|
||||
///
|
||||
/// This `ResponseBuilder` will be left in a useless state.
|
||||
#[inline]
|
||||
pub fn body<B: Into<AnyBody>>(&mut self, body: B) -> Response<AnyBody> {
|
||||
self.message_body(body.into())
|
||||
.unwrap_or_else(Response::from)
|
||||
pub fn body<B>(&mut self, body: B) -> Response<EitherBody<B>>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
match self.message_body(body) {
|
||||
Ok(res) => res.map_body(|_, body| EitherBody::left(body)),
|
||||
Err(err) => Response::from(err).map_body(|_, body| EitherBody::right(body)),
|
||||
}
|
||||
}
|
||||
|
||||
/// Generate response with a body.
|
||||
|
|
@ -253,24 +249,12 @@ impl ResponseBuilder {
|
|||
Ok(Response { head, body })
|
||||
}
|
||||
|
||||
/// Generate response with a streaming body.
|
||||
///
|
||||
/// This `ResponseBuilder` will be left in a useless state.
|
||||
#[inline]
|
||||
pub fn streaming<S, E>(&mut self, stream: S) -> Response<AnyBody>
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<Box<dyn StdError>> + 'static,
|
||||
{
|
||||
self.body(AnyBody::from_message(BodyStream::new(stream)))
|
||||
}
|
||||
|
||||
/// Generate response with an empty body.
|
||||
///
|
||||
/// This `ResponseBuilder` will be left in a useless state.
|
||||
#[inline]
|
||||
pub fn finish(&mut self) -> Response<AnyBody> {
|
||||
self.body(AnyBody::Empty)
|
||||
pub fn finish(&mut self) -> Response<EitherBody<()>> {
|
||||
self.body(())
|
||||
}
|
||||
|
||||
/// Create an owned `ResponseBuilder`, leaving the original in a useless state.
|
||||
|
|
@ -327,14 +311,6 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
|
|||
}
|
||||
}
|
||||
|
||||
impl Future for ResponseBuilder {
|
||||
type Output = Result<Response<AnyBody>, Error>;
|
||||
|
||||
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Poll::Ready(Ok(self.finish()))
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ResponseBuilder {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let head = self.head.as_ref().unwrap();
|
||||
|
|
@ -356,8 +332,9 @@ impl fmt::Debug for ResponseBuilder {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use bytes::Bytes;
|
||||
|
||||
use super::*;
|
||||
use crate::body::Body;
|
||||
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
|
||||
|
||||
#[test]
|
||||
|
|
@ -383,20 +360,28 @@ mod tests {
|
|||
#[test]
|
||||
fn test_force_close() {
|
||||
let resp = Response::build(StatusCode::OK).force_close().finish();
|
||||
assert!(!resp.keep_alive())
|
||||
assert!(!resp.keep_alive());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_content_type() {
|
||||
let resp = Response::build(StatusCode::OK)
|
||||
.content_type("text/plain")
|
||||
.body(Body::Empty);
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
|
||||
.body(Bytes::new());
|
||||
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain");
|
||||
|
||||
let resp = Response::build(StatusCode::OK)
|
||||
.content_type(mime::APPLICATION_JAVASCRIPT_UTF_8)
|
||||
.body(Bytes::new());
|
||||
assert_eq!(
|
||||
resp.headers().get(CONTENT_TYPE).unwrap(),
|
||||
"application/javascript; charset=utf-8"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_into_builder() {
|
||||
let mut resp: Response<Body> = "test".into();
|
||||
let mut resp: Response<_> = "test".into();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
|
||||
resp.headers_mut().insert(
|
||||
|
|
|
|||
|
|
@ -1,5 +1,4 @@
|
|||
use std::{
|
||||
error::Error as StdError,
|
||||
fmt,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
|
|
@ -9,18 +8,16 @@ use std::{
|
|||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use ::h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_service::{
|
||||
fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use pin_project::pin_project;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use crate::{
|
||||
body::{AnyBody, MessageBody},
|
||||
body::{BoxBody, MessageBody},
|
||||
builder::HttpServiceBuilder,
|
||||
config::{KeepAlive, ServiceConfig},
|
||||
error::DispatchError,
|
||||
|
|
@ -40,7 +37,7 @@ pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
|
|||
impl<T, S, B> HttpService<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
|
@ -55,12 +52,11 @@ where
|
|||
impl<T, S, B> HttpService<T, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
/// Create new `HttpService` instance.
|
||||
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
|
||||
|
|
@ -95,7 +91,7 @@ where
|
|||
impl<T, S, B, X, U> HttpService<T, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
|
@ -109,7 +105,7 @@ where
|
|||
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
|
||||
where
|
||||
X1: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X1::Error: Into<Response<AnyBody>>,
|
||||
X1::Error: Into<Response<BoxBody>>,
|
||||
X1::InitError: fmt::Debug,
|
||||
{
|
||||
HttpService {
|
||||
|
|
@ -153,17 +149,16 @@ impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
|
|||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<
|
||||
|
|
@ -172,7 +167,7 @@ where
|
|||
Response = (),
|
||||
>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create simple tcp stream service
|
||||
|
|
@ -195,9 +190,14 @@ where
|
|||
|
||||
#[cfg(feature = "openssl")]
|
||||
mod openssl {
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
use actix_tls::accept::{
|
||||
openssl::{
|
||||
reexports::{Error as SslError, SslAcceptor},
|
||||
Acceptor, TlsStream,
|
||||
},
|
||||
TlsError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
|
||||
|
|
@ -205,17 +205,16 @@ mod openssl {
|
|||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<
|
||||
|
|
@ -224,10 +223,10 @@ mod openssl {
|
|||
Response = (),
|
||||
>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create openssl based service
|
||||
/// Create OpenSSL based service.
|
||||
pub fn openssl(
|
||||
self,
|
||||
acceptor: SslAcceptor,
|
||||
|
|
@ -239,9 +238,11 @@ mod openssl {
|
|||
InitError = (),
|
||||
> {
|
||||
Acceptor::new(acceptor)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!())
|
||||
.and_then(|io: TlsStream<TcpStream>| async {
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
.map_err(TlsError::into_service_error)
|
||||
.map(|io: TlsStream<TcpStream>| {
|
||||
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
|
||||
if protos.windows(2).any(|window| window == b"h2") {
|
||||
Protocol::Http2
|
||||
|
|
@ -251,8 +252,9 @@ mod openssl {
|
|||
} else {
|
||||
Protocol::Http1
|
||||
};
|
||||
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
Ok((io, proto, peer_addr))
|
||||
(io, proto, peer_addr)
|
||||
})
|
||||
.and_then(self.map_err(TlsError::Service))
|
||||
}
|
||||
|
|
@ -263,27 +265,28 @@ mod openssl {
|
|||
mod rustls {
|
||||
use std::io;
|
||||
|
||||
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
use actix_tls::accept::{
|
||||
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
|
||||
TlsError,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use actix_service::ServiceFactoryExt;
|
||||
|
||||
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<
|
||||
|
|
@ -292,10 +295,10 @@ mod rustls {
|
|||
Response = (),
|
||||
>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
/// Create rustls based service
|
||||
/// Create Rustls based service.
|
||||
pub fn rustls(
|
||||
self,
|
||||
mut config: ServerConfig,
|
||||
|
|
@ -311,8 +314,10 @@ mod rustls {
|
|||
config.alpn_protocols = protos;
|
||||
|
||||
Acceptor::new(config)
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!())
|
||||
.map_init_err(|_| {
|
||||
unreachable!("TLS acceptor service factory does not error on init")
|
||||
})
|
||||
.map_err(TlsError::into_service_error)
|
||||
.and_then(|io: TlsStream<TcpStream>| async {
|
||||
let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
|
||||
if protos.windows(2).any(|window| window == b"h2") {
|
||||
|
|
@ -338,22 +343,21 @@ where
|
|||
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::InitError: fmt::Debug,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: ServiceFactory<Request, Config = (), Response = Request>,
|
||||
X::Future: 'static,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
X::InitError: fmt::Debug,
|
||||
|
||||
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
|
||||
U::Future: 'static,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
U::InitError: fmt::Debug,
|
||||
{
|
||||
type Response = ();
|
||||
|
|
@ -416,11 +420,11 @@ where
|
|||
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
|
||||
where
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
X: Service<Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
U: Service<(Request, Framed<T, h1::Codec>)>,
|
||||
U::Error: Into<Response<AnyBody>>,
|
||||
U::Error: Into<Response<BoxBody>>,
|
||||
{
|
||||
pub(super) fn new(
|
||||
cfg: ServiceConfig,
|
||||
|
|
@ -440,7 +444,7 @@ where
|
|||
pub(super) fn _poll_ready(
|
||||
&self,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Result<(), Response<AnyBody>>> {
|
||||
) -> Poll<Result<(), Response<BoxBody>>> {
|
||||
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
|
||||
|
||||
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
|
||||
|
|
@ -476,18 +480,17 @@ where
|
|||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display + Into<Response<AnyBody>>,
|
||||
U::Error: fmt::Display + Into<Response<BoxBody>>,
|
||||
{
|
||||
type Response = ();
|
||||
type Error = DispatchError;
|
||||
|
|
@ -509,76 +512,86 @@ where
|
|||
|
||||
match proto {
|
||||
Protocol::Http2 => HttpServiceHandlerResponse {
|
||||
state: State::H2Handshake(Some((
|
||||
h2_handshake(io),
|
||||
self.cfg.clone(),
|
||||
self.flow.clone(),
|
||||
on_connect_data,
|
||||
peer_addr,
|
||||
))),
|
||||
},
|
||||
|
||||
Protocol::Http1 => HttpServiceHandlerResponse {
|
||||
state: State::H1(h1::Dispatcher::new(
|
||||
io,
|
||||
state: State::H2Handshake {
|
||||
handshake: Some((
|
||||
h2::handshake_with_timeout(io, &self.cfg),
|
||||
self.cfg.clone(),
|
||||
self.flow.clone(),
|
||||
on_connect_data,
|
||||
peer_addr,
|
||||
)),
|
||||
},
|
||||
},
|
||||
|
||||
Protocol::Http1 => HttpServiceHandlerResponse {
|
||||
state: State::H1 {
|
||||
dispatcher: h1::Dispatcher::new(
|
||||
io,
|
||||
self.cfg.clone(),
|
||||
self.flow.clone(),
|
||||
on_connect_data,
|
||||
peer_addr,
|
||||
),
|
||||
},
|
||||
},
|
||||
|
||||
proto => unimplemented!("Unsupported HTTP version: {:?}.", proto),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = StateProj)]
|
||||
pin_project! {
|
||||
#[project = StateProj]
|
||||
enum State<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
T: AsyncRead,
|
||||
T: AsyncWrite,
|
||||
T: Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Future: 'static,
|
||||
S::Error: Into<Response<AnyBody>>,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
{
|
||||
H1(#[pin] h1::Dispatcher<T, S, B, X, U>),
|
||||
H2(#[pin] h2::Dispatcher<T, S, B, X, U>),
|
||||
H2Handshake(
|
||||
Option<(
|
||||
H2Handshake<T, Bytes>,
|
||||
H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> },
|
||||
H2 { #[pin] dispatcher: h2::Dispatcher<T, S, B, X, U> },
|
||||
H2Handshake {
|
||||
handshake: Option<(
|
||||
h2::HandshakeWithTimeout<T>,
|
||||
ServiceConfig,
|
||||
Rc<HttpFlow<S, X, U>>,
|
||||
OnConnectData,
|
||||
Option<net::SocketAddr>,
|
||||
)>,
|
||||
),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project]
|
||||
pin_project! {
|
||||
pub struct HttpServiceHandlerResponse<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
T: AsyncRead,
|
||||
T: AsyncWrite,
|
||||
T: Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>>,
|
||||
S::Error: 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
S::Response: Into<Response<B>>,
|
||||
S::Response: 'static,
|
||||
|
||||
B: MessageBody,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -586,21 +599,21 @@ where
|
|||
#[pin]
|
||||
state: State<T, S, B, X, U>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin,
|
||||
|
||||
S: Service<Request>,
|
||||
S::Error: Into<Response<AnyBody>> + 'static,
|
||||
S::Error: Into<Response<BoxBody>> + 'static,
|
||||
S::Future: 'static,
|
||||
S::Response: Into<Response<B>> + 'static,
|
||||
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
|
||||
X: Service<Request, Response = Request>,
|
||||
X::Error: Into<Response<AnyBody>>,
|
||||
X::Error: Into<Response<BoxBody>>,
|
||||
|
||||
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
|
||||
U::Error: fmt::Display,
|
||||
|
|
@ -609,27 +622,29 @@ where
|
|||
|
||||
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.as_mut().project().state.project() {
|
||||
StateProj::H1(disp) => disp.poll(cx),
|
||||
StateProj::H2(disp) => disp.poll(cx),
|
||||
StateProj::H2Handshake(data) => {
|
||||
StateProj::H1 { dispatcher } => dispatcher.poll(cx),
|
||||
StateProj::H2 { dispatcher } => dispatcher.poll(cx),
|
||||
StateProj::H2Handshake { handshake: data } => {
|
||||
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
|
||||
Ok(conn) => {
|
||||
let (_, cfg, srv, on_connect_data, peer_addr) =
|
||||
Ok((conn, timer)) => {
|
||||
let (_, config, flow, on_connect_data, peer_addr) =
|
||||
data.take().unwrap();
|
||||
self.as_mut().project().state.set(State::H2(
|
||||
h2::Dispatcher::new(
|
||||
srv,
|
||||
|
||||
self.as_mut().project().state.set(State::H2 {
|
||||
dispatcher: h2::Dispatcher::new(
|
||||
flow,
|
||||
conn,
|
||||
on_connect_data,
|
||||
cfg,
|
||||
config,
|
||||
peer_addr,
|
||||
timer,
|
||||
),
|
||||
));
|
||||
});
|
||||
self.poll(cx)
|
||||
}
|
||||
Err(err) => {
|
||||
trace!("H2 handshake error: {}", err);
|
||||
Poll::Ready(Err(err.into()))
|
||||
Poll::Ready(Err(err))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -63,8 +63,8 @@ pub enum Item {
|
|||
Last(Bytes),
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
/// WebSocket protocol codec.
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Codec {
|
||||
flags: Flags,
|
||||
max_size: usize,
|
||||
|
|
@ -89,7 +89,8 @@ impl Codec {
|
|||
|
||||
/// Set max frame size.
|
||||
///
|
||||
/// By default max size is set to 64kB.
|
||||
/// By default max size is set to 64KiB.
|
||||
#[must_use = "This returns the a new Codec, without modifying the original."]
|
||||
pub fn max_size(mut self, size: usize) -> Self {
|
||||
self.max_size = size;
|
||||
self
|
||||
|
|
@ -98,12 +99,19 @@ impl Codec {
|
|||
/// Set decoder to client mode.
|
||||
///
|
||||
/// By default decoder works in server mode.
|
||||
#[must_use = "This returns the a new Codec, without modifying the original."]
|
||||
pub fn client_mode(mut self) -> Self {
|
||||
self.flags.remove(Flags::SERVER);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Codec {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl Encoder<Message> for Codec {
|
||||
type Error = ProtocolError;
|
||||
|
||||
|
|
|
|||
|
|
@ -4,18 +4,22 @@ use std::task::{Context, Poll};
|
|||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_service::{IntoService, Service};
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use super::{Codec, Frame, Message};
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pin_project! {
|
||||
pub struct Dispatcher<S, T>
|
||||
where
|
||||
S: Service<Frame, Response = Message> + 'static,
|
||||
T: AsyncRead + AsyncWrite,
|
||||
S: Service<Frame, Response = Message>,
|
||||
S: 'static,
|
||||
T: AsyncRead,
|
||||
T: AsyncWrite,
|
||||
{
|
||||
#[pin]
|
||||
inner: inner::Dispatcher<S, T, Codec, Message>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> Dispatcher<S, T>
|
||||
where
|
||||
|
|
@ -72,7 +76,7 @@ mod inner {
|
|||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
|
||||
|
||||
use crate::{body::AnyBody, Response};
|
||||
use crate::{body::BoxBody, Response};
|
||||
|
||||
/// Framed transport errors
|
||||
pub enum DispatcherError<E, U, I>
|
||||
|
|
@ -136,7 +140,7 @@ mod inner {
|
|||
}
|
||||
}
|
||||
|
||||
impl<E, U, I> From<DispatcherError<E, U, I>> for Response<AnyBody>
|
||||
impl<E, U, I> From<DispatcherError<E, U, I>> for Response<BoxBody>
|
||||
where
|
||||
E: fmt::Debug + fmt::Display,
|
||||
U: Encoder<I> + Decoder,
|
||||
|
|
@ -144,7 +148,7 @@ mod inner {
|
|||
<U as Decoder>::Error: fmt::Debug,
|
||||
{
|
||||
fn from(err: DispatcherError<E, U, I>) -> Self {
|
||||
Response::internal_server_error().set_body(AnyBody::from(err.to_string()))
|
||||
Response::internal_server_error().set_body(BoxBody::new(err.to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -8,9 +8,9 @@ use std::io;
|
|||
use derive_more::{Display, Error, From};
|
||||
use http::{header, Method, StatusCode};
|
||||
|
||||
use crate::body::BoxBody;
|
||||
use crate::{
|
||||
body::AnyBody, header::HeaderValue, message::RequestHead, response::Response,
|
||||
ResponseBuilder,
|
||||
header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder,
|
||||
};
|
||||
|
||||
mod codec;
|
||||
|
|
@ -69,7 +69,7 @@ pub enum ProtocolError {
|
|||
}
|
||||
|
||||
/// WebSocket handshake errors
|
||||
#[derive(Debug, PartialEq, Display, Error)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Display, Error)]
|
||||
pub enum HandshakeError {
|
||||
/// Only get method is allowed.
|
||||
#[display(fmt = "Method not allowed.")]
|
||||
|
|
@ -96,8 +96,8 @@ pub enum HandshakeError {
|
|||
BadWebsocketKey,
|
||||
}
|
||||
|
||||
impl From<&HandshakeError> for Response<AnyBody> {
|
||||
fn from(err: &HandshakeError) -> Self {
|
||||
impl From<HandshakeError> for Response<BoxBody> {
|
||||
fn from(err: HandshakeError) -> Self {
|
||||
match err {
|
||||
HandshakeError::GetMethodRequired => {
|
||||
let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
|
||||
|
|
@ -139,9 +139,9 @@ impl From<&HandshakeError> for Response<AnyBody> {
|
|||
}
|
||||
}
|
||||
|
||||
impl From<HandshakeError> for Response<AnyBody> {
|
||||
fn from(err: HandshakeError) -> Self {
|
||||
(&err).into()
|
||||
impl From<&HandshakeError> for Response<BoxBody> {
|
||||
fn from(err: &HandshakeError) -> Self {
|
||||
(*err).into()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -210,7 +210,6 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
|
|||
|
||||
Response::build(StatusCode::SWITCHING_PROTOCOLS)
|
||||
.upgrade("websocket")
|
||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||
.insert_header((
|
||||
header::SEC_WEBSOCKET_ACCEPT,
|
||||
// key is known to be header value safe ascii
|
||||
|
|
@ -221,9 +220,10 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{header, Method};
|
||||
|
||||
use super::*;
|
||||
use crate::{body::AnyBody, test::TestRequest};
|
||||
use http::{header, Method};
|
||||
use crate::test::TestRequest;
|
||||
|
||||
#[test]
|
||||
fn test_handshake() {
|
||||
|
|
@ -337,17 +337,17 @@ mod tests {
|
|||
|
||||
#[test]
|
||||
fn test_ws_error_http_response() {
|
||||
let resp: Response<AnyBody> = HandshakeError::GetMethodRequired.into();
|
||||
let resp: Response<BoxBody> = HandshakeError::GetMethodRequired.into();
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
let resp: Response<AnyBody> = HandshakeError::NoWebsocketUpgrade.into();
|
||||
let resp: Response<BoxBody> = HandshakeError::NoWebsocketUpgrade.into();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: Response<AnyBody> = HandshakeError::NoConnectionUpgrade.into();
|
||||
let resp: Response<BoxBody> = HandshakeError::NoConnectionUpgrade.into();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: Response<AnyBody> = HandshakeError::NoVersionHeader.into();
|
||||
let resp: Response<BoxBody> = HandshakeError::NoVersionHeader.into();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: Response<AnyBody> = HandshakeError::UnsupportedVersion.into();
|
||||
let resp: Response<BoxBody> = HandshakeError::UnsupportedVersion.into();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: Response<AnyBody> = HandshakeError::BadWebsocketKey.into();
|
||||
let resp: Response<BoxBody> = HandshakeError::BadWebsocketKey.into();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,9 @@ use std::{
|
|||
fmt,
|
||||
};
|
||||
|
||||
/// Operation codes as part of RFC6455.
|
||||
/// Operation codes defined in [RFC 6455 §11.8].
|
||||
///
|
||||
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
|
||||
#[derive(Debug, Eq, PartialEq, Clone, Copy)]
|
||||
pub enum OpCode {
|
||||
/// Indicates a continuation frame of a fragmented message.
|
||||
|
|
@ -220,7 +222,8 @@ impl<T: Into<String>> From<(CloseCode, T)> for CloseReason {
|
|||
}
|
||||
}
|
||||
|
||||
/// The WebSocket GUID as stated in the spec. See https://tools.ietf.org/html/rfc6455#section-1.3.
|
||||
/// The WebSocket GUID as stated in the spec.
|
||||
/// See <https://datatracker.ietf.org/doc/html/rfc6455#section-1.3>.
|
||||
static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
|
||||
|
||||
/// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec.
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
use std::convert::Infallible;
|
||||
|
||||
use actix_http::{
|
||||
body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response,
|
||||
body::BoxBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response,
|
||||
};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::ServiceFactoryExt;
|
||||
|
|
@ -99,7 +99,7 @@ async fn test_with_query_parameter() {
|
|||
#[display(fmt = "expect failed")]
|
||||
struct ExpectFailed;
|
||||
|
||||
impl From<ExpectFailed> for Response<AnyBody> {
|
||||
impl From<ExpectFailed> for Response<BoxBody> {
|
||||
fn from(_: ExpectFailed) -> Self {
|
||||
Response::new(StatusCode::EXPECTATION_FAILED)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -0,0 +1,153 @@
|
|||
use std::io;
|
||||
|
||||
use actix_http::{error::Error, HttpService, Response};
|
||||
use actix_server::Server;
|
||||
use tokio::io::AsyncWriteExt;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_ping_pong() -> io::Result<()> {
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel(1);
|
||||
|
||||
let lst = std::net::TcpListener::bind("127.0.0.1:0")?;
|
||||
|
||||
let addr = lst.local_addr().unwrap();
|
||||
|
||||
let join = std::thread::spawn(move || {
|
||||
actix_rt::System::new().block_on(async move {
|
||||
let srv = Server::build()
|
||||
.disable_signals()
|
||||
.workers(1)
|
||||
.listen("h2_ping_pong", lst, || {
|
||||
HttpService::build()
|
||||
.keep_alive(3)
|
||||
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
|
||||
.tcp()
|
||||
})?
|
||||
.run();
|
||||
|
||||
tx.send(srv.handle()).unwrap();
|
||||
|
||||
srv.await
|
||||
})
|
||||
});
|
||||
|
||||
let handle = rx.recv().unwrap();
|
||||
|
||||
let (sync_tx, rx) = std::sync::mpsc::sync_channel(1);
|
||||
|
||||
// use a separate thread for h2 client so it can be blocked.
|
||||
std::thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
let (mut tx, conn) = h2::client::handshake(stream).await.unwrap();
|
||||
|
||||
tokio::spawn(async move { conn.await.unwrap() });
|
||||
|
||||
let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap();
|
||||
let res = res.await.unwrap();
|
||||
|
||||
assert_eq!(res.status().as_u16(), 200);
|
||||
|
||||
sync_tx.send(()).unwrap();
|
||||
|
||||
// intentionally block the client thread so it can not answer ping pong.
|
||||
std::thread::sleep(std::time::Duration::from_secs(1000));
|
||||
})
|
||||
});
|
||||
|
||||
rx.recv().unwrap();
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
// stop server gracefully. this step would take up to 30 seconds.
|
||||
handle.stop(true).await;
|
||||
|
||||
// join server thread. only when connection are all gone this step would finish.
|
||||
join.join().unwrap()?;
|
||||
|
||||
// check the time used for join server thread so it's known that the server shutdown
|
||||
// is from keep alive and not server graceful shutdown timeout.
|
||||
assert!(now.elapsed() < std::time::Duration::from_secs(30));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2_handshake_timeout() -> io::Result<()> {
|
||||
let (tx, rx) = std::sync::mpsc::sync_channel(1);
|
||||
|
||||
let lst = std::net::TcpListener::bind("127.0.0.1:0")?;
|
||||
|
||||
let addr = lst.local_addr().unwrap();
|
||||
|
||||
let join = std::thread::spawn(move || {
|
||||
actix_rt::System::new().block_on(async move {
|
||||
let srv = Server::build()
|
||||
.disable_signals()
|
||||
.workers(1)
|
||||
.listen("h2_ping_pong", lst, || {
|
||||
HttpService::build()
|
||||
.keep_alive(30)
|
||||
// set first request timeout to 5 seconds.
|
||||
// this is the timeout used for http2 handshake.
|
||||
.client_timeout(5000)
|
||||
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
|
||||
.tcp()
|
||||
})?
|
||||
.run();
|
||||
|
||||
tx.send(srv.handle()).unwrap();
|
||||
|
||||
srv.await
|
||||
})
|
||||
});
|
||||
|
||||
let handle = rx.recv().unwrap();
|
||||
|
||||
let (sync_tx, rx) = std::sync::mpsc::sync_channel(1);
|
||||
|
||||
// use a separate thread for tcp client so it can be blocked.
|
||||
std::thread::spawn(move || {
|
||||
tokio::runtime::Builder::new_current_thread()
|
||||
.enable_all()
|
||||
.build()
|
||||
.unwrap()
|
||||
.block_on(async move {
|
||||
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
|
||||
|
||||
// do not send the last new line intentionally.
|
||||
// This should hang the server handshake
|
||||
let malicious_buf = b"PRI * HTTP/2.0\r\n\r\nSM\r\n";
|
||||
stream.write_all(malicious_buf).await.unwrap();
|
||||
stream.flush().await.unwrap();
|
||||
|
||||
sync_tx.send(()).unwrap();
|
||||
|
||||
// intentionally block the client thread so it sit idle and not do handshake.
|
||||
std::thread::sleep(std::time::Duration::from_secs(1000));
|
||||
|
||||
drop(stream)
|
||||
})
|
||||
});
|
||||
|
||||
rx.recv().unwrap();
|
||||
|
||||
let now = std::time::Instant::now();
|
||||
|
||||
// stop server gracefully. this step would take up to 30 seconds.
|
||||
handle.stop(true).await;
|
||||
|
||||
// join server thread. only when connection are all gone this step would finish.
|
||||
join.join().unwrap()?;
|
||||
|
||||
// check the time used for join server thread so it's known that the server shutdown
|
||||
// is from handshake timeout and not server graceful shutdown timeout.
|
||||
assert!(now.elapsed() < std::time::Duration::from_secs(30));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
@ -5,10 +5,10 @@ extern crate tls_openssl as openssl;
|
|||
use std::{convert::Infallible, io};
|
||||
|
||||
use actix_http::{
|
||||
body::{AnyBody, Body, SizedStream},
|
||||
body::{BodyStream, BoxBody, SizedStream},
|
||||
error::PayloadError,
|
||||
http::{
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
header::{self, HeaderValue},
|
||||
Method, StatusCode, Version,
|
||||
},
|
||||
Error, HttpMessage, HttpService, Request, Response,
|
||||
|
|
@ -143,38 +143,25 @@ async fn test_h2_content_length() {
|
|||
})
|
||||
.await;
|
||||
|
||||
let header = HeaderName::from_static("content-length");
|
||||
let value = HeaderValue::from_static("0");
|
||||
static VALUE: HeaderValue = HeaderValue::from_static("0");
|
||||
|
||||
{
|
||||
for &i in &[0] {
|
||||
let req = srv
|
||||
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
|
||||
.send();
|
||||
let _response = req.await.expect_err("should timeout on recv 1xx frame");
|
||||
// assert_eq!(response.headers().get(&header), None);
|
||||
let req = srv.request(Method::HEAD, srv.surl("/0")).send();
|
||||
req.await.expect_err("should timeout on recv 1xx frame");
|
||||
|
||||
let req = srv
|
||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||
.send();
|
||||
let _response = req.await.expect_err("should timeout on recv 1xx frame");
|
||||
// assert_eq!(response.headers().get(&header), None);
|
||||
}
|
||||
let req = srv.request(Method::GET, srv.surl("/0")).send();
|
||||
req.await.expect_err("should timeout on recv 1xx frame");
|
||||
|
||||
for &i in &[1] {
|
||||
let req = srv
|
||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||
.send();
|
||||
let req = srv.request(Method::GET, srv.surl("/1")).send();
|
||||
let response = req.await.unwrap();
|
||||
assert_eq!(response.headers().get(&header), None);
|
||||
}
|
||||
assert!(response.headers().get("content-length").is_none());
|
||||
|
||||
for &i in &[2, 3] {
|
||||
let req = srv
|
||||
.request(Method::GET, srv.surl(&format!("/{}", i)))
|
||||
.send();
|
||||
let response = req.await.unwrap();
|
||||
assert_eq!(response.headers().get(&header), Some(&value));
|
||||
assert_eq!(response.headers().get("content-length"), Some(&VALUE));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -361,7 +348,7 @@ async fn test_h2_body_chunked_explicit() {
|
|||
ok::<_, Infallible>(
|
||||
Response::build(StatusCode::OK)
|
||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||
.streaming(body),
|
||||
.body(BodyStream::new(body)),
|
||||
)
|
||||
})
|
||||
.openssl(tls_config())
|
||||
|
|
@ -412,9 +399,11 @@ async fn test_h2_response_http_error_handling() {
|
|||
#[display(fmt = "error")]
|
||||
struct BadRequest;
|
||||
|
||||
impl From<BadRequest> for Response<AnyBody> {
|
||||
impl From<BadRequest> for Response<BoxBody> {
|
||||
fn from(err: BadRequest) -> Self {
|
||||
Response::build(StatusCode::BAD_REQUEST).body(err.to_string())
|
||||
Response::build(StatusCode::BAD_REQUEST)
|
||||
.body(err.to_string())
|
||||
.map_into_boxed_body()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -422,7 +411,7 @@ impl From<BadRequest> for Response<AnyBody> {
|
|||
async fn test_h2_service_error() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| err::<Response<Body>, _>(BadRequest))
|
||||
.h2(|_| err::<Response<BoxBody>, _>(BadRequest))
|
||||
.openssl(tls_config())
|
||||
.map_err(|_| ())
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ use std::{
|
|||
};
|
||||
|
||||
use actix_http::{
|
||||
body::{AnyBody, Body, SizedStream},
|
||||
body::{BodyStream, BoxBody, SizedStream},
|
||||
error::PayloadError,
|
||||
http::{
|
||||
header::{self, HeaderName, HeaderValue},
|
||||
|
|
@ -20,16 +20,13 @@ use actix_http::{
|
|||
};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::{fn_factory_with_config, fn_service};
|
||||
use actix_tls::connect::tls::rustls::webpki_roots_cert_store;
|
||||
use actix_tls::connect::rustls::webpki_roots_cert_store;
|
||||
use actix_utils::future::{err, ok};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use derive_more::{Display, Error};
|
||||
use futures_core::Stream;
|
||||
use futures_util::stream::{once, StreamExt as _};
|
||||
use rustls::{
|
||||
Certificate, OwnedTrustAnchor, PrivateKey, RootCertStore,
|
||||
ServerConfig as RustlsServerConfig, ServerName,
|
||||
};
|
||||
use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
|
||||
use rustls_pemfile::{certs, pkcs8_private_keys};
|
||||
|
||||
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
|
||||
|
|
@ -419,7 +416,7 @@ async fn test_h2_body_chunked_explicit() {
|
|||
ok::<_, Infallible>(
|
||||
Response::build(StatusCode::OK)
|
||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||
.streaming(body),
|
||||
.body(BodyStream::new(body)),
|
||||
)
|
||||
})
|
||||
.rustls(tls_config())
|
||||
|
|
@ -470,9 +467,9 @@ async fn test_h2_response_http_error_handling() {
|
|||
#[display(fmt = "error")]
|
||||
struct BadRequest;
|
||||
|
||||
impl From<BadRequest> for Response<AnyBody> {
|
||||
impl From<BadRequest> for Response<BoxBody> {
|
||||
fn from(_: BadRequest) -> Self {
|
||||
Response::bad_request().set_body(AnyBody::from("error"))
|
||||
Response::bad_request().set_body(BoxBody::new("error"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -480,7 +477,7 @@ impl From<BadRequest> for Response<AnyBody> {
|
|||
async fn test_h2_service_error() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h2(|_| err::<Response<Body>, _>(BadRequest))
|
||||
.h2(|_| err::<Response<BoxBody>, _>(BadRequest))
|
||||
.rustls(tls_config())
|
||||
})
|
||||
.await;
|
||||
|
|
@ -497,7 +494,7 @@ async fn test_h2_service_error() {
|
|||
async fn test_h1_service_error() {
|
||||
let mut srv = test_server(move || {
|
||||
HttpService::build()
|
||||
.h1(|_| err::<Response<Body>, _>(BadRequest))
|
||||
.h1(|_| err::<Response<BoxBody>, _>(BadRequest))
|
||||
.rustls(tls_config())
|
||||
})
|
||||
.await;
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
};
|
||||
|
||||
use actix_http::{
|
||||
body::{AnyBody, Body, SizedStream},
|
||||
body::{self, BodyStream, BoxBody, SizedStream},
|
||||
header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response,
|
||||
StatusCode,
|
||||
};
|
||||
|
|
@ -24,7 +24,7 @@ use regex::Regex;
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.client_timeout(1000)
|
||||
|
|
@ -39,11 +39,13 @@ async fn test_h1() {
|
|||
|
||||
let response = srv.get("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_2() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.client_timeout(1000)
|
||||
|
|
@ -59,13 +61,15 @@ async fn test_h1_2() {
|
|||
|
||||
let response = srv.get("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "expect failed")]
|
||||
struct ExpectFailed;
|
||||
|
||||
impl From<ExpectFailed> for Response<AnyBody> {
|
||||
impl From<ExpectFailed> for Response<BoxBody> {
|
||||
fn from(_: ExpectFailed) -> Self {
|
||||
Response::new(StatusCode::EXPECTATION_FAILED)
|
||||
}
|
||||
|
|
@ -73,7 +77,7 @@ impl From<ExpectFailed> for Response<AnyBody> {
|
|||
|
||||
#[actix_rt::test]
|
||||
async fn test_expect_continue() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.expect(fn_service(|req: Request| {
|
||||
if req.head().uri.query() == Some("yes=") {
|
||||
|
|
@ -98,11 +102,13 @@ async fn test_expect_continue() {
|
|||
let mut data = String::new();
|
||||
let _ = stream.read_to_string(&mut data);
|
||||
assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n"));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_expect_continue_h1() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.expect(fn_service(|req: Request| {
|
||||
sleep(Duration::from_millis(20)).then(move |_| {
|
||||
|
|
@ -129,6 +135,8 @@ async fn test_expect_continue_h1() {
|
|||
let mut data = String::new();
|
||||
let _ = stream.read_to_string(&mut data);
|
||||
assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n"));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -136,7 +144,7 @@ async fn test_chunked_payload() {
|
|||
let chunk_sizes = vec![32768, 32, 32768];
|
||||
let total_size: usize = chunk_sizes.iter().sum();
|
||||
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(fn_service(|mut request: Request| {
|
||||
request
|
||||
|
|
@ -188,11 +196,13 @@ async fn test_chunked_payload() {
|
|||
};
|
||||
|
||||
assert_eq!(returned_size, total_size);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_slow_request() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.client_timeout(100)
|
||||
.finish(|_| ok::<_, Infallible>(Response::ok()))
|
||||
|
|
@ -205,11 +215,13 @@ async fn test_slow_request() {
|
|||
let mut data = String::new();
|
||||
let _ = stream.read_to_string(&mut data);
|
||||
assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_malformed_request() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
|
|
@ -221,11 +233,13 @@ async fn test_http1_malformed_request() {
|
|||
let mut data = String::new();
|
||||
let _ = stream.read_to_string(&mut data);
|
||||
assert!(data.starts_with("HTTP/1.1 400 Bad Request"));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
|
|
@ -242,11 +256,13 @@ async fn test_http1_keepalive() {
|
|||
let mut data = vec![0; 1024];
|
||||
let _ = stream.read(&mut data);
|
||||
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_timeout() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(1)
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
|
|
@ -264,11 +280,13 @@ async fn test_http1_keepalive_timeout() {
|
|||
let mut data = vec![0; 1024];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_close() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
|
|
@ -285,11 +303,13 @@ async fn test_http1_keepalive_close() {
|
|||
let mut data = vec![0; 1024];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http10_keepalive_default_close() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
|
|
@ -305,11 +325,13 @@ async fn test_http10_keepalive_default_close() {
|
|||
let mut data = vec![0; 1024];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http10_keepalive() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
.tcp()
|
||||
|
|
@ -332,11 +354,13 @@ async fn test_http10_keepalive() {
|
|||
let mut data = vec![0; 1024];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_http1_keepalive_disabled() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.keep_alive(KeepAlive::Disabled)
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok()))
|
||||
|
|
@ -353,6 +377,8 @@ async fn test_http1_keepalive_disabled() {
|
|||
let mut data = vec![0; 1024];
|
||||
let res = stream.read(&mut data).unwrap();
|
||||
assert_eq!(res, 0);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -362,7 +388,7 @@ async fn test_content_length() {
|
|||
StatusCode,
|
||||
};
|
||||
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|req: Request| {
|
||||
let indx: usize = req.uri().path()[1..].parse().unwrap();
|
||||
|
|
@ -400,6 +426,8 @@ async fn test_content_length() {
|
|||
assert_eq!(response.headers().get(&header), Some(&value));
|
||||
}
|
||||
}
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -439,6 +467,8 @@ async fn test_h1_headers() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from(data2));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
|
||||
|
|
@ -478,6 +508,8 @@ async fn test_h1_body() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -503,6 +535,8 @@ async fn test_h1_head_empty() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -528,11 +562,13 @@ async fn test_h1_head_binary() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert!(bytes.is_empty());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_head_binary2() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
|
||||
.tcp()
|
||||
|
|
@ -549,6 +585,8 @@ async fn test_h1_head_binary2() {
|
|||
.unwrap();
|
||||
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
|
||||
}
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -571,6 +609,8 @@ async fn test_h1_body_length() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -582,7 +622,7 @@ async fn test_h1_body_chunked_explicit() {
|
|||
ok::<_, Infallible>(
|
||||
Response::build(StatusCode::OK)
|
||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||
.streaming(body),
|
||||
.body(BodyStream::new(body)),
|
||||
)
|
||||
})
|
||||
.tcp()
|
||||
|
|
@ -606,6 +646,8 @@ async fn test_h1_body_chunked_explicit() {
|
|||
|
||||
// decode
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -614,7 +656,9 @@ async fn test_h1_body_chunked_implicit() {
|
|||
HttpService::build()
|
||||
.h1(|_| {
|
||||
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
|
||||
ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body))
|
||||
ok::<_, Infallible>(
|
||||
Response::build(StatusCode::OK).body(BodyStream::new(body)),
|
||||
)
|
||||
})
|
||||
.tcp()
|
||||
})
|
||||
|
|
@ -635,6 +679,8 @@ async fn test_h1_body_chunked_implicit() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
|
|
@ -662,15 +708,17 @@ async fn test_h1_response_http_error_handling() {
|
|||
bytes,
|
||||
Bytes::from_static(b"error processing HTTP: failed to parse header value")
|
||||
);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[derive(Debug, Display, Error)]
|
||||
#[display(fmt = "error")]
|
||||
struct BadRequest;
|
||||
|
||||
impl From<BadRequest> for Response<AnyBody> {
|
||||
impl From<BadRequest> for Response<BoxBody> {
|
||||
fn from(_: BadRequest) -> Self {
|
||||
Response::bad_request().set_body(AnyBody::from("error"))
|
||||
Response::bad_request().set_body(BoxBody::new("error"))
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -678,7 +726,7 @@ impl From<BadRequest> for Response<AnyBody> {
|
|||
async fn test_h1_service_error() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|_| err::<Response<Body>, _>(BadRequest))
|
||||
.h1(|_| err::<Response<()>, _>(BadRequest))
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
|
@ -689,11 +737,13 @@ async fn test_h1_service_error() {
|
|||
// read response
|
||||
let bytes = srv.load_body(response).await.unwrap();
|
||||
assert_eq!(bytes, Bytes::from_static(b"error"));
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h1_on_connect() {
|
||||
let srv = test_server(|| {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.on_connect_ext(|_, data| {
|
||||
data.insert(20isize);
|
||||
|
|
@ -708,4 +758,92 @@ async fn test_h1_on_connect() {
|
|||
|
||||
let response = srv.get("/").send().await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
||||
/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1.
|
||||
/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
|
||||
#[actix_rt::test]
|
||||
async fn test_not_modified_spec_h1() {
|
||||
// TODO: this test needing a few seconds to complete reveals some weirdness with either the
|
||||
// dispatcher or the client, though similar hangs occur on other tests in this file, only
|
||||
// succeeding, it seems, because of the keepalive timer
|
||||
|
||||
static CL: header::HeaderName = header::CONTENT_LENGTH;
|
||||
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h1(|req: Request| {
|
||||
let res: Response<BoxBody> = match req.path() {
|
||||
// with no content-length
|
||||
"/none" => {
|
||||
Response::with_body(StatusCode::NOT_MODIFIED, body::None::new())
|
||||
.map_into_boxed_body()
|
||||
}
|
||||
|
||||
// with no content-length
|
||||
"/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234")
|
||||
.map_into_boxed_body(),
|
||||
|
||||
// with manual content-length header and specific None body
|
||||
"/cl-none" => {
|
||||
let mut res = Response::with_body(
|
||||
StatusCode::NOT_MODIFIED,
|
||||
body::None::new(),
|
||||
);
|
||||
res.headers_mut()
|
||||
.insert(CL.clone(), header::HeaderValue::from_static("24"));
|
||||
res.map_into_boxed_body()
|
||||
}
|
||||
|
||||
// with manual content-length header and ignore-able body
|
||||
"/cl-body" => {
|
||||
let mut res =
|
||||
Response::with_body(StatusCode::NOT_MODIFIED, "1234");
|
||||
res.headers_mut()
|
||||
.insert(CL.clone(), header::HeaderValue::from_static("4"));
|
||||
res.map_into_boxed_body()
|
||||
}
|
||||
|
||||
_ => panic!("unknown route"),
|
||||
};
|
||||
|
||||
ok::<_, Infallible>(res)
|
||||
})
|
||||
.tcp()
|
||||
})
|
||||
.await;
|
||||
|
||||
let res = srv.get("/none").send().await.unwrap();
|
||||
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
|
||||
assert_eq!(res.headers().get(&CL), None);
|
||||
assert!(srv.load_body(res).await.unwrap().is_empty());
|
||||
|
||||
let res = srv.get("/body").send().await.unwrap();
|
||||
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
|
||||
assert_eq!(res.headers().get(&CL), None);
|
||||
assert!(srv.load_body(res).await.unwrap().is_empty());
|
||||
|
||||
let res = srv.get("/cl-none").send().await.unwrap();
|
||||
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
|
||||
assert_eq!(
|
||||
res.headers().get(&CL),
|
||||
Some(&header::HeaderValue::from_static("24")),
|
||||
);
|
||||
assert!(srv.load_body(res).await.unwrap().is_empty());
|
||||
|
||||
let res = srv.get("/cl-body").send().await.unwrap();
|
||||
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
|
||||
assert_eq!(
|
||||
res.headers().get(&CL),
|
||||
Some(&header::HeaderValue::from_static("4")),
|
||||
);
|
||||
// server does not prevent payload from being sent but clients may choose not to read it
|
||||
// TODO: this is probably a bug, especially since CL header can differ in length from the body
|
||||
assert!(!srv.load_body(res).await.unwrap().is_empty());
|
||||
|
||||
// TODO: add stream response tests
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
use actix_http::{
|
||||
body::{AnyBody, BodySize},
|
||||
body::{BodySize, BoxBody},
|
||||
h1,
|
||||
ws::{self, CloseCode, Frame, Item, Message},
|
||||
Error, HttpService, Request, Response,
|
||||
|
|
@ -50,14 +50,14 @@ enum WsServiceError {
|
|||
Dispatcher,
|
||||
}
|
||||
|
||||
impl From<WsServiceError> for Response<AnyBody> {
|
||||
impl From<WsServiceError> for Response<BoxBody> {
|
||||
fn from(err: WsServiceError) -> Self {
|
||||
match err {
|
||||
WsServiceError::Http(err) => err.into(),
|
||||
WsServiceError::Ws(err) => err.into(),
|
||||
WsServiceError::Io(_err) => unreachable!(),
|
||||
WsServiceError::Dispatcher => Response::internal_server_error()
|
||||
.set_body(AnyBody::from(format!("{}", err))),
|
||||
.set_body(BoxBody::new(format!("{}", err))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,6 +3,23 @@
|
|||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.4.0-beta.9 - 2021-12-01
|
||||
* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
|
||||
|
||||
[#2463]: https://github.com/actix/actix-web/pull/2463
|
||||
|
||||
|
||||
## 0.4.0-beta.8 - 2021-11-22
|
||||
* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
|
||||
* Added `MultipartError::NoContentDisposition` variant. [#2451]
|
||||
* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
|
||||
* Added `Field::name` method for getting the field name. [#2451]
|
||||
* `MultipartError` now marks variants with inner errors as the source. [#2451]
|
||||
* `MultipartError` is now marked as non-exhaustive. [#2451]
|
||||
|
||||
[#2451]: https://github.com/actix/actix-web/pull/2451
|
||||
|
||||
|
||||
## 0.4.0-beta.7 - 2021-10-20
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-multipart"
|
||||
version = "0.4.0-beta.7"
|
||||
version = "0.4.0-beta.9"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Multipart form support for Actix Web"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
|
|
@ -14,13 +14,12 @@ name = "actix_multipart"
|
|||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.0.0-beta.10", default-features = false }
|
||||
actix-web = { version = "4.0.0-beta.11", default-features = false }
|
||||
actix-utils = "3.0.0"
|
||||
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
httparse = "1.3"
|
||||
local-waker = "0.1"
|
||||
log = "0.4"
|
||||
|
|
@ -29,6 +28,7 @@ twoway = "0.2"
|
|||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-http = "3.0.0-beta.14"
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
tokio-stream = "0.1"
|
||||
|
|
|
|||
|
|
@ -3,11 +3,11 @@
|
|||
> Multipart form support for Actix Web.
|
||||
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.7)
|
||||
[](https://docs.rs/actix-multipart/0.4.0-beta.9)
|
||||
[](https://blog.rust-lang.org/2021/05/06/Rust-1.52.0.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.7)
|
||||
[](https://deps.rs/crate/actix-multipart/0.4.0-beta.9)
|
||||
[](https://crates.io/crates/actix-multipart)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
|
|
|
|||
|
|
@ -2,39 +2,52 @@
|
|||
use actix_web::error::{ParseError, PayloadError};
|
||||
use actix_web::http::StatusCode;
|
||||
use actix_web::ResponseError;
|
||||
use derive_more::{Display, From};
|
||||
use derive_more::{Display, Error, From};
|
||||
|
||||
/// A set of errors that can occur during parsing multipart streams
|
||||
#[derive(Debug, Display, From)]
|
||||
#[non_exhaustive]
|
||||
#[derive(Debug, Display, From, Error)]
|
||||
pub enum MultipartError {
|
||||
/// Content-Disposition header is not found or is not equal to "form-data".
|
||||
///
|
||||
/// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a
|
||||
/// Content-Disposition header must always be present and equal to "form-data".
|
||||
#[display(fmt = "No Content-Disposition `form-data` header")]
|
||||
NoContentDisposition,
|
||||
|
||||
/// Content-Type header is not found
|
||||
#[display(fmt = "No Content-type header found")]
|
||||
#[display(fmt = "No Content-Type header found")]
|
||||
NoContentType,
|
||||
|
||||
/// Can not parse Content-Type header
|
||||
#[display(fmt = "Can not parse Content-Type header")]
|
||||
ParseContentType,
|
||||
|
||||
/// Multipart boundary is not found
|
||||
#[display(fmt = "Multipart boundary is not found")]
|
||||
Boundary,
|
||||
|
||||
/// Nested multipart is not supported
|
||||
#[display(fmt = "Nested multipart is not supported")]
|
||||
Nested,
|
||||
|
||||
/// Multipart stream is incomplete
|
||||
#[display(fmt = "Multipart stream is incomplete")]
|
||||
Incomplete,
|
||||
|
||||
/// Error during field parsing
|
||||
#[display(fmt = "{}", _0)]
|
||||
Parse(ParseError),
|
||||
|
||||
/// Payload error
|
||||
#[display(fmt = "{}", _0)]
|
||||
Payload(PayloadError),
|
||||
|
||||
/// Not consumed
|
||||
#[display(fmt = "Multipart stream is not consumed")]
|
||||
NotConsumed,
|
||||
}
|
||||
|
||||
impl std::error::Error for MultipartError {}
|
||||
|
||||
/// Return `BadRequest` for `MultipartError`
|
||||
impl ResponseError for MultipartError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
|
|
|
|||
|
|
@ -1,18 +1,22 @@
|
|||
//! Multipart response payload support.
|
||||
|
||||
use std::cell::{Cell, RefCell, RefMut};
|
||||
use std::convert::TryFrom;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{cmp, fmt};
|
||||
use std::{
|
||||
cell::{Cell, RefCell, RefMut},
|
||||
cmp,
|
||||
convert::TryFrom,
|
||||
fmt,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_web::error::{ParseError, PayloadError};
|
||||
use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue};
|
||||
use actix_web::{
|
||||
error::{ParseError, PayloadError},
|
||||
http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue},
|
||||
};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::stream::{LocalBoxStream, Stream};
|
||||
use futures_util::stream::StreamExt as _;
|
||||
use local_waker::LocalWaker;
|
||||
|
||||
use crate::error::MultipartError;
|
||||
|
|
@ -28,7 +32,7 @@ const MAX_HEADERS: usize = 32;
|
|||
pub struct Multipart {
|
||||
safety: Safety,
|
||||
error: Option<MultipartError>,
|
||||
inner: Option<Rc<RefCell<InnerMultipart>>>,
|
||||
inner: Option<InnerMultipart>,
|
||||
}
|
||||
|
||||
enum InnerMultipartItem {
|
||||
|
|
@ -40,10 +44,13 @@ enum InnerMultipartItem {
|
|||
enum InnerState {
|
||||
/// Stream eof
|
||||
Eof,
|
||||
|
||||
/// Skip data until first boundary
|
||||
FirstBoundary,
|
||||
|
||||
/// Reading boundary
|
||||
Boundary,
|
||||
|
||||
/// Reading Headers,
|
||||
Headers,
|
||||
}
|
||||
|
|
@ -59,7 +66,7 @@ impl Multipart {
|
|||
/// Create multipart instance for boundary.
|
||||
pub fn new<S>(headers: &HeaderMap, stream: S) -> Multipart
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
|
||||
{
|
||||
match Self::boundary(headers) {
|
||||
Ok(boundary) => Multipart::from_boundary(boundary, stream),
|
||||
|
|
@ -69,39 +76,32 @@ impl Multipart {
|
|||
|
||||
/// Extract boundary info from headers.
|
||||
pub(crate) fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
|
||||
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
|
||||
if let Ok(content_type) = content_type.to_str() {
|
||||
if let Ok(ct) = content_type.parse::<mime::Mime>() {
|
||||
if let Some(boundary) = ct.get_param(mime::BOUNDARY) {
|
||||
Ok(boundary.as_str().to_owned())
|
||||
} else {
|
||||
Err(MultipartError::Boundary)
|
||||
}
|
||||
} else {
|
||||
Err(MultipartError::ParseContentType)
|
||||
}
|
||||
} else {
|
||||
Err(MultipartError::ParseContentType)
|
||||
}
|
||||
} else {
|
||||
Err(MultipartError::NoContentType)
|
||||
}
|
||||
headers
|
||||
.get(&header::CONTENT_TYPE)
|
||||
.ok_or(MultipartError::NoContentType)?
|
||||
.to_str()
|
||||
.ok()
|
||||
.and_then(|content_type| content_type.parse::<mime::Mime>().ok())
|
||||
.ok_or(MultipartError::ParseContentType)?
|
||||
.get_param(mime::BOUNDARY)
|
||||
.map(|boundary| boundary.as_str().to_owned())
|
||||
.ok_or(MultipartError::Boundary)
|
||||
}
|
||||
|
||||
/// Create multipart instance for given boundary and stream
|
||||
pub(crate) fn from_boundary<S>(boundary: String, stream: S) -> Multipart
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static,
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
|
||||
{
|
||||
Multipart {
|
||||
error: None,
|
||||
safety: Safety::new(),
|
||||
inner: Some(Rc::new(RefCell::new(InnerMultipart {
|
||||
inner: Some(InnerMultipart {
|
||||
boundary,
|
||||
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))),
|
||||
payload: PayloadRef::new(PayloadBuffer::new(stream)),
|
||||
state: InnerState::FirstBoundary,
|
||||
item: InnerMultipartItem::None,
|
||||
}))),
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -118,20 +118,27 @@ impl Multipart {
|
|||
impl Stream for Multipart {
|
||||
type Item = Result<Field, MultipartError>;
|
||||
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if let Some(err) = self.error.take() {
|
||||
Poll::Ready(Some(Err(err)))
|
||||
} else if self.safety.current() {
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.get_mut();
|
||||
let mut inner = this.inner.as_mut().unwrap().borrow_mut();
|
||||
if let Some(mut payload) = inner.payload.get_mut(&this.safety) {
|
||||
payload.poll_stream(cx)?;
|
||||
}
|
||||
inner.poll(&this.safety, cx)
|
||||
} else if !self.safety.is_clean() {
|
||||
Poll::Ready(Some(Err(MultipartError::NotConsumed)))
|
||||
|
||||
match this.inner.as_mut() {
|
||||
Some(inner) => {
|
||||
if let Some(mut buffer) = inner.payload.get_mut(&this.safety) {
|
||||
// check safety and poll read payload to buffer.
|
||||
buffer.poll_stream(cx)?;
|
||||
} else if !this.safety.is_clean() {
|
||||
// safety violation
|
||||
return Poll::Ready(Some(Err(MultipartError::NotConsumed)));
|
||||
} else {
|
||||
Poll::Pending
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
inner.poll(&this.safety, cx)
|
||||
}
|
||||
None => Poll::Ready(Some(Err(this
|
||||
.error
|
||||
.take()
|
||||
.expect("Multipart polled after finish")))),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -152,17 +159,15 @@ impl InnerMultipart {
|
|||
Ok(httparse::Status::Complete((_, hdrs))) => {
|
||||
// convert headers
|
||||
let mut headers = HeaderMap::with_capacity(hdrs.len());
|
||||
|
||||
for h in hdrs {
|
||||
if let Ok(name) = HeaderName::try_from(h.name) {
|
||||
if let Ok(value) = HeaderValue::try_from(h.value) {
|
||||
let name =
|
||||
HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?;
|
||||
let value = HeaderValue::try_from(h.value)
|
||||
.map_err(|_| ParseError::Header)?;
|
||||
headers.append(name, value);
|
||||
} else {
|
||||
return Err(ParseError::Header.into());
|
||||
}
|
||||
} else {
|
||||
return Err(ParseError::Header.into());
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Some(headers))
|
||||
}
|
||||
Ok(httparse::Status::Partial) => Err(ParseError::Header.into()),
|
||||
|
|
@ -332,31 +337,55 @@ impl InnerMultipart {
|
|||
return Poll::Pending;
|
||||
};
|
||||
|
||||
// content type
|
||||
let mut mt = mime::APPLICATION_OCTET_STREAM;
|
||||
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
|
||||
if let Ok(content_type) = content_type.to_str() {
|
||||
if let Ok(ct) = content_type.parse::<mime::Mime>() {
|
||||
mt = ct;
|
||||
}
|
||||
}
|
||||
}
|
||||
// According to RFC 7578 §4.2, a Content-Disposition header must always be present and
|
||||
// set to "form-data".
|
||||
|
||||
let content_disposition = headers
|
||||
.get(&header::CONTENT_DISPOSITION)
|
||||
.and_then(|cd| ContentDisposition::from_raw(cd).ok())
|
||||
.filter(|content_disposition| {
|
||||
let is_form_data =
|
||||
content_disposition.disposition == header::DispositionType::FormData;
|
||||
|
||||
let has_field_name = content_disposition
|
||||
.parameters
|
||||
.iter()
|
||||
.any(|param| matches!(param, header::DispositionParam::Name(_)));
|
||||
|
||||
is_form_data && has_field_name
|
||||
});
|
||||
|
||||
let cd = if let Some(content_disposition) = content_disposition {
|
||||
content_disposition
|
||||
} else {
|
||||
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
|
||||
};
|
||||
|
||||
let ct: mime::Mime = headers
|
||||
.get(&header::CONTENT_TYPE)
|
||||
.and_then(|ct| ct.to_str().ok())
|
||||
.and_then(|ct| ct.parse().ok())
|
||||
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
|
||||
|
||||
self.state = InnerState::Boundary;
|
||||
|
||||
// nested multipart stream
|
||||
if mt.type_() == mime::MULTIPART {
|
||||
Poll::Ready(Some(Err(MultipartError::Nested)))
|
||||
} else {
|
||||
let field = Rc::new(RefCell::new(InnerField::new(
|
||||
self.payload.clone(),
|
||||
self.boundary.clone(),
|
||||
&headers,
|
||||
)?));
|
||||
// nested multipart stream is not supported
|
||||
if ct.type_() == mime::MULTIPART {
|
||||
return Poll::Ready(Some(Err(MultipartError::Nested)));
|
||||
}
|
||||
|
||||
let field =
|
||||
InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?;
|
||||
|
||||
self.item = InnerMultipartItem::Field(Rc::clone(&field));
|
||||
|
||||
Poll::Ready(Some(Ok(Field::new(safety.clone(cx), headers, mt, field))))
|
||||
}
|
||||
Poll::Ready(Some(Ok(Field::new(
|
||||
safety.clone(cx),
|
||||
headers,
|
||||
ct,
|
||||
cd,
|
||||
field,
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -371,6 +400,7 @@ impl Drop for InnerMultipart {
|
|||
/// A single field in a multipart stream
|
||||
pub struct Field {
|
||||
ct: mime::Mime,
|
||||
cd: ContentDisposition,
|
||||
headers: HeaderMap,
|
||||
inner: Rc<RefCell<InnerField>>,
|
||||
safety: Safety,
|
||||
|
|
@ -381,35 +411,52 @@ impl Field {
|
|||
safety: Safety,
|
||||
headers: HeaderMap,
|
||||
ct: mime::Mime,
|
||||
cd: ContentDisposition,
|
||||
inner: Rc<RefCell<InnerField>>,
|
||||
) -> Self {
|
||||
Field {
|
||||
ct,
|
||||
cd,
|
||||
headers,
|
||||
inner,
|
||||
safety,
|
||||
}
|
||||
}
|
||||
|
||||
/// Get a map of headers
|
||||
/// Returns a reference to the field's header map.
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get the content type of the field
|
||||
/// Returns a reference to the field's content (mime) type.
|
||||
pub fn content_type(&self) -> &mime::Mime {
|
||||
&self.ct
|
||||
}
|
||||
|
||||
/// Get the content disposition of the field, if it exists
|
||||
pub fn content_disposition(&self) -> Option<ContentDisposition> {
|
||||
// RFC 7578: 'Each part MUST contain a Content-Disposition header field
|
||||
// where the disposition type is "form-data".'
|
||||
if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) {
|
||||
ContentDisposition::from_raw(content_disposition).ok()
|
||||
} else {
|
||||
None
|
||||
/// Returns the field's Content-Disposition.
|
||||
///
|
||||
/// Per [RFC 7578 §4.2]: "Each part MUST contain a Content-Disposition header field where the
|
||||
/// disposition type is `form-data`. The Content-Disposition header field MUST also contain an
|
||||
/// additional parameter of `name`; the value of the `name` parameter is the original field name
|
||||
/// from the form."
|
||||
///
|
||||
/// This crate validates that it exists before returning a `Field`. As such, it is safe to
|
||||
/// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as
|
||||
/// a convenience.
|
||||
///
|
||||
/// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
|
||||
pub fn content_disposition(&self) -> &ContentDisposition {
|
||||
&self.cd
|
||||
}
|
||||
|
||||
/// Returns the field's name.
|
||||
///
|
||||
/// See [content_disposition](Self::content_disposition) regarding guarantees about existence of
|
||||
/// the name field.
|
||||
pub fn name(&self) -> &str {
|
||||
self.content_disposition()
|
||||
.get_name()
|
||||
.expect("field name should be guaranteed to exist in multipart form-data")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -417,17 +464,19 @@ impl Stream for Field {
|
|||
type Item = Result<Bytes, MultipartError>;
|
||||
|
||||
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
if self.safety.current() {
|
||||
let mut inner = self.inner.borrow_mut();
|
||||
if let Some(mut payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety) {
|
||||
payload.poll_stream(cx)?;
|
||||
}
|
||||
inner.poll(&self.safety)
|
||||
} else if !self.safety.is_clean() {
|
||||
Poll::Ready(Some(Err(MultipartError::NotConsumed)))
|
||||
let this = self.get_mut();
|
||||
let mut inner = this.inner.borrow_mut();
|
||||
if let Some(mut buffer) = inner.payload.as_ref().unwrap().get_mut(&this.safety) {
|
||||
// check safety and poll read payload to buffer.
|
||||
buffer.poll_stream(cx)?;
|
||||
} else if !this.safety.is_clean() {
|
||||
// safety violation
|
||||
return Poll::Ready(Some(Err(MultipartError::NotConsumed)));
|
||||
} else {
|
||||
Poll::Pending
|
||||
return Poll::Pending;
|
||||
}
|
||||
|
||||
inner.poll(&this.safety)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -451,20 +500,23 @@ struct InnerField {
|
|||
}
|
||||
|
||||
impl InnerField {
|
||||
fn new_in_rc(
|
||||
payload: PayloadRef,
|
||||
boundary: String,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<Rc<RefCell<InnerField>>, PayloadError> {
|
||||
Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this)))
|
||||
}
|
||||
|
||||
fn new(
|
||||
payload: PayloadRef,
|
||||
boundary: String,
|
||||
headers: &HeaderMap,
|
||||
) -> Result<InnerField, PayloadError> {
|
||||
let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) {
|
||||
if let Ok(s) = len.to_str() {
|
||||
if let Ok(len) = s.parse::<u64>() {
|
||||
Some(len)
|
||||
} else {
|
||||
return Err(PayloadError::Incomplete(None));
|
||||
}
|
||||
} else {
|
||||
return Err(PayloadError::Incomplete(None));
|
||||
match len.to_str().ok().and_then(|len| len.parse::<u64>().ok()) {
|
||||
Some(len) => Some(len),
|
||||
None => return Err(PayloadError::Incomplete(None)),
|
||||
}
|
||||
} else {
|
||||
None
|
||||
|
|
@ -638,10 +690,7 @@ impl PayloadRef {
|
|||
}
|
||||
}
|
||||
|
||||
fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<RefMut<'a, PayloadBuffer>>
|
||||
where
|
||||
'a: 'b,
|
||||
{
|
||||
fn get_mut(&self, s: &Safety) -> Option<RefMut<'_, PayloadBuffer>> {
|
||||
if s.current() {
|
||||
Some(self.payload.borrow_mut())
|
||||
} else {
|
||||
|
|
@ -658,9 +707,11 @@ impl Clone for PayloadRef {
|
|||
}
|
||||
}
|
||||
|
||||
/// Counter. It tracks of number of clones of payloads and give access to
|
||||
/// payload only to top most task panics if Safety get destroyed and it not top
|
||||
/// most task.
|
||||
/// Counter. It tracks of number of clones of payloads and give access to payload only to top most.
|
||||
/// * When dropped, parent task is awakened. This is to support the case where Field is
|
||||
/// dropped in a separate task than Multipart.
|
||||
/// * Assumes that parent owners don't move to different tasks; only the top-most is allowed to.
|
||||
/// * If dropped and is not top most owner, is_clean flag is set to false.
|
||||
#[derive(Debug)]
|
||||
struct Safety {
|
||||
task: LocalWaker,
|
||||
|
|
@ -703,15 +754,16 @@ impl Safety {
|
|||
|
||||
impl Drop for Safety {
|
||||
fn drop(&mut self) {
|
||||
// parent task is dead
|
||||
if Rc::strong_count(&self.payload) != self.level {
|
||||
self.clean.set(true);
|
||||
// Multipart dropped leaving a Field
|
||||
self.clean.set(false);
|
||||
}
|
||||
|
||||
self.task.wake();
|
||||
}
|
||||
}
|
||||
|
||||
/// Payload buffer
|
||||
/// Payload buffer.
|
||||
struct PayloadBuffer {
|
||||
eof: bool,
|
||||
buf: BytesMut,
|
||||
|
|
@ -719,7 +771,7 @@ struct PayloadBuffer {
|
|||
}
|
||||
|
||||
impl PayloadBuffer {
|
||||
/// Create new `PayloadBuffer` instance
|
||||
/// Constructs new `PayloadBuffer` instance.
|
||||
fn new<S>(stream: S) -> Self
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
|
||||
|
|
@ -727,7 +779,7 @@ impl PayloadBuffer {
|
|||
PayloadBuffer {
|
||||
eof: false,
|
||||
buf: BytesMut::new(),
|
||||
stream: stream.boxed_local(),
|
||||
stream: Box::pin(stream),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -767,7 +819,7 @@ impl PayloadBuffer {
|
|||
}
|
||||
|
||||
/// Read until specified ending
|
||||
pub fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
|
||||
fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
|
||||
let res = twoway::find_bytes(&self.buf, line)
|
||||
.map(|idx| self.buf.split_to(idx + line.len()).freeze());
|
||||
|
||||
|
|
@ -779,12 +831,12 @@ impl PayloadBuffer {
|
|||
}
|
||||
|
||||
/// Read bytes until new line delimiter
|
||||
pub fn readline(&mut self) -> Result<Option<Bytes>, MultipartError> {
|
||||
fn readline(&mut self) -> Result<Option<Bytes>, MultipartError> {
|
||||
self.read_until(b"\n")
|
||||
}
|
||||
|
||||
/// Read bytes until new line delimiter or eof
|
||||
pub fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> {
|
||||
fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> {
|
||||
match self.readline() {
|
||||
Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())),
|
||||
line => line,
|
||||
|
|
@ -792,7 +844,7 @@ impl PayloadBuffer {
|
|||
}
|
||||
|
||||
/// Put unprocessed data back to the buffer
|
||||
pub fn unprocessed(&mut self, data: Bytes) {
|
||||
fn unprocessed(&mut self, data: Bytes) {
|
||||
let buf = BytesMut::from(data.as_ref());
|
||||
let buf = std::mem::replace(&mut self.buf, buf);
|
||||
self.buf.extend_from_slice(&buf);
|
||||
|
|
@ -805,10 +857,12 @@ mod tests {
|
|||
|
||||
use actix_http::h1::Payload;
|
||||
use actix_web::http::header::{DispositionParam, DispositionType};
|
||||
use actix_web::rt;
|
||||
use actix_web::test::TestRequest;
|
||||
use actix_web::FromRequest;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::lazy;
|
||||
use futures_util::{future::lazy, StreamExt};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::mpsc;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
|
|
@ -914,6 +968,7 @@ mod tests {
|
|||
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
||||
test\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
||||
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
|
||||
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
||||
data\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
|
||||
|
|
@ -965,7 +1020,7 @@ mod tests {
|
|||
let mut multipart = Multipart::new(&headers, payload);
|
||||
match multipart.next().await {
|
||||
Some(Ok(mut field)) => {
|
||||
let cd = field.content_disposition().unwrap();
|
||||
let cd = field.content_disposition();
|
||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||
|
||||
|
|
@ -1027,7 +1082,7 @@ mod tests {
|
|||
let mut multipart = Multipart::new(&headers, payload);
|
||||
match multipart.next().await.unwrap() {
|
||||
Ok(mut field) => {
|
||||
let cd = field.content_disposition().unwrap();
|
||||
let cd = field.content_disposition();
|
||||
assert_eq!(cd.disposition, DispositionType::FormData);
|
||||
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
|
||||
|
||||
|
|
@ -1182,4 +1237,99 @@ mod tests {
|
|||
_ => unreachable!(),
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn no_content_disposition() {
|
||||
let bytes = Bytes::from(
|
||||
"testasdadsad\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
||||
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
||||
test\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
|
||||
);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static(
|
||||
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
|
||||
),
|
||||
);
|
||||
let payload = SlowStream::new(bytes);
|
||||
|
||||
let mut multipart = Multipart::new(&headers, payload);
|
||||
let res = multipart.next().await.unwrap();
|
||||
assert!(res.is_err());
|
||||
assert!(matches!(
|
||||
res.unwrap_err(),
|
||||
MultipartError::NoContentDisposition,
|
||||
));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn no_name_in_content_disposition() {
|
||||
let bytes = Bytes::from(
|
||||
"testasdadsad\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
|
||||
Content-Disposition: form-data; filename=\"fn.txt\"\r\n\
|
||||
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
|
||||
test\r\n\
|
||||
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
|
||||
);
|
||||
let mut headers = HeaderMap::new();
|
||||
headers.insert(
|
||||
header::CONTENT_TYPE,
|
||||
header::HeaderValue::from_static(
|
||||
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
|
||||
),
|
||||
);
|
||||
let payload = SlowStream::new(bytes);
|
||||
|
||||
let mut multipart = Multipart::new(&headers, payload);
|
||||
let res = multipart.next().await.unwrap();
|
||||
assert!(res.is_err());
|
||||
assert!(matches!(
|
||||
res.unwrap_err(),
|
||||
MultipartError::NoContentDisposition,
|
||||
));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_drop_multipart_dont_hang() {
|
||||
let (sender, payload) = create_stream();
|
||||
let (bytes, headers) = create_simple_request_with_header();
|
||||
sender.send(Ok(bytes)).unwrap();
|
||||
drop(sender); // eof
|
||||
|
||||
let mut multipart = Multipart::new(&headers, payload);
|
||||
let mut field = multipart.next().await.unwrap().unwrap();
|
||||
|
||||
drop(multipart);
|
||||
|
||||
// should fail immediately
|
||||
match field.next().await {
|
||||
Some(Err(MultipartError::NotConsumed)) => {}
|
||||
_ => panic!(),
|
||||
};
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_drop_field_awaken_multipart() {
|
||||
let (sender, payload) = create_stream();
|
||||
let (bytes, headers) = create_simple_request_with_header();
|
||||
sender.send(Ok(bytes)).unwrap();
|
||||
drop(sender); // eof
|
||||
|
||||
let mut multipart = Multipart::new(&headers, payload);
|
||||
let mut field = multipart.next().await.unwrap().unwrap();
|
||||
|
||||
let task = rt::spawn(async move {
|
||||
rt::time::sleep(Duration::from_secs(1)).await;
|
||||
assert_eq!(field.next().await.unwrap().unwrap(), "test");
|
||||
drop(field);
|
||||
});
|
||||
|
||||
// dropping field should awaken current task
|
||||
let _ = multipart.next().await.unwrap().unwrap();
|
||||
task.await.unwrap();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -30,7 +30,7 @@ serde = "1"
|
|||
[dev-dependencies]
|
||||
criterion = { version = "0.3", features = ["html_reports"] }
|
||||
firestorm = { version = "0.4", features = ["enable_system_time"] }
|
||||
http = "0.2.3"
|
||||
http = "0.2.5"
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
[[bench]]
|
||||
|
|
|
|||
|
|
@ -1770,6 +1770,12 @@ mod tests {
|
|||
match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn duplicate_segment_name() {
|
||||
ResourceDef::new("/user/{id}/post/{id}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn invalid_dynamic_segment_delimiter() {
|
||||
|
|
|
|||
|
|
@ -3,6 +3,16 @@
|
|||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 0.1.0-beta.7 - 2021-11-22
|
||||
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
|
||||
|
||||
[#2408]: https://github.com/actix/actix-web/pull/2408
|
||||
|
||||
|
||||
## 0.1.0-beta.6 - 2021-11-15
|
||||
* No significant changes from `0.1.0-beta.5`.
|
||||
|
||||
|
||||
## 0.1.0-beta.5 - 2021-10-20
|
||||
* Updated rustls to v0.20. [#2414]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "actix-test"
|
||||
version = "0.1.0-beta.5"
|
||||
version = "0.1.0-beta.7"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"Rob Ede <robjtede@icloud.com>",
|
||||
|
|
@ -28,14 +28,14 @@ rustls = ["tls-rustls", "actix-http/rustls", "awc/rustls"]
|
|||
openssl = ["tls-openssl", "actix-http/openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.0"
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-http-test = "3.0.0-beta.5"
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.14"
|
||||
actix-http-test = "3.0.0-beta.7"
|
||||
actix-service = "2.0.0"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = { version = "4.0.0-beta.10", default-features = false, features = ["cookies"] }
|
||||
actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
|
||||
actix-rt = "2.1"
|
||||
awc = { version = "3.0.0-beta.9", default-features = false, features = ["cookies"] }
|
||||
awc = { version = "3.0.0-beta.11", default-features = false, features = ["cookies"] }
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = [] }
|
||||
|
|
@ -45,3 +45,4 @@ serde_json = "1"
|
|||
serde_urlencoded = "0.7"
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.20.0", optional = true }
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
|
|
|
|||
|
|
@ -31,7 +31,7 @@ extern crate tls_openssl as openssl;
|
|||
#[cfg(feature = "rustls")]
|
||||
extern crate tls_rustls as rustls;
|
||||
|
||||
use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time};
|
||||
use std::{fmt, net, thread, time::Duration};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed};
|
||||
pub use actix_http::test::TestBuffer;
|
||||
|
|
@ -41,8 +41,10 @@ use actix_http::{
|
|||
};
|
||||
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
|
||||
use actix_web::{
|
||||
dev::{AppConfig, MessageBody, Server, Service},
|
||||
rt, web, Error,
|
||||
body::MessageBody,
|
||||
dev::{AppConfig, Server, ServerHandle, Service},
|
||||
rt::{self, System},
|
||||
web, Error,
|
||||
};
|
||||
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
|
||||
use futures_core::Stream;
|
||||
|
|
@ -52,6 +54,7 @@ pub use actix_web::test::{
|
|||
call_service, default_service, init_service, load_stream, ok_service, read_body,
|
||||
read_body_json, read_response, read_response_json, TestRequest,
|
||||
};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
/// Start default [`TestServer`].
|
||||
///
|
||||
|
|
@ -86,7 +89,6 @@ where
|
|||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
start_with(TestServerConfig::default(), factory)
|
||||
}
|
||||
|
|
@ -126,9 +128,12 @@ where
|
|||
S::Response: Into<Response<B>> + 'static,
|
||||
<S::Service as Service<Request>>::Future: 'static,
|
||||
B: MessageBody + 'static,
|
||||
B::Error: Into<Box<dyn StdError>>,
|
||||
{
|
||||
let (tx, rx) = mpsc::channel();
|
||||
// for sending handles and server info back from the spawned thread
|
||||
let (started_tx, started_rx) = std::sync::mpsc::channel();
|
||||
|
||||
// for signaling the shutdown of spawned server and system
|
||||
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
|
||||
|
||||
let tls = match cfg.stream {
|
||||
StreamType::Tcp => false,
|
||||
|
|
@ -138,21 +143,25 @@ where
|
|||
StreamType::Rustls(_) => true,
|
||||
};
|
||||
|
||||
// run server in separate thread
|
||||
// run server in separate orphaned thread
|
||||
thread::spawn(move || {
|
||||
let sys = rt::System::new();
|
||||
rt::System::new().block_on(async move {
|
||||
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
let factory = factory.clone();
|
||||
let srv_cfg = cfg.clone();
|
||||
let timeout = cfg.client_timeout;
|
||||
let builder = Server::build().workers(1).disable_signals();
|
||||
|
||||
let builder = Server::build().workers(1).disable_signals().system_exit();
|
||||
|
||||
let srv = match srv_cfg.stream {
|
||||
StreamType::Tcp => match srv_cfg.tp {
|
||||
HttpVer::Http1 => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -164,8 +173,11 @@ where
|
|||
.tcp()
|
||||
}),
|
||||
HttpVer::Http2 => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -177,8 +189,11 @@ where
|
|||
.tcp()
|
||||
}),
|
||||
HttpVer::Both => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -193,8 +208,11 @@ where
|
|||
#[cfg(feature = "openssl")]
|
||||
StreamType::Openssl(acceptor) => match cfg.tp {
|
||||
HttpVer::Http1 => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -206,8 +224,11 @@ where
|
|||
.openssl(acceptor.clone())
|
||||
}),
|
||||
HttpVer::Http2 => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -219,8 +240,11 @@ where
|
|||
.openssl(acceptor.clone())
|
||||
}),
|
||||
HttpVer::Both => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -235,8 +259,11 @@ where
|
|||
#[cfg(feature = "rustls")]
|
||||
StreamType::Rustls(config) => match cfg.tp {
|
||||
HttpVer::Http1 => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -248,8 +275,11 @@ where
|
|||
.rustls(config.clone())
|
||||
}),
|
||||
HttpVer::Http2 => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -261,8 +291,11 @@ where
|
|||
.rustls(config.clone())
|
||||
}),
|
||||
HttpVer::Both => builder.listen("test", tcp, move || {
|
||||
let app_cfg =
|
||||
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
|
||||
let app_cfg = AppConfig::__priv_test_new(
|
||||
false,
|
||||
local_addr.to_string(),
|
||||
local_addr,
|
||||
);
|
||||
|
||||
let fac = factory()
|
||||
.into_factory()
|
||||
|
|
@ -275,17 +308,24 @@ where
|
|||
}),
|
||||
},
|
||||
}
|
||||
.expect("test server could not be created");
|
||||
|
||||
let srv = srv.run();
|
||||
started_tx
|
||||
.send((System::current(), srv.handle(), local_addr))
|
||||
.unwrap();
|
||||
|
||||
sys.block_on(async {
|
||||
let srv = srv.run();
|
||||
tx.send((rt::System::current(), srv, local_addr)).unwrap();
|
||||
// drive server loop
|
||||
srv.await.unwrap();
|
||||
|
||||
// notify TestServer that server and system have shut down
|
||||
// all thread managed resources should be dropped at this point
|
||||
});
|
||||
|
||||
sys.run()
|
||||
let _ = thread_stop_tx.send(());
|
||||
});
|
||||
|
||||
let (system, server, addr) = rx.recv().unwrap();
|
||||
let (system, server, addr) = started_rx.recv().unwrap();
|
||||
|
||||
let client = {
|
||||
let connector = {
|
||||
|
|
@ -299,15 +339,15 @@ where
|
|||
.set_alpn_protos(b"\x02h2\x08http/1.1")
|
||||
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
|
||||
Connector::new()
|
||||
.conn_lifetime(time::Duration::from_secs(0))
|
||||
.timeout(time::Duration::from_millis(30000))
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
.ssl(builder.build())
|
||||
}
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
{
|
||||
Connector::new()
|
||||
.conn_lifetime(time::Duration::from_secs(0))
|
||||
.timeout(time::Duration::from_millis(30000))
|
||||
.conn_lifetime(Duration::from_secs(0))
|
||||
.timeout(Duration::from_millis(30000))
|
||||
}
|
||||
};
|
||||
|
||||
|
|
@ -315,11 +355,12 @@ where
|
|||
};
|
||||
|
||||
TestServer {
|
||||
addr,
|
||||
server,
|
||||
thread_stop_rx,
|
||||
client,
|
||||
system,
|
||||
addr,
|
||||
tls,
|
||||
server,
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -405,11 +446,12 @@ impl TestServerConfig {
|
|||
///
|
||||
/// See [`start`] for usage example.
|
||||
pub struct TestServer {
|
||||
addr: net::SocketAddr,
|
||||
server: ServerHandle,
|
||||
thread_stop_rx: mpsc::Receiver<()>,
|
||||
client: awc::Client,
|
||||
system: rt::System,
|
||||
addr: net::SocketAddr,
|
||||
tls: bool,
|
||||
server: Server,
|
||||
}
|
||||
|
||||
impl TestServer {
|
||||
|
|
@ -504,16 +546,31 @@ impl TestServer {
|
|||
self.client.headers()
|
||||
}
|
||||
|
||||
/// Gracefully stop HTTP server.
|
||||
pub async fn stop(self) {
|
||||
self.server.stop(true).await;
|
||||
/// Stop HTTP server.
|
||||
///
|
||||
/// Waits for spawned `Server` and `System` to shutdown (force) shutdown.
|
||||
pub async fn stop(mut self) {
|
||||
// signal server to stop
|
||||
self.server.stop(false).await;
|
||||
|
||||
// also signal system to stop
|
||||
// though this is handled by `ServerBuilder::exit_system` too
|
||||
self.system.stop();
|
||||
rt::time::sleep(time::Duration::from_millis(100)).await;
|
||||
|
||||
// wait for thread to be stopped but don't care about result
|
||||
let _ = self.thread_stop_rx.recv().await;
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestServer {
|
||||
fn drop(&mut self) {
|
||||
self.system.stop()
|
||||
// calls in this Drop impl should be enough to shut down the server, system, and thread
|
||||
// without needing to await anything
|
||||
|
||||
// signal server to stop
|
||||
let _ = self.server.stop(true);
|
||||
|
||||
// signal system to stop
|
||||
self.system.stop();
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,8 +1,12 @@
|
|||
# Changes
|
||||
|
||||
## Unreleased - 2021-xx-xx
|
||||
* Add `ws:WsResponseBuilder` for building WebSocket session response. [#1920]
|
||||
* Deprecate `ws::{start_with_addr, start_with_protocols}`. [#1920]
|
||||
* Minimum supported Rust version (MSRV) is now 1.52.
|
||||
|
||||
[#1920]: https://github.com/actix/actix-web/pull/1920
|
||||
|
||||
|
||||
## 4.0.0-beta.7 - 2021-09-09
|
||||
* Minimum supported Rust version (MSRV) is now 1.51.
|
||||
|
|
|
|||
|
|
@ -15,20 +15,20 @@ path = "src/lib.rs"
|
|||
|
||||
[dependencies]
|
||||
actix = { version = "0.12.0", default-features = false }
|
||||
actix-codec = "0.4.0"
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-web = { version = "4.0.0-beta.10", default-features = false }
|
||||
actix-codec = "0.4.1"
|
||||
actix-http = "3.0.0-beta.14"
|
||||
actix-web = { version = "4.0.0-beta.11", default-features = false }
|
||||
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
pin-project = "1.0.0"
|
||||
pin-project-lite = "0.2"
|
||||
tokio = { version = "1", features = ["sync"] }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-test = "0.1.0-beta.5"
|
||||
actix-test = "0.1.0-beta.7"
|
||||
|
||||
awc = { version = "3.0.0-beta.9", default-features = false }
|
||||
env_logger = "0.8"
|
||||
awc = { version = "3.0.0-beta.11", default-features = false }
|
||||
env_logger = "0.9"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
|
|
|
|||
|
|
@ -1,20 +1,24 @@
|
|||
//! Websocket integration.
|
||||
|
||||
use std::future::Future;
|
||||
use std::io;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{collections::VecDeque, convert::TryFrom};
|
||||
|
||||
use actix::dev::{
|
||||
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler, ToEnvelope,
|
||||
use std::{
|
||||
collections::VecDeque,
|
||||
convert::TryFrom,
|
||||
future::Future,
|
||||
io, mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use actix::fut::ActorFuture;
|
||||
|
||||
use actix::{
|
||||
dev::{
|
||||
AsyncContextParts, ContextFut, ContextParts, Envelope, Mailbox, StreamHandler,
|
||||
ToEnvelope,
|
||||
},
|
||||
fut::ActorFuture,
|
||||
Actor, ActorContext, ActorState, Addr, AsyncContext, Handler, Message as ActixMessage,
|
||||
SpawnHandle,
|
||||
};
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
use actix_codec::{Decoder as _, Encoder as _};
|
||||
pub use actix_http::ws::{
|
||||
CloseCode, CloseReason, Frame, HandshakeError, Message, ProtocolError,
|
||||
};
|
||||
|
|
@ -30,9 +34,189 @@ use actix_web::{
|
|||
use bytes::{Bytes, BytesMut};
|
||||
use bytestring::ByteString;
|
||||
use futures_core::Stream;
|
||||
use tokio::sync::oneshot::Sender;
|
||||
use pin_project_lite::pin_project;
|
||||
use tokio::sync::oneshot;
|
||||
|
||||
/// Builder for Websocket session response.
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// Create a Websocket session response with default configuration.
|
||||
/// ```ignore
|
||||
/// WsResponseBuilder::new(WsActor, &req, stream).start()
|
||||
/// ```
|
||||
///
|
||||
/// Create a Websocket session with a specific max frame size, [`Codec`], and protocols.
|
||||
/// ```ignore
|
||||
/// const MAX_FRAME_SIZE: usize = 16_384; // 16KiB
|
||||
///
|
||||
/// ws::WsResponseBuilder::new(WsActor, &req, stream)
|
||||
/// .codec(Codec::new())
|
||||
/// .protocols(&["A", "B"])
|
||||
/// .frame_size(MAX_FRAME_SIZE)
|
||||
/// .start()
|
||||
/// ```
|
||||
pub struct WsResponseBuilder<'a, A, T>
|
||||
where
|
||||
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
|
||||
T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
|
||||
{
|
||||
actor: A,
|
||||
req: &'a HttpRequest,
|
||||
stream: T,
|
||||
codec: Option<Codec>,
|
||||
protocols: Option<&'a [&'a str]>,
|
||||
frame_size: Option<usize>,
|
||||
}
|
||||
|
||||
impl<'a, A, T> WsResponseBuilder<'a, A, T>
|
||||
where
|
||||
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
|
||||
T: Stream<Item = Result<Bytes, PayloadError>> + 'static,
|
||||
{
|
||||
/// Construct a new `WsResponseBuilder` with actor, request, and payload stream.
|
||||
///
|
||||
/// For usage example, see docs on [`WsResponseBuilder`] struct.
|
||||
pub fn new(actor: A, req: &'a HttpRequest, stream: T) -> Self {
|
||||
WsResponseBuilder {
|
||||
actor,
|
||||
req,
|
||||
stream,
|
||||
codec: None,
|
||||
protocols: None,
|
||||
frame_size: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the protocols for the session.
|
||||
pub fn protocols(mut self, protocols: &'a [&'a str]) -> Self {
|
||||
self.protocols = Some(protocols);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the max frame size for each message (in bytes).
|
||||
///
|
||||
/// **Note**: This will override any given [`Codec`]'s max frame size.
|
||||
pub fn frame_size(mut self, frame_size: usize) -> Self {
|
||||
self.frame_size = Some(frame_size);
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the [`Codec`] for the session. If [`Self::frame_size`] is also set, the given
|
||||
/// [`Codec`]'s max frame size will be overridden.
|
||||
pub fn codec(mut self, codec: Codec) -> Self {
|
||||
self.codec = Some(codec);
|
||||
self
|
||||
}
|
||||
|
||||
fn handshake_resp(&self) -> Result<HttpResponseBuilder, HandshakeError> {
|
||||
match self.protocols {
|
||||
Some(protocols) => handshake_with_protocols(self.req, protocols),
|
||||
None => handshake(self.req),
|
||||
}
|
||||
}
|
||||
|
||||
fn set_frame_size(&mut self) {
|
||||
if let Some(frame_size) = self.frame_size {
|
||||
match &mut self.codec {
|
||||
Some(codec) => {
|
||||
// modify existing codec's max frame size
|
||||
let orig_codec = mem::take(codec);
|
||||
*codec = orig_codec.max_size(frame_size);
|
||||
}
|
||||
|
||||
None => {
|
||||
// create a new codec with the given size
|
||||
self.codec = Some(Codec::new().max_size(frame_size));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a new Websocket context from an actor, request stream, and codec.
|
||||
///
|
||||
/// Returns a pair, where the first item is an addr for the created actor, and the second item
|
||||
/// is a stream intended to be set as part of the response
|
||||
/// via [`HttpResponseBuilder::streaming()`].
|
||||
fn create_with_codec_addr<S>(
|
||||
actor: A,
|
||||
stream: S,
|
||||
codec: Codec,
|
||||
) -> (Addr<A>, impl Stream<Item = Result<Bytes, Error>>)
|
||||
where
|
||||
A: StreamHandler<Result<Message, ProtocolError>>,
|
||||
S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
|
||||
{
|
||||
let mb = Mailbox::default();
|
||||
let mut ctx = WebsocketContext {
|
||||
inner: ContextParts::new(mb.sender_producer()),
|
||||
messages: VecDeque::new(),
|
||||
};
|
||||
ctx.add_stream(WsStream::new(stream, codec.clone()));
|
||||
|
||||
let addr = ctx.address();
|
||||
|
||||
(addr, WebsocketContextFut::new(ctx, actor, mb, codec))
|
||||
}
|
||||
|
||||
/// Perform WebSocket handshake and start actor.
|
||||
///
|
||||
/// `req` is an [`HttpRequest`] that should be requesting a websocket protocol change.
|
||||
/// `stream` should be a [`Bytes`] stream (such as `actix_web::web::Payload`) that contains a
|
||||
/// stream of the body request.
|
||||
///
|
||||
/// If there is a problem with the handshake, an error is returned.
|
||||
///
|
||||
/// If successful, consume the [`WsResponseBuilder`] and return a [`HttpResponse`] wrapped in
|
||||
/// a [`Result`].
|
||||
pub fn start(mut self) -> Result<HttpResponse, Error> {
|
||||
let mut res = self.handshake_resp()?;
|
||||
self.set_frame_size();
|
||||
|
||||
match self.codec {
|
||||
Some(codec) => {
|
||||
let out_stream = WebsocketContext::with_codec(self.actor, self.stream, codec);
|
||||
Ok(res.streaming(out_stream))
|
||||
}
|
||||
None => {
|
||||
let out_stream = WebsocketContext::create(self.actor, self.stream);
|
||||
Ok(res.streaming(out_stream))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform WebSocket handshake and start actor.
|
||||
///
|
||||
/// `req` is an [`HttpRequest`] that should be requesting a websocket protocol change.
|
||||
/// `stream` should be a [`Bytes`] stream (such as `actix_web::web::Payload`) that contains a
|
||||
/// stream of the body request.
|
||||
///
|
||||
/// If there is a problem with the handshake, an error is returned.
|
||||
///
|
||||
/// If successful, returns a pair where the first item is an address for the created actor and
|
||||
/// the second item is the [`HttpResponse`] that should be returned from the websocket request.
|
||||
pub fn start_with_addr(mut self) -> Result<(Addr<A>, HttpResponse), Error> {
|
||||
let mut res = self.handshake_resp()?;
|
||||
self.set_frame_size();
|
||||
|
||||
match self.codec {
|
||||
Some(codec) => {
|
||||
let (addr, out_stream) =
|
||||
Self::create_with_codec_addr(self.actor, self.stream, codec);
|
||||
Ok((addr, res.streaming(out_stream)))
|
||||
}
|
||||
None => {
|
||||
let (addr, out_stream) =
|
||||
WebsocketContext::create_with_addr(self.actor, self.stream);
|
||||
Ok((addr, res.streaming(out_stream)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Perform WebSocket handshake and start actor.
|
||||
///
|
||||
/// To customize options, see [`WsResponseBuilder`].
|
||||
pub fn start<A, T>(actor: A, req: &HttpRequest, stream: T) -> Result<HttpResponse, Error>
|
||||
where
|
||||
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Result<Message, ProtocolError>>,
|
||||
|
|
@ -44,15 +228,15 @@ where
|
|||
|
||||
/// Perform WebSocket handshake and start actor.
|
||||
///
|
||||
/// `req` is an HTTP Request that should be requesting a websocket protocol
|
||||
/// change. `stream` should be a `Bytes` stream (such as
|
||||
/// `actix_web::web::Payload`) that contains a stream of the body request.
|
||||
/// `req` is an HTTP Request that should be requesting a websocket protocol change. `stream` should
|
||||
/// be a `Bytes` stream (such as `actix_web::web::Payload`) that contains a stream of the
|
||||
/// body request.
|
||||
///
|
||||
/// If there is a problem with the handshake, an error is returned.
|
||||
///
|
||||
/// If successful, returns a pair where the first item is an address for the
|
||||
/// created actor and the second item is the response that should be returned
|
||||
/// from the WebSocket request.
|
||||
/// If successful, returns a pair where the first item is an address for the created actor and the
|
||||
/// second item is the response that should be returned from the WebSocket request.
|
||||
#[deprecated(since = "4.0.0", note = "Prefer `WsResponseBuilder::start_with_addr`.")]
|
||||
pub fn start_with_addr<A, T>(
|
||||
actor: A,
|
||||
req: &HttpRequest,
|
||||
|
|
@ -70,6 +254,10 @@ where
|
|||
/// Do WebSocket handshake and start ws actor.
|
||||
///
|
||||
/// `protocols` is a sequence of known protocols.
|
||||
#[deprecated(
|
||||
since = "4.0.0",
|
||||
note = "Prefer `WsResponseBuilder` for setting protocols."
|
||||
)]
|
||||
pub fn start_with_protocols<A, T>(
|
||||
actor: A,
|
||||
protocols: &[&str],
|
||||
|
|
@ -86,20 +274,19 @@ where
|
|||
|
||||
/// Prepare WebSocket handshake response.
|
||||
///
|
||||
/// This function returns handshake `HttpResponse`, ready to send to peer.
|
||||
/// It does not perform any IO.
|
||||
/// This function returns handshake `HttpResponse`, ready to send to peer. It does not perform
|
||||
/// any IO.
|
||||
pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeError> {
|
||||
handshake_with_protocols(req, &[])
|
||||
}
|
||||
|
||||
/// Prepare WebSocket handshake response.
|
||||
///
|
||||
/// This function returns handshake `HttpResponse`, ready to send to peer.
|
||||
/// It does not perform any IO.
|
||||
/// This function returns handshake `HttpResponse`, ready to send to peer. It does not perform
|
||||
/// any IO.
|
||||
///
|
||||
/// `protocols` is a sequence of known protocols. On successful handshake,
|
||||
/// the returned response headers contain the first protocol in this list
|
||||
/// which the server also knows.
|
||||
/// `protocols` is a sequence of known protocols. On successful handshake, the returned response
|
||||
/// headers contain the first protocol in this list which the server also knows.
|
||||
pub fn handshake_with_protocols(
|
||||
req: &HttpRequest,
|
||||
protocols: &[&str],
|
||||
|
|
@ -246,8 +433,8 @@ impl<A> WebsocketContext<A>
|
|||
where
|
||||
A: Actor<Context = Self>,
|
||||
{
|
||||
/// Create a new Websocket context from a request and an actor.
|
||||
#[inline]
|
||||
/// Create a new Websocket context from a request and an actor
|
||||
pub fn create<S>(actor: A, stream: S) -> impl Stream<Item = Result<Bytes, Error>>
|
||||
where
|
||||
A: StreamHandler<Result<Message, ProtocolError>>,
|
||||
|
|
@ -257,12 +444,11 @@ where
|
|||
stream
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Create a new Websocket context from a request and an actor.
|
||||
///
|
||||
/// Returns a pair, where the first item is an addr for the created actor,
|
||||
/// and the second item is a stream intended to be set as part of the
|
||||
/// response via `HttpResponseBuilder::streaming()`.
|
||||
/// Returns a pair, where the first item is an addr for the created actor, and the second item
|
||||
/// is a stream intended to be set as part of the response
|
||||
/// via [`HttpResponseBuilder::streaming()`].
|
||||
pub fn create_with_addr<S>(
|
||||
actor: A,
|
||||
stream: S,
|
||||
|
|
@ -283,7 +469,6 @@ where
|
|||
(addr, WebsocketContextFut::new(ctx, actor, mb, Codec::new()))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Create a new Websocket context from a request, an actor, and a codec
|
||||
pub fn with_codec<S>(
|
||||
actor: A,
|
||||
|
|
@ -299,7 +484,7 @@ where
|
|||
inner: ContextParts::new(mb.sender_producer()),
|
||||
messages: VecDeque::new(),
|
||||
};
|
||||
ctx.add_stream(WsStream::new(stream, codec));
|
||||
ctx.add_stream(WsStream::new(stream, codec.clone()));
|
||||
|
||||
WebsocketContextFut::new(ctx, actor, mb, codec)
|
||||
}
|
||||
|
|
@ -457,12 +642,13 @@ where
|
|||
M: ActixMessage + Send + 'static,
|
||||
M::Result: Send,
|
||||
{
|
||||
fn pack(msg: M, tx: Option<Sender<M::Result>>) -> Envelope<A> {
|
||||
fn pack(msg: M, tx: Option<oneshot::Sender<M::Result>>) -> Envelope<A> {
|
||||
Envelope::new(msg, tx)
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pin_project! {
|
||||
#[derive(Debug)]
|
||||
struct WsStream<S> {
|
||||
#[pin]
|
||||
stream: S,
|
||||
|
|
@ -470,6 +656,7 @@ struct WsStream<S> {
|
|||
buf: BytesMut,
|
||||
closed: bool,
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> WsStream<S>
|
||||
where
|
||||
|
|
@ -547,9 +734,12 @@ where
|
|||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use actix_web::{
|
||||
http::{header, Method},
|
||||
test::TestRequest,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use actix_web::http::{header, Method};
|
||||
use actix_web::test::TestRequest;
|
||||
|
||||
#[test]
|
||||
fn test_handshake() {
|
||||
|
|
|
|||
|
|
@ -1,11 +1,9 @@
|
|||
use actix::prelude::*;
|
||||
use actix_web::{
|
||||
http::{header, StatusCode},
|
||||
web, App, HttpRequest, HttpResponse,
|
||||
};
|
||||
use actix_web_actors::*;
|
||||
use actix_http::ws::Codec;
|
||||
use actix_web::{web, App, HttpRequest};
|
||||
use actix_web_actors::ws;
|
||||
use bytes::Bytes;
|
||||
use futures_util::{SinkExt as _, StreamExt as _};
|
||||
use futures_util::{SinkExt, StreamExt};
|
||||
|
||||
struct Ws;
|
||||
|
||||
|
|
@ -15,37 +13,34 @@ impl Actor for Ws {
|
|||
|
||||
impl StreamHandler<Result<ws::Message, ws::ProtocolError>> for Ws {
|
||||
fn handle(&mut self, msg: Result<ws::Message, ws::ProtocolError>, ctx: &mut Self::Context) {
|
||||
match msg.unwrap() {
|
||||
ws::Message::Ping(msg) => ctx.pong(&msg),
|
||||
ws::Message::Text(text) => ctx.text(text),
|
||||
ws::Message::Binary(bin) => ctx.binary(bin),
|
||||
ws::Message::Close(reason) => ctx.close(reason),
|
||||
_ => {}
|
||||
match msg {
|
||||
Ok(ws::Message::Ping(msg)) => ctx.pong(&msg),
|
||||
Ok(ws::Message::Text(text)) => ctx.text(text),
|
||||
Ok(ws::Message::Binary(bin)) => ctx.binary(bin),
|
||||
Ok(ws::Message::Close(reason)) => ctx.close(reason),
|
||||
_ => ctx.close(Some(ws::CloseCode::Error.into())),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_simple() {
|
||||
let mut srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) },
|
||||
))
|
||||
});
|
||||
const MAX_FRAME_SIZE: usize = 10_000;
|
||||
const DEFAULT_FRAME_SIZE: usize = 10;
|
||||
|
||||
async fn common_test_code(mut srv: actix_test::TestServer, frame_size: usize) {
|
||||
// client service
|
||||
let mut framed = srv.ws().await.unwrap();
|
||||
framed.send(ws::Message::Text("text".into())).await.unwrap();
|
||||
|
||||
framed.send(ws::Message::Text("text".into())).await.unwrap();
|
||||
let item = framed.next().await.unwrap().unwrap();
|
||||
assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text")));
|
||||
|
||||
let bytes = Bytes::from(vec![0; frame_size]);
|
||||
framed
|
||||
.send(ws::Message::Binary("text".into()))
|
||||
.send(ws::Message::Binary(bytes.clone()))
|
||||
.await
|
||||
.unwrap();
|
||||
let item = framed.next().await.unwrap().unwrap();
|
||||
assert_eq!(item, ws::Frame::Binary(Bytes::from_static(b"text")));
|
||||
assert_eq!(item, ws::Frame::Binary(bytes));
|
||||
|
||||
framed.send(ws::Message::Ping("text".into())).await.unwrap();
|
||||
let item = framed.next().await.unwrap().unwrap();
|
||||
|
|
@ -55,55 +50,137 @@ async fn test_simple() {
|
|||
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let item = framed.next().await.unwrap().unwrap();
|
||||
assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_with_credentials() {
|
||||
let mut srv = actix_test::start(|| {
|
||||
async fn simple_builder() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
if req.headers().contains_key("Authorization") {
|
||||
ws::start(Ws, &req, stream)
|
||||
} else {
|
||||
Ok(HttpResponse::new(StatusCode::UNAUTHORIZED))
|
||||
}
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream).start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
// client service without credentials
|
||||
match srv.ws().await {
|
||||
Ok(_) => panic!("WebSocket client without credentials should panic"),
|
||||
Err(awc::error::WsClientError::InvalidResponseStatus(status)) => {
|
||||
assert_eq!(status, StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
Err(e) => panic!("Invalid error from WebSocket client: {}", e),
|
||||
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
|
||||
}
|
||||
|
||||
let headers = srv.client_headers().unwrap();
|
||||
headers.insert(
|
||||
header::AUTHORIZATION,
|
||||
header::HeaderValue::from_static("Bearer Something"),
|
||||
);
|
||||
#[actix_rt::test]
|
||||
async fn builder_with_frame_size() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream)
|
||||
.frame_size(MAX_FRAME_SIZE)
|
||||
.start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
// client service with credentials
|
||||
let client = srv.ws();
|
||||
|
||||
let mut framed = client.await.unwrap();
|
||||
|
||||
framed.send(ws::Message::Text("text".into())).await.unwrap();
|
||||
|
||||
let item = framed.next().await.unwrap().unwrap();
|
||||
assert_eq!(item, ws::Frame::Text(Bytes::from_static(b"text")));
|
||||
|
||||
framed
|
||||
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into())))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let item = framed.next().await.unwrap().unwrap();
|
||||
assert_eq!(item, ws::Frame::Close(Some(ws::CloseCode::Normal.into())));
|
||||
common_test_code(srv, MAX_FRAME_SIZE).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn builder_with_frame_size_exceeded() {
|
||||
const MAX_FRAME_SIZE: usize = 64;
|
||||
|
||||
let mut srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream)
|
||||
.frame_size(MAX_FRAME_SIZE)
|
||||
.start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
// client service
|
||||
let mut framed = srv.ws().await.unwrap();
|
||||
|
||||
// create a request with a frame size larger than expected
|
||||
let bytes = Bytes::from(vec![0; MAX_FRAME_SIZE + 1]);
|
||||
framed.send(ws::Message::Binary(bytes)).await.unwrap();
|
||||
|
||||
let frame = framed.next().await.unwrap().unwrap();
|
||||
let close_reason = match frame {
|
||||
ws::Frame::Close(Some(reason)) => reason,
|
||||
_ => panic!("close frame expected"),
|
||||
};
|
||||
assert_eq!(close_reason.code, ws::CloseCode::Error);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn builder_with_codec() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream)
|
||||
.codec(Codec::new())
|
||||
.start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn builder_with_protocols() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream)
|
||||
.protocols(&["A", "B"])
|
||||
.start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn builder_with_codec_and_frame_size() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream)
|
||||
.codec(Codec::new())
|
||||
.frame_size(MAX_FRAME_SIZE)
|
||||
.start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn builder_full() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move {
|
||||
ws::WsResponseBuilder::new(Ws, &req, stream)
|
||||
.frame_size(MAX_FRAME_SIZE)
|
||||
.codec(Codec::new())
|
||||
.protocols(&["A", "B"])
|
||||
.start()
|
||||
},
|
||||
))
|
||||
});
|
||||
|
||||
common_test_code(srv, MAX_FRAME_SIZE).await;
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn simple_start() {
|
||||
let srv = actix_test::start(|| {
|
||||
App::new().service(web::resource("/").to(
|
||||
|req: HttpRequest, stream: web::Payload| async move { ws::start(Ws, &req, stream) },
|
||||
))
|
||||
});
|
||||
|
||||
common_test_code(srv, DEFAULT_FRAME_SIZE).await;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -23,9 +23,9 @@ actix-router = "0.5.0-beta.2"
|
|||
[dev-dependencies]
|
||||
actix-rt = "2.2"
|
||||
actix-macros = "0.2.3"
|
||||
actix-test = "0.1.0-beta.5"
|
||||
actix-test = "0.1.0-beta.7"
|
||||
actix-utils = "3.0.0"
|
||||
actix-web = "4.0.0-beta.10"
|
||||
actix-web = "4.0.0-beta.11"
|
||||
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
trybuild = "1"
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ mod route;
|
|||
/// Creates resource handler, allowing multiple HTTP method guards.
|
||||
///
|
||||
/// # Syntax
|
||||
/// ```text
|
||||
/// ```plain
|
||||
/// #[route("path", method="HTTP_METHOD"[, attributes])]
|
||||
/// ```
|
||||
///
|
||||
|
|
@ -112,7 +112,7 @@ concat!("
|
|||
Creates route handler with `actix_web::guard::", stringify!($variant), "`.
|
||||
|
||||
# Syntax
|
||||
```text
|
||||
```plain
|
||||
#[", stringify!($method), r#"("path"[, attributes])]
|
||||
```
|
||||
|
||||
|
|
|
|||
|
|
@ -3,6 +3,20 @@
|
|||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.12 - 2021-11-30
|
||||
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
|
||||
|
||||
[#2474]: https://github.com/actix/actix-web/pull/2474
|
||||
|
||||
|
||||
## 3.0.0-beta.11 - 2021-11-22
|
||||
* No significant changes from `3.0.0-beta.10`.
|
||||
|
||||
|
||||
## 3.0.0-beta.10 - 2021-11-15
|
||||
* No significant changes from `3.0.0-beta.9`.
|
||||
|
||||
|
||||
## 3.0.0-beta.9 - 2021-10-20
|
||||
* Updated rustls to v0.20. [#2414]
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "awc"
|
||||
version = "3.0.0-beta.9"
|
||||
version = "3.0.0-beta.12"
|
||||
authors = [
|
||||
"Nikolay Kim <fafhrd91@gmail.com>",
|
||||
"fakeshadow <24548779@qq.com>",
|
||||
|
|
@ -52,12 +52,17 @@ trust-dns = ["trust-dns-resolver"]
|
|||
# Don't rely on these whatsoever. They may disappear at anytime.
|
||||
__compress = []
|
||||
|
||||
# Enable dangerous feature for testing and local network usage:
|
||||
# - HTTP/2 over TCP(No Tls).
|
||||
# DO NOT enable this over any internet use case.
|
||||
dangerous-h2c = []
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.0"
|
||||
actix-codec = "0.4.1"
|
||||
actix-service = "2.0.0"
|
||||
actix-http = "3.0.0-beta.11"
|
||||
actix-http = "3.0.0-beta.14"
|
||||
actix-rt = { version = "2.1", default-features = false }
|
||||
actix-tls = { version = "3.0.0-beta.7", features = ["connect"] }
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["connect", "uri"] }
|
||||
actix-utils = "3.0.0"
|
||||
|
||||
ahash = "0.7"
|
||||
|
|
@ -68,7 +73,7 @@ derive_more = "0.99.5"
|
|||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
h2 = "0.3"
|
||||
http = "0.2"
|
||||
http = "0.2.5"
|
||||
itoa = "0.4"
|
||||
log =" 0.4"
|
||||
mime = "0.3"
|
||||
|
|
@ -88,18 +93,19 @@ tls-rustls = { package = "rustls", version = "0.20.0", optional = true, features
|
|||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = { version = "4.0.0-beta.10", features = ["openssl"] }
|
||||
actix-http = { version = "3.0.0-beta.11", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] }
|
||||
actix-web = { version = "4.0.0-beta.11", features = ["openssl"] }
|
||||
actix-http = { version = "3.0.0-beta.14", features = ["openssl"] }
|
||||
actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
|
||||
actix-utils = "3.0.0"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-tls = { version = "3.0.0-beta.7", features = ["openssl", "rustls"] }
|
||||
actix-test = { version = "0.1.0-beta.5", features = ["openssl", "rustls"] }
|
||||
actix-server = "2.0.0-beta.9"
|
||||
actix-tls = { version = "3.0.0-rc.1", features = ["openssl", "rustls"] }
|
||||
actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
|
||||
|
||||
brotli2 = "0.3.2"
|
||||
env_logger = "0.8"
|
||||
env_logger = "0.9"
|
||||
flate2 = "1.0.13"
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
static_assertions = "1.1"
|
||||
rcgen = "0.8"
|
||||
rustls-pemfile = "0.2"
|
||||
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@
|
|||
> Async HTTP and WebSocket client library.
|
||||
|
||||
[](https://crates.io/crates/awc)
|
||||
[](https://docs.rs/awc/3.0.0-beta.9)
|
||||
[](https://docs.rs/awc/3.0.0-beta.12)
|
||||

|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.9)
|
||||
[](https://deps.rs/crate/awc/3.0.0-beta.12)
|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
## Documentation & Resources
|
||||
|
|
|
|||
|
|
@ -0,0 +1,266 @@
|
|||
use std::{
|
||||
borrow::Cow,
|
||||
fmt, mem,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use pin_project_lite::pin_project;
|
||||
|
||||
use actix_http::body::{BodySize, BodyStream, BoxBody, MessageBody, SizedStream};
|
||||
|
||||
use crate::BoxError;
|
||||
|
||||
pin_project! {
|
||||
/// Represents various types of HTTP message body.
|
||||
#[derive(Clone)]
|
||||
#[project = AnyBodyProj]
|
||||
pub enum AnyBody<B = BoxBody> {
|
||||
/// Empty response. `Content-Length` header is not set.
|
||||
None,
|
||||
|
||||
/// Complete, in-memory response body.
|
||||
Bytes { body: Bytes },
|
||||
|
||||
/// Generic / Other message body.
|
||||
Body { #[pin] body: B },
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyBody {
|
||||
/// Constructs a "body" representing an empty response.
|
||||
pub fn none() -> Self {
|
||||
Self::None
|
||||
}
|
||||
|
||||
/// Constructs a new, 0-length body.
|
||||
pub fn empty() -> Self {
|
||||
Self::Bytes { body: Bytes::new() }
|
||||
}
|
||||
|
||||
/// Create boxed body from generic message body.
|
||||
pub fn new_boxed<B>(body: B) -> Self
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
Self::Body {
|
||||
body: BoxBody::new(body),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs new `AnyBody` instance from a slice of bytes by copying it.
|
||||
///
|
||||
/// If your bytes container is owned, it may be cheaper to use a `From` impl.
|
||||
pub fn copy_from_slice(s: &[u8]) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::copy_from_slice(s),
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[deprecated(since = "4.0.0", note = "Renamed to `copy_from_slice`.")]
|
||||
pub fn from_slice(s: &[u8]) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::copy_from_slice(s),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> AnyBody<B> {
|
||||
/// Create body from generic message body.
|
||||
pub fn new(body: B) -> Self {
|
||||
Self::Body { body }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> AnyBody<B>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
{
|
||||
pub fn into_boxed(self) -> AnyBody {
|
||||
match self {
|
||||
Self::None => AnyBody::None,
|
||||
Self::Bytes { body: bytes } => AnyBody::Bytes { body: bytes },
|
||||
Self::Body { body } => AnyBody::new_boxed(body),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> MessageBody for AnyBody<B>
|
||||
where
|
||||
B: MessageBody,
|
||||
{
|
||||
type Error = crate::BoxError;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
match self {
|
||||
AnyBody::None => BodySize::None,
|
||||
AnyBody::Bytes { ref body } => BodySize::Sized(body.len() as u64),
|
||||
AnyBody::Body { ref body } => body.size(),
|
||||
}
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
match self.project() {
|
||||
AnyBodyProj::None => Poll::Ready(None),
|
||||
AnyBodyProj::Bytes { body } => {
|
||||
let len = body.len();
|
||||
if len == 0 {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
Poll::Ready(Some(Ok(mem::take(body))))
|
||||
}
|
||||
}
|
||||
|
||||
AnyBodyProj::Body { body } => body.poll_next(cx).map_err(|err| err.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq for AnyBody {
|
||||
fn eq(&self, other: &AnyBody) -> bool {
|
||||
match self {
|
||||
AnyBody::None => matches!(*other, AnyBody::None),
|
||||
AnyBody::Bytes { body } => match other {
|
||||
AnyBody::Bytes { body: b2 } => body == b2,
|
||||
_ => false,
|
||||
},
|
||||
AnyBody::Body { .. } => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: fmt::Debug> fmt::Debug for AnyBody<S> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match *self {
|
||||
AnyBody::None => write!(f, "AnyBody::None"),
|
||||
AnyBody::Bytes { ref body } => write!(f, "AnyBody::Bytes({:?})", body),
|
||||
AnyBody::Body { ref body } => write!(f, "AnyBody::Message({:?})", body),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<&'static str> for AnyBody<B> {
|
||||
fn from(string: &'static str) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from_static(string.as_ref()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<&'static [u8]> for AnyBody<B> {
|
||||
fn from(bytes: &'static [u8]) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from_static(bytes),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Vec<u8>> for AnyBody<B> {
|
||||
fn from(vec: Vec<u8>) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from(vec),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<String> for AnyBody<B> {
|
||||
fn from(string: String) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::from(string),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<&'_ String> for AnyBody<B> {
|
||||
fn from(string: &String) -> Self {
|
||||
Self::Bytes {
|
||||
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&string)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Cow<'_, str>> for AnyBody<B> {
|
||||
fn from(string: Cow<'_, str>) -> Self {
|
||||
match string {
|
||||
Cow::Owned(s) => Self::from(s),
|
||||
Cow::Borrowed(s) => Self::Bytes {
|
||||
body: Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<Bytes> for AnyBody<B> {
|
||||
fn from(bytes: Bytes) -> Self {
|
||||
Self::Bytes { body: bytes }
|
||||
}
|
||||
}
|
||||
|
||||
impl<B> From<BytesMut> for AnyBody<B> {
|
||||
fn from(bytes: BytesMut) -> Self {
|
||||
Self::Bytes {
|
||||
body: bytes.freeze(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<SizedStream<S>> for AnyBody
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
fn from(stream: SizedStream<S>) -> Self {
|
||||
AnyBody::new_boxed(stream)
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, E> From<BodyStream<S>> for AnyBody
|
||||
where
|
||||
S: Stream<Item = Result<Bytes, E>> + 'static,
|
||||
E: Into<BoxError> + 'static,
|
||||
{
|
||||
fn from(stream: BodyStream<S>) -> Self {
|
||||
AnyBody::new_boxed(stream)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::marker::PhantomPinned;
|
||||
|
||||
use static_assertions::{assert_impl_all, assert_not_impl_all};
|
||||
|
||||
use super::*;
|
||||
|
||||
struct PinType(PhantomPinned);
|
||||
|
||||
impl MessageBody for PinType {
|
||||
type Error = crate::BoxError;
|
||||
|
||||
fn size(&self) -> BodySize {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn poll_next(
|
||||
self: Pin<&mut Self>,
|
||||
_cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Result<Bytes, Self::Error>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
assert_impl_all!(AnyBody<()>: MessageBody, fmt::Debug, Send, Sync, Unpin);
|
||||
assert_impl_all!(AnyBody<AnyBody<()>>: MessageBody, fmt::Debug, Send, Sync, Unpin);
|
||||
assert_impl_all!(AnyBody<Bytes>: MessageBody, fmt::Debug, Send, Sync, Unpin);
|
||||
assert_impl_all!(AnyBody: MessageBody, fmt::Debug, Unpin);
|
||||
assert_impl_all!(AnyBody<PinType>: MessageBody);
|
||||
|
||||
assert_not_impl_all!(AnyBody: Send, Sync, Unpin);
|
||||
assert_not_impl_all!(AnyBody<PinType>: Send, Sync, Unpin);
|
||||
}
|
||||
|
|
@ -1,18 +1,16 @@
|
|||
use std::convert::TryFrom;
|
||||
use std::fmt;
|
||||
use std::net::IpAddr;
|
||||
use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{convert::TryFrom, fmt, net::IpAddr, rc::Rc, time::Duration};
|
||||
|
||||
use actix_http::http::{self, header, Error as HttpError, HeaderMap, HeaderName, Uri};
|
||||
use actix_rt::net::{ActixStream, TcpStream};
|
||||
use actix_service::{boxed, Service};
|
||||
|
||||
use crate::client::{Connector, ConnectorService, TcpConnect, TcpConnectError, TcpConnection};
|
||||
use crate::connect::DefaultConnector;
|
||||
use crate::error::SendRequestError;
|
||||
use crate::middleware::{NestTransform, Redirect, Transform};
|
||||
use crate::{Client, ClientConfig, ConnectRequest, ConnectResponse};
|
||||
use crate::{
|
||||
client::{ConnectInfo, Connector, ConnectorService, TcpConnectError, TcpConnection},
|
||||
connect::DefaultConnector,
|
||||
error::SendRequestError,
|
||||
middleware::{NestTransform, Redirect, Transform},
|
||||
Client, ClientConfig, ConnectRequest, ConnectResponse,
|
||||
};
|
||||
|
||||
/// An HTTP Client builder
|
||||
///
|
||||
|
|
@ -35,7 +33,7 @@ impl ClientBuilder {
|
|||
#[allow(clippy::new_ret_no_self)]
|
||||
pub fn new() -> ClientBuilder<
|
||||
impl Service<
|
||||
TcpConnect<Uri>,
|
||||
ConnectInfo<Uri>,
|
||||
Response = TcpConnection<Uri, TcpStream>,
|
||||
Error = TcpConnectError,
|
||||
> + Clone,
|
||||
|
|
@ -58,7 +56,7 @@ impl ClientBuilder {
|
|||
|
||||
impl<S, Io, M> ClientBuilder<S, M>
|
||||
where
|
||||
S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
|
||||
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
Io: ActixStream + fmt::Debug + 'static,
|
||||
|
|
@ -67,7 +65,7 @@ where
|
|||
pub fn connector<S1, Io1>(self, connector: Connector<S1>) -> ClientBuilder<S1, M>
|
||||
where
|
||||
S1: Service<
|
||||
TcpConnect<Uri>,
|
||||
ConnectInfo<Uri>,
|
||||
Response = TcpConnection<Uri, Io1>,
|
||||
Error = TcpConnectError,
|
||||
> + Clone
|
||||
|
|
|
|||
|
|
@ -12,9 +12,9 @@ use bytes::Bytes;
|
|||
use futures_core::future::LocalBoxFuture;
|
||||
use h2::client::SendRequest;
|
||||
|
||||
use actix_http::{
|
||||
body::MessageBody, h1::ClientCodec, Error, Payload, RequestHeadType, ResponseHead,
|
||||
};
|
||||
use actix_http::{body::MessageBody, h1::ClientCodec, Payload, RequestHeadType, ResponseHead};
|
||||
|
||||
use crate::BoxError;
|
||||
|
||||
use super::error::SendRequestError;
|
||||
use super::pool::Acquired;
|
||||
|
|
@ -173,6 +173,7 @@ impl H2ConnectionInner {
|
|||
/// Cancel spawned connection task on drop.
|
||||
impl Drop for H2ConnectionInner {
|
||||
fn drop(&mut self) {
|
||||
// TODO: this can end up sending extraneous requests; see if there is a better way to handle
|
||||
if self
|
||||
.sender
|
||||
.send_request(http::Request::new(()), true)
|
||||
|
|
@ -183,8 +184,8 @@ impl Drop for H2ConnectionInner {
|
|||
}
|
||||
}
|
||||
|
||||
/// Unified connection type cover HTTP/1 Plain/TLS and HTTP/2 protocols.
|
||||
#[allow(dead_code)]
|
||||
/// Unified connection type cover Http1 Plain/Tls and Http2 protocols
|
||||
pub enum Connection<A, B = Box<dyn ConnectionIo>>
|
||||
where
|
||||
A: ConnectionIo,
|
||||
|
|
@ -253,7 +254,7 @@ where
|
|||
where
|
||||
H: Into<RequestHeadType> + 'static,
|
||||
RB: MessageBody + 'static,
|
||||
RB::Error: Into<Error>,
|
||||
RB::Error: Into<BoxError>,
|
||||
{
|
||||
Box::pin(async move {
|
||||
match self {
|
||||
|
|
|
|||
|
|
@ -15,8 +15,8 @@ use actix_rt::{
|
|||
};
|
||||
use actix_service::Service;
|
||||
use actix_tls::connect::{
|
||||
new_connector, Connect as TcpConnect, ConnectError as TcpConnectError,
|
||||
Connection as TcpConnection, Resolver,
|
||||
ConnectError as TcpConnectError, ConnectInfo, Connection as TcpConnection,
|
||||
Connector as TcpConnector, Resolver,
|
||||
};
|
||||
use futures_core::{future::LocalBoxFuture, ready};
|
||||
use http::Uri;
|
||||
|
|
@ -28,13 +28,15 @@ use super::error::ConnectError;
|
|||
use super::pool::ConnectionPool;
|
||||
use super::Connect;
|
||||
|
||||
enum SslConnector {
|
||||
#[allow(dead_code)]
|
||||
enum OurTlsConnector {
|
||||
#[allow(dead_code)] // only dead when no TLS feature is enabled
|
||||
None,
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
Openssl(actix_tls::connect::ssl::openssl::SslConnector),
|
||||
Openssl(actix_tls::connect::openssl::reexports::SslConnector),
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
Rustls(std::sync::Arc<actix_tls::connect::ssl::rustls::ClientConfig>),
|
||||
Rustls(std::sync::Arc<actix_tls::connect::rustls::reexports::ClientConfig>),
|
||||
}
|
||||
|
||||
/// Manages HTTP client network connectivity.
|
||||
|
|
@ -53,38 +55,39 @@ enum SslConnector {
|
|||
pub struct Connector<T> {
|
||||
connector: T,
|
||||
config: ConnectorConfig,
|
||||
#[allow(dead_code)]
|
||||
ssl: SslConnector,
|
||||
|
||||
#[allow(dead_code)] // only dead when no TLS feature is enabled
|
||||
ssl: OurTlsConnector,
|
||||
}
|
||||
|
||||
impl Connector<()> {
|
||||
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
|
||||
pub fn new() -> Connector<
|
||||
impl Service<
|
||||
TcpConnect<Uri>,
|
||||
ConnectInfo<Uri>,
|
||||
Response = TcpConnection<Uri, TcpStream>,
|
||||
Error = actix_tls::connect::ConnectError,
|
||||
> + Clone,
|
||||
> {
|
||||
Connector {
|
||||
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
|
||||
connector: new_connector(resolver::resolver()),
|
||||
connector: TcpConnector::new(resolver::resolver()).service(),
|
||||
config: ConnectorConfig::default(),
|
||||
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
|
||||
}
|
||||
}
|
||||
|
||||
/// Provides an empty TLS connector when no TLS feature is enabled.
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {
|
||||
SslConnector::None
|
||||
fn build_ssl(_: Vec<Vec<u8>>) -> OurTlsConnector {
|
||||
OurTlsConnector::None
|
||||
}
|
||||
|
||||
/// Build TLS connector with rustls, based on supplied ALPN protocols
|
||||
///
|
||||
/// Note that if both `openssl` and `rustls` features are enabled, rustls will be used.
|
||||
#[cfg(feature = "rustls")]
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
|
||||
use actix_tls::connect::tls::rustls::{webpki_roots_cert_store, ClientConfig};
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> OurTlsConnector {
|
||||
use actix_tls::connect::rustls::{reexports::ClientConfig, webpki_roots_cert_store};
|
||||
|
||||
let mut config = ClientConfig::builder()
|
||||
.with_safe_defaults()
|
||||
|
|
@ -93,13 +96,13 @@ impl Connector<()> {
|
|||
|
||||
config.alpn_protocols = protocols;
|
||||
|
||||
SslConnector::Rustls(std::sync::Arc::new(config))
|
||||
OurTlsConnector::Rustls(std::sync::Arc::new(config))
|
||||
}
|
||||
|
||||
/// Build TLS connector with openssl, based on supplied ALPN protocols
|
||||
#[cfg(all(feature = "openssl", not(feature = "rustls")))]
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
|
||||
use actix_tls::connect::tls::openssl::{SslConnector as OpensslConnector, SslMethod};
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> OurTlsConnector {
|
||||
use actix_tls::connect::openssl::reexports::{SslConnector, SslMethod};
|
||||
use bytes::{BufMut, BytesMut};
|
||||
|
||||
let mut alpn = BytesMut::with_capacity(20);
|
||||
|
|
@ -108,12 +111,12 @@ impl Connector<()> {
|
|||
alpn.put(proto.as_slice());
|
||||
}
|
||||
|
||||
let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
|
||||
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
if let Err(err) = ssl.set_alpn_protos(&alpn) {
|
||||
log::error!("Can not set ALPN protocol: {:?}", err);
|
||||
}
|
||||
|
||||
SslConnector::Openssl(ssl.build())
|
||||
OurTlsConnector::Openssl(ssl.build())
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -123,7 +126,7 @@ impl<S> Connector<S> {
|
|||
where
|
||||
Io1: ActixStream + fmt::Debug + 'static,
|
||||
S1: Service<
|
||||
TcpConnect<Uri>,
|
||||
ConnectInfo<Uri>,
|
||||
Response = TcpConnection<Uri, Io1>,
|
||||
Error = TcpConnectError,
|
||||
> + Clone,
|
||||
|
|
@ -136,7 +139,7 @@ impl<S> Connector<S> {
|
|||
}
|
||||
}
|
||||
|
||||
impl<S, Io> Connector<S>
|
||||
impl<S, IO> Connector<S>
|
||||
where
|
||||
// Note:
|
||||
// Input Io type is bound to ActixStream trait but internally in client module they
|
||||
|
|
@ -145,8 +148,8 @@ where
|
|||
//
|
||||
// This remap is to hide ActixStream's trait methods. They are not meant to be called
|
||||
// from user code.
|
||||
Io: ActixStream + fmt::Debug + 'static,
|
||||
S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
|
||||
IO: ActixStream + fmt::Debug + 'static,
|
||||
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, IO>, Error = TcpConnectError>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
|
|
@ -166,18 +169,21 @@ where
|
|||
|
||||
#[cfg(feature = "openssl")]
|
||||
/// Use custom `SslConnector` instance.
|
||||
pub fn ssl(mut self, connector: actix_tls::connect::ssl::openssl::SslConnector) -> Self {
|
||||
self.ssl = SslConnector::Openssl(connector);
|
||||
pub fn ssl(
|
||||
mut self,
|
||||
connector: actix_tls::connect::openssl::reexports::SslConnector,
|
||||
) -> Self {
|
||||
self.ssl = OurTlsConnector::Openssl(connector);
|
||||
self
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
/// Use custom `SslConnector` instance.
|
||||
/// Use custom `ClientConfig` instance.
|
||||
pub fn rustls(
|
||||
mut self,
|
||||
connector: std::sync::Arc<actix_tls::connect::ssl::rustls::ClientConfig>,
|
||||
connector: std::sync::Arc<actix_tls::connect::rustls::reexports::ClientConfig>,
|
||||
) -> Self {
|
||||
self.ssl = SslConnector::Rustls(connector);
|
||||
self.ssl = OurTlsConnector::Rustls(connector);
|
||||
self
|
||||
}
|
||||
|
||||
|
|
@ -189,7 +195,7 @@ where
|
|||
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
|
||||
http::Version::HTTP_2 => vec![b"h2".to_vec(), b"http/1.1".to_vec()],
|
||||
_ => {
|
||||
unimplemented!("actix-http:client: supported versions http/1.1, http/2")
|
||||
unimplemented!("actix-http client only supports versions http/1.1 & http/2")
|
||||
}
|
||||
};
|
||||
self.ssl = Connector::build_ssl(versions);
|
||||
|
|
@ -266,7 +272,7 @@ where
|
|||
/// Finish configuration process and create connector service.
|
||||
/// The Connector builder always concludes by calling `finish()` last in
|
||||
/// its combinator chain.
|
||||
pub fn finish(self) -> ConnectorService<S, Io> {
|
||||
pub fn finish(self) -> ConnectorService<S, IO> {
|
||||
let local_address = self.config.local_address;
|
||||
let timeout = self.config.timeout;
|
||||
|
||||
|
|
@ -279,14 +285,70 @@ where
|
|||
};
|
||||
|
||||
let tls_service = match self.ssl {
|
||||
SslConnector::None => None,
|
||||
OurTlsConnector::None => {
|
||||
#[cfg(not(feature = "dangerous-h2c"))]
|
||||
{
|
||||
None
|
||||
}
|
||||
|
||||
#[cfg(feature = "dangerous-h2c")]
|
||||
{
|
||||
use std::io;
|
||||
|
||||
use actix_tls::connect::Connection;
|
||||
use actix_utils::future::{ready, Ready};
|
||||
|
||||
impl IntoConnectionIo for TcpConnection<Uri, Box<dyn ConnectionIo>> {
|
||||
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
|
||||
let io = self.into_parts().0;
|
||||
(io, Protocol::Http2)
|
||||
}
|
||||
}
|
||||
|
||||
/// With the `dangerous-h2c` feature enabled, this connector uses a no-op TLS
|
||||
/// connection service that passes through plain TCP as a TLS connection.
|
||||
///
|
||||
/// The protocol version of this fake TLS connection is set to be HTTP/2.
|
||||
#[derive(Clone)]
|
||||
struct NoOpTlsConnectorService;
|
||||
|
||||
impl<R, IO> Service<Connection<R, IO>> for NoOpTlsConnectorService
|
||||
where
|
||||
IO: ActixStream + 'static,
|
||||
{
|
||||
type Response = Connection<R, Box<dyn ConnectionIo>>;
|
||||
type Error = io::Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&self, connection: Connection<R, IO>) -> Self::Future {
|
||||
let (io, connection) = connection.replace_io(());
|
||||
let (_, connection) = connection.replace_io(Box::new(io) as _);
|
||||
|
||||
ready(Ok(connection))
|
||||
}
|
||||
}
|
||||
|
||||
let handshake_timeout = self.config.handshake_timeout;
|
||||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
tls_service: NoOpTlsConnectorService,
|
||||
timeout: handshake_timeout,
|
||||
};
|
||||
|
||||
Some(actix_service::boxed::rc_service(tls_service))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
SslConnector::Openssl(tls) => {
|
||||
OurTlsConnector::Openssl(tls) => {
|
||||
const H2: &[u8] = b"h2";
|
||||
|
||||
use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream};
|
||||
use actix_tls::connect::openssl::{reexports::AsyncSslStream, TlsConnector};
|
||||
|
||||
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, SslStream<Io>> {
|
||||
impl<IO: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncSslStream<IO>> {
|
||||
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
|
||||
let sock = self.into_parts().0;
|
||||
let h2 = sock
|
||||
|
|
@ -305,19 +367,20 @@ where
|
|||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
tls_service: OpensslConnector::service(tls),
|
||||
tls_service: TlsConnector::service(tls),
|
||||
timeout: handshake_timeout,
|
||||
};
|
||||
|
||||
Some(actix_service::boxed::rc_service(tls_service))
|
||||
}
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
SslConnector::Rustls(tls) => {
|
||||
OurTlsConnector::Rustls(tls) => {
|
||||
const H2: &[u8] = b"h2";
|
||||
|
||||
use actix_tls::connect::ssl::rustls::{RustlsConnector, TlsStream};
|
||||
use actix_tls::connect::rustls::{reexports::AsyncTlsStream, TlsConnector};
|
||||
|
||||
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> {
|
||||
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, AsyncTlsStream<Io>> {
|
||||
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
|
||||
let sock = self.into_parts().0;
|
||||
let h2 = sock
|
||||
|
|
@ -337,7 +400,7 @@ where
|
|||
|
||||
let tls_service = TlsConnectorService {
|
||||
tcp_service: tcp_service_inner,
|
||||
tls_service: RustlsConnector::service(tls),
|
||||
tls_service: TlsConnector::service(tls),
|
||||
timeout: handshake_timeout,
|
||||
};
|
||||
|
||||
|
|
@ -406,26 +469,28 @@ where
|
|||
|
||||
/// service for establish tcp connection and do client tls handshake.
|
||||
/// operation is canceled when timeout limit reached.
|
||||
struct TlsConnectorService<S, St> {
|
||||
/// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting.
|
||||
tcp_service: S,
|
||||
/// tls connection is canceled on `TlsConnectorService`'s timeout setting.
|
||||
tls_service: St,
|
||||
struct TlsConnectorService<Tcp, Tls> {
|
||||
/// TCP connection is canceled on `TcpConnectorInnerService`'s timeout setting.
|
||||
tcp_service: Tcp,
|
||||
|
||||
/// TLS connection is canceled on `TlsConnectorService`'s timeout setting.
|
||||
tls_service: Tls,
|
||||
|
||||
timeout: Duration,
|
||||
}
|
||||
|
||||
impl<S, St, Io> Service<Connect> for TlsConnectorService<S, St>
|
||||
impl<Tcp, Tls, IO> Service<Connect> for TlsConnectorService<Tcp, Tls>
|
||||
where
|
||||
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
|
||||
Tcp: Service<Connect, Response = TcpConnection<Uri, IO>, Error = ConnectError>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
St: Service<TcpConnection<Uri, Io>, Error = std::io::Error> + Clone + 'static,
|
||||
Io: ConnectionIo,
|
||||
St::Response: IntoConnectionIo,
|
||||
Tls: Service<TcpConnection<Uri, IO>, Error = std::io::Error> + Clone + 'static,
|
||||
Tls::Response: IntoConnectionIo,
|
||||
IO: ConnectionIo,
|
||||
{
|
||||
type Response = (Box<dyn ConnectionIo>, Protocol);
|
||||
type Error = ConnectError;
|
||||
type Future = TlsConnectorFuture<St, S::Future, St::Future>;
|
||||
type Future = TlsConnectorFuture<Tls, Tcp::Future, Tls::Future>;
|
||||
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
ready!(self.tcp_service.poll_ready(cx))?;
|
||||
|
|
@ -525,7 +590,7 @@ impl<S: Clone> TcpConnectorInnerService<S> {
|
|||
|
||||
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
|
||||
where
|
||||
S: Service<TcpConnect<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
|
||||
S: Service<ConnectInfo<Uri>, Response = TcpConnection<Uri, Io>, Error = TcpConnectError>
|
||||
+ Clone
|
||||
+ 'static,
|
||||
{
|
||||
|
|
@ -536,7 +601,7 @@ where
|
|||
actix_service::forward_ready!(service);
|
||||
|
||||
fn call(&self, req: Connect) -> Self::Future {
|
||||
let mut req = TcpConnect::new(req.uri).set_addr(req.addr);
|
||||
let mut req = ConnectInfo::new(req.uri).set_addr(req.addr);
|
||||
|
||||
if let Some(local_addr) = self.local_address {
|
||||
req = req.set_local_addr(local_addr);
|
||||
|
|
@ -575,8 +640,8 @@ where
|
|||
}
|
||||
|
||||
/// Connector service for pooled Plain/Tls Tcp connections.
|
||||
pub type ConnectorService<S, Io> = ConnectorServicePriv<
|
||||
TcpConnectorService<TcpConnectorInnerService<S>>,
|
||||
pub type ConnectorService<Svc, IO> = ConnectorServicePriv<
|
||||
TcpConnectorService<TcpConnectorInnerService<Svc>>,
|
||||
Rc<
|
||||
dyn Service<
|
||||
Connect,
|
||||
|
|
@ -588,7 +653,7 @@ pub type ConnectorService<S, Io> = ConnectorServicePriv<
|
|||
>,
|
||||
>,
|
||||
>,
|
||||
Io,
|
||||
IO,
|
||||
Box<dyn ConnectionIo>,
|
||||
>;
|
||||
|
||||
|
|
@ -687,7 +752,7 @@ mod resolver {
|
|||
use super::*;
|
||||
|
||||
pub(super) fn resolver() -> Resolver {
|
||||
Resolver::Default
|
||||
Resolver::default()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -729,8 +794,7 @@ mod resolver {
|
|||
}
|
||||
}
|
||||
|
||||
// dns struct is cached in thread local.
|
||||
// so new client constructor can reuse the existing dns resolver.
|
||||
// resolver struct is cached in thread local so new clients can reuse the existing instance
|
||||
thread_local! {
|
||||
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
|
||||
}
|
||||
|
|
@ -738,8 +802,10 @@ mod resolver {
|
|||
// get from thread local or construct a new trust-dns resolver.
|
||||
TRUST_DNS_RESOLVER.with(|local| {
|
||||
let resolver = local.borrow().as_ref().map(Clone::clone);
|
||||
|
||||
match resolver {
|
||||
Some(resolver) => resolver,
|
||||
|
||||
None => {
|
||||
let (cfg, opts) = match read_system_conf() {
|
||||
Ok((cfg, opts)) => (cfg, opts),
|
||||
|
|
@ -752,11 +818,51 @@ mod resolver {
|
|||
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
|
||||
|
||||
// box trust dns resolver and put it in thread local.
|
||||
let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
|
||||
let resolver = Resolver::custom(TrustDnsResolver(resolver));
|
||||
*local.borrow_mut() = Some(resolver.clone());
|
||||
|
||||
resolver
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "dangerous-h2c")]
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::convert::Infallible;
|
||||
|
||||
use actix_http::{HttpService, Request, Response, Version};
|
||||
use actix_http_test::test_server;
|
||||
use actix_service::ServiceFactoryExt as _;
|
||||
|
||||
use super::*;
|
||||
use crate::Client;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn h2c_connector() {
|
||||
let mut srv = test_server(|| {
|
||||
HttpService::build()
|
||||
.h2(|_req: Request| async { Ok::<_, Infallible>(Response::ok()) })
|
||||
.tcp()
|
||||
.map_err(|_| ())
|
||||
})
|
||||
.await;
|
||||
|
||||
let connector = Connector {
|
||||
connector: TcpConnector::new(resolver::resolver()).service(),
|
||||
config: ConnectorConfig::default(),
|
||||
ssl: OurTlsConnector::None,
|
||||
};
|
||||
|
||||
let client = Client::builder().connector(connector).finish();
|
||||
|
||||
let request = client.get(srv.surl("/")).send();
|
||||
let response = request.await.unwrap();
|
||||
assert!(response.status().is_success());
|
||||
assert_eq!(response.version(), Version::HTTP_2);
|
||||
|
||||
srv.stop().await;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue