1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-04 18:06:23 +02:00

Compare commits

..

5 Commits

Author SHA1 Message Date
bc54cdd772 prepare actix-http release 2.2.2 2022-01-21 21:23:22 +00:00
ad7e3c06e7 migrate to brotli crate 2022-01-21 21:16:23 +00:00
0669ed0f06 soft-deprecate NormalizePath::default in v3 (#2529) 2021-12-18 22:57:23 +00:00
c9c36679e4 bump actix http version 2021-08-11 19:21:40 +01:00
655d7b4f05 sec fixes 2021-08-08 21:21:48 +01:00
204 changed files with 8138 additions and 9105 deletions

View File

@ -1,21 +1,21 @@
<!-- Thanks for considering contributing actix! -->
<!-- Please fill out the following to get your PR reviewed quicker. -->
<!-- Please fill out the following to make our reviews easy. -->
## PR Type
<!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
PR_TYPE
INSERT_PR_TYPE
## PR Checklist
<!-- Check your PR fulfills the following items. ->>
Check your PR fulfills the following:
<!-- For draft PRs check the boxes as you complete them. -->
- [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated.
- [ ] A changelog entry has been made for the appropriate packages.
- [ ] Format code with the latest stable rustfmt.
- [ ] (Team) Label with affected crates and semver status.
- [ ] Format code with the latest stable rustfmt
## Overview

View File

@ -1,4 +1,4 @@
name: Benchmark
name: Benchmark (Linux)
on:
pull_request:

View File

@ -1,39 +1,32 @@
name: Lint
on:
pull_request:
types: [opened, synchronize, reopened]
name: Clippy and rustfmt Check
jobs:
fmt:
clippy_check:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
components: rustfmt
override: true
- name: Check with rustfmt
uses: actions-rs/cargo@v1
with:
command: fmt
args: --all -- --check
clippy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install Rust
uses: actions-rs/toolchain@v1
- uses: actions-rs/toolchain@v1
with:
toolchain: stable
toolchain: nightly
components: clippy
override: true
- name: Check with Clippy
uses: actions-rs/clippy-check@v1
with:
token: ${{ secrets.GITHUB_TOKEN }}
args: --workspace --tests --all-features
args: --all-features --all --tests

View File

@ -13,7 +13,7 @@ jobs:
fail-fast: false
matrix:
version:
- 1.46.0 # MSRV
- 1.42.0 # MSRV
- stable
- nightly
@ -30,13 +30,6 @@ jobs:
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- name: check build
uses: actions-rs/cargo@v1
with:
@ -65,17 +58,12 @@ jobs:
args: --package=awc --no-default-features --features=rustls -- --nocapture
- name: Generate coverage file
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
run: |
cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --out Xml
- name: Upload to Codecov
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
if: matrix.version == 'stable' && (github.ref == 'refs/heads/master' || github.event_name == 'pull_request')
uses: codecov/codecov-action@v1
with:
file: cobertura.xml
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -29,13 +29,6 @@ jobs:
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- name: check build
uses: actions-rs/cargo@v1
with:
@ -49,8 +42,3 @@ jobs:
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -24,7 +24,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: doc
args: --workspace --all-features --no-deps
args: --no-deps --workspace --all-features
- name: Tweak HTML
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html

View File

@ -41,13 +41,6 @@ jobs:
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.0.1
- name: check build
uses: actions-rs/cargo@v1
with:
@ -69,8 +62,3 @@ jobs:
--skip=test_connection_force_close
--skip=test_connection_server_close
--skip=test_connection_wait_queue_force_close
- name: Clear the cargo caches
run: |
cargo install cargo-cache --no-default-features --features ci-autoclean
cargo-cache

View File

@ -1,78 +1,14 @@
# Changes
## Unreleased - 2021-xx-xx
## Unreleased - 2020-xx-xx
## 4.0.0-beta.2 - 2021-xx-xx
### Added
* The method `Either<web::Json<T>, web::Form<T>>::into_inner()` which returns the inner type for
whichever variant was created. Also works for `Either<web::Form<T>, web::Json<T>>`. [#1894]
* Add `services!` macro for helping register multiple services to `App`. [#1933]
* Enable registering a vec of services of the same type to `App` [#1933]
## 3.3.3 - 2021-12-18
### Changed
* Rework `Responder` trait to be sync and returns `Response`/`HttpResponse` directly.
Making it simpler and more performant. [#1891]
* `ServiceRequest::into_parts` and `ServiceRequest::from_parts` can no longer fail. [#1893]
* `ServiceRequest::from_request` can no longer fail. [#1893]
* Our `Either` type now uses `Left`/`Right` variants (instead of `A`/`B`) [#1894]
* `test::{call_service, read_response, read_response_json, send_request}` take `&Service`
in argument [#1905]
* `App::wrap_fn`, `Resource::wrap_fn` and `Scope::wrap_fn` provide `&Service` in closure
argument. [#1905]
* `web::block` no longer requires the output is a Result. [#1957]
* Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529]
### Fixed
* Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906]
[#2529]: https://github.com/actix/actix-web/pull/2529
### Removed
* Public field of `web::Path` has been made private. [#1894]
* Public field of `web::Query` has been made private. [#1894]
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
* `AppService::set_service_data`; for custom HTTP service factories adding application data, use the
layered data model by calling `ServiceRequest::add_data_container` when handling
requests instead. [#1906]
[#1891]: https://github.com/actix/actix-web/pull/1891
[#1893]: https://github.com/actix/actix-web/pull/1893
[#1894]: https://github.com/actix/actix-web/pull/1894
[#1869]: https://github.com/actix/actix-web/pull/1869
[#1905]: https://github.com/actix/actix-web/pull/1905
[#1906]: https://github.com/actix/actix-web/pull/1906
[#1933]: https://github.com/actix/actix-web/pull/1933
[#1957]: https://github.com/actix/actix-web/pull/1957
## 4.0.0-beta.1 - 2021-01-07
### Added
* `Compat` middleware enabling generic response body/error type of middlewares like `Logger` and
`Compress` to be used in `middleware::Condition` and `Resource`, `Scope` services. [#1865]
### Changed
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
* Bumped `rand` to `0.8`.
* Update `rust-tls` to `0.19`. [#1813]
* Rename `Handler` to `HandlerService` and rename `Factory` to `Handler`. [#1852]
* The default `TrailingSlash` is now `Trim`, in line with existing documentation. See migration
guide for implications. [#1875]
* Rename `DefaultHeaders::{content_type => add_content_type}`. [#1875]
* MSRV is now 1.46.0.
### Fixed
* Added the underlying parse error to `test::read_body_json`'s panic message. [#1812]
### Removed
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
exposed directly by the `middleware` module.
* Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
from `actix_web::error` module. [#1878]
[#1812]: https://github.com/actix/actix-web/pull/1812
[#1813]: https://github.com/actix/actix-web/pull/1813
[#1852]: https://github.com/actix/actix-web/pull/1852
[#1865]: https://github.com/actix/actix-web/pull/1865
[#1875]: https://github.com/actix/actix-web/pull/1875
[#1878]: https://github.com/actix/actix-web/pull/1878
## 3.3.2 - 2020-12-01
### Fixed

View File

@ -1,6 +1,6 @@
[package]
name = "actix-web"
version = "4.0.0-beta.2"
version = "3.3.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
readme = "README.md"
@ -34,7 +34,7 @@ members = [
"actix-multipart",
"actix-web-actors",
"actix-web-codegen",
"actix-http-test",
"test-server",
]
[features]
@ -47,10 +47,10 @@ compress = ["actix-http/compress", "awc/compress"]
secure-cookies = ["actix-http/secure-cookies"]
# openssl
openssl = ["tls_openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"]
openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"]
# rustls
rustls = ["tls_rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"]
rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"]
[[example]]
name = "basic"
@ -73,26 +73,28 @@ name = "client"
required-features = ["rustls"]
[dependencies]
actix-codec = "0.4.0-beta.1"
actix-macros = "0.2.0"
actix-router = "0.2.7"
actix-rt = "2"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0-beta.4"
actix-utils = "3.0.0-beta.2"
actix-tls = { version = "3.0.0-beta.3", default-features = false, optional = true }
actix-codec = "0.3.0"
actix-service = "1.0.6"
actix-utils = "2.0.0"
actix-router = "0.2.4"
actix-rt = "1.1.1"
actix-server = "1.0.0"
actix-testing = "1.0.0"
actix-macros = "0.1.0"
actix-threadpool = "0.3.1"
actix-tls = "2.0.0"
actix-web-codegen = "0.4.0"
actix-http = "3.0.0-beta.2"
awc = { version = "3.0.0-beta.2", default-features = false }
actix-http = "2.2.0"
awc = { version = "2.0.3", default-features = false }
ahash = "0.7"
bytes = "1"
bytes = "0.5.3"
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false }
futures-util = { version = "0.3.5", default-features = false }
fxhash = "0.2.1"
log = "0.4"
mime = "0.3"
socket2 = "0.3.16"
@ -101,18 +103,19 @@ regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] }
time = { version = "0.2.7", default-features = false, features = ["std"] }
url = "2.1"
tls_openssl = { package = "openssl", version = "0.10.9", optional = true }
tls_rustls = { package = "rustls", version = "0.19.0", optional = true }
smallvec = "1.6"
open-ssl = { package = "openssl", version = "0.10", optional = true }
rust-tls = { package = "rustls", version = "0.18.0", optional = true }
tinyvec = { version = "1", features = ["alloc"] }
[dev-dependencies]
actix = { version = "0.11.0-beta.2", default-features = false }
rand = "0.8"
actix = "0.10.0"
actix-http = { version = "2.1.0", features = ["actors"] }
rand = "0.7"
env_logger = "0.8"
serde_derive = "1.0"
brotli2 = "0.3.2"
brotli = "3.3.3"
flate2 = "1.0.13"
criterion = "0.3"
@ -124,11 +127,10 @@ codegen-units = 1
[patch.crates-io]
actix-web = { path = "." }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-web-actors = { path = "actix-web-actors" }
actix-http-test = { path = "test-server" }
actix-web-codegen = { path = "actix-web-codegen" }
actix-multipart = { path = "actix-multipart" }
actix-files = { path = "actix-files" }
actix-multipart = { path = "actix-multipart" }
awc = { path = "awc" }
[[bench]]
@ -138,7 +140,3 @@ harness = false
[[bench]]
name = "service"
harness = false
[[bench]]
name = "responder"
harness = false

View File

@ -1,15 +1,5 @@
## Unreleased
* The default `NormalizePath` behavior now strips trailing slashes by default. This was
previously documented to be the case in v3 but the behavior now matches. The effect is that
routes defined with trailing slashes will become inaccessible when
using `NormalizePath::default()`.
Before: `#[get("/test/")`
After: `#[get("/test")`
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
## 3.0.0

View File

@ -1,19 +1,20 @@
<div align="center">
<h1>Actix Web</h1>
<h1>Actix web</h1>
<p>
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
</p>
<p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.2)](https://docs.rs/actix-web/4.0.0-beta.2)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.2/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.2)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=3.3.3)](https://docs.rs/actix-web/3.3.3)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
![License](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/3.3.3/status.svg)](https://deps.rs/crate/actix-web/3.3.3)
<br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions)
[![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web)
[![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web)
![downloads](https://img.shields.io/crates/d/actix-web.svg)
[![Download](https://img.shields.io/crates/d/actix-web.svg)](https://crates.io/crates/actix-web)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
</p>
@ -33,7 +34,7 @@
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Supports [Actix actor framework](https://github.com/actix/actix)
* Runs on stable Rust 1.46+
* Runs on stable Rust 1.42+
## Documentation
@ -98,13 +99,13 @@ One of the fastest web frameworks available according to the
This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
[http://www.apache.org/licenses/LICENSE-2.0])
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
[http://opensource.org/licenses/MIT])
[http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
at your option.
## Code of Conduct
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant.
The Actix team promises to intervene to uphold that code of conduct.
Contribution to the actix-web crate is organized under the terms of the Contributor Covenant, the
maintainers of Actix web, promises to intervene to uphold that code of conduct.

View File

@ -1,27 +1,6 @@
# Changes
## Unreleased - 2021-xx-xx
## 0.6.0-beta.2 - 2021-02-10
* Fix If-Modified-Since and If-Unmodified-Since to not compare using sub-second timestamps. [#1887]
* Replace `v_htmlescape` with `askama_escape`. [#1953]
[#1887]: https://github.com/actix/actix-web/pull/1887
[#1953]: https://github.com/actix/actix-web/pull/1953
## 0.6.0-beta.1 - 2021-01-07
* `HttpRange::parse` now has its own error type.
* Update `bytes` to `1.0`. [#1813]
[#1813]: https://github.com/actix/actix-web/pull/1813
## 0.5.0 - 2020-12-26
* Optionally support hidden files/directories. [#1811]
[#1811]: https://github.com/actix/actix-web/pull/1811
## Unreleased - 2020-xx-xx
## 0.4.1 - 2020-11-24

View File

@ -1,6 +1,6 @@
[package]
name = "actix-files"
version = "0.6.0-beta.2"
version = "0.4.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static file serving for Actix Web"
readme = "README.md"
@ -17,20 +17,19 @@ name = "actix_files"
path = "src/lib.rs"
[dependencies]
actix-web = { version = "4.0.0-beta.2", default-features = false }
actix-service = "2.0.0-beta.4"
askama_escape = "0.10"
actix-web = { version = "3.0.0", default-features = false }
actix-service = "1.0.6"
bitflags = "1"
bytes = "1"
bytes = "0.5.3"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
derive_more = "0.99.5"
derive_more = "0.99.2"
log = "0.4"
mime = "0.3"
mime_guess = "2.0.1"
percent-encoding = "2.1"
v_htmlescape = "0.11"
[dev-dependencies]
actix-rt = "2"
actix-web = "4.0.0-beta.2"
actix-rt = "1.0.0"
actix-web = { version = "3.0.0", features = ["openssl"] }

View File

@ -3,11 +3,11 @@
> Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.5.0)](https://docs.rs/actix-files/0.5.0)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.4.1)](https://docs.rs/actix-files/0.4.1)
[![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
![License](https://img.shields.io/crates/l/actix-files.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-files/0.5.0/status.svg)](https://deps.rs/crate/actix-files/0.5.0)
[![dependency status](https://deps.rs/crate/actix-files/0.4.1/status.svg)](https://deps.rs/crate/actix-files/0.4.1)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
@ -16,4 +16,4 @@
- [API Documentation](https://docs.rs/actix-files/)
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum supported Rust version: 1.46 or later
- Minimum supported Rust version: 1.42 or later

View File

@ -9,35 +9,26 @@ use std::{
use actix_web::{
error::{BlockingError, Error},
rt::task::{spawn_blocking, JoinHandle},
web,
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use futures_util::future::{FutureExt, LocalBoxFuture};
use crate::handle_error;
type ChunkedBoxFuture =
LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>;
#[doc(hidden)]
/// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`.
pub struct ChunkedReadFile {
size: u64,
offset: u64,
state: ChunkedReadFileState,
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,
}
}
pub(crate) size: u64,
pub(crate) offset: u64,
pub(crate) file: Option<File>,
pub(crate) fut: Option<ChunkedBoxFuture>,
pub(crate) counter: u64,
}
impl fmt::Debug for ChunkedReadFile {
@ -53,52 +44,51 @@ impl Stream for ChunkedReadFile {
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 let Some(ref mut fut) = self.fut {
return match ready!(Pin::new(fut).poll(cx)) {
Ok((file, bytes)) => {
self.fut.take();
self.file = Some(file);
if size == counter {
Poll::Ready(None)
} else {
let mut file = file
.take()
.expect("ChunkedReadFile polled after completion");
self.offset += bytes.len() as u64;
self.counter += bytes.len() as u64;
let fut = spawn_blocking(move || {
let max_bytes =
cmp::min(size.saturating_sub(counter), 65_536) as usize;
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)?;
if n_bytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into());
}
Ok((file, Bytes::from(buf)))
});
this.state = ChunkedReadFileState::Future(fut);
self.poll_next(cx)
Poll::Ready(Some(Ok(bytes)))
}
}
ChunkedReadFileState::Future(ref mut fut) => {
let (file, bytes) =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
this.state = ChunkedReadFileState::File(Some(file));
Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
};
}
this.offset += bytes.len() as u64;
this.counter += bytes.len() as u64;
let size = self.size;
let offset = self.offset;
let counter = self.counter;
Poll::Ready(Some(Ok(bytes)))
}
if size == counter {
Poll::Ready(None)
} else {
let mut file = self.file.take().expect("Use after completion");
self.fut = Some(
web::block(move || {
let max_bytes =
cmp::min(size.saturating_sub(counter), 65_536) as usize;
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)?;
if n_bytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into());
}
Ok((file, Bytes::from(buf)))
})
.boxed_local(),
);
self.poll_next(cx)
}
}
}

View File

@ -1,8 +1,8 @@
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use askama_escape::{escape as escape_html_entity, Html};
use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
/// A directory; responds with the generated directory listing.
#[derive(Debug)]
@ -50,7 +50,7 @@ macro_rules! encode_file_url {
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f;
macro_rules! encode_file_name {
($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
escape_html_entity(&$entry.file_name().to_string_lossy())
};
}

View File

@ -1,6 +1,6 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory};
use actix_web::{
dev::{
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
@ -39,7 +39,6 @@ pub struct Files {
mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>,
hidden_files: bool,
}
impl fmt::Debug for Files {
@ -61,7 +60,6 @@ impl Clone for Files {
path: self.path.clone(),
mime_override: self.mime_override.clone(),
guards: self.guards.clone(),
hidden_files: self.hidden_files,
}
}
}
@ -82,9 +80,8 @@ impl Files {
/// be inaccessible. Register more specific handlers and services first.
///
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
/// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are
/// adjusted with work load. More threads would spawn when need and threads goes idle for a
/// period of time would be de-spawned.
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
/// by setting ACTIX_THREADPOOL environment variable.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() {
@ -106,7 +103,6 @@ impl Files {
mime_override: None,
file_flags: named::Flags::default(),
guards: None,
hidden_files: false,
}
}
@ -202,10 +198,10 @@ impl Files {
/// Sets default handler which is used when no matched file could be found.
pub fn default_handler<F, U>(mut self, f: F) -> Self
where
F: IntoServiceFactory<U, ServiceRequest>,
F: IntoServiceFactory<U>,
U: ServiceFactory<
ServiceRequest,
Config = (),
Request = ServiceRequest,
Response = ServiceResponse,
Error = Error,
> + 'static,
@ -217,13 +213,6 @@ impl Files {
self
}
/// Enables serving hidden files and directories, allowing a leading dots in url fragments.
#[inline]
pub fn use_hidden_files(mut self) -> Self {
self.hidden_files = true;
self
}
}
impl HttpServiceFactory for Files {
@ -242,7 +231,8 @@ impl HttpServiceFactory for Files {
}
}
impl ServiceFactory<ServiceRequest> for Files {
impl ServiceFactory for Files {
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type Config = ();
@ -261,7 +251,6 @@ impl ServiceFactory<ServiceRequest> for Files {
mime_override: self.mime_override.clone(),
file_flags: self.file_flags,
guards: self.guards.clone(),
hidden_files: self.hidden_files,
};
if let Some(ref default) = *self.default.borrow() {

View File

@ -14,10 +14,12 @@
#![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)]
use std::io;
use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{
dev::{ServiceRequest, ServiceResponse},
error::Error,
error::{BlockingError, Error, ErrorInternalServerError},
http::header::DispositionType,
};
use mime_guess::from_ext;
@ -54,6 +56,13 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
from_ext(ext).first_or_octet_stream()
}
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> Error {
match err {
BlockingError::Error(err) => err.into(),
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
}
}
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
#[cfg(test)]
@ -73,8 +82,7 @@ mod tests {
},
middleware::Compress,
test::{self, TestRequest},
web::{self, Bytes},
App, HttpResponse, Responder,
web, App, HttpResponse, Responder,
};
use futures_util::future::ok;
@ -102,19 +110,7 @@ mod tests {
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.insert_header((header::IF_MODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
}
#[actix_rt::test]
async fn test_if_modified_since_without_if_none_match_same() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since = file.last_modified().unwrap();
let req = TestRequest::default()
.insert_header((header::IF_MODIFIED_SINCE, since))
.header(header::IF_MODIFIED_SINCE, since)
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
@ -127,37 +123,13 @@ mod tests {
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default()
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
.insert_header((header::IF_MODIFIED_SINCE, since))
.header(header::IF_NONE_MATCH, "miss_etag")
.header(header::IF_MODIFIED_SINCE, since)
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
}
#[actix_rt::test]
async fn test_if_unmodified_since() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since = file.last_modified().unwrap();
let req = TestRequest::default()
.insert_header((header::IF_UNMODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_if_unmodified_since_failed() {
let file = NamedFile::open("Cargo.toml").unwrap();
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
let req = TestRequest::default()
.insert_header((header::IF_UNMODIFIED_SINCE, since))
.to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
}
#[actix_rt::test]
async fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err());
@ -366,7 +338,7 @@ mod tests {
DispositionType::Attachment
}
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(
Files::new("/", ".")
.mime_override(all_attachment)
@ -376,7 +348,7 @@ mod tests {
.await;
let request = TestRequest::get().uri("/").to_request();
let response = test::call_service(&srv, request).await;
let response = test::call_service(&mut srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let content_disposition = response
@ -391,7 +363,7 @@ mod tests {
#[actix_rt::test]
async fn test_named_file_ranges_status_code() {
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
)
.await;
@ -399,17 +371,17 @@ mod tests {
// Valid range header
let request = TestRequest::get()
.uri("/t%65st/Cargo.toml")
.insert_header((header::RANGE, "bytes=10-20"))
.header(header::RANGE, "bytes=10-20")
.to_request();
let response = test::call_service(&srv, request).await;
let response = test::call_service(&mut srv, request).await;
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
// Invalid range header
let request = TestRequest::get()
.uri("/t%65st/Cargo.toml")
.insert_header((header::RANGE, "bytes=1-0"))
.header(header::RANGE, "bytes=1-0")
.to_request();
let response = test::call_service(&srv, request).await;
let response = test::call_service(&mut srv, request).await;
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
}
@ -421,7 +393,7 @@ mod tests {
// Valid range header
let response = srv
.get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=10-20"))
.header(header::RANGE, "bytes=10-20")
.send()
.await
.unwrap();
@ -431,7 +403,7 @@ mod tests {
// Invalid range header
let response = srv
.get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=10-5"))
.header(header::RANGE, "bytes=10-5")
.send()
.await
.unwrap();
@ -446,7 +418,7 @@ mod tests {
// Valid range header
let response = srv
.get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=10-20"))
.header(header::RANGE, "bytes=10-20")
.send()
.await
.unwrap();
@ -456,7 +428,7 @@ mod tests {
// Valid range header, starting from 0
let response = srv
.get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=0-20"))
.header(header::RANGE, "bytes=0-20")
.send()
.await
.unwrap();
@ -496,14 +468,14 @@ mod tests {
#[actix_rt::test]
async fn test_static_files_with_spaces() {
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
)
.await;
let request = TestRequest::get()
.uri("/tests/test%20space.binary")
.to_request();
let response = test::call_service(&srv, request).await;
let response = test::call_service(&mut srv, request).await;
assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await;
@ -513,28 +485,28 @@ mod tests {
#[actix_rt::test]
async fn test_files_not_allowed() {
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST)
.to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let req = TestRequest::default()
.method(Method::PUT)
.uri("/Cargo.toml")
.to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
}
#[actix_rt::test]
async fn test_files_guards() {
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").use_guards(guard::Post())),
)
.await;
@ -544,13 +516,13 @@ mod tests {
.method(Method::POST)
.to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::OK);
}
#[actix_rt::test]
async fn test_named_file_content_encoding() {
let srv = test::init_service(App::new().wrap(Compress::default()).service(
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async {
NamedFile::open("Cargo.toml")
.unwrap()
@ -561,16 +533,16 @@ mod tests {
let request = TestRequest::get()
.uri("/")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.header(header::ACCEPT_ENCODING, "gzip")
.to_request();
let res = test::call_service(&srv, request).await;
let res = test::call_service(&mut srv, request).await;
assert_eq!(res.status(), StatusCode::OK);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
}
#[actix_rt::test]
async fn test_named_file_content_encoding_gzip() {
let srv = test::init_service(App::new().wrap(Compress::default()).service(
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async {
NamedFile::open("Cargo.toml")
.unwrap()
@ -581,9 +553,9 @@ mod tests {
let request = TestRequest::get()
.uri("/")
.insert_header((header::ACCEPT_ENCODING, "gzip"))
.header(header::ACCEPT_ENCODING, "gzip")
.to_request();
let res = test::call_service(&srv, request).await;
let res = test::call_service(&mut srv, request).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers()
@ -605,27 +577,27 @@ mod tests {
#[actix_rt::test]
async fn test_static_files() {
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").show_files_listing()),
)
.await;
let req = TestRequest::with_uri("/missing").to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
let req = TestRequest::default().to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").show_files_listing()),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8"
@ -638,16 +610,16 @@ mod tests {
#[actix_rt::test]
async fn test_redirect_to_slash_directory() {
// should not redirect if no index
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// should redirect if index present
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(
Files::new("/", ".")
.index_file("test.png")
@ -656,12 +628,12 @@ mod tests {
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&srv, req).await;
let resp = test::call_service(&mut srv, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
@ -673,7 +645,7 @@ mod tests {
#[actix_rt::test]
async fn test_default_handler_file_missing() {
let st = Files::new("/", ".")
let mut st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| {
ok(req.into_response(HttpResponse::Ok().body("default content")))
})
@ -682,7 +654,7 @@ mod tests {
.unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request();
let resp = test::call_service(&st, req).await;
let resp = test::call_service(&mut st, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
@ -751,49 +723,54 @@ mod tests {
// );
// }
#[actix_rt::test]
async fn integration_serve_index() {
let srv = test::init_service(
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
)
.await;
// #[actix_rt::test]
// fn integration_serve_index() {
// let mut srv = test::TestServer::with_factory(|| {
// App::new().handler(
// "test",
// Files::new(".").index_file("Cargo.toml"),
// )
// });
let req = TestRequest::get().uri("/test").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
// let request = srv.get().uri(srv.url("/test")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// let bytes = srv.execute(response.body()).unwrap();
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(bytes, data);
let bytes = test::read_body(res).await;
// let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// let bytes = srv.execute(response.body()).unwrap();
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
// assert_eq!(bytes, data);
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
// // nonexistent index file
// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
let req = TestRequest::get().uri("/test/").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
// }
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
// #[actix_rt::test]
// fn integration_percent_encoded() {
// let mut srv = test::TestServer::with_factory(|| {
// App::new().handler(
// "test",
// Files::new(".").index_file("Cargo.toml"),
// )
// });
// nonexistent index file
let req = TestRequest::get().uri("/test/unknown").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = TestRequest::get().uri("/test/unknown/").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn integration_percent_encoded() {
let srv = test::init_service(
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
)
.await;
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
}
// let request = srv
// .get()
// .uri(srv.url("/test/%43argo.toml"))
// .finish()
// .unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// }
}

View File

@ -16,9 +16,10 @@ use actix_web::{
},
ContentEncoding, StatusCode,
},
HttpMessage, HttpRequest, HttpResponse, Responder,
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
};
use bitflags::bitflags;
use futures_util::future::{ready, Ready};
use mime_guess::from_path;
use crate::ChunkedReadFile;
@ -276,31 +277,37 @@ 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) -> Result<HttpResponse, Error> {
if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
res.header(header::CONTENT_TYPE, ct.to_string());
} else {
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
res.header(header::CONTENT_TYPE, self.content_type.to_string());
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.insert_header((
res.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
));
);
}
if let Some(current_encoding) = self.encoding {
res.encoding(current_encoding);
}
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
let reader = ChunkedReadFile {
size: self.md.len(),
offset: 0,
file: Some(self.file),
fut: None,
counter: 0,
};
return res.streaming(reader);
return Ok(res.streaming(reader));
}
let etag = if self.flags.contains(Flags::ETAG) {
@ -325,7 +332,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
(Ok(t1), Ok(t2)) => t1 > t2,
_ => false,
}
} else {
@ -344,7 +351,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
(Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
(Ok(t1), Ok(t2)) => t1 <= t2,
_ => false,
}
} else {
@ -355,16 +362,16 @@ impl NamedFile {
if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone());
resp.insert_header((header::CONTENT_TYPE, ct.to_string()));
resp.header(header::CONTENT_TYPE, ct.to_string());
} else {
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
}
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.insert_header((
resp.header(
header::CONTENT_DISPOSITION,
self.content_disposition.to_string(),
));
);
}
// default compressing
@ -373,14 +380,14 @@ impl NamedFile {
}
if let Some(lm) = last_modified {
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
resp.header(header::LAST_MODIFIED, lm.to_string());
}
if let Some(etag) = etag {
resp.insert_header((header::ETAG, etag.to_string()));
resp.header(header::ETAG, etag.to_string());
}
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
resp.header(header::ACCEPT_RANGES, "bytes");
let mut length = self.md.len();
let mut offset = 0;
@ -393,7 +400,7 @@ impl NamedFile {
offset = ranges[0].start;
resp.encoding(ContentEncoding::Identity);
resp.insert_header((
resp.header(
header::CONTENT_RANGE,
format!(
"bytes {}-{}/{}",
@ -401,32 +408,35 @@ impl NamedFile {
offset + length - 1,
self.md.len()
),
));
);
} else {
resp.insert_header((
header::CONTENT_RANGE,
format!("bytes */{}", length),
));
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
};
} else {
return resp.status(StatusCode::BAD_REQUEST).finish();
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
};
};
if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
} else if not_modified {
return resp.status(StatusCode::NOT_MODIFIED).finish();
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
}
let reader = ChunkedReadFile::new(length, offset, self.file);
let reader = ChunkedReadFile {
offset,
size: length,
file: Some(self.file),
fut: None,
counter: 0,
};
if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT);
}
resp.body(SizedStream::new(length, reader))
Ok(resp.body(SizedStream::new(length, reader)))
}
}
@ -485,7 +495,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
}
impl Responder for NamedFile {
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
self.into_response(req)
type Error = Error;
type Future = Ready<Result<HttpResponse, Error>>;
fn respond_to(self, req: &HttpRequest) -> Self::Future {
ready(self.into_response(req))
}
}

View File

@ -15,19 +15,12 @@ impl FromStr for PathBufWrap {
type Err = UriSegmentError;
fn from_str(path: &str) -> Result<Self, Self::Err> {
Self::parse_path(path, false)
}
}
impl PathBufWrap {
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
let mut buf = PathBuf::new();
for segment in path.split('/') {
if segment == ".." {
buf.pop();
} else if !hidden_files && segment.starts_with('.') {
} else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*'));
@ -103,17 +96,4 @@ mod tests {
PathBuf::from_iter(vec!["seg2"])
);
}
#[test]
fn test_parse_path() {
assert_eq!(
PathBufWrap::parse_path("/test/.tt", false).map(|t| t.0),
Err(UriSegmentError::BadStart('.'))
);
assert_eq!(
PathBufWrap::parse_path("/test/.tt", true).unwrap().0,
PathBuf::from_iter(vec!["test", ".tt"])
);
}
}

View File

@ -1,5 +1,3 @@
use derive_more::{Display, Error};
/// HTTP Range header representation.
#[derive(Debug, Clone, Copy)]
pub struct HttpRange {
@ -13,21 +11,17 @@ pub struct HttpRange {
const PREFIX: &str = "bytes=";
const PREFIX_LEN: usize = 6;
#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "Parse HTTP Range failed")]
pub struct ParseRangeErr(#[error(not(source))] ());
impl HttpRange {
/// Parses Range HTTP header string as per RFC 2616.
///
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file).
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ()> {
if header.is_empty() {
return Ok(Vec::new());
}
if !header.starts_with(PREFIX) {
return Err(ParseRangeErr(()));
return Err(());
}
let size_sig = size as i64;
@ -40,14 +34,13 @@ impl HttpRange {
.map(|ra| {
let mut start_end_iter = ra.split('-');
let start_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
let start_str = start_end_iter.next().ok_or(())?.trim();
let end_str = start_end_iter.next().ok_or(())?.trim();
if start_str.is_empty() {
// If no start is specified, end specifies the
// range start relative to the end of the file.
let mut length: i64 =
end_str.parse().map_err(|_| ParseRangeErr(()))?;
let mut length: i64 = end_str.parse().map_err(|_| ())?;
if length > size_sig {
length = size_sig;
@ -58,10 +51,10 @@ impl HttpRange {
length: length as u64,
}))
} else {
let start: i64 = start_str.parse().map_err(|_| ParseRangeErr(()))?;
let start: i64 = start_str.parse().map_err(|_| ())?;
if start < 0 {
return Err(ParseRangeErr(()));
return Err(());
}
if start >= size_sig {
no_overlap = true;
@ -72,11 +65,10 @@ impl HttpRange {
// If no end is specified, range extends to end of the file.
size_sig - start
} else {
let mut end: i64 =
end_str.parse().map_err(|_| ParseRangeErr(()))?;
let mut end: i64 = end_str.parse().map_err(|_| ())?;
if start > end {
return Err(ParseRangeErr(()));
return Err(());
}
if end >= size_sig {
@ -97,7 +89,7 @@ impl HttpRange {
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
if no_overlap && ranges.is_empty() {
return Err(ParseRangeErr(()));
return Err(());
}
Ok(ranges)
@ -341,7 +333,8 @@ mod tests {
if expected.is_empty() {
continue;
} else {
panic!(
assert!(
false,
"parse({}, {}) returned error {:?}",
header,
size,
@ -353,24 +346,28 @@ mod tests {
let got = res.unwrap();
if got.len() != expected.len() {
panic!(
assert!(
false,
"len(parseRange({}, {})) = {}, want {}",
header,
size,
got.len(),
expected.len()
);
continue;
}
for i in 0..expected.len() {
if got[i].start != expected[i].start {
panic!(
assert!(
false,
"parseRange({}, {})[{}].start = {}, want {}",
header, size, i, got[i].start, expected[i].start
)
}
if got[i].length != expected[i].length {
panic!(
assert!(
false,
"parseRange({}, {})[{}].length = {}, want {}",
header, size, i, got[i].length, expected[i].length
)

View File

@ -1,4 +1,9 @@
use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll};
use std::{
fmt, io,
path::PathBuf,
rc::Rc,
task::{Context, Poll},
};
use actix_service::Service;
use actix_web::{
@ -26,7 +31,6 @@ pub struct FilesService {
pub(crate) mime_override: Option<Rc<MimeOverride>>,
pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool,
}
type FilesServiceFuture = Either<
@ -35,10 +39,10 @@ type FilesServiceFuture = Either<
>;
impl FilesService {
fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
log::debug!("Failed to handle {}: {}", req.path(), e);
if let Some(ref default) = self.default {
if let Some(ref mut default) = self.default {
Either::Right(default.call(req))
} else {
Either::Left(ok(req.error_response(e)))
@ -52,14 +56,17 @@ impl fmt::Debug for FilesService {
}
}
impl Service<ServiceRequest> for FilesService {
impl Service for FilesService {
type Request = ServiceRequest;
type Response = ServiceResponse;
type Error = Error;
type Future = FilesServiceFuture;
actix_service::always_ready!();
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&self, req: ServiceRequest) -> Self::Future {
fn call(&mut self, req: ServiceRequest) -> Self::Future {
let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards
(**guard).check(req.head())
@ -71,16 +78,15 @@ impl Service<ServiceRequest> for FilesService {
if !is_method_valid {
return Either::Left(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.header(header::CONTENT_TYPE, "text/plain")
.body("Request did not meet this resource's requirements."),
)));
}
let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) {
Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))),
};
let real_path: PathBufWrap = match req.match_info().path().parse() {
Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))),
};
// full file path
let path = match self.directory.join(&real_path).canonicalize() {
@ -95,7 +101,7 @@ impl Service<ServiceRequest> for FilesService {
return Either::Left(ok(req.into_response(
HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to))
.header(header::LOCATION, redirect_to)
.body("")
.into_body(),
)));
@ -113,8 +119,10 @@ impl Service<ServiceRequest> for FilesService {
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res)))
Either::Left(ok(match named_file.into_response(&req) {
Ok(item) => ServiceResponse::new(req, item),
Err(e) => ServiceResponse::from_err(e, req),
}))
}
Err(e) => self.handle_err(e, req),
}
@ -145,8 +153,12 @@ impl Service<ServiceRequest> for FilesService {
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res)))
match named_file.into_response(&req) {
Ok(item) => {
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
}
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
}
Err(e) => self.handle_err(e, req),
}

View File

@ -11,10 +11,11 @@ use actix_web::{
#[actix_rt::test]
async fn test_utf8_file_contents() {
// use default ISO-8859-1 encoding
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let mut srv =
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&srv, req).await;
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
@ -23,13 +24,13 @@ async fn test_utf8_file_contents() {
);
// prefer UTF-8 encoding
let srv = test::init_service(
let mut srv = test::init_service(
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
)
.await;
let req = TestRequest::with_uri("/utf8.txt").to_request();
let res = test::call_service(&srv, req).await;
let res = test::call_service(&mut srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(

View File

@ -1,86 +1,18 @@
# Changes
## Unreleased - 2021-xx-xx
## Unreleased - 2020-xx-xx
## 3.0.0-beta.2 - 2021-02-19
### Added
* `IntoHeaderPair` trait that allows using typed and untyped headers in the same methods. [#1869]
* `ResponseBuilder::insert_header` method which allows using typed headers. [#1869]
* `ResponseBuilder::append_header` method which allows using typed headers. [#1869]
* `TestRequest::insert_header` method which allows using typed headers. [#1869]
* `ContentEncoding` implements all necessary header traits. [#1912]
* `HeaderMap::len_keys` has the behavior of the old `len` method. [#1964]
* `HeaderMap::drain` as an efficient draining iterator. [#1964]
* Implement `IntoIterator` for owned `HeaderMap`. [#1964]
* `trust-dns` optional feature to enable `trust-dns-resolver` as client dns resolver. [#1969]
## 2.2.2 - 2022-01-21
### Changed
* `ResponseBuilder::content_type` now takes an `impl IntoHeaderValue` to support using typed
`mime` types. [#1894]
* Renamed `IntoHeaderValue::{try_into => try_into_value}` to avoid ambiguity with std
`TryInto` trait. [#1894]
* `Extensions::insert` returns Option of replaced item. [#1904]
* Remove `HttpResponseBuilder::json2()`. [#1903]
* Enable `HttpResponseBuilder::json()` to receive data by value and reference. [#1903]
* `client::error::ConnectError` Resolver variant contains `Box<dyn std::error::Error>` type. [#1905]
* `client::ConnectorConfig` default timeout changed to 5 seconds. [#1905]
* Simplify `BlockingError` type to a unit struct. It's now only triggered when blocking thread pool
is dead. [#1957]
* `HeaderMap::len` now returns number of values instead of number of keys. [#1964]
* `HeaderMap::insert` now returns iterator of removed values. [#1964]
* `HeaderMap::remove` now returns iterator of removed values. [#1964]
- Migrate to `brotli` crate. [ad7e3c06]
### Removed
* `ResponseBuilder::set`; use `ResponseBuilder::insert_header`. [#1869]
* `ResponseBuilder::set_header`; use `ResponseBuilder::insert_header`. [#1869]
* `ResponseBuilder::header`; use `ResponseBuilder::append_header`. [#1869]
* `TestRequest::with_hdr`; use `TestRequest::default().insert_header()`. [#1869]
* `TestRequest::with_header`; use `TestRequest::default().insert_header()`. [#1869]
* `actors` optional feature. [#1969]
* `ResponseError` impl for `actix::MailboxError`. [#1969]
### Documentation
* Vastly improve docs and add examples for `HeaderMap`. [#1964]
[#1869]: https://github.com/actix/actix-web/pull/1869
[#1894]: https://github.com/actix/actix-web/pull/1894
[#1903]: https://github.com/actix/actix-web/pull/1903
[#1904]: https://github.com/actix/actix-web/pull/1904
[#1905]: https://github.com/actix/actix-web/pull/1905
[#1912]: https://github.com/actix/actix-web/pull/1912
[#1957]: https://github.com/actix/actix-web/pull/1957
[#1964]: https://github.com/actix/actix-web/pull/1964
[#1969]: https://github.com/actix/actix-web/pull/1969
[ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06
## 3.0.0-beta.1 - 2021-01-07
### Added
* Add `Http3` to `Protocol` enum for future compatibility and also mark `#[non_exhaustive]`.
### Changed
* Update `actix-*` dependencies to tokio `1.0` based versions. [#1813]
* Bumped `rand` to `0.8`.
* Update `bytes` to `1.0`. [#1813]
* Update `h2` to `0.3`. [#1813]
* The `ws::Message::Text` enum variant now contains a `bytestring::ByteString`. [#1864]
### Removed
* Deprecated `on_connect` methods have been removed. Prefer the new
`on_connect_ext` technique. [#1857]
* Remove `ResponseError` impl for `actix::actors::resolver::ResolverError`
due to deprecate of resolver actor. [#1813]
* Remove `ConnectError::SslHandshakeError` and re-export of `HandshakeError`.
due to the removal of this type from `tokio-openssl` crate. openssl handshake
error would return as `ConnectError::SslError`. [#1813]
* Remove `actix-threadpool` dependency. Use `actix_rt::task::spawn_blocking`.
Due to this change `actix_threadpool::BlockingError` type is moved into
`actix_http::error` module. [#1878]
[#1813]: https://github.com/actix/actix-web/pull/1813
[#1857]: https://github.com/actix/actix-web/pull/1857
[#1864]: https://github.com/actix/actix-web/pull/1864
[#1878]: https://github.com/actix/actix-web/pull/1878
## 2.2.1 - 2021-08-09
### Fixed
- Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
## 2.2.0 - 2020-11-25
@ -127,14 +59,15 @@
* Update actix-connect and actix-tls dependencies.
## 2.0.0-beta.3 - 2020-08-14
## [2.0.0-beta.3] - 2020-08-14
### Fixed
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
[#1626]: https://github.com/actix/actix-web/pull/1626
## 2.0.0-beta.2 - 2020-07-21
## [2.0.0-beta.2] - 2020-07-21
### Fixed
* Potential UB in h1 decoder using uninitialized memory. [#1614]
@ -145,8 +78,10 @@
[#1615]: https://github.com/actix/actix-web/pull/1615
## 2.0.0-beta.1 - 2020-07-11
## [2.0.0-beta.1] - 2020-07-11
### Changed
* Migrate cookie handling to `cookie` crate. [#1558]
* Update `sha-1` to 0.9. [#1586]
* Fix leak in client pool. [#1580]
@ -156,30 +91,33 @@
[#1586]: https://github.com/actix/actix-web/pull/1586
[#1580]: https://github.com/actix/actix-web/pull/1580
## [2.0.0-alpha.4] - 2020-05-21
## 2.0.0-alpha.4 - 2020-05-21
### Changed
* Bump minimum supported Rust version to 1.40
* content_length function is removed, and you can set Content-Length by calling
no_chunking function [#1439]
* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439]
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`.
* Update `base64` dependency to 0.12
### Fixed
* Support parsing of `SameSite=None` [#1503]
[#1439]: https://github.com/actix/actix-web/pull/1439
[#1503]: https://github.com/actix/actix-web/pull/1503
## [2.0.0-alpha.3] - 2020-05-08
## 2.0.0-alpha.3 - 2020-05-08
### Fixed
* Correct spelling of ConnectError::Unresolved [#1487]
* Fix a mistake in the encoding of websocket continuation messages wherein
Item::FirstText and Item::FirstBinary are each encoded as the other.
### Changed
* Implement `std::error::Error` for our custom errors [#1422]
* Remove `failure` support for `ResponseError` since that crate
will be deprecated in the near future.
@ -187,247 +125,338 @@
[#1422]: https://github.com/actix/actix-web/pull/1422
[#1487]: https://github.com/actix/actix-web/pull/1487
## [2.0.0-alpha.2] - 2020-03-07
## 2.0.0-alpha.2 - 2020-03-07
### Changed
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
respectively to improve download speed for awc when downloading large objects. [#1394]
* client::Connector accepts initial_window_size and initial_connection_window_size
HTTP2 configuration. [#1394]
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively
to improve download speed for awc when downloading large objects. [#1394]
* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394]
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
[#1394]: https://github.com/actix/actix-web/pull/1394
[#1395]: https://github.com/actix/actix-web/pull/1395
## [2.0.0-alpha.1] - 2020-02-27
## 2.0.0-alpha.1 - 2020-02-27
### Changed
* Update the `time` dependency to 0.2.7.
* Moved actors messages support from actix crate, enabled with feature `actors`.
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
`&mut self` in the poll_next().
* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next().
* MessageBody is not implemented for &'static [u8] anymore.
### Fixed
* Allow `SameSite=None` cookies to be sent in a response.
## [1.0.1] - 2019-12-20
## 1.0.1 - 2019-12-20
### Fixed
* Poll upgrade service's readiness from HTTP service handlers
* Replace brotli with brotli2 #1224
## [1.0.0] - 2019-12-13
## 1.0.0 - 2019-12-13
### Added
* Add websockets continuation frame support
### Changed
* Replace `flate2-xxx` features with `compress`
## [1.0.0-alpha.5] - 2019-12-09
## 1.0.0-alpha.5 - 2019-12-09
### Fixed
* Check `Upgrade` service readiness before calling it
* Fix buffer remaining capacity calculation
* Fix buffer remaining capacity calcualtion
### Changed
* Websockets: Ping and Pong should have binary data #1049
## [1.0.0-alpha.4] - 2019-12-08
## 1.0.0-alpha.4 - 2019-12-08
### Added
* Add impl ResponseBuilder for Error
### Changed
* Use rust based brotli compression library
## 1.0.0-alpha.3 - 2019-12-07
## [1.0.0-alpha.3] - 2019-12-07
### Changed
* Migrate to tokio 0.2
* Migrate to `std::future`
## 0.2.11 - 2019-11-06
## [0.2.11] - 2019-11-06
### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
* Add an additional `filename*` param in the `Content-Disposition` header of
`actix_files::NamedFile` to be more compatible. (#1151)
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
charset=utf-8` header [#1118]
[#1878]: https://github.com/actix/actix-web/pull/1878
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118
## 0.2.10 - 2019-09-11
## [0.2.10] - 2019-09-11
### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
with `RequestHead`
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
### Fixed
* h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009
## 0.2.9 - 2019-08-13
## [0.2.9] - 2019-08-13
### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1
### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
## 0.2.8 - 2019-08-01
## [0.2.8] - 2019-08-01
### Added
* Add `rustls` support
* Add `Clone` impl for `HeaderMap`
### Fixed
* awc client panic #1016
* Invalid response with compression middleware enabled, but compression-related features
disabled #997
* Invalid response with compression middleware enabled, but compression-related features disabled #997
## 0.2.7 - 2019-07-18
## [0.2.7] - 2019-07-18
### Added
* Add support for downcasting response errors #986
## 0.2.6 - 2019-07-17
## [0.2.6] - 2019-07-17
### Changed
* Replace `ClonableService` with local copy
* Upgrade `rand` dependency version to 0.7
## 0.2.5 - 2019-06-28
## [0.2.5] - 2019-06-28
### Added
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
* Add `Copy` and `Clone` impls for `ws::Codec`
## 0.2.4 - 2019-06-16
## [0.2.4] - 2019-06-16
### Fixed
* Do not compress NoContent (204) responses #918
## 0.2.3 - 2019-06-02
## [0.2.3] - 2019-06-02
### Added
* Debug impl for ResponseBuilder
* From SizedStream and BodyStream for Body
### Changed
* SizedStream uses u64
## 0.2.2 - 2019-05-29
## [0.2.2] - 2019-05-29
### Fixed
* Parse incoming stream before closing stream on disconnect #868
## 0.2.1 - 2019-05-25
## [0.2.1] - 2019-05-25
### Fixed
* Handle socket read disconnect
## 0.2.0 - 2019-05-12
## [0.2.0] - 2019-05-12
### Changed
* Update actix-service to 0.4
* Expect and upgrade services accept `ServerConfig` config.
### Deleted
* `OneRequest` service
## 0.1.5 - 2019-05-04
## [0.1.5] - 2019-05-04
### Fixed
* Clean up response extensions in response pool #817
## 0.1.4 - 2019-04-24
## [0.1.4] - 2019-04-24
### Added
* Allow to render h1 request headers in `Camel-Case`
### Fixed
* Read until eof for http/1.0 responses #771
## 0.1.3 - 2019-04-23
## [0.1.3] - 2019-04-23
### Fixed
* Fix http client pool management
* Fix http client wait queue management #794
## 0.1.2 - 2019-04-23
## [0.1.2] - 2019-04-23
### Fixed
* Fix BorrowMutError panic in client connector #793
## 0.1.1 - 2019-04-19
## [0.1.1] - 2019-04-19
### Changed
* Cookie::max_age() accepts value in seconds
* Cookie::max_age_time() accepts value in time::Duration
* Allow to specify server address for client connector
## 0.1.0 - 2019-04-16
## [0.1.0] - 2019-04-16
### Added
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
### Changed
* `actix_http::encoding` always available
* use trust-dns-resolver 0.11.0
## 0.1.0-alpha.5 - 2019-04-12
## [0.1.0-alpha.5] - 2019-04-12
### Added
* Allow to use custom service for upgrade requests
* Added `h1::SendResponse` future.
### Changed
* MessageBody::length() renamed to MessageBody::size() for consistency
* ws handshake verification functions take RequestHead instead of Request
## 0.1.0-alpha.4 - 2019-04-08
## [0.1.0-alpha.4] - 2019-04-08
### Added
* Allow to use custom `Expect` handler
* Add minimal `std::error::Error` impl for `Error`
### Changed
* Export IntoHeaderValue
* Render error and return as response body
* Use thread pool for response body compression
* Use thread pool for response body comression
### Deleted
* Removed PayloadBuffer
## 0.1.0-alpha.3 - 2019-04-02
## [0.1.0-alpha.3] - 2019-04-02
### Added
* Warn when an unsealed private cookie isn't valid UTF-8
### Fixed
* Rust 1.31.0 compatibility
* Preallocate read buffer for h1 codec
* Detect socket disconnection during protocol selection
## 0.1.0-alpha.2 - 2019-03-29
## [0.1.0-alpha.2] - 2019-03-29
### Added
* Added ws::Message::Nop, no-op websockets message
### Changed
* Do not use thread pool for decompression if chunk size is smaller than 2048.
* Do not use thread pool for decomression if chunk size is smaller than 2048.
## 0.1.0-alpha.1 - 2019-03-28
## [0.1.0-alpha.1] - 2019-03-28
* Initial impl

View File

@ -1,6 +1,6 @@
[package]
name = "actix-http"
version = "3.0.0-beta.2"
version = "2.2.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem"
readme = "README.md"
@ -25,40 +25,44 @@ path = "src/lib.rs"
default = []
# openssl
openssl = ["actix-tls/openssl"]
openssl = ["actix-tls/openssl", "actix-connect/openssl"]
# rustls support
rustls = ["actix-tls/rustls"]
rustls = ["actix-tls/rustls", "actix-connect/rustls"]
# enable compressison support
compress = ["flate2", "brotli2"]
compress = ["flate2", "brotli"]
# support for secure cookies
secure-cookies = ["cookie/secure"]
# trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"]
# support for actix Actor messages
actors = ["actix"]
[dependencies]
actix-service = "2.0.0-beta.4"
actix-codec = "0.4.0-beta.1"
actix-utils = "3.0.0-beta.2"
actix-rt = "2"
actix-tls = "3.0.0-beta.2"
actix-service = "1.0.6"
actix-codec = "0.3.0"
actix-connect = "2.0.0"
actix-utils = "2.0.0"
actix-rt = "1.0.0"
actix-threadpool = "0.3.1"
actix-tls = { version = "2.0.0", optional = true }
actix = { version = "0.10.0", optional = true }
base64 = "0.13"
bitflags = "1.2"
bytes = "1"
bytestring = "1"
bytes = "0.5.3"
cookie = { version = "0.14.1", features = ["percent-encode"] }
derive_more = "0.99.5"
copyless = "0.1.4"
derive_more = "0.99.2"
either = "1.5.3"
encoding_rs = "0.8"
futures-channel = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
ahash = "0.7"
h2 = "0.3.0"
http = "0.2.2"
futures-channel = { version = "0.3.5", default-features = false }
futures-core = { version = "0.3.5", default-features = false }
futures-util = { version = "0.3.5", default-features = false }
fxhash = "0.2.1"
h2 = "0.2.1"
http = "0.2.0"
httparse = "1.3"
indexmap = "1.3"
itoa = "0.4"
@ -68,34 +72,32 @@ log = "0.4"
mime = "0.3"
percent-encoding = "2.1"
pin-project = "1.0.0"
rand = "0.8"
rand = "0.7"
regex = "1.3"
serde = "1.0"
serde_json = "1.0"
sha-1 = "0.9"
smallvec = "1.6"
slab = "0.4"
serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] }
time = { version = "0.2.7", default-features = false, features = ["std"] }
# compression
brotli2 = { version="0.3.2", optional = true }
brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies]
actix-server = "2.0.0-beta.3"
actix-http-test = { version = "3.0.0-beta.2", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
actix-server = "1.0.1"
actix-connect = { version = "2.0.0", features = ["openssl"] }
actix-http-test = { version = "2.0.0", features = ["openssl"] }
actix-tls = { version = "2.0.0", features = ["openssl"] }
criterion = "0.3"
env_logger = "0.8"
env_logger = "0.7"
serde_derive = "1.0"
tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" }
open-ssl = { version="0.10", package = "openssl" }
rust-tls = { version="0.18", package = "rustls" }
[[bench]]
name = "write-camel-case"
name = "content-length"
harness = false
[[bench]]

View File

@ -3,19 +3,16 @@
> HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.2)](https://docs.rs/actix-http/3.0.0-beta.2)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg)
<br />
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.2/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.2)
[![Download](https://img.shields.io/crates/d/actix-http.svg)](https://crates.io/crates/actix-http)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=2.2.2)](https://docs.rs/actix-http/2.2.2)
![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http)
[![Dependency Status](https://deps.rs/crate/actix-http/2.2.2/status.svg)](https://deps.rs/crate/actix-http/2.2.2)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http)
- [Chat on Gitter](https://gitter.im/actix/actix-web)
- Minimum Supported Rust Version (MSRV): 1.46.0
- Minimum Supported Rust Version (MSRV): 1.42.0
## Example

View File

@ -0,0 +1,291 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
use bytes::BytesMut;
// benchmark sending all requests at the same time
fn bench_write_content_length(c: &mut Criterion) {
let mut group = c.benchmark_group("write_content_length");
let sizes = [
0, 1, 11, 83, 101, 653, 1001, 6323, 10001, 56329, 100001, 123456, 98724245,
4294967202,
];
for i in sizes.iter() {
group.bench_with_input(BenchmarkId::new("Original (unsafe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_original::write_content_length(i, &mut b)
})
});
group.bench_with_input(BenchmarkId::new("New (safe)", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_new::write_content_length(i, &mut b)
})
});
group.bench_with_input(BenchmarkId::new("itoa", i), i, |b, &i| {
b.iter(|| {
let mut b = BytesMut::with_capacity(35);
_itoa::write_content_length(i, &mut b)
})
});
}
group.finish();
}
criterion_group!(benches, bench_write_content_length);
criterion_main!(benches);
mod _itoa {
use bytes::{BufMut, BytesMut};
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
return;
}
let mut buf = itoa::Buffer::new();
bytes.put_slice(b"\r\ncontent-length: ");
bytes.put_slice(buf.format(n).as_bytes());
bytes.put_slice(b"\r\n");
}
}
mod _new {
use bytes::{BufMut, BytesMut};
const DIGITS_START: u8 = b'0';
/// NOTE: bytes object has to contain enough space
pub fn write_content_length(n: usize, bytes: &mut BytesMut) {
if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n");
return;
}
bytes.put_slice(b"\r\ncontent-length: ");
if n < 10 {
bytes.put_u8(DIGITS_START + (n as u8));
} else if n < 100 {
let n = n as u8;
let d10 = n / 10;
let d1 = n % 10;
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 1000 {
let n = n as u16;
let d100 = (n / 100) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 10_000 {
let n = n as u16;
let d1000 = (n / 1000) as u8;
let d100 = ((n / 100) % 10) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d1000);
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 100_000 {
let n = n as u32;
let d10000 = (n / 10000) as u8;
let d1000 = ((n / 1000) % 10) as u8;
let d100 = ((n / 100) % 10) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d10000);
bytes.put_u8(DIGITS_START + d1000);
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else if n < 1_000_000 {
let n = n as u32;
let d100000 = (n / 100000) as u8;
let d10000 = ((n / 10000) % 10) as u8;
let d1000 = ((n / 1000) % 10) as u8;
let d100 = ((n / 100) % 10) as u8;
let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100000);
bytes.put_u8(DIGITS_START + d10000);
bytes.put_u8(DIGITS_START + d1000);
bytes.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1);
} else {
write_usize(n, bytes);
}
bytes.put_slice(b"\r\n");
}
fn write_usize(n: usize, bytes: &mut BytesMut) {
let mut n = n;
// 20 chars is max length of a usize (2^64)
// digits will be added to the buffer from lsd to msd
let mut buf = BytesMut::with_capacity(20);
while n > 9 {
// "pop" the least-significant digit
let lsd = (n % 10) as u8;
// remove the lsd from n
n = n / 10;
buf.put_u8(DIGITS_START + lsd);
}
// put msd to result buffer
bytes.put_u8(DIGITS_START + (n as u8));
// put, in reverse (msd to lsd), remaining digits to buffer
for i in (0..buf.len()).rev() {
bytes.put_u8(buf[i]);
}
}
}
mod _original {
use std::{mem, ptr, slice};
use bytes::{BufMut, BytesMut};
const DEC_DIGITS_LUT: &[u8] = b"0001020304050607080910111213141516171819\
2021222324252627282930313233343536373839\
4041424344454647484950515253545556575859\
6061626364656667686970717273747576777879\
8081828384858687888990919293949596979899";
/// NOTE: bytes object has to contain enough space
pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
if n < 10 {
let mut buf: [u8; 21] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'\r', b'\n',
];
buf[18] = (n as u8) + b'0';
bytes.put_slice(&buf);
} else if n < 100 {
let mut buf: [u8; 22] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'\r', b'\n',
];
let d1 = n << 1;
unsafe {
ptr::copy_nonoverlapping(
DEC_DIGITS_LUT.as_ptr().add(d1),
buf.as_mut_ptr().offset(18),
2,
);
}
bytes.put_slice(&buf);
} else if n < 1000 {
let mut buf: [u8; 23] = [
b'\r', b'\n', b'c', b'o', b'n', b't', b'e', b'n', b't', b'-', b'l',
b'e', b'n', b'g', b't', b'h', b':', b' ', b'0', b'0', b'0', b'\r',
b'\n',
];
// decode 2 more chars, if > 2 chars
let d1 = (n % 100) << 1;
n /= 100;
unsafe {
ptr::copy_nonoverlapping(
DEC_DIGITS_LUT.as_ptr().add(d1),
buf.as_mut_ptr().offset(19),
2,
)
};
// decode last 1
buf[18] = (n as u8) + b'0';
bytes.put_slice(&buf);
} else {
bytes.put_slice(b"\r\ncontent-length: ");
convert_usize(n, bytes);
}
}
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
let mut curr: isize = 39;
let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() };
buf[39] = b'\r';
buf[40] = b'\n';
let buf_ptr = buf.as_mut_ptr();
let lut_ptr = DEC_DIGITS_LUT.as_ptr();
// eagerly decode 4 characters at a time
while n >= 10_000 {
let rem = (n % 10_000) as isize;
n /= 10_000;
let d1 = (rem / 100) << 1;
let d2 = (rem % 100) << 1;
curr -= 4;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
ptr::copy_nonoverlapping(
lut_ptr.offset(d2),
buf_ptr.offset(curr + 2),
2,
);
}
}
// if we reach here numbers are <= 9999, so at most 4 chars long
let mut n = n as isize; // possibly reduce 64bit math
// decode 2 more chars, if > 2 chars
if n >= 100 {
let d1 = (n % 100) << 1;
n /= 100;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
}
}
// decode last 1 or 2 chars
if n < 10 {
curr -= 1;
unsafe {
*buf_ptr.offset(curr) = (n as u8) + b'0';
}
} else {
let d1 = n << 1;
curr -= 2;
unsafe {
ptr::copy_nonoverlapping(lut_ptr.offset(d1), buf_ptr.offset(curr), 2);
}
}
unsafe {
bytes.extend_from_slice(slice::from_raw_parts(
buf_ptr.offset(curr),
41 - curr as usize,
));
}
}
}

View File

@ -176,7 +176,7 @@ mod _original {
buf[5] = b'0';
buf[7] = b'9';
}
_ => {}
_ => (),
}
let mut curr: isize = 12;

View File

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

View File

@ -26,10 +26,7 @@ async fn main() -> io::Result<()> {
info!("request body: {:?}", body);
Ok::<_, Error>(
Response::Ok()
.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
))
.header("x-head", HeaderValue::from_static("dummy value!"))
.body(body),
)
})

View File

@ -15,7 +15,7 @@ async fn handle_request(mut req: Request) -> Result<Response, Error> {
info!("request body: {:?}", body);
Ok(Response::Ok()
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.header("x-head", HeaderValue::from_static("dummy value!"))
.body(body))
}

View File

@ -19,10 +19,7 @@ async fn main() -> io::Result<()> {
.finish(|_req| {
info!("{:?}", _req);
let mut res = Response::Ok();
res.insert_header((
"x-head",
HeaderValue::from_static("dummy value!"),
));
res.header("x-head", HeaderValue::from_static("dummy value!"));
future::ok::<_, ()>(res.body("Hello world!"))
})
.tcp()

View File

@ -1,9 +1,11 @@
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, mem};
use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream};
use futures_core::Stream;
use futures_util::ready;
use pin_project::pin_project;
use crate::error::Error;
@ -66,7 +68,7 @@ impl<T: MessageBody + Unpin> MessageBody for Box<T> {
#[pin_project(project = ResponseBodyProj)]
pub enum ResponseBody<B> {
Body(#[pin] B),
Other(Body),
Other(#[pin] Body),
}
impl ResponseBody<Body> {
@ -108,7 +110,7 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
ResponseBodyProj::Other(body) => body.poll_next(cx),
}
}
}
@ -122,11 +124,12 @@ impl<B: MessageBody> Stream for ResponseBody<B> {
) -> Poll<Option<Self::Item>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
ResponseBodyProj::Other(body) => body.poll_next(cx),
}
}
}
#[pin_project(project = BodyProj)]
/// Represents various types of http message body.
pub enum Body {
/// Empty response. `Content-Length` header is not set.
@ -165,10 +168,10 @@ impl MessageBody for Body {
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.get_mut() {
Body::None => Poll::Ready(None),
Body::Empty => Poll::Ready(None),
Body::Bytes(ref mut bin) => {
match self.project() {
BodyProj::None => Poll::Ready(None),
BodyProj::Empty => Poll::Ready(None),
BodyProj::Bytes(ref mut bin) => {
let len = bin.len();
if len == 0 {
Poll::Ready(None)
@ -176,7 +179,7 @@ impl MessageBody for Body {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
BodyProj::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx),
}
}
}
@ -263,12 +266,12 @@ where
}
}
impl<S, E> From<BodyStream<S>> for Body
impl<S, E> From<BodyStream<S, E>> for Body
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static,
{
fn from(s: BodyStream<S>) -> Body {
fn from(s: BodyStream<S, E>) -> Body {
Body::from_message(s)
}
}
@ -364,21 +367,27 @@ impl MessageBody for String {
/// Type represent streaming body.
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
pub struct BodyStream<S: Unpin> {
#[pin_project]
pub struct BodyStream<S: Unpin, E> {
#[pin]
stream: S,
_t: PhantomData<E>,
}
impl<S, E> BodyStream<S>
impl<S, E> BodyStream<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
pub fn new(stream: S) -> Self {
BodyStream { stream }
BodyStream {
stream,
_t: PhantomData,
}
}
}
impl<S, E> MessageBody for BodyStream<S>
impl<S, E> MessageBody for BodyStream<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
@ -393,12 +402,13 @@ where
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
mut self: Pin<&mut Self>,
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut stream = self.project().stream;
loop {
let stream = &mut self.as_mut().stream;
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
let stream = stream.as_mut();
return Poll::Ready(match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)),
});
@ -408,8 +418,10 @@ where
/// Type represent streaming body. This body implementation should be used
/// if total size of stream is known. Data get sent as is without using transfer encoding.
#[pin_project]
pub struct SizedStream<S: Unpin> {
size: u64,
#[pin]
stream: S,
}
@ -436,12 +448,13 @@ where
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
mut self: Pin<&mut Self>,
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut stream: Pin<&mut S> = self.project().stream;
loop {
let stream = &mut self.as_mut().stream;
return Poll::Ready(match ready!(Pin::new(stream).poll_next(cx)) {
let stream = stream.as_mut();
return Poll::Ready(match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val,
});

View File

@ -10,6 +10,7 @@ use crate::config::{KeepAlive, ServiceConfig};
use crate::error::Error;
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
use crate::h2::H2Service;
use crate::helpers::{Data, DataFactory};
use crate::request::Request;
use crate::response::Response;
use crate::service::HttpService;
@ -19,7 +20,7 @@ use crate::{ConnectCallback, Extensions};
///
/// This type can be used to construct an instance of [`HttpService`] through a
/// builder-like pattern.
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler<T>> {
keep_alive: KeepAlive,
client_timeout: u64,
client_disconnect: u64,
@ -27,16 +28,18 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
local_addr: Option<net::SocketAddr>,
expect: X,
upgrade: Option<U>,
// DEPRECATED: in favor of on_connect_ext
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<S>,
_t: PhantomData<(T, S)>,
}
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler<T>>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
{
/// Create instance of `ServiceConfigBuilder`
pub fn new() -> Self {
@ -48,26 +51,27 @@ where
local_addr: None,
expect: ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_phantom: PhantomData,
_t: PhantomData,
}
}
}
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
<S::Service as Service>::Future: 'static,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
<U::Service as Service>::Future: 'static,
{
/// Set server keep-alive setting.
///
@ -123,11 +127,11 @@ where
/// request will be forwarded to main service.
pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
where
F: IntoServiceFactory<X1, Request>,
X1: ServiceFactory<Request, Config = (), Response = Request>,
F: IntoServiceFactory<X1>,
X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
X1::Error: Into<Error>,
X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
<X1::Service as Service>::Future: 'static,
{
HttpServiceBuilder {
keep_alive: self.keep_alive,
@ -137,8 +141,9 @@ where
local_addr: self.local_addr,
expect: expect.into_factory(),
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
@ -148,11 +153,15 @@ where
/// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
F: IntoServiceFactory<U1>,
U1: ServiceFactory<
Config = (),
Request = (Request, Framed<T, Codec>),
Response = (),
>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
<U1::Service as Service>::Future: 'static,
{
HttpServiceBuilder {
keep_alive: self.keep_alive,
@ -162,11 +171,26 @@ where
local_addr: self.local_addr,
expect: self.expect,
upgrade: Some(upgrade.into_factory()),
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
/// Set on-connect callback.
///
/// Called once per connection. Return value of the call is stored in request extensions.
///
/// *SOFT DEPRECATED*: Prefer the `on_connect_ext` style callback.
pub fn on_connect<F, I>(mut self, f: F) -> Self
where
F: Fn(&T) -> I + 'static,
I: Clone + 'static,
{
self.on_connect = Some(Rc::new(move |io| Box::new(Data(f(io)))));
self
}
/// Sets the callback to be run on connection establishment.
///
/// Has mutable access to a data container that will be merged into request extensions.
@ -184,7 +208,7 @@ where
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where
B: MessageBody,
F: IntoServiceFactory<S, Request>,
F: IntoServiceFactory<S>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
@ -200,6 +224,7 @@ where
H1Service::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
@ -207,11 +232,11 @@ where
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>,
F: IntoServiceFactory<S>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
{
let cfg = ServiceConfig::new(
self.keep_alive,
@ -222,6 +247,7 @@ where
);
H2Service::with_config(cfg, service.into_factory())
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
@ -229,11 +255,11 @@ where
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>,
F: IntoServiceFactory<S>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
{
let cfg = ServiceConfig::new(
self.keep_alive,
@ -246,6 +272,7 @@ where
HttpService::with_config(cfg, service.into_factory())
.expect(self.expect)
.upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext)
}
}

View File

@ -1,7 +1,8 @@
use std::time::Duration;
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
// These values are taken from hyper/src/proto/h2/client.rs
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
/// Connector configuration
#[derive(Clone)]
@ -18,7 +19,7 @@ pub(crate) struct ConnectorConfig {
impl Default for ConnectorConfig {
fn default() -> Self {
Self {
timeout: Duration::from_secs(5),
timeout: Duration::from_secs(1),
conn_lifetime: Duration::from_secs(75),
conn_keep_alive: Duration::from_secs(15),
disconnect_timeout: Some(Duration::from_millis(3000)),

View File

@ -1,14 +1,11 @@
use std::future::Future;
use std::ops::{Deref, DerefMut};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, io, time};
use std::{fmt, io, mem, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
use actix_rt::task::JoinHandle;
use bytes::Bytes;
use futures_core::future::LocalBoxFuture;
use futures_util::future::{err, Either, FutureExt, Ready};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use bytes::{Buf, Bytes};
use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready};
use h2::client::SendRequest;
use pin_project::pin_project;
@ -23,53 +20,7 @@ use super::{h1proto, h2proto};
pub(crate) enum ConnectionType<Io> {
H1(Io),
H2(H2Connection),
}
// h2 connection has two parts: SendRequest and Connection.
// Connection is spawned as async task on runtime and H2Connection would hold a handle for
// this task. So it can wake up and quit the task when SendRequest is dropped.
pub(crate) struct H2Connection {
handle: JoinHandle<()>,
sender: SendRequest<Bytes>,
}
impl H2Connection {
pub(crate) fn new<Io>(
sender: SendRequest<Bytes>,
connection: h2::client::Connection<Io>,
) -> Self
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
let handle = actix_rt::spawn(async move {
let _ = connection.await;
});
Self { handle, sender }
}
}
// wake up waker when drop
impl Drop for H2Connection {
fn drop(&mut self) {
self.handle.abort();
}
}
// only expose sender type to public.
impl Deref for H2Connection {
type Target = SendRequest<Bytes>;
fn deref(&self) -> &Self::Target {
&self.sender
}
}
impl DerefMut for H2Connection {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.sender
}
H2(SendRequest<Bytes>),
}
pub trait Connection {
@ -272,13 +223,23 @@ where
fn poll_read(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
match self.project() {
EitherIoProj::A(val) => val.poll_read(cx, buf),
EitherIoProj::B(val) => val.poll_read(cx, buf),
}
}
unsafe fn prepare_uninitialized_buffer(
&self,
buf: &mut [mem::MaybeUninit<u8>],
) -> bool {
match self {
EitherIo::A(ref val) => val.prepare_uninitialized_buffer(buf),
EitherIo::B(ref val) => val.prepare_uninitialized_buffer(buf),
}
}
}
impl<A, B> AsyncWrite for EitherIo<A, B>
@ -313,36 +274,18 @@ where
EitherIoProj::B(val) => val.poll_shutdown(cx),
}
}
}
#[cfg(test)]
mod test {
use std::net;
use actix_rt::net::TcpStream;
use super::*;
#[actix_rt::test]
async fn test_h2_connection_drop() {
let addr = "127.0.0.1:0".parse::<net::SocketAddr>().unwrap();
let listener = net::TcpListener::bind(addr).unwrap();
let local = listener.local_addr().unwrap();
std::thread::spawn(move || while listener.accept().is_ok() {});
let tcp = TcpStream::connect(local).await.unwrap();
let (sender, connection) = h2::client::handshake(tcp).await.unwrap();
let conn = H2Connection::new(sender.clone(), connection);
assert!(sender.clone().ready().await.is_ok());
assert!(h2::client::SendRequest::clone(&*conn).ready().await.is_ok());
drop(conn);
match sender.ready().await {
Ok(_) => panic!("connection should be gone and can not be ready"),
Err(e) => assert!(e.is_io()),
};
fn poll_write_buf<U: Buf>(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut U,
) -> Poll<Result<usize, io::Error>>
where
Self: Sized,
{
match self.project() {
EitherIoProj::A(val) => val.poll_write_buf(cx, buf),
EitherIoProj::B(val) => val.poll_write_buf(cx, buf),
}
}
}

View File

@ -3,11 +3,11 @@ use std::marker::PhantomData;
use std::time::Duration;
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream;
use actix_service::{apply_fn, Service, ServiceExt};
use actix_tls::connect::{
new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
use actix_connect::{
default_connector, Connect as TcpConnect, Connection as TcpConnection,
};
use actix_rt::net::TcpStream;
use actix_service::{apply_fn, Service};
use actix_utils::timeout::{TimeoutError, TimeoutService};
use http::Uri;
@ -18,9 +18,10 @@ use super::pool::{ConnectionPool, Protocol};
use super::Connect;
#[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
use actix_connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::ClientConfig;
use actix_connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")]
use std::sync::Arc;
@ -51,7 +52,7 @@ pub struct Connector<T, U> {
config: ConnectorConfig,
#[allow(dead_code)]
ssl: SslConnector,
_phantom: PhantomData<U>,
_t: PhantomData<U>,
}
trait Io: AsyncRead + AsyncWrite + Unpin {}
@ -61,24 +62,24 @@ impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector<
impl Service<
TcpConnect<Uri>,
Request = TcpConnect<Uri>,
Response = TcpConnection<Uri, TcpStream>,
Error = actix_tls::connect::ConnectError,
Error = actix_connect::ConnectError,
> + Clone,
TcpStream,
> {
Connector {
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
connector: new_connector(resolver::resolver()),
connector: default_connector(),
config: ConnectorConfig::default(),
_phantom: PhantomData,
_t: PhantomData,
}
}
// Build Ssl connector with openssl, based on supplied alpn protocols
#[cfg(feature = "openssl")]
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
use actix_tls::connect::ssl::openssl::SslMethod;
use actix_connect::ssl::openssl::SslMethod;
use bytes::{BufMut, BytesMut};
let mut alpn = BytesMut::with_capacity(20);
@ -99,9 +100,9 @@ impl Connector<(), ()> {
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
let mut config = ClientConfig::new();
config.set_protocols(&protocols);
config.root_store.add_server_trust_anchors(
&actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
);
config
.root_store
.add_server_trust_anchors(&actix_tls::rustls::TLS_SERVER_ROOTS);
SslConnector::Rustls(Arc::new(config))
}
@ -116,16 +117,16 @@ impl<T, U> Connector<T, U> {
where
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug,
T1: Service<
TcpConnect<Uri>,
Request = TcpConnect<Uri>,
Response = TcpConnection<Uri, U1>,
Error = actix_tls::connect::ConnectError,
Error = actix_connect::ConnectError,
> + Clone,
{
Connector {
connector,
config: self.config,
ssl: self.ssl,
_phantom: PhantomData,
_t: PhantomData,
}
}
}
@ -134,9 +135,9 @@ impl<T, U> Connector<T, U>
where
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static,
T: Service<
TcpConnect<Uri>,
Request = TcpConnect<Uri>,
Response = TcpConnection<Uri, U>,
Error = actix_tls::connect::ConnectError,
Error = actix_connect::ConnectError,
> + Clone
+ 'static,
{
@ -240,8 +241,8 @@ where
/// its combinator chain.
pub fn finish(
self,
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
{
) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
+ Clone {
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
{
let connector = TimeoutService::new(
@ -267,11 +268,11 @@ where
#[cfg(any(feature = "openssl", feature = "rustls"))]
{
const H2: &[u8] = b"h2";
use actix_service::{boxed::service, pipeline};
#[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::OpensslConnector;
use actix_connect::ssl::openssl::OpensslConnector;
#[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session};
use actix_connect::ssl::rustls::{RustlsConnector, Session};
use actix_service::{boxed::service, pipeline};
let ssl_service = TimeoutService::new(
self.config.timeout,
@ -362,7 +363,8 @@ mod connect_impl {
pub(crate) struct InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
pub(crate) tcp_pool: ConnectionPool<T, Io>,
}
@ -370,7 +372,8 @@ mod connect_impl {
impl<T, Io> Clone for InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
fn clone(&self) -> Self {
InnerConnector {
@ -379,23 +382,25 @@ mod connect_impl {
}
}
impl<T, Io> Service<Connect> for InnerConnector<T, Io>
impl<T, Io> Service for InnerConnector<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
type Request = Connect;
type Response = IoConnection<Io>;
type Error = ConnectError;
type Future = Either<
<ConnectionPool<T, Io> as Service<Connect>>::Future,
<ConnectionPool<T, Io> as Service>::Future,
Ready<Result<IoConnection<Io>, ConnectError>>,
>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready(cx)
}
fn call(&self, req: Connect) -> Self::Future {
fn call(&mut self, req: Connect) -> Self::Future {
match req.uri.scheme_str() {
Some("https") | Some("wss") => {
Either::Right(err(ConnectError::SslIsNotSupported))
@ -423,8 +428,8 @@ mod connect_impl {
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>,
{
pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
@ -434,8 +439,10 @@ mod connect_impl {
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{
fn clone(&self) -> Self {
InnerConnector {
@ -445,13 +452,16 @@ mod connect_impl {
}
}
impl<T1, T2, Io1, Io2> Service<Connect> for InnerConnector<T1, T2, Io1, Io2>
impl<T1, T2, Io1, Io2> Service for InnerConnector<T1, T2, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
T1: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
T2: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{
type Request = Connect;
type Response = EitherConnection<Io1, Io2>;
type Error = ConnectError;
type Future = Either<
@ -459,19 +469,19 @@ mod connect_impl {
InnerConnectorResponseB<T2, Io1, Io2>,
>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready(cx)
}
fn call(&self, req: Connect) -> Self::Future {
fn call(&mut self, req: Connect) -> Self::Future {
match req.uri.scheme_str() {
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
fut: self.ssl_pool.call(req),
_phantom: PhantomData,
_t: PhantomData,
}),
_ => Either::Left(InnerConnectorResponseA {
fut: self.tcp_pool.call(req),
_phantom: PhantomData,
_t: PhantomData,
}),
}
}
@ -481,16 +491,18 @@ mod connect_impl {
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
where
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
{
#[pin]
fut: <ConnectionPool<T, Io1> as Service<Connect>>::Future,
_phantom: PhantomData<Io2>,
fut: <ConnectionPool<T, Io1> as Service>::Future,
_t: PhantomData<Io2>,
}
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
where
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io1, Protocol), Error = ConnectError>
+ 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
@ -508,16 +520,18 @@ mod connect_impl {
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
where
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
{
#[pin]
fut: <ConnectionPool<T, Io2> as Service<Connect>>::Future,
_phantom: PhantomData<Io1>,
fut: <ConnectionPool<T, Io2> as Service>::Future,
_t: PhantomData<Io1>,
}
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
where
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io2, Protocol), Error = ConnectError>
+ 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
@ -531,82 +545,3 @@ mod connect_impl {
}
}
}
#[cfg(not(feature = "trust-dns"))]
mod resolver {
use super::*;
pub(super) fn resolver() -> Resolver {
Resolver::Default
}
}
#[cfg(feature = "trust-dns")]
mod resolver {
use std::{cell::RefCell, net::SocketAddr};
use actix_tls::connect::Resolve;
use futures_core::future::LocalBoxFuture;
use trust_dns_resolver::{
config::{ResolverConfig, ResolverOpts},
system_conf::read_system_conf,
TokioAsyncResolver,
};
use super::*;
pub(super) fn resolver() -> Resolver {
// new type for impl Resolve trait for TokioAsyncResolver.
struct TrustDnsResolver(TokioAsyncResolver);
impl Resolve for TrustDnsResolver {
fn lookup<'a>(
&'a self,
host: &'a str,
port: u16,
) -> LocalBoxFuture<'a, Result<Vec<SocketAddr>, Box<dyn std::error::Error>>>
{
Box::pin(async move {
let res = self
.0
.lookup_ip(host)
.await?
.iter()
.map(|ip| SocketAddr::new(ip, port))
.collect();
Ok(res)
})
}
}
// dns struct is cached in thread local.
// so new client constructor can reuse the existing dns resolver.
thread_local! {
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
}
// 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),
Err(e) => {
log::error!("TRust-DNS can not load system config: {}", e);
(ResolverConfig::default(), ResolverOpts::default())
}
};
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
// box trust dns resolver and put it in thread local.
let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
*local.borrow_mut() = Some(resolver.clone());
resolver
}
}
})
}
}

View File

@ -1,9 +1,10 @@
use std::io;
use actix_connect::resolver::ResolveError;
use derive_more::{Display, From};
#[cfg(feature = "openssl")]
use actix_tls::accept::openssl::SslError;
use actix_connect::ssl::openssl::{HandshakeError, SslError};
use crate::error::{Error, ParseError, ResponseError};
use crate::http::{Error as HttpError, StatusCode};
@ -20,9 +21,14 @@ pub enum ConnectError {
#[display(fmt = "{}", _0)]
SslError(SslError),
/// SSL Handshake error
#[cfg(feature = "openssl")]
#[display(fmt = "{}", _0)]
SslHandshakeError(String),
/// Failed to resolve the hostname
#[display(fmt = "Failed resolving hostname: {}", _0)]
Resolver(Box<dyn std::error::Error>),
Resolver(ResolveError),
/// No dns records
#[display(fmt = "No dns records found for the input")]
@ -51,18 +57,25 @@ pub enum ConnectError {
impl std::error::Error for ConnectError {}
impl From<actix_tls::connect::ConnectError> for ConnectError {
fn from(err: actix_tls::connect::ConnectError) -> ConnectError {
impl From<actix_connect::ConnectError> for ConnectError {
fn from(err: actix_connect::ConnectError) -> ConnectError {
match err {
actix_tls::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
actix_tls::connect::ConnectError::NoRecords => ConnectError::NoRecords,
actix_tls::connect::ConnectError::InvalidInput => panic!(),
actix_tls::connect::ConnectError::Unresolved => ConnectError::Unresolved,
actix_tls::connect::ConnectError::Io(e) => ConnectError::Io(e),
actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
actix_connect::ConnectError::NoRecords => ConnectError::NoRecords,
actix_connect::ConnectError::InvalidInput => panic!(),
actix_connect::ConnectError::Unresolved => ConnectError::Unresolved,
actix_connect::ConnectError::Io(e) => ConnectError::Io(e),
}
}
}
#[cfg(feature = "openssl")]
impl<T: std::fmt::Debug> From<HandshakeError<T>> for ConnectError {
fn from(err: HandshakeError<T>) -> ConnectError {
ConnectError::SslHandshakeError(format!("{:?}", err))
}
}
#[derive(Debug, Display, From)]
pub enum InvalidUrl {
#[display(fmt = "Missing url scheme")]

View File

@ -1,10 +1,10 @@
use std::io::Write;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{io, time};
use std::{io, mem, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
use bytes::buf::BufMut;
use actix_codec::{AsyncRead, AsyncWrite, Framed};
use bytes::buf::BufMutExt;
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use futures_util::future::poll_fn;
@ -45,14 +45,14 @@ where
Some(port) => write!(wrt, "{}:{}", host, port),
};
match wrt.get_mut().split().freeze().try_into_value() {
match wrt.get_mut().split().freeze().try_into() {
Ok(value) => match head {
RequestHeadType::Owned(ref mut head) => {
head.headers.insert(HOST, value);
head.headers.insert(HOST, value)
}
RequestHeadType::Rc(_, ref mut extra_headers) => {
let headers = extra_headers.get_or_insert(HeaderMap::new());
headers.insert(HOST, value);
headers.insert(HOST, value)
}
},
Err(e) => log::error!("Can not set HOST header {}", e),
@ -72,7 +72,7 @@ where
// send request body
match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {}
BodySize::None | BodySize::Empty | BodySize::Sized(0) => (),
_ => send_body(body, Pin::new(&mut framed_inner)).await?,
};
@ -204,11 +204,18 @@ where
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
unsafe fn prepare_uninitialized_buffer(
&self,
buf: &mut [mem::MaybeUninit<u8>],
) -> bool {
self.io.as_ref().unwrap().prepare_uninitialized_buffer(buf)
}
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
}
}

View File

@ -22,10 +22,9 @@ use super::config::ConnectorConfig;
use super::connection::{ConnectionType, IoConnection};
use super::error::SendRequestError;
use super::pool::Acquired;
use crate::client::connection::H2Connection;
pub(crate) async fn send_request<T, B>(
mut io: H2Connection,
mut io: SendRequest<Bytes>,
head: RequestHeadType,
body: B,
created: time::Instant,
@ -90,7 +89,7 @@ where
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
CONTENT_LENGTH if skip_len => continue,
// DATE => has_date = true,
_ => {}
_ => (),
}
req.headers_mut().append(key, value.clone());
}
@ -172,9 +171,9 @@ async fn send_body<B: MessageBody>(
}
}
/// release SendRequest object
// release SendRequest object
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: H2Connection,
io: SendRequest<Bytes>,
pool: Option<Acquired<T>>,
created: time::Instant,
close: bool,

View File

@ -6,15 +6,13 @@ use std::rc::Rc;
use std::task::{Context, Poll};
use std::time::{Duration, Instant};
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use actix_rt::time::{sleep, Sleep};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{delay_for, Delay};
use actix_service::Service;
use actix_utils::task::LocalWaker;
use ahash::AHashMap;
use actix_utils::{oneshot, task::LocalWaker};
use bytes::Bytes;
use futures_channel::oneshot;
use futures_core::future::LocalBoxFuture;
use futures_util::future::{poll_fn, FutureExt};
use futures_util::future::{poll_fn, FutureExt, LocalBoxFuture};
use fxhash::FxHashMap;
use h2::client::{Connection, SendRequest};
use http::uri::Authority;
use indexmap::IndexSet;
@ -26,7 +24,6 @@ use super::connection::{ConnectionType, IoConnection};
use super::error::ConnectError;
use super::h2proto::handshake;
use super::Connect;
use crate::client::connection::H2Connection;
#[derive(Clone, Copy, PartialEq)]
/// Protocol version
@ -47,21 +44,22 @@ impl From<Authority> for Key {
}
/// Connections pool
pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<T>, Rc<RefCell<Inner<Io>>>);
pub(crate) struct ConnectionPool<T, Io: 'static>(Rc<RefCell<T>>, Rc<RefCell<Inner<Io>>>);
impl<T, Io> ConnectionPool<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
pub(crate) fn new(connector: T, config: ConnectorConfig) -> Self {
let connector_rc = Rc::new(connector);
let connector_rc = Rc::new(RefCell::new(connector));
let inner_rc = Rc::new(RefCell::new(Inner {
config,
acquired: 0,
waiters: Slab::new(),
waiters_queue: IndexSet::new(),
available: AHashMap::default(),
available: FxHashMap::default(),
waker: LocalWaker::new(),
}));
@ -91,21 +89,23 @@ impl<T, Io> Drop for ConnectionPool<T, Io> {
}
}
impl<T, Io> Service<Connect> for ConnectionPool<T, Io>
impl<T, Io> Service for ConnectionPool<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>
+ 'static,
{
type Request = Connect;
type Response = IoConnection<Io>;
type Error = ConnectError;
type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.poll_ready(cx)
}
fn call(&self, req: Connect) -> Self::Future {
let connector = self.0.clone();
fn call(&mut self, req: Connect) -> Self::Future {
let mut connector = self.0.clone();
let inner = self.1.clone();
let fut = async move {
@ -140,9 +140,10 @@ where
Some(guard.consume()),
))
} else {
let (sender, connection) = handshake(io, &config).await?;
let (snd, connection) = handshake(io, &config).await?;
actix_rt::spawn(connection.map(|_| ()));
Ok(IoConnection::new(
ConnectionType::H2(H2Connection::new(sender, connection)),
ConnectionType::H2(snd),
Instant::now(),
Some(guard.consume()),
))
@ -258,7 +259,7 @@ struct AvailableConnection<Io> {
pub(crate) struct Inner<Io> {
config: ConnectorConfig,
acquired: usize,
available: AHashMap<Key, VecDeque<AvailableConnection<Io>>>,
available: FxHashMap<Key, VecDeque<AvailableConnection<Io>>>,
waiters: Slab<
Option<(
Connect,
@ -326,22 +327,21 @@ where
{
if let Some(timeout) = self.config.disconnect_timeout {
if let ConnectionType::H1(io) = conn.io {
actix_rt::spawn(CloseConnection::new(io, timeout));
actix_rt::spawn(CloseConnection::new(io, timeout))
}
}
} else {
let mut io = conn.io;
let mut buf = [0; 2];
let mut read_buf = ReadBuf::new(&mut buf);
if let ConnectionType::H1(ref mut s) = io {
match Pin::new(s).poll_read(cx, &mut read_buf) {
Poll::Pending => {}
Poll::Ready(Ok(())) if !read_buf.filled().is_empty() => {
match Pin::new(s).poll_read(cx, &mut buf) {
Poll::Pending => (),
Poll::Ready(Ok(n)) if n > 0 => {
if let Some(timeout) = self.config.disconnect_timeout {
if let ConnectionType::H1(io) = io {
actix_rt::spawn(CloseConnection::new(
io, timeout,
));
))
}
}
continue;
@ -373,7 +373,7 @@ where
self.acquired -= 1;
if let Some(timeout) = self.config.disconnect_timeout {
if let ConnectionType::H1(io) = io {
actix_rt::spawn(CloseConnection::new(io, timeout));
actix_rt::spawn(CloseConnection::new(io, timeout))
}
}
self.check_availability();
@ -386,11 +386,9 @@ where
}
}
#[pin_project::pin_project]
struct CloseConnection<T> {
io: T,
#[pin]
timeout: Sleep,
timeout: Delay,
}
impl<T> CloseConnection<T>
@ -400,7 +398,7 @@ where
fn new(io: T, timeout: Duration) -> Self {
CloseConnection {
io,
timeout: sleep(timeout),
timeout: delay_for(timeout),
}
}
}
@ -412,11 +410,11 @@ where
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<()> {
let this = self.project();
let this = self.get_mut();
match this.timeout.poll(cx) {
match Pin::new(&mut this.timeout).poll(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => match Pin::new(this.io).poll_shutdown(cx) {
Poll::Pending => match Pin::new(&mut this.io).poll_shutdown(cx) {
Poll::Ready(_) => Poll::Ready(()),
Poll::Pending => Poll::Pending,
},
@ -429,14 +427,14 @@ struct ConnectorPoolSupport<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
connector: Rc<T>,
connector: T,
inner: Rc<RefCell<Inner<Io>>>,
}
impl<T, Io> Future for ConnectorPoolSupport<T, Io>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError>,
T: Service<Request = Connect, Response = (Io, Protocol), Error = ConnectError>,
T::Future: 'static,
{
type Output = ();
@ -536,7 +534,7 @@ where
rx: Some(rx),
inner: Some(inner),
config,
});
})
}
}
@ -566,10 +564,11 @@ where
if let Some(ref mut h2) = this.h2 {
return match Pin::new(h2).poll(cx) {
Poll::Ready(Ok((sender, connection))) => {
Poll::Ready(Ok((snd, connection))) => {
actix_rt::spawn(connection.map(|_| ()));
let rx = this.rx.take().unwrap();
let _ = rx.send(Ok(IoConnection::new(
ConnectionType::H2(H2Connection::new(sender, connection)),
ConnectionType::H2(snd),
Instant::now(),
Some(Acquired(this.key.clone(), this.inner.take())),
)));

View File

@ -0,0 +1,40 @@
use std::cell::RefCell;
use std::rc::Rc;
use std::task::{Context, Poll};
use actix_service::Service;
#[doc(hidden)]
/// Service that allows to turn non-clone service to a service with `Clone` impl
///
/// # Panics
/// CloneableService might panic with some creative use of thread local storage.
/// See https://github.com/actix/actix-web/issues/1295 for example
pub(crate) struct CloneableService<T: Service>(Rc<RefCell<T>>);
impl<T: Service> CloneableService<T> {
pub(crate) fn new(service: T) -> Self {
Self(Rc::new(RefCell::new(service)))
}
}
impl<T: Service> Clone for CloneableService<T> {
fn clone(&self) -> Self {
Self(self.0.clone())
}
}
impl<T: Service> Service for CloneableService<T> {
type Request = T::Request;
type Response = T::Response;
type Error = T::Error;
type Future = T::Future;
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.0.borrow_mut().poll_ready(cx)
}
fn call(&mut self, req: T::Request) -> Self::Future {
self.0.borrow_mut().call(req)
}
}

View File

@ -4,12 +4,12 @@ use std::rc::Rc;
use std::time::Duration;
use std::{fmt, net};
use actix_rt::time::{sleep, sleep_until, Instant, Sleep};
use actix_rt::time::{delay_for, delay_until, Delay, Instant};
use bytes::BytesMut;
use futures_util::{future, FutureExt};
use time::OffsetDateTime;
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
const DATE_VALUE_LENGTH: usize = 29;
#[derive(Debug, PartialEq, Clone, Copy)]
@ -121,10 +121,10 @@ impl ServiceConfig {
#[inline]
/// Client timeout for first request.
pub fn client_timer(&self) -> Option<Sleep> {
pub fn client_timer(&self) -> Option<Delay> {
let delay_time = self.0.client_timeout;
if delay_time != 0 {
Some(sleep_until(
Some(delay_until(
self.0.timer.now() + Duration::from_millis(delay_time),
))
} else {
@ -154,9 +154,9 @@ impl ServiceConfig {
#[inline]
/// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Sleep> {
pub fn keep_alive_timer(&self) -> Option<Delay> {
if let Some(ka) = self.0.keep_alive {
Some(sleep_until(self.0.timer.now() + ka))
Some(delay_until(self.0.timer.now() + ka))
} else {
None
}
@ -266,7 +266,7 @@ impl DateService {
// periodic date update
let s = self.clone();
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
actix_rt::spawn(delay_for(Duration::from_millis(500)).then(move |_| {
s.0.reset();
future::ready(())
}));

View File

@ -3,14 +3,14 @@ use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliDecoder;
use actix_threadpool::{run, CpuFuture};
use brotli::DecompressorWriter as BrotliDecoder;
use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream};
use super::Writer;
use crate::error::{BlockingError, PayloadError};
use crate::error::PayloadError;
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
const INPLACE: usize = 2049;
@ -19,7 +19,7 @@ pub struct Decoder<S> {
decoder: Option<ContentDecoder>,
stream: S,
eof: bool,
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>,
}
impl<S> Decoder<S>
@ -31,7 +31,7 @@ where
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding {
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
BrotliDecoder::new(Writer::new()),
BrotliDecoder::new(Writer::new(), 8 * 1024),
))),
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()),
@ -79,8 +79,10 @@ where
) -> Poll<Option<Self::Item>> {
loop {
if let Some(ref mut fut) = self.fut {
let (chunk, decoder) =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
self.decoder = Some(decoder);
self.fut.take();
if let Some(chunk) = chunk {
@ -103,7 +105,7 @@ where
return Poll::Ready(Some(Ok(chunk)));
}
} else {
self.fut = Some(spawn_blocking(move || {
self.fut = Some(run(move || {
let chunk = decoder.feed_data(chunk)?;
Ok((chunk, decoder))
}));

View File

@ -4,8 +4,8 @@ use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliEncoder;
use actix_threadpool::{run, CpuFuture};
use brotli::CompressorWriter as BrotliEncoder;
use bytes::Bytes;
use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready;
@ -17,7 +17,6 @@ use crate::http::{HeaderValue, StatusCode};
use crate::{Error, ResponseHead};
use super::Writer;
use crate::error::BlockingError;
const INPLACE: usize = 1024;
@ -27,7 +26,7 @@ pub struct Encoder<B> {
#[pin]
body: EncoderBody<B>,
encoder: Option<ContentEncoder>,
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
fut: Option<CpuFuture<ContentEncoder, io::Error>>,
}
impl<B: MessageBody> Encoder<B> {
@ -136,8 +135,10 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
}
if let Some(ref mut fut) = this.fut {
let mut encoder =
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
let chunk = encoder.take();
*this.encoder = Some(encoder);
this.fut.take();
@ -159,7 +160,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(Some(Ok(chunk)));
}
} else {
*this.fut = Some(spawn_blocking(move || {
*this.fut = Some(run(move || {
encoder.write(&chunk)?;
Ok(encoder)
}));
@ -211,9 +212,12 @@ impl ContentEncoder {
Writer::new(),
flate2::Compression::fast(),
))),
ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
}
ContentEncoding::Br => Some(ContentEncoder::Br(BrotliEncoder::new(
Writer::new(),
32 * 1024,
3,
22,
))),
_ => None,
}
}
@ -229,8 +233,8 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> {
match self {
ContentEncoder::Br(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()),
ContentEncoder::Br(mut encoder) => match encoder.flush() {
Ok(()) => Ok(encoder.into_inner().buf.freeze()),
Err(err) => Err(err),
},
ContentEncoder::Gzip(encoder) => match encoder.finish() {

View File

@ -7,6 +7,7 @@ use std::string::FromUtf8Error;
use std::{fmt, io, result};
use actix_codec::{Decoder, Encoder};
pub use actix_threadpool::BlockingError;
use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
use actix_utils::timeout::TimeoutError;
use bytes::BytesMut;
@ -18,12 +19,13 @@ use serde::de::value::Error as DeError;
use serde_json::error::Error as JsonError;
use serde_urlencoded::ser::Error as FormError;
// re-export for convenience
use crate::body::Body;
pub use crate::cookie::ParseError as CookieParseError;
use crate::helpers::Writer;
use crate::response::{Response, ResponseBuilder};
/// A specialized [`std::result::Result`]
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
/// for actix web operations
///
/// This typedef is generally used to avoid writing out
@ -53,7 +55,7 @@ impl Error {
/// Similar to `as_response_error` but downcasts.
pub fn as_error<T: ResponseError + 'static>(&self) -> Option<&T> {
ResponseError::downcast_ref(self.cause.as_ref())
<dyn ResponseError>::downcast_ref(self.cause.as_ref())
}
}
@ -98,6 +100,10 @@ impl fmt::Debug for Error {
}
impl std::error::Error for Error {
fn cause(&self) -> Option<&dyn std::error::Error> {
None
}
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None
}
@ -147,10 +153,7 @@ impl From<ResponseBuilder> for Error {
}
}
/// Inspects the underlying enum and returns an appropriate status code.
///
/// If the variant is [`TimeoutError::Service`], the error code of the service is returned.
/// Otherwise, [`StatusCode::GATEWAY_TIMEOUT`] is returned.
/// Return `GATEWAY_TIMEOUT` for `TimeoutError`
impl<E: ResponseError> ResponseError for TimeoutError<E> {
fn status_code(&self) -> StatusCode {
match self {
@ -164,44 +167,48 @@ impl<E: ResponseError> ResponseError for TimeoutError<E> {
#[display(fmt = "UnknownError")]
struct UnitError;
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`].
/// `InternalServerError` for `UnitError`
impl ResponseError for UnitError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`JsonError`].
/// `InternalServerError` for `JsonError`
impl ResponseError for JsonError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`FormError`].
/// `InternalServerError` for `FormError`
impl ResponseError for FormError {}
#[cfg(feature = "openssl")]
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`].
impl ResponseError for actix_tls::accept::openssl::SslError {}
/// `InternalServerError` for `openssl::ssl::Error`
impl ResponseError for actix_connect::ssl::openssl::SslError {}
/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`].
#[cfg(feature = "openssl")]
/// `InternalServerError` for `openssl::ssl::HandshakeError`
impl<T: std::fmt::Debug> ResponseError for actix_tls::openssl::HandshakeError<T> {}
/// Return `BAD_REQUEST` for `de::value::Error`
impl ResponseError for DeError {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`Canceled`].
/// `InternalServerError` for `Canceled`
impl ResponseError for Canceled {}
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
/// `InternalServerError` for `BlockingError`
impl<E: fmt::Debug> ResponseError for BlockingError<E> {}
/// Return `BAD_REQUEST` for `Utf8Error`
impl ResponseError for Utf8Error {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
}
}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`HttpError`].
/// Return `InternalServerError` for `HttpError`,
/// Response generation can return `HttpError`, so it is internal error
impl ResponseError for HttpError {}
/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code.
///
/// If the error is [`io::ErrorKind::NotFound`], [`StatusCode::NOT_FOUND`] is returned. If the
/// error is [`io::ErrorKind::PermissionDenied`], [`StatusCode::FORBIDDEN`] is returned. Otherwise,
/// [`StatusCode::INTERNAL_SERVER_ERROR`] is returned.
/// Return `InternalServerError` for `io::Error`
impl ResponseError for io::Error {
fn status_code(&self) -> StatusCode {
match self.kind() {
@ -212,7 +219,7 @@ impl ResponseError for io::Error {
}
}
/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`].
/// `BadRequest` for `InvalidHeaderValue`
impl ResponseError for header::InvalidHeaderValue {
fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST
@ -301,60 +308,33 @@ impl From<httparse::Error> for ParseError {
}
}
/// A set of errors that can occur running blocking tasks in thread pool.
#[derive(Debug, Display)]
#[display(fmt = "Blocking thread pool is gone")]
pub struct BlockingError;
impl std::error::Error for BlockingError {}
/// `InternalServerError` for `BlockingError`
impl ResponseError for BlockingError {}
#[derive(Display, Debug)]
/// A set of errors that can occur during payload parsing
pub enum PayloadError {
/// A payload reached EOF, but is not complete.
#[display(
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
fmt = "A payload reached EOF, but is not complete. With error: {:?}",
_0
)]
Incomplete(Option<io::Error>),
/// Content encoding stream corruption.
/// Content encoding stream corruption
#[display(fmt = "Can not decode content-encoding.")]
EncodingCorrupted,
/// Payload reached size limit.
#[display(fmt = "Payload reached size limit.")]
/// A payload reached size limit.
#[display(fmt = "A payload reached size limit.")]
Overflow,
/// Payload length is unknown.
#[display(fmt = "Payload length is unknown.")]
/// A payload length is unknown.
#[display(fmt = "A payload length is unknown.")]
UnknownLength,
/// HTTP/2 payload error.
/// Http2 payload error
#[display(fmt = "{}", _0)]
Http2Payload(h2::Error),
/// Generic I/O error.
/// Io error
#[display(fmt = "{}", _0)]
Io(io::Error),
}
impl std::error::Error for PayloadError {
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
match self {
PayloadError::Incomplete(None) => None,
PayloadError::Incomplete(Some(err)) => Some(err as &dyn std::error::Error),
PayloadError::EncodingCorrupted => None,
PayloadError::Overflow => None,
PayloadError::UnknownLength => None,
PayloadError::Http2Payload(err) => Some(err as &dyn std::error::Error),
PayloadError::Io(err) => Some(err as &dyn std::error::Error),
}
}
}
impl std::error::Error for PayloadError {}
impl From<h2::Error> for PayloadError {
fn from(err: h2::Error) -> Self {
@ -374,12 +354,15 @@ impl From<io::Error> for PayloadError {
}
}
impl From<BlockingError> for PayloadError {
fn from(_: BlockingError) -> Self {
PayloadError::Io(io::Error::new(
io::ErrorKind::Other,
"Operation is canceled",
))
impl From<BlockingError<io::Error>> for PayloadError {
fn from(err: BlockingError<io::Error>) -> Self {
match err {
BlockingError::Error(e) => PayloadError::Io(e),
BlockingError::Canceled => PayloadError::Io(io::Error::new(
io::ErrorKind::Other,
"Operation is canceled",
)),
}
}
}
@ -968,6 +951,16 @@ where
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
}
#[cfg(feature = "actors")]
/// `InternalServerError` for `actix::MailboxError`
/// This is supported on feature=`actors` only
impl ResponseError for actix::MailboxError {}
#[cfg(feature = "actors")]
/// `InternalServerError` for `actix::ResolverError`
/// This is supported on feature=`actors` only
impl ResponseError for actix::actors::resolver::ResolverError {}
#[cfg(test)]
mod tests {
use super::*;
@ -1025,22 +1018,22 @@ mod tests {
fn test_payload_error() {
let err: PayloadError =
io::Error::new(io::ErrorKind::Other, "ParseError").into();
assert!(err.to_string().contains("ParseError"));
assert!(format!("{}", err).contains("ParseError"));
let err = PayloadError::Incomplete(None);
assert_eq!(
err.to_string(),
"A payload reached EOF, but is not complete. Inner error: None"
format!("{}", err),
"A payload reached EOF, but is not complete. With error: None"
);
}
macro_rules! from {
($from:expr => $error:pat) => {
match ParseError::from($from) {
err @ $error => {
assert!(err.to_string().len() >= 5);
e @ $error => {
assert!(format!("{}", e).len() >= 5);
}
err => unreachable!("{:?}", err),
e => unreachable!("{:?}", e),
}
};
}
@ -1083,7 +1076,7 @@ mod tests {
let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "Payload reached size limit.");
assert_eq!(err.to_string(), "A payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none());
}

View File

@ -1,119 +1,62 @@
use std::{
any::{Any, TypeId},
fmt, mem,
};
use std::any::{Any, TypeId};
use std::{fmt, mem};
use ahash::AHashMap;
use fxhash::FxHashMap;
/// A type map for request extensions.
///
/// All entries into this map must be owned types (or static references).
#[derive(Default)]
/// A type map of request extensions.
pub struct Extensions {
/// Use FxHasher with a std HashMap with for faster
/// lookups on the small `TypeId` (u64 equivalent) keys.
map: AHashMap<TypeId, Box<dyn Any>>,
map: FxHashMap<TypeId, Box<dyn Any>>,
}
impl Extensions {
/// Creates an empty `Extensions`.
/// Create an empty `Extensions`.
#[inline]
pub fn new() -> Extensions {
Extensions {
map: AHashMap::default(),
map: FxHashMap::default(),
}
}
/// Insert an item into the map.
/// Insert a type into this `Extensions`.
///
/// If an item of this type was already stored, it will be replaced and returned.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert_eq!(map.insert(""), None);
/// assert_eq!(map.insert(1u32), None);
/// assert_eq!(map.insert(2u32), Some(1u32));
/// assert_eq!(*map.get::<u32>().unwrap(), 2u32);
/// ```
pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
self.map
.insert(TypeId::of::<T>(), Box::new(val))
.and_then(downcast_owned)
/// If a extension of this type already existed, it will
/// be returned.
pub fn insert<T: 'static>(&mut self, val: T) {
self.map.insert(TypeId::of::<T>(), Box::new(val));
}
/// Check if map contains an item of a given type.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert!(!map.contains::<u32>());
///
/// assert_eq!(map.insert(1u32), None);
/// assert!(map.contains::<u32>());
/// ```
/// Check if container contains entry
pub fn contains<T: 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>())
}
/// Get a reference to an item of a given type.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// map.insert(1u32);
/// assert_eq!(map.get::<u32>(), Some(&1u32));
/// ```
/// Get a reference to a type previously inserted on this `Extensions`.
pub fn get<T: 'static>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref())
}
/// Get a mutable reference to an item of a given type.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// map.insert(1u32);
/// assert_eq!(map.get_mut::<u32>(), Some(&mut 1u32));
/// ```
/// Get a mutable reference to a type previously inserted on this `Extensions`.
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.map
.get_mut(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_mut())
}
/// Remove an item from the map of a given type.
/// Remove a type from this `Extensions`.
///
/// If an item of this type was already stored, it will be returned.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
///
/// map.insert(1u32);
/// assert_eq!(map.get::<u32>(), Some(&1u32));
///
/// assert_eq!(map.remove::<u32>(), Some(1u32));
/// assert!(!map.contains::<u32>());
/// ```
/// If a extension of this type existed, it will be returned.
pub fn remove<T: 'static>(&mut self) -> Option<T> {
self.map.remove(&TypeId::of::<T>()).and_then(downcast_owned)
self.map
.remove(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed))
}
/// Clear the `Extensions` of all inserted extensions.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
///
/// map.insert(1u32);
/// assert!(map.contains::<u32>());
///
/// map.clear();
/// assert!(!map.contains::<u32>());
/// ```
#[inline]
pub fn clear(&mut self) {
self.map.clear();
@ -136,10 +79,6 @@ impl fmt::Debug for Extensions {
}
}
fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
boxed.downcast().ok().map(|boxed| *boxed)
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -223,3 +223,15 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
Ok(())
}
}
pub struct Writer<'a>(pub &'a mut BytesMut);
impl<'a> io::Write for Writer<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}

View File

@ -58,7 +58,6 @@ impl Codec {
} else {
Flags::empty()
};
Codec {
config,
flags,
@ -70,26 +69,26 @@ impl Codec {
}
}
/// Check if request is upgrade.
#[inline]
/// Check if request is upgrade
pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade
}
/// Check if last response is keep-alive.
#[inline]
/// Check if last response is keep-alive
pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive
}
/// Check if keep-alive enabled on server level.
#[inline]
/// Check if keep-alive enabled on server level
pub fn keepalive_enabled(&self) -> bool {
self.flags.contains(Flags::KEEPALIVE_ENABLED)
}
/// Check last request's message type.
#[inline]
/// Check last request's message type
pub fn message_type(&self) -> MessageType {
if self.flags.contains(Flags::STREAM) {
MessageType::Stream
@ -111,8 +110,8 @@ impl Decoder for Codec {
type Error = ParseError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if let Some(ref mut payload) = self.payload {
Ok(match payload.decode(src)? {
if self.payload.is_some() {
Ok(match self.payload.as_mut().unwrap().decode(src)? {
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
Some(PayloadItem::Eof) => {
self.payload.take();

View File

@ -14,7 +14,7 @@ use crate::header::HeaderMap;
use crate::message::{ConnectionType, ResponseHead};
use crate::request::Request;
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
const MAX_BUFFER_SIZE: usize = 131_072;
const MAX_HEADERS: usize = 96;
/// Incoming message decoder
@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized {
let mut has_upgrade_websocket = false;
let mut expect = false;
let mut chunked = false;
let mut seen_te = false;
let mut content_length = None;
{
@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized {
};
match name {
header::CONTENT_LENGTH => {
if let Ok(s) = value.to_str() {
header::CONTENT_LENGTH if content_length.is_some() => {
debug!("multiple Content-Length");
return Err(ParseError::Header);
}
header::CONTENT_LENGTH => match value.to_str() {
Ok(s) if s.trim().starts_with("+") => {
debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header);
}
Ok(s) => {
if let Ok(len) = s.parse::<u64>() {
if len != 0 {
content_length = Some(len);
@ -95,15 +105,31 @@ pub(crate) trait MessageType: Sized {
debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header);
}
} else {
}
Err(_) => {
debug!("illegal Content-Length: {:?}", value);
return Err(ParseError::Header);
}
}
},
// transfer-encoding
header::TRANSFER_ENCODING if seen_te => {
debug!("multiple Transfer-Encoding not allowed");
return Err(ParseError::Header);
}
header::TRANSFER_ENCODING => {
seen_te = true;
if let Ok(s) = value.to_str().map(|s| s.trim()) {
chunked = s.eq_ignore_ascii_case("chunked");
if s.eq_ignore_ascii_case("chunked") {
chunked = true;
} else if s.eq_ignore_ascii_case("identity") {
// allow silently since multiple TE headers are already checked
} else {
debug!("illegal Transfer-Encoding: {:?}", s);
return Err(ParseError::Header);
}
} else {
return Err(ParseError::Header);
}
@ -137,7 +163,7 @@ pub(crate) trait MessageType: Sized {
expect = true;
}
}
_ => {}
_ => (),
}
headers.append(name, value);
@ -203,15 +229,7 @@ impl MessageType for Request {
(len, method, uri, version, req.headers.len())
}
httparse::Status::Partial => {
return if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
Err(ParseError::TooLarge)
} else {
// Return None to notify more read are needed for parsing request
Ok(None)
};
}
httparse::Status::Partial => return Ok(None),
}
};
@ -230,6 +248,9 @@ impl MessageType for Request {
PayloadLength::None => {
if method == Method::CONNECT {
PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else {
PayloadType::None
}
@ -278,14 +299,7 @@ impl MessageType for ResponseHead {
(len, version, status, res.headers.len())
}
httparse::Status::Partial => {
return if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
Err(ParseError::TooLarge)
} else {
Ok(None)
}
}
httparse::Status::Partial => return Ok(None),
}
};
@ -301,6 +315,9 @@ impl MessageType for ResponseHead {
} else if status == StatusCode::SWITCHING_PROTOCOLS {
// switching protocol or connect
PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else {
// for HTTP/1.0 read to eof and close connection
if msg.version == Version::HTTP_10 {
@ -519,19 +536,11 @@ impl ChunkedState {
size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> {
let radix = 16;
match byte!(rdr) {
b @ b'0'..=b'9' => {
*size *= radix;
*size += u64::from(b - b'0');
}
b @ b'a'..=b'f' => {
*size *= radix;
*size += u64::from(b + 10 - b'a');
}
b @ b'A'..=b'F' => {
*size *= radix;
*size += u64::from(b + 10 - b'A');
}
let rem = match byte!(rdr) {
b @ b'0'..=b'9' => b - b'0',
b @ b'a'..=b'f' => b + 10 - b'a',
b @ b'A'..=b'F' => b + 10 - b'A',
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
@ -541,8 +550,23 @@ impl ChunkedState {
"Invalid chunk size line: Invalid Size",
)));
}
};
match size.checked_mul(radix) {
Some(n) => {
*size = n as u64;
*size += rem as u64;
Poll::Ready(Ok(ChunkedState::Size))
}
None => {
debug!("chunk size would overflow");
Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid chunk size line: Invalid Size",
)))
}
}
Poll::Ready(Ok(ChunkedState::Size))
}
fn read_size_lws(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
@ -561,6 +585,11 @@ impl ChunkedState {
fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)),
// strictly 0x20 (space) should be disallowed but we don't parse quoted strings here
0x00..=0x08 | 0x0a..=0x1f | 0x7f => Poll::Ready(Err(io::Error::new(
io::ErrorKind::InvalidInput,
"Invalid character in chunk extension",
))),
_ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
}
}
@ -694,7 +723,7 @@ mod tests {
match MessageDecoder::<Request>::default().decode($e) {
Err(err) => match err {
ParseError::Io(_) => unreachable!("Parse error expected"),
_ => {}
_ => (),
},
_ => unreachable!("Error expected"),
}
@ -830,8 +859,8 @@ mod tests {
.get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned())
.collect();
assert_eq!(val[0], "c1=cookie1");
assert_eq!(val[1], "c2=cookie2");
assert_eq!(val[1], "c1=cookie1");
assert_eq!(val[0], "c2=cookie2");
}
#[test]
@ -986,13 +1015,7 @@ mod tests {
"GET /test HTTP/1.1\r\n\
transfer-encoding: chnked\r\n\r\n",
);
let req = parse_ready!(&mut buf);
if let Ok(val) = req.chunked() {
assert!(!val);
} else {
unreachable!("Error");
}
expect_parse_err!(&mut buf);
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@ -8,7 +8,7 @@ use bytes::{BufMut, BytesMut};
use crate::body::BodySize;
use crate::config::ServiceConfig;
use crate::header::{map::Value, HeaderName};
use crate::header::map;
use crate::helpers;
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use crate::http::{HeaderMap, StatusCode, Version};
@ -21,7 +21,7 @@ const AVERAGE_HEADER_SIZE: usize = 30;
pub(crate) struct MessageEncoder<T: MessageType> {
pub length: BodySize,
pub te: TransferEncoding,
_phantom: PhantomData<T>,
_t: PhantomData<T>,
}
impl<T: MessageType> Default for MessageEncoder<T> {
@ -29,7 +29,7 @@ impl<T: MessageType> Default for MessageEncoder<T> {
MessageEncoder {
length: BodySize::None,
te: TransferEncoding::empty(),
_phantom: PhantomData,
_t: PhantomData,
}
}
}
@ -80,6 +80,7 @@ pub(crate) trait MessageType: Sized {
match length {
BodySize::Stream => {
if chunked {
skip_len = true;
if camel_case {
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
} else {
@ -118,14 +119,24 @@ pub(crate) trait MessageType: Sized {
dst.put_slice(b"connection: close\r\n")
}
}
_ => {}
_ => (),
}
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
let empty_headers = HeaderMap::new();
let extra_headers = self.extra_headers().unwrap_or(&empty_headers);
let headers = self
.headers()
.inner
.iter()
.filter(|(name, _)| !extra_headers.contains_key(*name))
.chain(extra_headers.inner.iter());
// write headers
let mut has_date = false;
let mut buf = dst.chunk_mut().as_mut_ptr();
let mut buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
let mut remaining = dst.capacity() - dst.len();
// tracks bytes written since last buffer resize
@ -133,67 +144,117 @@ pub(crate) trait MessageType: Sized {
// container's knowledge, this is used to sync the containers cursor after data is written
let mut pos = 0;
self.write_headers(|key, value| {
for (key, value) in headers {
match *key {
CONNECTION => return,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => return,
CONNECTION => continue,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {}
_ => (),
}
let k = key.as_str().as_bytes();
let k_len = k.len();
// TODO: drain?
for val in value.iter() {
let v = val.as_ref();
let v_len = v.len();
match value {
map::Value::One(ref val) => {
let v = val.as_ref();
let v_len = v.len();
// key length + value length + colon + space + \r\n
let len = k_len + v_len + 4;
// key length + value length + colon + space + \r\n
let len = k_len + v_len + 4;
if len > remaining {
// SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
unsafe {
dst.advance_mut(pos);
if len > remaining {
// not enough room in buffer for this header; reserve more space
// SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// SAFETY: on each write, it is enough to ensure that the advancement of the
// cursor matches the number of bytes written
unsafe {
// use upper Camel-Case
if camel_case {
write_camel_case(k, from_raw_parts_mut(buf, k_len))
} else {
write_data(k, buf, k_len)
}
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.chunk_mut().as_mut_ptr();
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
}
pos += len;
remaining -= len;
}
// SAFETY: on each write, it is enough to ensure that the advancement of
// the cursor matches the number of bytes written
unsafe {
if camel_case {
// use Camel-Case headers
write_camel_case(k, from_raw_parts_mut(buf, k_len));
} else {
write_data(k, buf, k_len);
map::Value::Multi(ref vec) => {
for val in vec {
let v = val.as_ref();
let v_len = v.len();
let len = k_len + v_len + 4;
if len > remaining {
// SAFETY: all the bytes written up to position "pos" are initialized
// the written byte count and pointer advancement are kept in sync
unsafe {
dst.advance_mut(pos);
}
pos = 0;
dst.reserve(len * 2);
remaining = dst.capacity() - dst.len();
// re-assign buf raw pointer since it's possible that the buffer was
// reallocated and/or resized
buf = dst.bytes_mut().as_mut_ptr() as *mut u8;
}
// SAFETY: on each write, it is enough to ensure that the advancement of
// the cursor matches the number of bytes written
unsafe {
if camel_case {
write_camel_case(k, from_raw_parts_mut(buf, k_len));
} else {
write_data(k, buf, k_len);
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
};
pos += len;
remaining -= len;
}
buf = buf.add(k_len);
write_data(b": ", buf, 2);
buf = buf.add(2);
write_data(v, buf, v_len);
buf = buf.add(v_len);
write_data(b"\r\n", buf, 2);
buf = buf.add(2);
};
pos += len;
remaining -= len;
}
}
});
}
// final cursor synchronization with the bytes container
//
@ -213,24 +274,6 @@ pub(crate) trait MessageType: Sized {
Ok(())
}
fn write_headers<F>(&mut self, mut f: F)
where
F: FnMut(&HeaderName, &Value),
{
match self.extra_headers() {
Some(headers) => {
// merging headers from head and extra headers.
self.headers()
.inner
.iter()
.filter(|(name, _)| !headers.contains_key(*name))
.chain(headers.inner.iter())
.for_each(|(k, v)| f(k, v))
}
None => self.headers().inner.iter().for_each(|(k, v)| f(k, v)),
}
}
}
impl MessageType for Response<()> {
@ -287,7 +330,7 @@ impl MessageType for RequestHeadType {
let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!(
helpers::Writer(dst),
Writer(dst),
"{} {} {}",
head.method,
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
@ -420,7 +463,7 @@ impl TransferEncoding {
*eof = true;
buf.extend_from_slice(b"0\r\n\r\n");
} else {
writeln!(helpers::Writer(buf), "{:X}\r", msg.len())
writeln!(Writer(buf), "{:X}\r", msg.len())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2);
@ -470,6 +513,18 @@ impl TransferEncoding {
}
}
struct Writer<'a>(pub &'a mut BytesMut);
impl<'a> io::Write for Writer<'a> {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
/// # Safety
/// Callers must ensure that the given length matches given value length.
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
@ -478,29 +533,30 @@ unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
}
fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
// first copy entire (potentially wrong) slice to output
buffer[..value.len()].copy_from_slice(value);
let mut index = 0;
let key = value;
let mut key_iter = key.iter();
let mut iter = value.iter();
// first character should be uppercase
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[0] = c & 0b1101_1111;
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
} else {
return;
}
// track 1 ahead of the current position since that's the location being assigned to
let mut index = 2;
// remaining characters after hyphens should also be uppercase
while let Some(&c) = iter.next() {
if c == b'-' {
// advance iter by one and uppercase if needed
if let Some(c @ b'a'..=b'z') = iter.next() {
buffer[index] = c & 0b1101_1111;
while let Some(c) = key_iter.next() {
buffer[index] = *c;
index += 1;
if *c == b'-' {
if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
buffer[index] = *c ^ b' ';
index += 1;
}
}
}
index += 1;
}
}
@ -549,8 +605,6 @@ mod tests {
);
let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
eprintln!("{}", &data);
assert!(data.contains("Content-Length: 0\r\n"));
assert!(data.contains("Connection: close\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n"));

View File

@ -1,36 +1,38 @@
use std::task::Poll;
use std::task::{Context, Poll};
use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready};
use futures_util::future::{ok, Ready};
use crate::error::Error;
use crate::request::Request;
pub struct ExpectHandler;
impl ServiceFactory<Request> for ExpectHandler {
impl ServiceFactory for ExpectHandler {
type Config = ();
type Request = Request;
type Response = Request;
type Error = Error;
type Config = ();
type Service = ExpectHandler;
type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: Self::Config) -> Self::Future {
ready(Ok(ExpectHandler))
fn new_service(&self, _: ()) -> Self::Future {
ok(ExpectHandler)
}
}
impl Service<Request> for ExpectHandler {
impl Service for ExpectHandler {
type Request = Request;
type Response = Request;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&self, req: Request) -> Self::Future {
ready(Ok(req))
// TODO: add some way to trigger error
// Err(error::ErrorExpectationFailed("test"))
fn call(&mut self, req: Request) -> Self::Future {
ok(req)
}
}

View File

@ -17,7 +17,7 @@ pub use self::codec::Codec;
pub use self::dispatcher::Dispatcher;
pub use self::expect::ExpectHandler;
pub use self::payload::Payload;
pub use self::service::{H1Service, H1ServiceHandler};
pub use self::service::{H1Service, H1ServiceHandler, OneRequest};
pub use self::upgrade::UpgradeHandler;
pub use self::utils::SendResponse;

View File

@ -182,7 +182,9 @@ impl Inner {
self.len += data.len();
self.items.push_back(data);
self.need_read = self.len < MAX_BUFFER_SIZE;
self.task.wake();
if let Some(task) = self.task.take() {
task.wake()
}
}
#[cfg(test)]

View File

@ -9,40 +9,42 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
use futures_core::ready;
use futures_util::future::ready;
use futures_util::future::{ok, Ready};
use crate::body::MessageBody;
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error};
use crate::error::{DispatchError, Error, ParseError};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::service::HttpFlow;
use crate::{ConnectCallback, OnConnectData};
use crate::{ConnectCallback, Extensions};
use super::codec::Codec;
use super::dispatcher::Dispatcher;
use super::{ExpectHandler, UpgradeHandler};
use super::{ExpectHandler, Message, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler<T>> {
srv: S,
cfg: ServiceConfig,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B> H1Service<T, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
{
/// Create new `HttpService` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
cfg: ServiceConfig,
service: F,
) -> Self {
@ -51,23 +53,28 @@ where
srv: service.into_factory(),
expect: ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_phantom: PhantomData,
_t: PhantomData,
}
}
}
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
U: ServiceFactory<
Config = (),
Request = (Request, Framed<TcpStream, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
@ -75,15 +82,15 @@ where
pub fn tcp(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = (),
> {
pipeline_factory(|io: TcpStream| {
let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr)))
ok((io, peer_addr))
})
.and_then(self)
}
@ -93,23 +100,22 @@ where
mod openssl {
use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
use actix_tls::accept::TlsError;
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, TlsError};
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<SslStream<TcpStream>, Codec>),
Config = (),
Request = (Request, Framed<SslStream<TcpStream>, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
@ -120,10 +126,10 @@ mod openssl {
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<SslError, DispatchError>,
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
InitError = (),
> {
pipeline_factory(
@ -133,7 +139,7 @@ mod openssl {
)
.and_then(|io: SslStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok();
ready(Ok((io, peer_addr)))
ok((io, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
@ -143,24 +149,23 @@ 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 actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::TlsError;
use std::{fmt, io};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>>,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (),
Request = (Request, Framed<TlsStream<TcpStream>, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
@ -171,8 +176,8 @@ mod rustls {
self,
config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
@ -184,7 +189,7 @@ mod rustls {
)
.and_then(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok();
ready(Ok((io, peer_addr)))
ok((io, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
@ -193,7 +198,7 @@ mod rustls {
impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
@ -201,7 +206,7 @@ where
{
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where
X1: ServiceFactory<Request, Response = Request>,
X1: ServiceFactory<Request = Request, Response = Request>,
X1::Error: Into<Error>,
X1::InitError: fmt::Debug,
{
@ -210,14 +215,15 @@ where
cfg: self.cfg,
srv: self.srv,
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> H1Service<T, S, B, X, U1>
where
U1: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U1: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
{
@ -226,11 +232,21 @@ where
cfg: self.cfg,
srv: self.srv,
expect: self.expect,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
@ -238,27 +254,27 @@ where
}
}
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
for H1Service<T, S, B, X, U>
impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
{
type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Config = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type Future = H1ServiceResponse<T, S, B, X, U>;
fn new_service(&self, _: ()) -> Self::Future {
@ -268,9 +284,10 @@ where
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_phantom: PhantomData,
_t: PhantomData,
}
}
}
@ -279,13 +296,13 @@ where
#[pin_project::pin_project]
pub struct H1ServiceResponse<T, S, B, X, U>
where
S: ServiceFactory<Request>,
S: ServiceFactory<Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request, Response = Request>,
X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
@ -297,23 +314,24 @@ where
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request>,
S: ServiceFactory<Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
S::InitError: fmt::Debug,
B: MessageBody,
X: ServiceFactory<Request, Response = Request>,
X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
@ -337,7 +355,7 @@ where
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
this.fut_ex.set(None);
}
let result = ready!(this
@ -353,6 +371,7 @@ where
service,
this.expect.take().unwrap(),
this.upgrade.take(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
)
}))
@ -360,65 +379,66 @@ where
}
/// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
X: Service<Request>,
U: Service<(Request, Framed<T, Codec>)>,
{
flow: Rc<HttpFlow<S, X, U>>,
pub struct H1ServiceHandler<T, S: Service, B, X: Service, U: Service> {
srv: CloneableService<S>,
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
srv: S,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler {
flow: HttpFlow::new(service, expect, upgrade),
srv: CloneableService::new(srv),
expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new),
cfg,
on_connect,
on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
for H1ServiceHandler<T, S, B, X, U>
impl<T, S, B, X, U> Service for H1ServiceHandler<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>,
{
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Future = Dispatcher<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self
.flow
.expect
.poll_ready(cx)
.map_err(|e| {
@ -429,8 +449,7 @@ where
.is_ready();
let ready = self
.flow
.service
.srv
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
@ -440,7 +459,7 @@ where
.is_ready()
&& ready;
let ready = if let Some(ref upg) = self.flow.upgrade {
let ready = if let Some(ref mut upg) = self.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
@ -460,16 +479,124 @@ where
}
}
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
let mut connect_extensions = Extensions::new();
if let Some(ref handler) = self.on_connect_ext {
// run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
Dispatcher::new(
io,
self.cfg.clone(),
self.flow.clone(),
on_connect_data,
self.srv.clone(),
self.expect.clone(),
self.upgrade.clone(),
deprecated_on_connect,
connect_extensions,
addr,
)
}
}
/// `ServiceFactory` implementation for `OneRequestService` service
#[derive(Default)]
pub struct OneRequest<T> {
config: ServiceConfig,
_t: PhantomData<T>,
}
impl<T> OneRequest<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
/// Create new `H1SimpleService` instance.
pub fn new() -> Self {
OneRequest {
config: ServiceConfig::default(),
_t: PhantomData,
}
}
}
impl<T> ServiceFactory for OneRequest<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Config = ();
type Request = T;
type Response = (Request, Framed<T, Codec>);
type Error = ParseError;
type InitError = ();
type Service = OneRequestService<T>;
type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(OneRequestService {
_t: PhantomData,
config: self.config.clone(),
})
}
}
/// `Service` implementation for HTTP1 transport. Reads one request and returns
/// request and framed object.
pub struct OneRequestService<T> {
_t: PhantomData<T>,
config: ServiceConfig,
}
impl<T> Service for OneRequestService<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Request = T;
type Response = (Request, Framed<T, Codec>);
type Error = ParseError;
type Future = OneRequestServiceResponse<T>;
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&mut self, req: Self::Request) -> Self::Future {
OneRequestServiceResponse {
framed: Some(Framed::new(req, Codec::new(self.config.clone()))),
}
}
}
#[doc(hidden)]
#[pin_project::pin_project]
pub struct OneRequestServiceResponse<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
#[pin]
framed: Option<Framed<T, Codec>>,
}
impl<T> Future for OneRequestServiceResponse<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Output = Result<(Request, Framed<T, Codec>), ParseError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
match ready!(this.framed.as_pin_mut().unwrap().next_item(cx)) {
Some(Ok(req)) => match req {
Message::Item(req) => {
let mut this = self.as_mut().project();
Poll::Ready(Ok((req, this.framed.take().unwrap())))
}
Message::Chunk(_) => unreachable!("Something is wrong"),
},
Some(Err(err)) => Poll::Ready(Err(err)),
None => Poll::Ready(Err(ParseError::Incomplete)),
}
}
}

View File

@ -1,20 +1,22 @@
use std::task::Poll;
use std::marker::PhantomData;
use std::task::{Context, Poll};
use actix_codec::Framed;
use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready};
use futures_util::future::Ready;
use crate::error::Error;
use crate::h1::Codec;
use crate::request::Request;
pub struct UpgradeHandler;
pub struct UpgradeHandler<T>(PhantomData<T>);
impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
impl<T> ServiceFactory for UpgradeHandler<T> {
type Config = ();
type Request = (Request, Framed<T, Codec>);
type Response = ();
type Error = Error;
type Config = ();
type Service = UpgradeHandler;
type Service = UpgradeHandler<T>;
type InitError = Error;
type Future = Ready<Result<Self::Service, Self::InitError>>;
@ -23,14 +25,17 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
}
}
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
impl<T> Service for UpgradeHandler<T> {
type Request = (Request, Framed<T, Codec>);
type Response = ();
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
Poll::Ready(Ok(()))
}
fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
ready(Ok(()))
fn call(&mut self, _: Self::Request) -> Self::Future {
unimplemented!()
}
}

View File

@ -1,65 +1,66 @@
use std::convert::TryFrom;
use std::future::Future;
use std::marker::PhantomData;
use std::net;
use std::pin::Pin;
use std::rc::Rc;
use std::task::{Context, Poll};
use std::{cmp, convert::TryFrom};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{Instant, Sleep};
use actix_rt::time::{Delay, Instant};
use actix_service::Service;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
use h2::server::{Connection, SendResponse};
use h2::SendStream;
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace};
use crate::body::{BodySize, MessageBody, ResponseBody};
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage;
use crate::message::ResponseHead;
use crate::payload::Payload;
use crate::request::Request;
use crate::response::Response;
use crate::service::HttpFlow;
use crate::OnConnectData;
use crate::Extensions;
const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol.
/// Dispatcher for HTTP/2 protocol
#[pin_project::pin_project]
pub struct Dispatcher<T, S, B, X, U>
pub struct Dispatcher<T, S: Service<Request = Request>, B: MessageBody>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
B: MessageBody,
{
flow: Rc<HttpFlow<S, X, U>>,
service: CloneableService<S>,
connection: Connection<T, Bytes>,
on_connect_data: OnConnectData,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig,
peer_addr: Option<net::SocketAddr>,
ka_expire: Instant,
ka_timer: Option<Sleep>,
_phantom: PhantomData<B>,
ka_timer: Option<Delay>,
_t: PhantomData<B>,
}
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
impl<T, S, B> Dispatcher<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error>,
// S::Future: 'static,
S::Response: Into<Response<B>>,
B: MessageBody,
{
pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>,
service: CloneableService<S>,
connection: Connection<T, Bytes>,
on_connect_data: OnConnectData,
on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig,
timeout: Option<Sleep>,
timeout: Option<Delay>,
peer_addr: Option<net::SocketAddr>,
) -> Self {
// let keepalive = config.keep_alive_enabled();
@ -79,22 +80,23 @@ where
};
Dispatcher {
flow,
service,
config,
peer_addr,
connection,
on_connect,
on_connect_data,
ka_expire,
ka_timer,
_phantom: PhantomData,
_t: PhantomData,
}
}
}
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
impl<T, S, B> Future for Dispatcher<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -107,12 +109,10 @@ where
let this = self.get_mut();
loop {
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) {
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err.into())),
Some(Ok((req, res))) => {
match Pin::new(&mut this.connection).poll_accept(cx) {
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Ready(Some(Err(err))) => return Poll::Ready(Err(err.into())),
Poll::Ready(Some(Ok((req, res)))) => {
// update keep-alive expire
if this.ka_timer.is_some() {
if let Some(expire) = this.config.keep_alive_expire() {
@ -121,9 +121,11 @@ where
}
let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
let mut req = Request::with_payload(Payload::<
crate::payload::PayloadStream,
>::H2(
crate::h2::Payload::new(body)
));
let head = &mut req.head_mut();
head.uri = parts.uri;
@ -132,21 +134,31 @@ where
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
// merge on_connect_ext data into request extensions
this.on_connect_data.merge_into(&mut req);
// DEPRECATED
// set on_connect data
if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut());
}
let svc = ServiceResponse::<S::Future, S::Response, S::Error, B> {
// merge on_connect_ext data into request extensions
req.extensions_mut().drain_from(&mut this.on_connect_data);
actix_rt::spawn(ServiceResponse::<
S::Future,
S::Response,
S::Error,
B,
> {
state: ServiceResponseState::ServiceCall(
this.flow.service.call(req),
this.service.call(req),
Some(res),
),
config: this.config.clone(),
buffer: None,
_phantom: PhantomData,
};
actix_rt::spawn(svc);
_t: PhantomData,
});
}
Poll::Pending => return Poll::Pending,
}
}
}
@ -158,7 +170,7 @@ struct ServiceResponse<F, I, E, B> {
state: ServiceResponseState<F, B>,
config: ServiceConfig,
buffer: Option<Bytes>,
_phantom: PhantomData<(I, E)>,
_t: PhantomData<(I, E)>,
}
#[pin_project::pin_project(project = ServiceResponseStateProj)]
@ -195,9 +207,8 @@ where
skip_len = true;
*size = BodySize::Stream;
}
_ => {}
_ => (),
}
let _ = match size {
BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
@ -212,13 +223,11 @@ where
// copy headers
for (key, value) in head.headers.iter() {
match *key {
// omit HTTP/1 only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {}
_ => (),
}
res.headers_mut().append(key, value.clone());
}
@ -250,117 +259,109 @@ where
let mut this = self.as_mut().project();
match this.state.project() {
ServiceResponseStateProj::ServiceCall(call, send) => {
match ready!(call.poll(cx)) {
Ok(res) => {
let (res, body) = res.into().replace_body(());
ServiceResponseStateProj::ServiceCall(call, send) => match call.poll(cx) {
Poll::Ready(Ok(res)) => {
let (res, body) = res.into().replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state
.set(ServiceResponseState::SendPayload(stream, body));
self.poll(cx)
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending h2 response: {:?}", e);
return Poll::Ready(());
}
}
Ok(stream) => stream,
};
Err(e) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state.set(ServiceResponseState::SendPayload(
stream,
body.into_body(),
));
self.poll(cx)
}
if size.is_eof() {
Poll::Ready(())
} else {
this.state
.set(ServiceResponseState::SendPayload(stream, body));
self.poll(cx)
}
}
}
Poll::Pending => Poll::Pending,
Poll::Ready(Err(e)) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending h2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state.set(ServiceResponseState::SendPayload(
stream,
body.into_body(),
));
self.poll(cx)
}
}
},
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
loop {
loop {
match this.buffer {
Some(ref mut buffer) => {
match ready!(stream.poll_capacity(cx)) {
None => return Poll::Ready(()),
if let Some(ref mut buffer) = this.buffer {
match stream.poll_capacity(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => return Poll::Ready(()),
Poll::Ready(Some(Ok(cap))) => {
let len = buffer.len();
let bytes = buffer.split_to(std::cmp::min(cap, len));
Some(Ok(cap)) => {
let len = buffer.len();
let bytes = buffer.split_to(cmp::min(cap, len));
if let Err(e) = stream.send_data(bytes, false) {
warn!("{:?}", e);
return Poll::Ready(());
} else if !buffer.is_empty() {
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Some(Err(e)) => {
if let Err(e) = stream.send_data(bytes, false) {
warn!("{:?}", e);
return Poll::Ready(());
} else if !buffer.is_empty() {
let cap =
std::cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Poll::Ready(Some(Err(e))) => {
warn!("{:?}", e);
return Poll::Ready(());
}
}
None => match ready!(body.as_mut().poll_next(cx)) {
None => {
} else {
match body.as_mut().poll_next(cx) {
Poll::Pending => return Poll::Pending,
Poll::Ready(None) => {
if let Err(e) = stream.send_data(Bytes::new(), true)
{
warn!("{:?}", e);
}
return Poll::Ready(());
}
Some(Ok(chunk)) => {
stream.reserve_capacity(cmp::min(
Poll::Ready(Some(Ok(chunk))) => {
stream.reserve_capacity(std::cmp::min(
chunk.len(),
CHUNK_SIZE,
));
*this.buffer = Some(chunk);
}
Some(Err(e)) => {
Poll::Ready(Some(Err(e))) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
}
},
}
}
}
}

View File

@ -1,12 +1,9 @@
//! HTTP/2 implementation.
use std::{
pin::Pin,
task::{Context, Poll},
};
//! HTTP/2 implementation
use std::pin::Pin;
use std::task::{Context, Poll};
use bytes::Bytes;
use futures_core::{ready, Stream};
use futures_core::Stream;
use h2::RecvStream;
mod dispatcher;
@ -16,14 +13,14 @@ pub use self::dispatcher::Dispatcher;
pub use self::service::H2Service;
use crate::error::PayloadError;
/// HTTP/2 peer stream.
/// H2 receive stream
pub struct Payload {
stream: RecvStream,
pl: RecvStream,
}
impl Payload {
pub(crate) fn new(stream: RecvStream) -> Self {
Self { stream }
pub(crate) fn new(pl: RecvStream) -> Self {
Self { pl }
}
}
@ -36,17 +33,18 @@ impl Stream for Payload {
) -> Poll<Option<Self::Item>> {
let this = self.get_mut();
match ready!(Pin::new(&mut this.stream).poll_data(cx)) {
Some(Ok(chunk)) => {
match Pin::new(&mut this.pl).poll_data(cx) {
Poll::Ready(Some(Ok(chunk))) => {
let len = chunk.len();
match this.stream.flow_control().release_capacity(len) {
Ok(()) => Poll::Ready(Some(Ok(chunk))),
Err(err) => Poll::Ready(Some(Err(err.into()))),
if let Err(err) = this.pl.flow_control().release_capacity(len) {
Poll::Ready(Some(Err(err.into())))
} else {
Poll::Ready(Some(Ok(chunk)))
}
}
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
None => Poll::Ready(None),
Poll::Ready(Some(Err(err))) => Poll::Ready(Some(Err(err.into()))),
Poll::Pending => Poll::Pending,
Poll::Ready(None) => Poll::Ready(None),
}
}
}

View File

@ -17,44 +17,57 @@ use h2::server::{self, Handshake};
use log::error;
use crate::body::MessageBody;
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::service::HttpFlow;
use crate::{ConnectCallback, OnConnectData};
use crate::{ConnectCallback, Extensions};
use super::dispatcher::Dispatcher;
/// `ServiceFactory` implementation for HTTP/2 transport
/// `ServiceFactory` implementation for HTTP2 transport
pub struct H2Service<T, S, B> {
srv: S,
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<(T, B)>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B> H2Service<T, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create new `H2Service` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
/// Create new `HttpService` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
cfg: ServiceConfig,
service: F,
) -> Self {
H2Service {
cfg,
on_connect: None,
on_connect_ext: None,
srv: service.into_factory(),
_phantom: PhantomData,
_t: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
@ -64,18 +77,18 @@ where
impl<S, B> H2Service<TcpStream, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create plain TCP based service
/// Create simple tcp based service
pub fn tcp(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = S::InitError,
@ -92,29 +105,29 @@ where
#[cfg(feature = "openssl")]
mod openssl {
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
use actix_tls::accept::TlsError;
use actix_service::{fn_factory, fn_service};
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, TlsError};
use super::*;
impl<S, B> H2Service<SslStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create OpenSSL based service
/// Create ssl based service
pub fn openssl(
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<SslError, DispatchError>,
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
InitError = S::InitError,
> {
pipeline_factory(
@ -136,26 +149,25 @@ 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 actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::TlsError;
use std::io;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create Rustls based service
/// Create openssl based service
pub fn rustls(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError,
@ -179,52 +191,52 @@ mod rustls {
}
}
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
impl<T, S, B> ServiceFactory for H2Service<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Config = ();
type Service = H2ServiceHandler<T, S::Service, B>;
type InitError = S::InitError;
type Service = H2ServiceHandler<T, S::Service, B>;
type Future = H2ServiceResponse<T, S, B>;
fn new_service(&self, _: ()) -> Self::Future {
H2ServiceResponse {
fut: self.srv.new_service(()),
cfg: Some(self.cfg.clone()),
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
_phantom: PhantomData,
_t: PhantomData,
}
}
}
#[doc(hidden)]
#[pin_project::pin_project]
pub struct H2ServiceResponse<T, S, B>
where
S: ServiceFactory<Request>,
{
pub struct H2ServiceResponse<T, S: ServiceFactory, B> {
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
@ -232,31 +244,30 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
this.fut.poll(cx).map_ok(|service| {
Poll::Ready(ready!(this.fut.poll(cx)).map(|service| {
let this = self.as_mut().project();
H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
service,
)
})
}))
}
}
/// `Service` implementation for http/2 transport
pub struct H2ServiceHandler<T, S, B>
where
S: Service<Request>,
{
flow: Rc<HttpFlow<S, (), ()>>,
pub struct H2ServiceHandler<T, S: Service, B> {
srv: CloneableService<S>,
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B> H2ServiceHandler<T, S, B>
where
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -264,66 +275,76 @@ where
{
fn new(
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
service: S,
srv: S,
) -> H2ServiceHandler<T, S, B> {
H2ServiceHandler {
flow: HttpFlow::new(service, (), None),
cfg,
on_connect,
on_connect_ext,
_phantom: PhantomData,
srv: CloneableService::new(srv),
_t: PhantomData,
}
}
}
impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B>
impl<T, S, B> Service for H2ServiceHandler<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
{
type Request = (T, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Future = H2ServiceHandlerResponse<T, S, B>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.flow.service.poll_ready(cx).map_err(|e| {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.srv.poll_ready(cx).map_err(|e| {
let e = e.into();
error!("Service readiness error: {:?}", e);
DispatchError::Service(e)
})
}
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
let mut connect_extensions = Extensions::new();
if let Some(ref handler) = self.on_connect_ext {
// run on_connect_ext callback, populating connect extensions
handler(&io, &mut connect_extensions);
}
H2ServiceHandlerResponse {
state: State::Handshake(
Some(self.flow.clone()),
Some(self.srv.clone()),
Some(self.cfg.clone()),
addr,
on_connect_data,
deprecated_on_connect,
Some(connect_extensions),
server::handshake(io),
),
}
}
}
enum State<T, S: Service<Request>, B: MessageBody>
enum State<T, S: Service<Request = Request>, B: MessageBody>
where
T: AsyncRead + AsyncWrite + Unpin,
S::Future: 'static,
{
Incoming(Dispatcher<T, S, B, (), ()>),
Incoming(Dispatcher<T, S, B>),
Handshake(
Option<Rc<HttpFlow<S, (), ()>>>,
Option<CloneableService<S>>,
Option<ServiceConfig>,
Option<net::SocketAddr>,
OnConnectData,
Option<Box<dyn DataFactory>>,
Option<Extensions>,
Handshake<T, Bytes>,
),
}
@ -331,7 +352,7 @@ where
pub struct H2ServiceHandlerResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -343,7 +364,7 @@ where
impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
@ -358,25 +379,27 @@ where
ref mut srv,
ref mut config,
ref peer_addr,
ref mut on_connect,
ref mut on_connect_data,
ref mut handshake,
) => match ready!(Pin::new(handshake).poll(cx)) {
Ok(conn) => {
let on_connect_data = std::mem::take(on_connect_data);
) => match Pin::new(handshake).poll(cx) {
Poll::Ready(Ok(conn)) => {
self.state = State::Incoming(Dispatcher::new(
srv.take().unwrap(),
conn,
on_connect_data,
on_connect.take(),
on_connect_data.take().unwrap(),
config.take().unwrap(),
None,
*peer_addr,
));
self.poll(cx)
}
Err(err) => {
Poll::Ready(Err(err)) => {
trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err.into()))
}
Poll::Pending => Poll::Pending,
},
}
}

View File

@ -1,46 +0,0 @@
//! Helper trait for types that can be effectively borrowed as a [HeaderValue].
use std::{borrow::Cow, str::FromStr};
use http::header::{HeaderName, InvalidHeaderName};
pub trait AsHeaderName: Sealed {}
pub trait Sealed {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
}
impl Sealed for HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self))
}
}
impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self))
}
}
impl AsHeaderName for &HeaderName {}
impl Sealed for &str {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &str {}
impl Sealed for String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for String {}
impl Sealed for &String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned)
}
}
impl AsHeaderName for &String {}

View File

@ -32,36 +32,50 @@ header! {
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
///
/// # Examples
/// ```
/// ```rust
/// # extern crate actix_http;
/// extern crate mime;
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
///
/// builder.set(
/// Accept(vec![
/// qitem(mime::TEXT_HTML),
/// ])
/// );
/// # }
/// ```
///
/// ```
/// ```rust
/// # extern crate actix_http;
/// extern crate mime;
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
///
/// builder.set(
/// Accept(vec![
/// qitem(mime::APPLICATION_JSON),
/// ])
/// );
/// # }
/// ```
///
/// ```
/// ```rust
/// # extern crate actix_http;
/// extern crate mime;
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, QualityItem, q, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
///
/// builder.set(
/// Accept(vec![
/// qitem(mime::TEXT_HTML),
/// qitem("application/xhtml+xml".parse().unwrap()),
@ -76,6 +90,7 @@ header! {
/// ),
/// ])
/// );
/// # }
/// ```
(Accept, header::ACCEPT) => (QualityItem<Mime>)+
@ -117,7 +132,7 @@ header! {
#[test]
fn test_fuzzing1() {
use crate::test::TestRequest;
let req = TestRequest::default().insert_header((crate::header::ACCEPT, "chunk#;e")).finish();
let req = TestRequest::with_header(crate::header::ACCEPT, "chunk#;e").finish();
let header = Accept::parse(&req);
assert!(header.is_ok());
}

View File

@ -21,37 +21,44 @@ header! {
/// * `iso-8859-5, unicode-1-1;q=0.8`
///
/// # Examples
/// ```
/// ```rust
/// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
/// );
/// # }
/// ```
///
/// ```
/// ```rust
/// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// AcceptCharset(vec![
/// QualityItem::new(Charset::Us_Ascii, q(900)),
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
/// ])
/// );
/// # }
/// ```
///
/// ```
/// ```rust
/// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
/// );
/// # }
/// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+

View File

@ -22,35 +22,41 @@ header! {
///
/// # Examples
///
/// ```
/// use language_tags::langtag;
/// ```rust
/// # extern crate actix_http;
/// # extern crate language_tags;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem};
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// let mut langtag: LanguageTag = Default::default();
/// langtag.language = Some("en".to_owned());
/// langtag.region = Some("US".to_owned());
/// builder.insert_header(
/// builder.set(
/// AcceptLanguage(vec![
/// qitem(langtag),
/// ])
/// );
/// # }
/// ```
///
/// ```
/// use language_tags::langtag;
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// use actix_http::Response;
/// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem};
///
/// #
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// AcceptLanguage(vec![
/// qitem(langtag!(da)),
/// QualityItem::new(langtag!(en;;;GB), q(800)),
/// QualityItem::new(langtag!(en), q(700)),
/// ])
/// );
/// # }
/// ```
(AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+

View File

@ -22,28 +22,38 @@ header! {
///
/// # Examples
///
/// ```
/// ```rust
/// # extern crate http;
/// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::{header::Allow, Method};
/// use actix_http::http::header::Allow;
/// use http::Method;
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// Allow(vec![Method::GET])
/// );
/// # }
/// ```
///
/// ```
/// ```rust
/// # extern crate http;
/// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::{header::Allow, Method};
/// use actix_http::http::header::Allow;
/// use http::Method;
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// Allow(vec![
/// Method::GET,
/// Method::POST,
/// Method::PATCH,
/// ])
/// );
/// # }
/// ```
(Allow, header::ALLOW) => (Method)*

View File

@ -28,12 +28,12 @@ use crate::header::{
/// * `max-age=30`
///
/// # Examples
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// ```
///
/// ```rust
@ -41,7 +41,7 @@ use crate::header::{
/// use actix_http::http::header::{CacheControl, CacheDirective};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(CacheControl(vec![
/// builder.set(CacheControl(vec![
/// CacheDirective::NoCache,
/// CacheDirective::Private,
/// CacheDirective::MaxAge(360u32),
@ -82,7 +82,7 @@ impl fmt::Display for CacheControl {
impl IntoHeaderValue for CacheControl {
type Error = header::InvalidHeaderValue;
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take())
@ -196,8 +196,7 @@ mod tests {
#[test]
fn test_parse_multiple_headers() {
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "no-cache, private"))
let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private")
.finish();
let cache = Header::parse(&req);
assert_eq!(
@ -211,9 +210,9 @@ mod tests {
#[test]
fn test_parse_argument() {
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "max-age=100, private"))
.finish();
let req =
TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private")
.finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
@ -226,9 +225,8 @@ mod tests {
#[test]
fn test_parse_quote_form() {
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "max-age=\"200\""))
.finish();
let req =
TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
@ -238,9 +236,8 @@ mod tests {
#[test]
fn test_parse_extension() {
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "foo, bar=baz"))
.finish();
let req =
TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish();
let cache = Header::parse(&req);
assert_eq!(
cache.ok(),
@ -253,9 +250,7 @@ mod tests {
#[test]
fn test_parse_bad_syntax() {
let req = TestRequest::default()
.insert_header((header::CACHE_CONTROL, "foo="))
.finish();
let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish();
let cache: Result<CacheControl, _> = Header::parse(&req);
assert_eq!(cache.ok(), None)
}

View File

@ -1,10 +1,10 @@
//! # References
//!
//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/
//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
// # References
//
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
use lazy_static::lazy_static;
use regex::Regex;
@ -318,8 +318,9 @@ impl ContentDisposition {
return Err(crate::error::ParseError::Header);
}
left = new_left;
if let Some(param_name) = param_name.strip_suffix('*') {
if param_name.ends_with('*') {
// extended parameters
let param_name = &param_name[..param_name.len() - 1]; // trim asterisk
let (ext_value, new_left) = split_once_and_trim(left, ';');
left = new_left;
let ext_value = header::parse_extended_value(ext_value)?;
@ -454,7 +455,7 @@ impl ContentDisposition {
impl IntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValue;
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take())
@ -549,7 +550,8 @@ impl fmt::Display for ContentDisposition {
write!(f, "{}", self.disposition)?;
self.parameters
.iter()
.try_for_each(|param| write!(f, "; {}", param))
.map(|param| write!(f, "; {}", param))
.collect()
}
}

View File

@ -1,106 +0,0 @@
use std::{convert::Infallible, str::FromStr};
use http::header::InvalidHeaderValue;
use crate::{
error::ParseError,
header::{self, from_one_raw_str, Header, HeaderName, HeaderValue, IntoHeaderValue},
HttpMessage,
};
/// Represents a supported content encoding.
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation.
Auto,
/// A format using the Brotli algorithm.
Br,
/// A format using the zlib structure with deflate algorithm.
Deflate,
/// Gzip algorithm.
Gzip,
/// Indicates the identity function (i.e. no compression, nor modification).
Identity,
}
impl ContentEncoding {
/// Is the content compressed?
#[inline]
pub fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
/// Convert content encoding to string
#[inline]
pub fn as_str(self) -> &'static str {
match self {
ContentEncoding::Br => "br",
ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
}
}
/// Default Q-factor (quality) value.
#[inline]
pub fn quality(self) -> f64 {
match self {
ContentEncoding::Br => 1.1,
ContentEncoding::Gzip => 1.0,
ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
}
}
}
impl Default for ContentEncoding {
fn default() -> Self {
Self::Identity
}
}
impl FromStr for ContentEncoding {
type Err = Infallible;
fn from_str(val: &str) -> Result<Self, Self::Err> {
Ok(Self::from(val))
}
}
impl From<&str> for ContentEncoding {
fn from(val: &str) -> ContentEncoding {
let val = val.trim();
if val.eq_ignore_ascii_case("br") {
ContentEncoding::Br
} else if val.eq_ignore_ascii_case("gzip") {
ContentEncoding::Gzip
} else if val.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate
} else {
ContentEncoding::default()
}
}
}
impl IntoHeaderValue for ContentEncoding {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<http::HeaderValue, Self::Error> {
Ok(HeaderValue::from_static(self.as_str()))
}
}
impl Header for ContentEncoding {
fn name() -> HeaderName {
header::CONTENT_ENCODING
}
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
from_one_raw_str(msg.headers().get(Self::name()))
}
}

View File

@ -23,31 +23,38 @@ header! {
///
/// # Examples
///
/// ```
/// use language_tags::langtag;
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// use actix_http::Response;
/// use actix_http::http::header::{ContentLanguage, qitem};
///
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// ContentLanguage(vec![
/// qitem(langtag!(en)),
/// ])
/// );
/// # }
/// ```
///
/// ```
/// use language_tags::langtag;
/// ```rust
/// # extern crate actix_http;
/// # #[macro_use] extern crate language_tags;
/// use actix_http::Response;
/// use actix_http::http::header::{ContentLanguage, qitem};
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// ContentLanguage(vec![
/// qitem(langtag!(da)),
/// qitem(langtag!(en;;;GB)),
/// ])
/// );
/// # }
/// ```
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+

View File

@ -200,7 +200,7 @@ impl Display for ContentRangeSpec {
impl IntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take())

View File

@ -30,24 +30,31 @@ header! {
///
/// # Examples
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::ContentType;
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// ContentType::json()
/// );
/// # }
/// ```
///
/// ```
/// ```rust
/// # extern crate mime;
/// # extern crate actix_http;
/// use mime::TEXT_HTML;
/// use actix_http::Response;
/// use actix_http::http::header::ContentType;
///
/// # fn main() {
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// ContentType(mime::TEXT_HTML)
/// builder.set(
/// ContentType(TEXT_HTML)
/// );
/// # }
/// ```
(ContentType, CONTENT_TYPE) => [Mime]
@ -92,7 +99,6 @@ impl ContentType {
pub fn form_url_encoded() -> ContentType {
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
}
/// A constructor to easily create a `Content-Type: image/jpeg` header.
#[inline]
pub fn jpeg() -> ContentType {

View File

@ -19,15 +19,13 @@ header! {
///
/// # Example
///
/// ```
/// use std::time::SystemTime;
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::Date;
/// use std::time::SystemTime;
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// Date(SystemTime::now().into())
/// );
/// builder.set(Date(SystemTime::now().into()));
/// ```
(Date, DATE) => [HttpDate]

View File

@ -27,24 +27,20 @@ header! {
///
/// # Examples
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::{ETag, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// ETag(EntityTag::new(false, "xyzzy".to_owned()))
/// );
/// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned())));
/// ```
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::{ETag, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// ETag(EntityTag::new(true, "xyzzy".to_owned()))
/// );
/// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned())));
/// ```
(ETag, ETAG) => [EntityTag]

View File

@ -21,16 +21,14 @@ header! {
///
/// # Example
///
/// ```
/// use std::time::{SystemTime, Duration};
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::Expires;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// Expires(expiration.into())
/// );
/// builder.set(Expires(expiration.into()));
/// ```
(Expires, EXPIRES) => [HttpDate]

View File

@ -29,20 +29,20 @@ header! {
///
/// # Examples
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfMatch;
///
/// let mut builder = Response::Ok();
/// builder.insert_header(IfMatch::Any);
/// builder.set(IfMatch::Any);
/// ```
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::{IfMatch, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// IfMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),

View File

@ -21,16 +21,14 @@ header! {
///
/// # Example
///
/// ```
/// use std::time::{SystemTime, Duration};
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfModifiedSince;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// IfModifiedSince(modified.into())
/// );
/// builder.set(IfModifiedSince(modified.into()));
/// ```
(IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate]

View File

@ -31,20 +31,20 @@ header! {
///
/// # Examples
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfNoneMatch;
///
/// let mut builder = Response::Ok();
/// builder.insert_header(IfNoneMatch::Any);
/// builder.set(IfNoneMatch::Any);
/// ```
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::{IfNoneMatch, EntityTag};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// builder.set(
/// IfNoneMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()),
@ -73,15 +73,13 @@ mod tests {
fn test_if_none_match() {
let mut if_none_match: Result<IfNoneMatch, _>;
let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, "*"))
.finish();
let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish();
if_none_match = Header::parse(&req);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
let req = TestRequest::default()
.insert_header((IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]))
.finish();
let req =
TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])
.finish();
if_none_match = Header::parse(&req);
let mut entities: Vec<EntityTag> = Vec::new();

View File

@ -35,34 +35,31 @@ use crate::httpmessage::HttpMessage;
///
/// # Examples
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::{EntityTag, IfRange};
///
/// let mut builder = Response::Ok();
/// builder.insert_header(
/// IfRange::EntityTag(
/// EntityTag::new(false, "abc".to_owned())
/// )
/// );
/// builder.set(IfRange::EntityTag(EntityTag::new(
/// false,
/// "xyzzy".to_owned(),
/// )));
/// ```
///
/// ```
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfRange;
/// use std::time::{Duration, SystemTime};
/// use actix_http::{http::header::IfRange, Response};
///
/// let mut builder = Response::Ok();
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// IfRange::Date(fetched.into())
/// );
/// builder.set(IfRange::Date(fetched.into()));
/// ```
#[derive(Clone, Debug, PartialEq)]
pub enum IfRange {
/// The entity-tag the client has of the resource.
/// The entity-tag the client has of the resource
EntityTag(EntityTag),
/// The date when the client retrieved the resource.
/// The date when the client retrieved the resource
Date(HttpDate),
}
@ -101,7 +98,7 @@ impl Display for IfRange {
impl IntoHeaderValue for IfRange {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take())
@ -113,8 +110,7 @@ mod test_if_range {
use super::IfRange as HeaderField;
use crate::header::*;
use std::str;
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
test_header!(test2, vec![b"\"abc\""]);
test_header!(test2, vec![b"\"xyzzy\""]);
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
}

View File

@ -22,16 +22,14 @@ header! {
///
/// # Example
///
/// ```
/// use std::time::{SystemTime, Duration};
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfUnmodifiedSince;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// IfUnmodifiedSince(modified.into())
/// );
/// builder.set(IfUnmodifiedSince(modified.into()));
/// ```
(IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate]

View File

@ -21,16 +21,14 @@ header! {
///
/// # Example
///
/// ```
/// use std::time::{SystemTime, Duration};
/// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::LastModified;
/// use std::time::{SystemTime, Duration};
///
/// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header(
/// LastModified(modified.into())
/// );
/// builder.set(LastModified(modified.into()));
/// ```
(LastModified, LAST_MODIFIED) => [HttpDate]

View File

@ -3,7 +3,7 @@
//! ## Mime
//!
//! Several header fields use MIME values for their contents. Keeping with the
//! strongly-typed theme, the [mime] crate
//! strongly-typed theme, the [mime](https://docs.rs/mime) crate
//! is used, such as `ContentType(pub Mime)`.
#![cfg_attr(rustfmt, rustfmt_skip)]
@ -18,7 +18,6 @@ pub use self::content_disposition::{
};
pub use self::content_language::ContentLanguage;
pub use self::content_range::{ContentRange, ContentRangeSpec};
pub use self::content_encoding::{ContentEncoding};
pub use self::content_type::ContentType;
pub use self::date::Date;
pub use self::etag::ETag;
@ -84,7 +83,7 @@ macro_rules! test_header {
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default();
for item in a {
req = req.insert_header((HeaderField::name(), item)).take();
req = req.header(HeaderField::name(), item).take();
}
let req = req.finish();
let value = HeaderField::parse(&req);
@ -111,7 +110,7 @@ macro_rules! test_header {
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default();
for item in a {
req.insert_header((HeaderField::name(), item));
req.header(HeaderField::name(), item);
}
let req = req.finish();
let val = HeaderField::parse(&req);
@ -169,7 +168,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
@ -205,7 +204,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
@ -241,8 +240,8 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
self.0.try_into_value()
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
self.0.try_into()
}
}
};
@ -290,7 +289,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue;
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self);
@ -334,14 +333,13 @@ macro_rules! header {
}
mod accept_charset;
// mod accept_encoding;
//mod accept_encoding;
mod accept;
mod accept_language;
mod allow;
mod cache_control;
mod content_disposition;
mod content_language;
mod content_encoding;
mod content_range;
mod content_type;
mod date;

View File

@ -1,117 +0,0 @@
use std::convert::TryFrom;
use http::{
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
Error as HttpError, HeaderValue,
};
use super::{Header, IntoHeaderValue};
/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s.
pub trait IntoHeaderPair: Sized {
type Error: Into<HttpError>;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error>;
}
#[derive(Debug)]
pub enum InvalidHeaderPart {
Name(InvalidHeaderName),
Value(InvalidHeaderValue),
}
impl From<InvalidHeaderPart> for HttpError {
fn from(part_err: InvalidHeaderPart) -> Self {
match part_err {
InvalidHeaderPart::Name(err) => err.into(),
InvalidHeaderPart::Value(err) => err.into(),
}
}
}
impl<V> IntoHeaderPair for (HeaderName, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name, value))
}
}
impl<V> IntoHeaderPair for (&HeaderName, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name.clone(), value))
}
}
impl<V> IntoHeaderPair for (&[u8], V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name, value))
}
}
impl<V> IntoHeaderPair for (&str, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
let name = HeaderName::try_from(name).map_err(InvalidHeaderPart::Name)?;
let value = value
.try_into_value()
.map_err(|err| InvalidHeaderPart::Value(err.into()))?;
Ok((name, value))
}
}
impl<V> IntoHeaderPair for (String, V)
where
V: IntoHeaderValue,
V::Error: Into<InvalidHeaderValue>,
{
type Error = InvalidHeaderPart;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
let (name, value) = self;
(name.as_str(), value).try_into_header_pair()
}
}
impl<T: Header> IntoHeaderPair for T {
type Error = <T as IntoHeaderValue>::Error;
fn try_into_header_pair(self) -> Result<(HeaderName, HeaderValue), Self::Error> {
Ok((T::name(), self.try_into_value()?))
}
}

View File

@ -1,131 +0,0 @@
use std::convert::TryFrom;
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`
pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error.
type Error: Into<HttpError>;
/// Try to convert value to a HeaderValue.
fn try_into_value(self) -> Result<HeaderValue, Self::Error>;
}
impl IntoHeaderValue for HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
Ok(self)
}
}
impl IntoHeaderValue for &HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
Ok(self.clone())
}
}
impl IntoHeaderValue for &str {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
self.parse()
}
}
impl IntoHeaderValue for &[u8] {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_bytes(self)
}
}
impl IntoHeaderValue for Bytes {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_maybe_shared(self)
}
}
impl IntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for String {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for usize {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for i64 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for u64 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for i32 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for u32 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self.to_string())
}
}
impl IntoHeaderValue for Mime {
type Error = InvalidHeaderValue;
#[inline]
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_str(self.as_ref())
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,12 @@
//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other
//! header utility methods.
//! Various http headers
// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header)
use std::fmt;
use std::convert::TryFrom;
use std::{fmt, str::FromStr};
use bytes::{Bytes, BytesMut};
use http::Error as HttpError;
use mime::Mime;
use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*;
@ -11,29 +14,22 @@ pub use http::header::*;
use crate::error::ParseError;
use crate::httpmessage::HttpMessage;
mod as_name;
mod into_pair;
mod into_value;
mod utils;
mod common;
pub(crate) mod map;
mod shared;
pub use self::common::*;
#[doc(hidden)]
pub use self::shared::*;
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::*;
/// A trait for any object that already represents a valid header field and value.
pub trait Header: IntoHeaderValue {
/// A trait for any object that will represent a header field and value.
pub trait Header
where
Self: IntoHeaderValue,
{
/// Returns the name of the header field
fn name() -> HeaderName;
@ -41,16 +37,170 @@ pub trait Header: IntoHeaderValue {
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
}
#[derive(Debug, Default)]
/// A trait for any object that can be Converted to a `HeaderValue`
pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error.
type Error: Into<HttpError>;
/// Try to convert value to a Header value.
fn try_into(self) -> Result<HeaderValue, Self::Error>;
}
impl IntoHeaderValue for HeaderValue {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
Ok(self)
}
}
impl<'a> IntoHeaderValue for &'a str {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
self.parse()
}
}
impl<'a> IntoHeaderValue for &'a [u8] {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_bytes(self)
}
}
impl IntoHeaderValue for Bytes {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::from_maybe_shared(self)
}
}
impl IntoHeaderValue for Vec<u8> {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for String {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(self)
}
}
impl IntoHeaderValue for usize {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let s = format!("{}", self);
HeaderValue::try_from(s)
}
}
impl IntoHeaderValue for u64 {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let s = format!("{}", self);
HeaderValue::try_from(s)
}
}
impl IntoHeaderValue for Mime {
type Error = InvalidHeaderValue;
#[inline]
fn try_into(self) -> Result<HeaderValue, Self::Error> {
HeaderValue::try_from(format!("{}", self))
}
}
/// Represents supported types of content encodings
#[derive(Copy, Clone, PartialEq, Debug)]
pub enum ContentEncoding {
/// Automatically select encoding based on encoding negotiation
Auto,
/// A format using the Brotli algorithm
Br,
/// A format using the zlib structure with deflate algorithm
Deflate,
/// Gzip algorithm
Gzip,
/// Indicates the identity function (i.e. no compression, nor modification)
Identity,
}
impl ContentEncoding {
#[inline]
/// Is the content compressed?
pub fn is_compression(self) -> bool {
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
}
#[inline]
/// Convert content encoding to string
pub fn as_str(self) -> &'static str {
match self {
ContentEncoding::Br => "br",
ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate",
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
}
}
#[inline]
/// default quality value
pub fn quality(self) -> f64 {
match self {
ContentEncoding::Br => 1.1,
ContentEncoding::Gzip => 1.0,
ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
}
}
}
impl<'a> From<&'a str> for ContentEncoding {
fn from(s: &'a str) -> ContentEncoding {
let s = s.trim();
if s.eq_ignore_ascii_case("br") {
ContentEncoding::Br
} else if s.eq_ignore_ascii_case("gzip") {
ContentEncoding::Gzip
} else if s.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate
} else {
ContentEncoding::Identity
}
}
}
#[doc(hidden)]
pub(crate) struct Writer {
buf: BytesMut,
}
impl Writer {
fn new() -> Writer {
Writer::default()
Writer {
buf: BytesMut::new(),
}
}
fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
@ -69,15 +219,176 @@ impl fmt::Write for Writer {
}
}
/// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap {
HeaderMap::from_drain(map.drain())
#[inline]
#[doc(hidden)]
/// Reads a comma-delimited raw header into a Vec.
pub fn from_comma_delimited<'a, I: Iterator<Item = &'a HeaderValue> + 'a, T: FromStr>(
all: I,
) -> Result<Vec<T>, ParseError> {
let mut result = Vec::new();
for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend(
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.trim().parse().ok()),
)
}
Ok(result)
}
#[inline]
#[doc(hidden)]
/// Reads a single string when parsing a header.
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)
}
#[inline]
#[doc(hidden)]
/// Format an array into a comma-delimited string.
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
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(())
}
// 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`),
/// and a character sequence representing the actual value (`value`), separated by single quote
/// characters. It is defined in [RFC 5987](https://tools.ietf.org/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.
pub charset: Charset,
/// The human language details of the `value`, if available.
pub language_tag: Option<LanguageTag>,
/// The parameter value, as expressed in octets.
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).
///
/// Extended values are denoted by parameter names that end with `*`.
///
/// ## ABNF
///
/// ```text
/// ext-value = charset "'" [ language ] "'" value-chars
/// ; like RFC 2231's <extended-initial-value>
/// ; (see [RFC2231], Section 7)
///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
///
/// mime-charset = 1*mime-charsetc
/// mime-charsetc = ALPHA / DIGIT
/// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~"
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
/// ; 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>
///
/// value-chars = *( pct-encoded / attr-char )
///
/// pct-encoded = "%" HEXDIG HEXDIG
/// ; see [RFC3986], Section 2.1
///
/// attr-char = ALPHA / DIGIT
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
/// / "^" / "_" / "`" / "|" / "~"
/// ; token except ( "*" / "'" / "%" )
/// ```
pub fn parse_extended_value(
val: &str,
) -> Result<ExtendedValue, crate::error::ParseError> {
// Break into three pieces separated by the single-quote character
let mut parts = val.splitn(3, '\'');
// Interpret the first piece as a Charset
let charset: Charset = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
};
// Interpret the second piece as a language tag
let language_tag: Option<LanguageTag> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some("") => None,
Some(s) => match s.parse() {
Ok(lt) => Some(lt),
Err(_) => return Err(crate::error::ParseError::Header),
},
};
// Interpret the third piece as a sequence of value characters
let value: Vec<u8> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
};
Ok(ExtendedValue {
value,
charset,
language_tag,
})
}
impl fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded_value =
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else {
write!(f, "{}''{}", self.charset, encoded_value)
}
}
}
/// This encode set is used for HTTP header values and is defined at
/// https://tools.ietf.org/html/rfc5987#section-3.2.
/// Percent encode a sequence of bytes with a character set defined in
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
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)
}
/// Convert http::HeaderMap to a HeaderMap
impl From<http::HeaderMap> for HeaderMap {
fn from(map: http::HeaderMap) -> HeaderMap {
let mut new_map = HeaderMap::with_capacity(map.capacity());
for (h, v) in map.iter() {
new_map.append(h.clone(), v.clone());
}
new_map
}
}
// This encode set is used for HTTP header values and is defined at
// https://tools.ietf.org/html/rfc5987#section-3.2
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ')
.add(b'"')
@ -99,3 +410,91 @@ pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b']')
.add(b'{')
.add(b'}');
#[cfg(test)]
mod tests {
use super::shared::Charset;
use super::{parse_extended_value, ExtendedValue};
use language_tags::LanguageTag;
#[test]
fn test_parse_extended_value_with_encoding_and_language_tag() {
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
assert!(extended_value.language_tag.is_some());
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
assert_eq!(
vec![163, b' ', b'r', b'a', b't', b'e', b's'],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_with_encoding() {
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
// and U+20AC (EURO SIGN)
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
assert!(extended_value.language_tag.is_none());
assert_eq!(
vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_missing_language_tag_and_encoding() {
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
let result = parse_extended_value("foo%20bar.html");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted() {
let result = parse_extended_value("UTF-8'missing third part");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted_blank() {
let result = parse_extended_value("blank second part'");
assert!(result.is_err());
}
#[test]
fn test_fmt_extended_value_with_encoding_and_language_tag() {
let extended_value = ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: Some("en".parse().expect("Could not parse language tag")),
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
};
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
}
#[test]
fn test_fmt_extended_value_with_encoding() {
let extended_value = ExtendedValue {
charset: Charset::Ext("UTF-8".to_string()),
language_tag: None,
value: vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
};
assert_eq!(
"UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
format!("{}", extended_value)
);
}
}

View File

@ -7,12 +7,10 @@ use crate::header::{HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer};
/// 1. `%x21`, or
/// 2. in the range `%x23` to `%x7E`, or
/// 3. above `%x80`
fn entity_validate_char(c: u8) -> bool {
c == 0x21 || (0x23..=0x7e).contains(&c) || (c >= 0x80)
}
fn check_slice_validity(slice: &str) -> bool {
slice.bytes().all(entity_validate_char)
slice
.bytes()
.all(|c| c == b'\x21' || (c >= b'\x23' && c <= b'\x7e') | (c >= b'\x80'))
}
/// An entity tag, defined in [RFC7232](https://tools.ietf.org/html/rfc7232#section-2.3)
@ -161,7 +159,7 @@ impl FromStr for EntityTag {
impl IntoHeaderValue for EntityTag {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = Writer::new();
write!(wrt, "{}", self).unwrap();
HeaderValue::from_maybe_shared(wrt.take())

View File

@ -1,193 +0,0 @@
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).
#[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string.
pub charset: Charset,
/// The human language details of the `value`, if available.
pub language_tag: Option<LanguageTag>,
/// The parameter value, as expressed in octets.
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).
///
/// Extended values are denoted by parameter names that end with `*`.
///
/// ## ABNF
///
/// ```text
/// ext-value = charset "'" [ language ] "'" value-chars
/// ; like RFC 2231's <extended-initial-value>
/// ; (see [RFC2231], Section 7)
///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
///
/// mime-charset = 1*mime-charsetc
/// mime-charsetc = ALPHA / DIGIT
/// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~"
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
/// ; 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>
///
/// value-chars = *( pct-encoded / attr-char )
///
/// pct-encoded = "%" HEXDIG HEXDIG
/// ; see [RFC3986], Section 2.1
///
/// attr-char = ALPHA / DIGIT
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
/// / "^" / "_" / "`" / "|" / "~"
/// ; token except ( "*" / "'" / "%" )
/// ```
pub fn parse_extended_value(
val: &str,
) -> Result<ExtendedValue, crate::error::ParseError> {
// Break into three pieces separated by the single-quote character
let mut parts = val.splitn(3, '\'');
// Interpret the first piece as a Charset
let charset: Charset = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
};
// Interpret the second piece as a language tag
let language_tag: Option<LanguageTag> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some("") => None,
Some(s) => match s.parse() {
Ok(lt) => Some(lt),
Err(_) => return Err(crate::error::ParseError::Header),
},
};
// Interpret the third piece as a sequence of value characters
let value: Vec<u8> = match parts.next() {
None => return Err(crate::error::ParseError::Header),
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
};
Ok(ExtendedValue {
value,
charset,
language_tag,
})
}
impl fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let encoded_value =
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else {
write!(f, "{}''{}", self.charset, encoded_value)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_extended_value_with_encoding_and_language_tag() {
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
assert!(extended_value.language_tag.is_some());
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
assert_eq!(
vec![163, b' ', b'r', b'a', b't', b'e', b's'],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_with_encoding() {
// RFC 5987, Section 3.2.2
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
// and U+20AC (EURO SIGN)
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
assert!(result.is_ok());
let extended_value = result.unwrap();
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
assert!(extended_value.language_tag.is_none());
assert_eq!(
vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
extended_value.value
);
}
#[test]
fn test_parse_extended_value_missing_language_tag_and_encoding() {
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
let result = parse_extended_value("foo%20bar.html");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted() {
let result = parse_extended_value("UTF-8'missing third part");
assert!(result.is_err());
}
#[test]
fn test_parse_extended_value_partially_formatted_blank() {
let result = parse_extended_value("blank second part'");
assert!(result.is_err());
}
#[test]
fn test_fmt_extended_value_with_encoding_and_language_tag() {
let extended_value = ExtendedValue {
charset: Charset::Iso_8859_1,
language_tag: Some("en".parse().expect("Could not parse language tag")),
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
};
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
}
#[test]
fn test_fmt_extended_value_with_encoding() {
let extended_value = ExtendedValue {
charset: Charset::Ext("UTF-8".to_string()),
language_tag: None,
value: vec![
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
b't', b'e', b's',
],
};
assert_eq!(
"UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
format!("{}", extended_value)
);
}
}

View File

@ -3,8 +3,7 @@ use std::io::Write;
use std::str::FromStr;
use std::time::{SystemTime, UNIX_EPOCH};
use bytes::buf::BufMut;
use bytes::BytesMut;
use bytes::{buf::BufMutExt, BytesMut};
use http::header::{HeaderValue, InvalidHeaderValue};
use time::{offset, OffsetDateTime, PrimitiveDateTime};
@ -48,7 +47,7 @@ impl From<SystemTime> for HttpDate {
impl IntoHeaderValue for HttpDate {
type Error = InvalidHeaderValue;
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
fn try_into(self) -> Result<HeaderValue, Self::Error> {
let mut wrt = BytesMut::with_capacity(29).writer();
write!(
wrt,

View File

@ -1,16 +1,14 @@
//! Originally taken from `hyper::header::shared`.
mod charset;
mod encoding;
mod entity;
mod extended;
mod httpdate;
mod quality_item;
//! Copied for `hyper::header::shared`;
pub use self::charset::Charset;
pub use self::encoding::Encoding;
pub use self::entity::EntityTag;
pub use self::extended::{parse_extended_value, ExtendedValue};
pub use self::httpdate::HttpDate;
pub use self::quality_item::{q, qitem, Quality, QualityItem};
pub use language_tags::LanguageTag;
mod charset;
mod encoding;
mod entity;
mod httpdate;
mod quality_item;

View File

@ -1,63 +0,0 @@
use std::{fmt, str::FromStr};
use http::HeaderValue;
use crate::{error::ParseError, header::HTTP_VALUE};
/// Reads a comma-delimited raw header into a Vec.
#[inline]
pub fn from_comma_delimited<'a, I, T>(all: I) -> Result<Vec<T>, ParseError>
where
I: Iterator<Item = &'a HeaderValue> + 'a,
T: FromStr,
{
let mut result = Vec::new();
for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend(
s.split(',')
.filter_map(|x| match x.trim() {
"" => None,
y => Some(y),
})
.filter_map(|x| x.trim().parse().ok()),
)
}
Ok(result)
}
/// Reads a single string when parsing a header.
#[inline]
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)
}
/// Format an array into a comma-delimited string.
#[inline]
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
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>
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)
}

View File

@ -3,6 +3,8 @@ use std::io;
use bytes::{BufMut, BytesMut};
use http::Version;
use crate::extensions::Extensions;
const DIGITS_START: u8 = b'0';
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) {
@ -54,6 +56,18 @@ impl<'a> io::Write for Writer<'a> {
}
}
pub(crate) trait DataFactory {
fn set(&self, ext: &mut Extensions);
}
pub(crate) struct Data<T>(pub(crate) T);
impl<T: Clone + 'static> DataFactory for Data<T> {
fn set(&self, ext: &mut Extensions) {
ext.insert(self.0.clone())
}
}
#[cfg(test)]
mod tests {
use std::str::from_utf8;

View File

@ -13,7 +13,7 @@ use crate::payload::Payload;
struct Cookies(Vec<Cookie<'static>>);
/// Trait that implements general purpose operations on HTTP messages.
/// Trait that implements general purpose operations on http messages
pub trait HttpMessage: Sized {
/// Type of message payload stream
type Stream;
@ -30,8 +30,8 @@ pub trait HttpMessage: Sized {
/// Mutable reference to a the request's extensions container
fn extensions_mut(&self) -> RefMut<'_, Extensions>;
/// Get a header.
#[doc(hidden)]
/// Get a header
fn get_header<H: Header>(&self) -> Option<H>
where
Self: Sized,
@ -43,8 +43,8 @@ pub trait HttpMessage: Sized {
}
}
/// Read the request content type. If request did not contain a *Content-Type* header, an empty
/// string is returned.
/// Read the request content type. If request does not contain
/// *Content-Type* header, empty str get returned.
fn content_type(&self) -> &str {
if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() {
@ -90,7 +90,7 @@ pub trait HttpMessage: Sized {
Ok(None)
}
/// Check if request has chunked transfer encoding.
/// Check if request has chunked transfer encoding
fn chunked(&self) -> Result<bool, ParseError> {
if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) {
if let Ok(s) = encodings.to_str() {
@ -173,13 +173,11 @@ mod tests {
#[test]
fn test_content_type() {
let req = TestRequest::default()
.insert_header(("content-type", "text/plain"))
.finish();
let req = TestRequest::with_header("content-type", "text/plain").finish();
assert_eq!(req.content_type(), "text/plain");
let req = TestRequest::default()
.insert_header(("content-type", "application/json; charset=utf=8"))
.finish();
let req =
TestRequest::with_header("content-type", "application/json; charset=utf=8")
.finish();
assert_eq!(req.content_type(), "application/json");
let req = TestRequest::default().finish();
assert_eq!(req.content_type(), "");
@ -187,15 +185,13 @@ mod tests {
#[test]
fn test_mime_type() {
let req = TestRequest::default()
.insert_header(("content-type", "application/json"))
.finish();
let req = TestRequest::with_header("content-type", "application/json").finish();
assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON));
let req = TestRequest::default().finish();
assert_eq!(req.mime_type().unwrap(), None);
let req = TestRequest::default()
.insert_header(("content-type", "application/json; charset=utf-8"))
.finish();
let req =
TestRequest::with_header("content-type", "application/json; charset=utf-8")
.finish();
let mt = req.mime_type().unwrap().unwrap();
assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8));
assert_eq!(mt.type_(), mime::APPLICATION);
@ -204,9 +200,11 @@ mod tests {
#[test]
fn test_mime_type_error() {
let req = TestRequest::default()
.insert_header(("content-type", "applicationadfadsfasdflknadsfklnadsfjson"))
.finish();
let req = TestRequest::with_header(
"content-type",
"applicationadfadsfasdflknadsfklnadsfjson",
)
.finish();
assert_eq!(Err(ContentTypeError::ParseError), req.mime_type());
}
@ -215,27 +213,27 @@ mod tests {
let req = TestRequest::default().finish();
assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
let req = TestRequest::default()
.insert_header(("content-type", "application/json"))
.finish();
let req = TestRequest::with_header("content-type", "application/json").finish();
assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
let req = TestRequest::default()
.insert_header(("content-type", "application/json; charset=ISO-8859-2"))
.finish();
let req = TestRequest::with_header(
"content-type",
"application/json; charset=ISO-8859-2",
)
.finish();
assert_eq!(ISO_8859_2, req.encoding().unwrap());
}
#[test]
fn test_encoding_error() {
let req = TestRequest::default()
.insert_header(("content-type", "applicatjson"))
.finish();
let req = TestRequest::with_header("content-type", "applicatjson").finish();
assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err());
let req = TestRequest::default()
.insert_header(("content-type", "application/json; charset=kkkttktk"))
.finish();
let req = TestRequest::with_header(
"content-type",
"application/json; charset=kkkttktk",
)
.finish();
assert_eq!(
Some(ContentTypeError::UnknownEncoding),
req.encoding().err()
@ -247,16 +245,15 @@ mod tests {
let req = TestRequest::default().finish();
assert!(!req.chunked().unwrap());
let req = TestRequest::default()
.insert_header((header::TRANSFER_ENCODING, "chunked"))
.finish();
let req =
TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish();
assert!(req.chunked().unwrap());
let req = TestRequest::default()
.insert_header((
.header(
header::TRANSFER_ENCODING,
Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"),
))
)
.finish();
assert!(req.chunked().is_err());
}

View File

@ -7,6 +7,7 @@
clippy::new_without_default,
clippy::borrow_interior_mutable_const
)]
#![allow(clippy::manual_strip)] // Allow this to keep MSRV(1.42).
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
@ -19,6 +20,7 @@ mod macros;
pub mod body;
mod builder;
pub mod client;
mod cloneable;
mod config;
#[cfg(feature = "compress")]
pub mod encoding;
@ -53,7 +55,7 @@ pub use self::response::{Response, ResponseBuilder};
pub use self::service::HttpService;
pub mod http {
//! Various HTTP related types.
//! Various HTTP related types
// re-exports
pub use http::header::{HeaderName, HeaderValue};
@ -64,7 +66,7 @@ pub mod http {
pub use crate::cookie::{Cookie, CookieBuilder};
pub use crate::header::HeaderMap;
/// A collection of HTTP headers and helpers.
/// Various http headers
pub mod header {
pub use crate::header::*;
}
@ -72,49 +74,11 @@ pub mod http {
pub use crate::message::ConnectionType;
}
/// A major HTTP protocol version.
/// Http protocol
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum Protocol {
Http1,
Http2,
Http3,
}
type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions);
/// Container for data that extract with ConnectCallback.
///
/// # Implementation Details
/// Uses Option to reduce necessary allocations when merging with request extensions.
pub(crate) struct OnConnectData(Option<Extensions>);
impl Default for OnConnectData {
fn default() -> Self {
Self(None)
}
}
impl OnConnectData {
/// Construct by calling the on-connect callback with the underlying transport I/O.
pub(crate) fn from_io<T>(
io: &T,
on_connect_ext: Option<&ConnectCallback<T>>,
) -> Self {
let ext = on_connect_ext.map(|handler| {
let mut extensions = Extensions::new();
handler(io, &mut extensions);
extensions
});
Self(ext)
}
/// Merge self into given request's extensions.
#[inline]
pub(crate) fn merge_into(&mut self, req: &mut Request) {
if let Some(ref mut ext) = self.0 {
req.head.extensions.get_mut().drain_from(ext);
}
}
}

View File

@ -3,6 +3,7 @@ use std::net;
use std::rc::Rc;
use bitflags::bitflags;
use copyless::BoxHelper;
use crate::extensions::Extensions;
use crate::header::HeaderMap;
@ -34,9 +35,7 @@ bitflags! {
pub trait Head: Default + 'static {
fn clear(&mut self);
fn with_pool<F, R>(f: F) -> R
where
F: FnOnce(&MessagePool<Self>) -> R;
fn pool() -> &'static MessagePool<Self>;
}
#[derive(Debug)]
@ -68,14 +67,11 @@ impl Head for RequestHead {
fn clear(&mut self) {
self.flags = Flags::empty();
self.headers.clear();
self.extensions.get_mut().clear();
self.extensions.borrow_mut().clear();
}
fn with_pool<F, R>(f: F) -> R
where
F: FnOnce(&MessagePool<Self>) -> R,
{
REQUEST_POOL.with(|p| f(p))
fn pool() -> &'static MessagePool<Self> {
REQUEST_POOL.with(|p| *p)
}
}
@ -343,15 +339,21 @@ impl ResponseHead {
}
pub struct Message<T: Head> {
// Rc here should not be cloned by anyone.
// It's used to reuse allocation of T and no shared ownership is allowed.
head: Rc<T>,
}
impl<T: Head> Message<T> {
/// Get new message from the pool of objects
pub fn new() -> Self {
T::with_pool(|p| p.get_message())
T::pool().get_message()
}
}
impl<T: Head> Clone for Message<T> {
fn clone(&self) -> Self {
Message {
head: self.head.clone(),
}
}
}
@ -371,7 +373,9 @@ impl<T: Head> std::ops::DerefMut for Message<T> {
impl<T: Head> Drop for Message<T> {
fn drop(&mut self) {
T::with_pool(|p| p.release(self.head.clone()))
if Rc::strong_count(&self.head) == 1 {
T::pool().release(self.head.clone());
}
}
}
@ -423,23 +427,22 @@ pub struct MessagePool<T: Head>(RefCell<Vec<Rc<T>>>);
/// Request's objects pool
pub struct BoxedResponsePool(RefCell<Vec<Box<ResponseHead>>>);
thread_local!(static REQUEST_POOL: MessagePool<RequestHead> = MessagePool::<RequestHead>::create());
thread_local!(static RESPONSE_POOL: BoxedResponsePool = BoxedResponsePool::create());
thread_local!(static REQUEST_POOL: &'static MessagePool<RequestHead> = MessagePool::<RequestHead>::create());
thread_local!(static RESPONSE_POOL: &'static BoxedResponsePool = BoxedResponsePool::create());
impl<T: Head> MessagePool<T> {
fn create() -> MessagePool<T> {
MessagePool(RefCell::new(Vec::with_capacity(128)))
fn create() -> &'static MessagePool<T> {
let pool = MessagePool(RefCell::new(Vec::with_capacity(128)));
Box::leak(Box::new(pool))
}
/// Get message from the pool
#[inline]
fn get_message(&self) -> Message<T> {
fn get_message(&'static self) -> Message<T> {
if let Some(mut msg) = self.0.borrow_mut().pop() {
// Message is put in pool only when it's the last copy.
// which means it's guaranteed to be unique when popped out.
Rc::get_mut(&mut msg)
.expect("Multiple copies exist")
.clear();
if let Some(r) = Rc::get_mut(&mut msg) {
r.clear();
}
Message { head: msg }
} else {
Message {
@ -459,13 +462,14 @@ impl<T: Head> MessagePool<T> {
}
impl BoxedResponsePool {
fn create() -> BoxedResponsePool {
BoxedResponsePool(RefCell::new(Vec::with_capacity(128)))
fn create() -> &'static BoxedResponsePool {
let pool = BoxedResponsePool(RefCell::new(Vec::with_capacity(128)));
Box::leak(Box::new(pool))
}
/// Get message from the pool
#[inline]
fn get_message(&self, status: StatusCode) -> BoxedResponseHead {
fn get_message(&'static self, status: StatusCode) -> BoxedResponseHead {
if let Some(mut head) = self.0.borrow_mut().pop() {
head.reason = None;
head.status = status;
@ -474,17 +478,17 @@ impl BoxedResponsePool {
BoxedResponseHead { head: Some(head) }
} else {
BoxedResponseHead {
head: Some(Box::new(ResponseHead::new(status))),
head: Some(Box::alloc().init(ResponseHead::new(status))),
}
}
}
#[inline]
/// Release request instance
fn release(&self, mut msg: Box<ResponseHead>) {
fn release(&self, msg: Box<ResponseHead>) {
let v = &mut self.0.borrow_mut();
if v.len() < 128 {
msg.extensions.get_mut().clear();
msg.extensions.borrow_mut().clear();
v.push(msg);
}
}

View File

@ -1,9 +1,5 @@
//! HTTP requests.
use std::{
cell::{Ref, RefMut},
fmt, net,
};
use std::cell::{Ref, RefMut};
use std::{fmt, net};
use http::{header, Method, Uri, Version};
@ -27,10 +23,6 @@ impl<P> HttpMessage for Request<P> {
&self.head().headers
}
fn take_payload(&mut self) -> Payload<P> {
std::mem::replace(&mut self.payload, Payload::None)
}
/// Request extensions
#[inline]
fn extensions(&self) -> Ref<'_, Extensions> {
@ -42,6 +34,10 @@ impl<P> HttpMessage for Request<P> {
fn extensions_mut(&self) -> RefMut<'_, Extensions> {
self.head.extensions_mut()
}
fn take_payload(&mut self) -> Payload<P> {
std::mem::replace(&mut self.payload, Payload::None)
}
}
impl From<Message<RequestHead>> for Request<PayloadStream> {
@ -177,17 +173,13 @@ impl<P> fmt::Debug for Request<P> {
self.method(),
self.path()
)?;
if let Some(q) = self.uri().query().as_ref() {
writeln!(f, " query: ?{:?}", q)?;
}
writeln!(f, " headers:")?;
for (key, val) in self.headers().iter() {
for (key, val) in self.headers() {
writeln!(f, " {:?}: {:?}", key, val)?;
}
Ok(())
}
}

View File

@ -1,15 +1,10 @@
//! HTTP responses.
use std::{
cell::{Ref, RefMut},
convert::TryInto,
fmt,
future::Future,
ops,
pin::Pin,
str,
task::{Context, Poll},
};
//! Http response
use std::cell::{Ref, RefMut};
use std::convert::TryFrom;
use std::future::Future;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, str};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
@ -19,7 +14,7 @@ use crate::body::{Body, BodyStream, MessageBody, ResponseBody};
use crate::cookie::{Cookie, CookieJar};
use crate::error::Error;
use crate::extensions::Extensions;
use crate::header::{IntoHeaderPair, IntoHeaderValue};
use crate::header::{Header, IntoHeaderValue};
use crate::http::header::{self, HeaderName, HeaderValue};
use crate::http::{Error as HttpError, HeaderMap, StatusCode};
use crate::message::{BoxedResponseHead, ConnectionType, ResponseHead};
@ -346,98 +341,93 @@ impl ResponseBuilder {
self
}
/// Insert a header, replacing any that were set with an equivalent field name.
/// Set a header.
///
/// ```rust
/// # use actix_http::Response;
/// use actix_http::http::header::ContentType;
/// use actix_http::{http, Request, Response, Result};
///
/// Response::Ok()
/// .insert_header(ContentType(mime::APPLICATION_JSON))
/// .insert_header(("X-TEST", "value"))
/// .finish();
/// fn index(req: Request) -> Result<Response> {
/// Ok(Response::Ok()
/// .set(http::header::IfModifiedSince(
/// "Sun, 07 Nov 1994 08:48:37 GMT".parse()?,
/// ))
/// .finish())
/// }
/// ```
pub fn insert_header<H>(&mut self, header: H) -> &mut Self
where
H: IntoHeaderPair,
{
#[doc(hidden)]
pub fn set<H: Header>(&mut self, hdr: H) -> &mut Self {
if let Some(parts) = parts(&mut self.head, &self.err) {
match header.try_into_header_pair() {
Ok((key, value)) => {
parts.headers.insert(key, value);
match hdr.try_into() {
Ok(value) => {
parts.headers.append(H::name(), value);
}
Err(e) => self.err = Some(e.into()),
};
}
}
self
}
/// Append a header, keeping any that were set with an equivalent field name.
/// Append a header to existing headers.
///
/// ```rust
/// # use actix_http::Response;
/// use actix_http::http::header::ContentType;
/// use actix_http::{http, Request, Response};
///
/// Response::Ok()
/// .append_header(ContentType(mime::APPLICATION_JSON))
/// .append_header(("X-TEST", "value1"))
/// .append_header(("X-TEST", "value2"))
/// .finish();
/// fn index(req: Request) -> Response {
/// Response::Ok()
/// .header("X-TEST", "value")
/// .header(http::header::CONTENT_TYPE, "application/json")
/// .finish()
/// }
/// ```
pub fn append_header<H>(&mut self, header: H) -> &mut Self
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
H: IntoHeaderPair,
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue,
{
if let Some(parts) = parts(&mut self.head, &self.err) {
match header.try_into_header_pair() {
Ok((key, value)) => parts.headers.append(key, value),
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
parts.headers.append(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Replaced with [`Self::insert_header()`].
#[deprecated = "Replaced with `insert_header((key, value))`."]
/// Set a header.
///
/// ```rust
/// use actix_http::{http, Request, Response};
///
/// fn index(req: Request) -> Response {
/// Response::Ok()
/// .set_header("X-TEST", "value")
/// .set_header(http::header::CONTENT_TYPE, "application/json")
/// .finish()
/// }
/// ```
pub fn set_header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: TryInto<HeaderName>,
K::Error: Into<HttpError>,
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue,
{
if self.err.is_some() {
return self;
if let Some(parts) = parts(&mut self.head, &self.err) {
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
};
}
match (key.try_into(), value.try_into_value()) {
(Ok(name), Ok(value)) => return self.insert_header((name, value)),
(Err(err), _) => self.err = Some(err.into()),
(_, Err(err)) => self.err = Some(err.into()),
}
self
}
/// Replaced with [`Self::append_header()`].
#[deprecated = "Replaced with `append_header((key, value))`."]
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
K: TryInto<HeaderName>,
K::Error: Into<HttpError>,
V: IntoHeaderValue,
{
if self.err.is_some() {
return self;
}
match (key.try_into(), value.try_into_value()) {
(Ok(name), Ok(value)) => return self.append_header((name, value)),
(Err(err), _) => self.err = Some(err.into()),
(_, Err(err)) => self.err = Some(err.into()),
}
self
}
@ -468,12 +458,7 @@ impl ResponseBuilder {
if let Some(parts) = parts(&mut self.head, &self.err) {
parts.set_connection_type(ConnectionType::Upgrade);
}
if let Ok(value) = value.try_into_value() {
self.insert_header((header::UPGRADE, value));
}
self
self.set_header(header::UPGRADE, value)
}
/// Force close connection, even if it is marked as keep-alive
@ -488,7 +473,7 @@ impl ResponseBuilder {
/// Disable chunked transfer encoding for HTTP/1.1 streaming responses.
#[inline]
pub fn no_chunking(&mut self, len: u64) -> &mut Self {
self.insert_header((header::CONTENT_LENGTH, len));
self.header(header::CONTENT_LENGTH, len);
if let Some(parts) = parts(&mut self.head, &self.err) {
parts.no_chunking(true);
@ -496,14 +481,15 @@ impl ResponseBuilder {
self
}
/// Set response content type.
/// Set response content type
#[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self
where
V: IntoHeaderValue,
HeaderValue: TryFrom<V>,
<HeaderValue as TryFrom<V>>::Error: Into<HttpError>,
{
if let Some(parts) = parts(&mut self.head, &self.err) {
match value.try_into_value() {
match HeaderValue::try_from(value) {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
@ -654,24 +640,27 @@ impl ResponseBuilder {
self.body(Body::from_message(BodyStream::new(stream)))
}
#[inline]
/// Set a json body and generate `Response`
///
/// `ResponseBuilder` can not be used after this call.
pub fn json<T>(&mut self, value: T) -> Response
where
T: ops::Deref,
T::Target: Serialize,
{
match serde_json::to_string(&*value) {
pub fn json<T: Serialize>(&mut self, value: T) -> Response {
self.json2(&value)
}
/// Set a json body and generate `Response`
///
/// `ResponseBuilder` can not be used after this call.
pub fn json2<T: Serialize>(&mut self, value: &T) -> Response {
match serde_json::to_string(value) {
Ok(body) => {
let contains = if let Some(parts) = parts(&mut self.head, &self.err) {
parts.headers.contains_key(header::CONTENT_TYPE)
} else {
true
};
if !contains {
self.insert_header(header::ContentType(mime::APPLICATION_JSON));
self.header(header::CONTENT_TYPE, "application/json");
}
self.body(Body::from(body))
@ -754,11 +743,9 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
let mut msg = BoxedResponseHead::new(head.status);
msg.version = head.version;
msg.reason = head.reason;
for (k, v) in head.headers.iter() {
for (k, v) in &head.headers {
msg.headers.append(k.clone(), v.clone());
}
msg.no_chunking(!head.chunked());
ResponseBuilder {
@ -815,7 +802,7 @@ impl From<ResponseBuilder> for Response {
impl From<&'static str> for Response {
fn from(val: &'static str) -> Self {
Response::Ok()
.content_type(mime::TEXT_PLAIN_UTF_8)
.content_type("text/plain; charset=utf-8")
.body(val)
}
}
@ -823,7 +810,7 @@ impl From<&'static str> for Response {
impl From<&'static [u8]> for Response {
fn from(val: &'static [u8]) -> Self {
Response::Ok()
.content_type(mime::APPLICATION_OCTET_STREAM)
.content_type("application/octet-stream")
.body(val)
}
}
@ -831,7 +818,7 @@ impl From<&'static [u8]> for Response {
impl From<String> for Response {
fn from(val: String) -> Self {
Response::Ok()
.content_type(mime::TEXT_PLAIN_UTF_8)
.content_type("text/plain; charset=utf-8")
.body(val)
}
}
@ -839,7 +826,7 @@ impl From<String> for Response {
impl<'a> From<&'a String> for Response {
fn from(val: &'a String) -> Self {
Response::Ok()
.content_type(mime::TEXT_PLAIN_UTF_8)
.content_type("text/plain; charset=utf-8")
.body(val)
}
}
@ -847,7 +834,7 @@ impl<'a> From<&'a String> for Response {
impl From<Bytes> for Response {
fn from(val: Bytes) -> Self {
Response::Ok()
.content_type(mime::APPLICATION_OCTET_STREAM)
.content_type("application/octet-stream")
.body(val)
}
}
@ -855,15 +842,13 @@ impl From<Bytes> for Response {
impl From<BytesMut> for Response {
fn from(val: BytesMut) -> Self {
Response::Ok()
.content_type(mime::APPLICATION_OCTET_STREAM)
.content_type("application/octet-stream")
.body(val)
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
use crate::body::Body;
use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE, SET_COOKIE};
@ -871,8 +856,8 @@ mod tests {
#[test]
fn test_debug() {
let resp = Response::Ok()
.append_header((COOKIE, HeaderValue::from_static("cookie1=value1; ")))
.append_header((COOKIE, HeaderValue::from_static("cookie2=value2; ")))
.header(COOKIE, HeaderValue::from_static("cookie1=value1; "))
.header(COOKIE, HeaderValue::from_static("cookie2=value2; "))
.finish();
let dbg = format!("{:?}", resp);
assert!(dbg.contains("Response"));
@ -883,8 +868,8 @@ mod tests {
use crate::httpmessage::HttpMessage;
let req = crate::test::TestRequest::default()
.append_header((COOKIE, "cookie1=value1"))
.append_header((COOKIE, "cookie2=value2"))
.header(COOKIE, "cookie1=value1")
.header(COOKIE, "cookie2=value2")
.finish();
let cookies = req.cookies().unwrap();
@ -897,20 +882,16 @@ mod tests {
.max_age(time::Duration::days(1))
.finish(),
)
.del_cookie(&cookies[0])
.del_cookie(&cookies[1])
.finish();
let mut val = resp
let mut val: Vec<_> = resp
.headers()
.get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned())
.collect::<Vec<_>>();
.collect();
val.sort();
// the .del_cookie call
assert!(val[0].starts_with("cookie1=; Max-Age=0;"));
// the .cookie call
assert_eq!(
val[1],
"name=value; HttpOnly; Path=/test; Domain=www.rust-lang.org; Max-Age=86400"
@ -935,14 +916,14 @@ mod tests {
let mut iter = r.cookies();
let v = iter.next().unwrap();
assert_eq!((v.name(), v.value()), ("original", "val100"));
let v = iter.next().unwrap();
assert_eq!((v.name(), v.value()), ("cookie3", "val300"));
let v = iter.next().unwrap();
assert_eq!((v.name(), v.value()), ("original", "val100"));
}
#[test]
fn test_basic_builder() {
let resp = Response::Ok().insert_header(("X-TEST", "value")).finish();
let resp = Response::Ok().header("X-TEST", "value").finish();
assert_eq!(resp.status(), StatusCode::OK);
}
@ -983,8 +964,26 @@ mod tests {
#[test]
fn test_json_ct() {
let resp = Response::build(StatusCode::OK)
.insert_header((CONTENT_TYPE, "text/json"))
.json(&vec!["v1", "v2", "v3"]);
.header(CONTENT_TYPE, "text/json")
.json(vec!["v1", "v2", "v3"]);
let ct = resp.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(ct, HeaderValue::from_static("text/json"));
assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]");
}
#[test]
fn test_json2() {
let resp = Response::build(StatusCode::OK).json2(&vec!["v1", "v2", "v3"]);
let ct = resp.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(ct, HeaderValue::from_static("application/json"));
assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]");
}
#[test]
fn test_json2_ct() {
let resp = Response::build(StatusCode::OK)
.header(CONTENT_TYPE, "text/json")
.json2(&vec!["v1", "v2", "v3"]);
let ct = resp.headers().get(CONTENT_TYPE).unwrap();
assert_eq!(ct, HeaderValue::from_static("text/json"));
assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]");
@ -1083,54 +1082,4 @@ mod tests {
let cookie = resp.cookies().next().unwrap();
assert_eq!((cookie.name(), cookie.value()), ("cookie1", "val100"));
}
#[test]
fn response_builder_header_insert_kv() {
let mut res = Response::Ok();
res.insert_header(("Content-Type", "application/octet-stream"));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_insert_typed() {
let mut res = Response::Ok();
res.insert_header(header::ContentType(mime::APPLICATION_OCTET_STREAM));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_append_kv() {
let mut res = Response::Ok();
res.append_header(("Content-Type", "application/octet-stream"));
res.append_header(("Content-Type", "application/json"));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
#[test]
fn response_builder_header_append_typed() {
let mut res = Response::Ok();
res.append_header(header::ContentType(mime::APPLICATION_OCTET_STREAM));
res.append_header(header::ContentType(mime::APPLICATION_JSON));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
}

View File

@ -8,34 +8,39 @@ use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
use bytes::Bytes;
use futures_core::{ready, Future};
use futures_util::future::ok;
use h2::server::{self, Handshake};
use pin_project::pin_project;
use crate::body::MessageBody;
use crate::builder::HttpServiceBuilder;
use crate::cloneable::CloneableService;
use crate::config::{KeepAlive, ServiceConfig};
use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::request::Request;
use crate::response::Response;
use crate::{h1, h2::Dispatcher, ConnectCallback, OnConnectData, Protocol};
use crate::{h1, h2::Dispatcher, ConnectCallback, Extensions, Protocol};
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler<T>> {
srv: S,
cfg: ServiceConfig,
expect: X,
upgrade: Option<U>,
// DEPRECATED: in favor of on_connect_ext
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B> HttpService<T, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create builder for `HttpService` instance.
@ -46,15 +51,15 @@ where
impl<T, S, B> HttpService<T, S, B>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
{
/// Create new `HttpService` instance.
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
pub fn new<F: IntoServiceFactory<S>>(service: F) -> Self {
let cfg = ServiceConfig::new(KeepAlive::Timeout(5), 5000, 0, false, None);
HttpService {
@ -62,13 +67,14 @@ where
srv: service.into_factory(),
expect: h1::ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_phantom: PhantomData,
_t: PhantomData,
}
}
/// Create new `HttpService` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
pub(crate) fn with_config<F: IntoServiceFactory<S>>(
cfg: ServiceConfig,
service: F,
) -> Self {
@ -77,19 +83,20 @@ where
srv: service.into_factory(),
expect: h1::ExpectHandler,
upgrade: None,
on_connect: None,
on_connect_ext: None,
_phantom: PhantomData,
_t: PhantomData,
}
}
}
impl<T, S, B, X, U> HttpService<T, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody,
{
/// Provide service for `EXPECT: 100-Continue` support.
@ -99,18 +106,19 @@ where
/// request will be forwarded to main service.
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
where
X1: ServiceFactory<Request, Config = (), Response = Request>,
X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
X1::Error: Into<Error>,
X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
<X1::Service as Service>::Future: 'static,
{
HttpService {
expect,
cfg: self.cfg,
srv: self.srv,
upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
@ -120,21 +128,35 @@ where
/// and this service get called with original request and framed object.
pub fn upgrade<U1>(self, upgrade: Option<U1>) -> HttpService<T, S, B, X, U1>
where
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U1: ServiceFactory<
Config = (),
Request = (Request, Framed<T, h1::Codec>),
Response = (),
>,
U1::Error: fmt::Display,
U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
<U1::Service as Service>::Future: 'static,
{
HttpService {
upgrade,
cfg: self.cfg,
srv: self.srv,
expect: self.expect,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext,
_phantom: PhantomData,
_t: PhantomData,
}
}
/// Set on connect callback.
pub(crate) fn on_connect(
mut self,
f: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
) -> Self {
self.on_connect = f;
self
}
/// Set connect callback with mutable access to request data container.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f;
@ -144,38 +166,38 @@ where
impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<
(Request, Framed<TcpStream, h1::Codec>),
Config = (),
Request = (Request, Framed<TcpStream, h1::Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TcpStream, h1::Codec>)>>::Future: 'static,
<U::Service as Service>::Future: 'static,
{
/// Create simple tcp stream service
pub fn tcp(
self,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = DispatchError,
InitError = (),
> {
pipeline_factory(|io: TcpStream| async {
pipeline_factory(|io: TcpStream| {
let peer_addr = io.peer_addr().ok();
Ok((io, Protocol::Http1, peer_addr))
ok((io, Protocol::Http1, peer_addr))
})
.and_then(self)
}
@ -184,40 +206,39 @@ where
#[cfg(feature = "openssl")]
mod openssl {
use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
use actix_tls::accept::TlsError;
use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::{openssl::HandshakeError, TlsError};
impl<S, B, X, U> HttpService<SslStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<
(Request, Framed<SslStream<TcpStream>, h1::Codec>),
Config = (),
Request = (Request, Framed<SslStream<TcpStream>, h1::Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<SslStream<TcpStream>, h1::Codec>)>>::Future: 'static,
<U::Service as Service>::Future: 'static,
{
/// Create openssl based service
pub fn openssl(
self,
acceptor: SslAcceptor,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<SslError, DispatchError>,
Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
InitError = (),
> {
pipeline_factory(
@ -225,7 +246,7 @@ mod openssl {
.map_err(TlsError::Tls)
.map_init_err(|_| panic!()),
)
.and_then(|io: SslStream<TcpStream>| async {
.and_then(|io: SslStream<TcpStream>| {
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
@ -236,7 +257,7 @@ mod openssl {
Protocol::Http1
};
let peer_addr = io.get_ref().peer_addr().ok();
Ok((io, proto, peer_addr))
ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
@ -245,42 +266,39 @@ mod openssl {
#[cfg(feature = "rustls")]
mod rustls {
use std::io;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, Session, TlsStream};
use actix_tls::accept::TlsError;
use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::rustls::{Acceptor, ServerConfig, Session, TlsStream};
use actix_tls::TlsError;
use std::io;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (),
Request = (Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TlsStream<TcpStream>, h1::Codec>)>>::Future: 'static,
<U::Service as Service>::Future: 'static,
{
/// Create openssl based service
pub fn rustls(
self,
mut config: ServerConfig,
) -> impl ServiceFactory<
TcpStream,
Config = (),
Request = TcpStream,
Response = (),
Error = TlsError<io::Error, DispatchError>,
InitError = (),
@ -293,7 +311,7 @@ mod rustls {
.map_err(TlsError::Tls)
.map_init_err(|_| panic!()),
)
.and_then(|io: TlsStream<TcpStream>| async {
.and_then(|io: TlsStream<TcpStream>| {
let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2
@ -304,37 +322,41 @@ mod rustls {
Protocol::Http1
};
let peer_addr = io.get_ref().0.peer_addr().ok();
Ok((io, proto, peer_addr))
ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
}
}
}
impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)>
for HttpService<T, S, B, X, U>
impl<T, S, B, X, U> ServiceFactory for HttpService<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Config = (), Response = Request>,
X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<
Config = (),
Request = (Request, Framed<T, h1::Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
<U::Service as Service>::Future: 'static,
{
type Config = ();
type Request = (T, Protocol, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Config = ();
type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = ();
type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>;
type Future = HttpServiceResponse<T, S, B, X, U>;
fn new_service(&self, _: ()) -> Self::Future {
@ -344,21 +366,23 @@ where
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: self.cfg.clone(),
_phantom: PhantomData,
_t: PhantomData,
}
}
}
#[doc(hidden)]
#[pin_project]
pub struct HttpServiceResponse<T, S, B, X, U>
where
S: ServiceFactory<Request>,
X: ServiceFactory<Request>,
U: ServiceFactory<(Request, Framed<T, h1::Codec>)>,
{
pub struct HttpServiceResponse<
T,
S: ServiceFactory,
B,
X: ServiceFactory,
U: ServiceFactory,
> {
#[pin]
fut: S::Future,
#[pin]
@ -367,28 +391,29 @@ where
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
_t: PhantomData<(T, B)>,
}
impl<T, S, B, X, U> Future for HttpServiceResponse<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request>,
S: ServiceFactory<Request = Request>,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
<S::Service as Service>::Future: 'static,
B: MessageBody + 'static,
X: ServiceFactory<Request, Response = Request>,
X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Response = ()>,
<X::Service as Service>::Future: 'static,
U: ServiceFactory<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
<U::Service as Service>::Future: 'static,
{
type Output =
Result<HttpServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
@ -411,7 +436,7 @@ where
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
this.fut_ex.set(None);
}
let result = ready!(this
@ -426,6 +451,7 @@ where
service,
this.expect.take().unwrap(),
this.upgrade.take(),
this.on_connect.clone(),
this.on_connect_ext.clone(),
)
}))
@ -433,84 +459,68 @@ where
}
/// `Service` implementation for http transport
pub struct HttpServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
X: Service<Request>,
U: Service<(Request, Framed<T, h1::Codec>)>,
{
flow: Rc<HttpFlow<S, X, U>>,
pub struct HttpServiceHandler<T, S: Service, B, X: Service, U: Service> {
srv: CloneableService<S>,
expect: CloneableService<X>,
upgrade: Option<CloneableService<U>>,
cfg: ServiceConfig,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
}
/// A collection of services that describe an HTTP request flow.
pub(super) struct HttpFlow<S, X, U> {
pub(super) service: S,
pub(super) expect: X,
pub(super) upgrade: Option<U>,
}
impl<S, X, U> HttpFlow<S, X, U> {
pub(super) fn new(service: S, expect: X, upgrade: Option<U>) -> Rc<Self> {
Rc::new(Self {
service,
expect,
upgrade,
})
}
_t: PhantomData<(T, B, X)>,
}
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
srv: S,
expect: X,
upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler {
cfg,
on_connect,
on_connect_ext,
flow: HttpFlow::new(service, expect, upgrade),
_phantom: PhantomData,
srv: CloneableService::new(srv),
expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new),
_t: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)>
for HttpServiceHandler<T, S, B, X, U>
impl<T, S, B, X, U> Service for HttpServiceHandler<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>,
{
type Request = (T, Protocol, Option<net::SocketAddr>);
type Response = ();
type Error = DispatchError;
type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self
.flow
.expect
.poll_ready(cx)
.map_err(|e| {
@ -521,8 +531,7 @@ where
.is_ready();
let ready = self
.flow
.service
.srv
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
@ -532,7 +541,7 @@ where
.is_ready()
&& ready;
let ready = if let Some(ref upg) = self.flow.upgrade {
let ready = if let Some(ref mut upg) = self.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
@ -552,20 +561,22 @@ where
}
}
fn call(
&self,
(io, proto, peer_addr): (T, Protocol, Option<net::SocketAddr>),
) -> Self::Future {
let on_connect_data =
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
fn call(&mut self, (io, proto, peer_addr): Self::Request) -> Self::Future {
let mut connect_extensions = Extensions::new();
let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
if let Some(ref handler) = self.on_connect_ext {
handler(&io, &mut connect_extensions);
}
match proto {
Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake(Some((
server::handshake(io),
self.cfg.clone(),
self.flow.clone(),
on_connect_data,
self.srv.clone(),
deprecated_on_connect,
connect_extensions,
peer_addr,
))),
},
@ -574,13 +585,14 @@ where
state: State::H1(h1::Dispatcher::new(
io,
self.cfg.clone(),
self.flow.clone(),
on_connect_data,
self.srv.clone(),
self.expect.clone(),
self.upgrade.clone(),
deprecated_on_connect,
connect_extensions,
peer_addr,
)),
},
proto => unimplemented!("Unsupported HTTP version: {:?}.", proto),
}
}
}
@ -588,24 +600,25 @@ where
#[pin_project(project = StateProj)]
enum State<T, S, B, X, U>
where
S: Service<Request>,
S: Service<Request = Request>,
S::Future: 'static,
S::Error: Into<Error>,
T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
H1(#[pin] h1::Dispatcher<T, S, B, X, U>),
H2(#[pin] Dispatcher<T, S, B, X, U>),
H2(#[pin] Dispatcher<T, S, B>),
H2Handshake(
Option<(
Handshake<T, Bytes>,
ServiceConfig,
Rc<HttpFlow<S, X, U>>,
OnConnectData,
CloneableService<S>,
Option<Box<dyn DataFactory>>,
Extensions,
Option<net::SocketAddr>,
)>,
),
@ -615,14 +628,14 @@ where
pub struct HttpServiceHandlerResponse<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
#[pin]
@ -632,42 +645,67 @@ where
impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody,
X: Service<Request, Response = Request>,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
type Output = Result<(), DispatchError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project().state.project() {
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project().state.poll(cx)
}
}
impl<T, S, B, X, U> State<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<Request = (Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn poll(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), DispatchError>> {
match self.as_mut().project() {
StateProj::H1(disp) => disp.poll(cx),
StateProj::H2(disp) => disp.poll(cx),
StateProj::H2Handshake(data) => {
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
Ok(conn) => {
let (_, cfg, srv, on_connect_data, peer_addr) =
data.take().unwrap();
self.as_mut().project().state.set(State::H2(Dispatcher::new(
srv,
conn,
on_connect_data,
cfg,
None,
peer_addr,
)));
self.poll(cx)
StateProj::H2Handshake(ref mut data) => {
let conn = if let Some(ref mut item) = data {
match Pin::new(&mut item.0).poll(cx) {
Poll::Ready(Ok(conn)) => conn,
Poll::Ready(Err(err)) => {
trace!("H2 handshake error: {}", err);
return Poll::Ready(Err(err.into()));
}
Poll::Pending => return Poll::Pending,
}
Err(err) => {
trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err.into()))
}
}
} else {
panic!()
};
let (_, cfg, srv, on_connect, on_connect_data, peer_addr) =
data.take().unwrap();
self.set(State::H2(Dispatcher::new(
srv,
conn,
on_connect,
on_connect_data,
cfg,
None,
peer_addr,
)));
self.poll(cx)
}
}
}

View File

@ -1,27 +1,20 @@
//! Various testing helpers for use in internal and app tests.
//! Test Various helpers for Actix applications to use during testing.
use std::convert::TryFrom;
use std::io::{self, Read, Write};
use std::pin::Pin;
use std::str::FromStr;
use std::task::{Context, Poll};
use std::{
cell::{Ref, RefCell},
io::{self, Read, Write},
pin::Pin,
rc::Rc,
str::FromStr,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use actix_codec::{AsyncRead, AsyncWrite};
use bytes::{Bytes, BytesMut};
use http::{
header::{self, HeaderValue},
Method, Uri, Version,
};
use http::header::{self, HeaderName, HeaderValue};
use http::{Error as HttpError, Method, Uri, Version};
use crate::{
cookie::{Cookie, CookieJar},
header::{HeaderMap, IntoHeaderPair},
payload::Payload,
Request,
};
use crate::cookie::{Cookie, CookieJar};
use crate::header::HeaderMap;
use crate::header::{Header, IntoHeaderValue};
use crate::payload::Payload;
use crate::Request;
/// Test `Request` builder
///
@ -38,7 +31,7 @@ use crate::{
/// }
/// }
///
/// let resp = TestRequest::default().insert_header("content-type", "text/plain")
/// let resp = TestRequest::with_header("content-type", "text/plain")
/// .run(&index)
/// .unwrap();
/// assert_eq!(resp.status(), StatusCode::OK);
@ -71,73 +64,76 @@ impl Default for TestRequest {
}
impl TestRequest {
/// Create a default TestRequest and then set its URI.
/// Create TestRequest and set request uri
pub fn with_uri(path: &str) -> TestRequest {
TestRequest::default().uri(path).take()
}
/// Set HTTP version of this request.
/// Create TestRequest and set header
pub fn with_hdr<H: Header>(hdr: H) -> TestRequest {
TestRequest::default().set(hdr).take()
}
/// Create TestRequest and set header
pub fn with_header<K, V>(key: K, value: V) -> TestRequest
where
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue,
{
TestRequest::default().header(key, value).take()
}
/// Set HTTP version of this request
pub fn version(&mut self, ver: Version) -> &mut Self {
parts(&mut self.0).version = ver;
self
}
/// Set HTTP method of this request.
/// Set HTTP method of this request
pub fn method(&mut self, meth: Method) -> &mut Self {
parts(&mut self.0).method = meth;
self
}
/// Set URI of this request.
///
/// # Panics
/// If provided URI is invalid.
/// Set HTTP Uri of this request
pub fn uri(&mut self, path: &str) -> &mut Self {
parts(&mut self.0).uri = Uri::from_str(path).unwrap();
self
}
/// Insert a header, replacing any that were set with an equivalent field name.
pub fn insert_header<H>(&mut self, header: H) -> &mut Self
where
H: IntoHeaderPair,
{
match header.try_into_header_pair() {
Ok((key, value)) => {
parts(&mut self.0).headers.insert(key, value);
}
Err(err) => {
panic!("Error inserting test header: {}.", err.into());
}
/// Set a header
pub fn set<H: Header>(&mut self, hdr: H) -> &mut Self {
if let Ok(value) = hdr.try_into() {
parts(&mut self.0).headers.append(H::name(), value);
return self;
}
self
panic!("Can not set header");
}
/// Append a header, keeping any that were set with an equivalent field name.
pub fn append_header<H>(&mut self, header: H) -> &mut Self
/// Set a header
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
where
H: IntoHeaderPair,
HeaderName: TryFrom<K>,
<HeaderName as TryFrom<K>>::Error: Into<HttpError>,
V: IntoHeaderValue,
{
match header.try_into_header_pair() {
Ok((key, value)) => {
if let Ok(key) = HeaderName::try_from(key) {
if let Ok(value) = value.try_into() {
parts(&mut self.0).headers.append(key, value);
}
Err(err) => {
panic!("Error inserting test header: {}.", err.into());
return self;
}
}
self
panic!("Can not create header");
}
/// Set cookie for this request.
/// Set cookie for this request
pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self {
parts(&mut self.0).cookies.add(cookie.into_owned());
self
}
/// Set request payload.
/// Set request payload
pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self {
let mut payload = crate::h1::Payload::empty();
payload.unread_data(data.into());
@ -149,7 +145,7 @@ impl TestRequest {
TestRequest(self.0.take())
}
/// Complete request creation and generate `Request` instance.
/// Complete request creation and generate `Request` instance
pub fn finish(&mut self) -> Request {
let inner = self.0.take().expect("cannot reuse test request builder");
@ -187,7 +183,7 @@ fn parts(parts: &mut Option<Inner>) -> &mut Inner {
parts.as_mut().expect("cannot reuse test request builder")
}
/// Async I/O test buffer.
/// Async io buffer
pub struct TestBuffer {
pub read_buf: BytesMut,
pub write_buf: BytesMut,
@ -195,24 +191,24 @@ pub struct TestBuffer {
}
impl TestBuffer {
/// Create new `TestBuffer` instance with initial read buffer.
pub fn new<T>(data: T) -> Self
/// Create new TestBuffer instance
pub fn new<T>(data: T) -> TestBuffer
where
T: Into<BytesMut>,
BytesMut: From<T>,
{
Self {
read_buf: data.into(),
TestBuffer {
read_buf: BytesMut::from(data),
write_buf: BytesMut::new(),
err: None,
}
}
/// Create new empty `TestBuffer` instance.
pub fn empty() -> Self {
Self::new("")
/// Create new empty TestBuffer instance
pub fn empty() -> TestBuffer {
TestBuffer::new("")
}
/// Add data to read buffer.
/// Add extra data to read buffer.
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.read_buf.extend_from_slice(data.as_ref())
}
@ -240,7 +236,6 @@ impl io::Write for TestBuffer {
self.write_buf.extend(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
@ -250,11 +245,9 @@ impl AsyncRead for TestBuffer {
fn poll_read(
self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let dst = buf.initialize_unfilled();
let res = self.get_mut().read(dst).map(|n| buf.advance(n));
Poll::Ready(res)
buf: &mut [u8],
) -> Poll<io::Result<usize>> {
Poll::Ready(self.get_mut().read(buf))
}
}
@ -275,117 +268,3 @@ impl AsyncWrite for TestBuffer {
Poll::Ready(Ok(()))
}
}
/// Async I/O test buffer with ability to incrementally add to the read buffer.
#[derive(Clone)]
pub struct TestSeqBuffer(Rc<RefCell<TestSeqInner>>);
impl TestSeqBuffer {
/// Create new `TestBuffer` instance with initial read buffer.
pub fn new<T>(data: T) -> Self
where
T: Into<BytesMut>,
{
Self(Rc::new(RefCell::new(TestSeqInner {
read_buf: data.into(),
write_buf: BytesMut::new(),
err: None,
})))
}
/// Create new empty `TestBuffer` instance.
pub fn empty() -> Self {
Self::new("")
}
pub fn read_buf(&self) -> Ref<'_, BytesMut> {
Ref::map(self.0.borrow(), |inner| &inner.read_buf)
}
pub fn write_buf(&self) -> Ref<'_, BytesMut> {
Ref::map(self.0.borrow(), |inner| &inner.write_buf)
}
pub fn err(&self) -> Ref<'_, Option<io::Error>> {
Ref::map(self.0.borrow(), |inner| &inner.err)
}
/// Add data to read buffer.
pub fn extend_read_buf<T: AsRef<[u8]>>(&mut self, data: T) {
self.0
.borrow_mut()
.read_buf
.extend_from_slice(data.as_ref())
}
}
pub struct TestSeqInner {
read_buf: BytesMut,
write_buf: BytesMut,
err: Option<io::Error>,
}
impl io::Read for TestSeqBuffer {
fn read(&mut self, dst: &mut [u8]) -> Result<usize, io::Error> {
if self.0.borrow().read_buf.is_empty() {
if self.0.borrow().err.is_some() {
Err(self.0.borrow_mut().err.take().unwrap())
} else {
Err(io::Error::new(io::ErrorKind::WouldBlock, ""))
}
} else {
let size = std::cmp::min(self.0.borrow().read_buf.len(), dst.len());
let b = self.0.borrow_mut().read_buf.split_to(size);
dst[..size].copy_from_slice(&b);
Ok(size)
}
}
}
impl io::Write for TestSeqBuffer {
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.borrow_mut().write_buf.extend(buf);
Ok(buf.len())
}
fn flush(&mut self) -> io::Result<()> {
Ok(())
}
}
impl AsyncRead for TestSeqBuffer {
fn poll_read(
self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
let dst = buf.initialize_unfilled();
let r = self.get_mut().read(dst);
match r {
Ok(n) => {
buf.advance(n);
Poll::Ready(Ok(()))
}
Err(err) if err.kind() == io::ErrorKind::WouldBlock => Poll::Pending,
Err(err) => Poll::Ready(Err(err)),
}
}
}
impl AsyncWrite for TestSeqBuffer {
fn poll_write(
self: Pin<&mut Self>,
_: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Poll::Ready(self.get_mut().write(buf))
}
fn poll_flush(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}

View File

@ -1,60 +1,47 @@
use actix_codec::{Decoder, Encoder};
use bitflags::bitflags;
use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use super::frame::Parser;
use super::proto::{CloseReason, OpCode};
use super::ProtocolError;
/// A WebSocket message.
/// `WebSocket` Message
#[derive(Debug, PartialEq)]
pub enum Message {
/// Text message.
Text(ByteString),
/// Binary message.
/// Text message
Text(String),
/// Binary message
Binary(Bytes),
/// Continuation.
/// Continuation
Continuation(Item),
/// Ping message.
/// Ping message
Ping(Bytes),
/// Pong message.
/// Pong message
Pong(Bytes),
/// Close message with optional reason.
/// Close message with optional reason
Close(Option<CloseReason>),
/// No-op. Useful for low-level services.
/// No-op. Useful for actix-net services
Nop,
}
/// A WebSocket frame.
/// `WebSocket` frame
#[derive(Debug, PartialEq)]
pub enum Frame {
/// Text frame. Note that the codec does not validate UTF-8 encoding.
/// Text frame, codec does not verify utf8 encoding
Text(Bytes),
/// Binary frame.
/// Binary frame
Binary(Bytes),
/// Continuation.
/// Continuation
Continuation(Item),
/// Ping message.
/// Ping message
Ping(Bytes),
/// Pong message.
/// Pong message
Pong(Bytes),
/// Close message with optional reason.
/// Close message with optional reason
Close(Option<CloseReason>),
}
/// A `WebSocket` continuation item.
/// `WebSocket` continuation item
#[derive(Debug, PartialEq)]
pub enum Item {
FirstText(Bytes),
@ -64,13 +51,13 @@ pub enum Item {
}
#[derive(Debug, Copy, Clone)]
/// WebSocket protocol codec.
/// WebSockets protocol codec
pub struct Codec {
flags: Flags,
max_size: usize,
}
bitflags! {
bitflags::bitflags! {
struct Flags: u8 {
const SERVER = 0b0000_0001;
const CONTINUATION = 0b0000_0010;
@ -79,7 +66,7 @@ bitflags! {
}
impl Codec {
/// Create new websocket frames decoder.
/// Create new websocket frames decoder
pub fn new() -> Codec {
Codec {
max_size: 65_536,
@ -87,9 +74,9 @@ impl Codec {
}
}
/// Set max frame size.
/// Set max frame size
///
/// By default max size is set to 64kB.
/// By default max size is set to 64kb
pub fn max_size(mut self, size: usize) -> Self {
self.max_size = size;
self
@ -197,7 +184,7 @@ impl Encoder<Message> for Codec {
}
}
},
Message::Nop => {}
Message::Nop => (),
}
Ok(())
}

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