1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-07 19:23:58 +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
246 changed files with 13860 additions and 16534 deletions

View File

@ -1,3 +0,0 @@
[alias]
chk = "hack check --workspace --all-features --tests --examples"
lint = "hack --clean-per-run clippy --workspace --tests --examples"

View File

@ -1,21 +1,21 @@
<!-- Thanks for considering contributing actix! --> <!-- 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 ## PR Type
<!-- What kind of change does this PR make? --> <!-- What kind of change does this PR make? -->
<!-- Bug Fix / Feature / Refactor / Code Style / Other --> <!-- Bug Fix / Feature / Refactor / Code Style / Other -->
PR_TYPE INSERT_PR_TYPE
## PR Checklist ## 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. --> <!-- For draft PRs check the boxes as you complete them. -->
- [ ] Tests for the changes have been added / updated. - [ ] Tests for the changes have been added / updated.
- [ ] Documentation comments have been added / updated. - [ ] Documentation comments have been added / updated.
- [ ] A changelog entry has been made for the appropriate packages. - [ ] A changelog entry has been made for the appropriate packages.
- [ ] Format code with the latest stable rustfmt. - [ ] Format code with the latest stable rustfmt
- [ ] (Team) Label with affected crates and semver status.
## Overview ## Overview

View File

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

View File

@ -1,127 +0,0 @@
name: CI
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches: [master]
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc }
version:
- 1.46.0 # MSRV
- stable
- nightly
name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }}
env:
VCPKGRS_DYNAMIC: 1
steps:
- uses: actions/checkout@v2
# install OpenSSL on Windows
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: vcpkg install openssl:x64-windows
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with:
command: generate-lockfile
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check minimal
uses: actions-rs/cargo@v1
with:
command: hack
args: check --workspace --no-default-features
- name: check minimal + tests
uses: actions-rs/cargo@v1
with:
command: hack
args: check --workspace --no-default-features --tests --examples
- name: check full
uses: actions-rs/cargo@v1
with:
command: check
args: --workspace --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: -v --workspace --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
- name: tests (actix-http)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-http --no-default-features --features=rustls -- --nocapture
- name: tests (awc)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=awc --no-default-features --features=rustls -- --nocapture
- name: Generate coverage file
if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
run: |
cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --out Xml --verbose
- name: Upload to Codecov
if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
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

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

69
.github/workflows/linux.yml vendored Normal file
View File

@ -0,0 +1,69 @@
name: CI (Linux)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- 1.42.0 # MSRV
- stable
- nightly
name: ${{ matrix.version }} - x86_64-unknown-linux-gnu
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
- name: tests (actix-http)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=actix-http --no-default-features --features=rustls -- --nocapture
- name: tests (awc)
uses: actions-rs/cargo@v1
timeout-minutes: 40
with:
command: test
args: --package=awc --no-default-features --features=rustls -- --nocapture
- name: Generate coverage file
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' || github.event_name == 'pull_request')
uses: codecov/codecov-action@v1
with:
file: cobertura.xml

44
.github/workflows/macos.yml vendored Normal file
View File

@ -0,0 +1,44 @@
name: CI (macOS)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-apple-darwin
runs-on: macOS-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-apple-darwin
profile: minimal
override: true
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls

View File

@ -1,12 +1,14 @@
name: Upload Documentation name: Upload documentation
on: on:
push: push:
branches: [master] branches:
- master
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'actix/actix-web'
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@ -18,14 +20,14 @@ jobs:
profile: minimal profile: minimal
override: true override: true
- name: Build Docs - name: check build
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: doc command: doc
args: --workspace --all-features --no-deps args: --no-deps --workspace --all-features
- name: Tweak HTML - name: Tweak HTML
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
- name: Deploy to GitHub Pages - name: Deploy to GitHub Pages
uses: JamesIves/github-pages-deploy-action@3.7.1 uses: JamesIves/github-pages-deploy-action@3.7.1

64
.github/workflows/windows.yml vendored Normal file
View File

@ -0,0 +1,64 @@
name: CI (Windows)
on:
pull_request:
types: [opened, synchronize, reopened]
push:
branches:
- master
env:
VCPKGRS_DYNAMIC: 1
jobs:
build_and_test:
strategy:
fail-fast: false
matrix:
version:
- stable
- nightly
name: ${{ matrix.version }} - x86_64-pc-windows-msvc
runs-on: windows-latest
steps:
- uses: actions/checkout@v2
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-x86_64-pc-windows-msvc
profile: minimal
override: true
- name: Install OpenSSL
run: |
vcpkg integrate install
vcpkg install openssl:x64-windows
Copy-Item C:\vcpkg\installed\x64-windows\bin\libcrypto-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libcrypto.dll
Copy-Item C:\vcpkg\installed\x64-windows\bin\libssl-1_1-x64.dll C:\vcpkg\installed\x64-windows\bin\libssl.dll
Get-ChildItem C:\vcpkg\installed\x64-windows\bin
Get-ChildItem C:\vcpkg\installed\x64-windows\lib
- name: check build
uses: actions-rs/cargo@v1
with:
command: check
args: --all --bins --examples --tests
- name: tests
uses: actions-rs/cargo@v1
with:
command: test
args: --all --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls
--skip=test_params
--skip=test_simple
--skip=test_expect_continue
--skip=test_http10_keepalive
--skip=test_slow_request
--skip=test_connection_force_close
--skip=test_connection_server_close
--skip=test_connection_wait_queue_force_close

View File

@ -1,117 +1,14 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2020-xx-xx
## 4.0.0-beta.5 - 2021-04-02 ## 3.3.3 - 2021-12-18
### Added
* `Header` extractor for extracting common HTTP headers in handlers. [#2094]
* Added `TestServer::client_headers` method. [#2097]
### Fixed
* Double ampersand in Logger format is escaped correctly. [#2067]
### Changed ### Changed
* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed * Soft-deprecate `NormalizePath::default()`, noting upcoming behavior change in v4. [#2529]
instead of skipping. (Only the first error is kept when multiple error occur) [#2093]
### Removed [#2529]: https://github.com/actix/actix-web/pull/2529
* The `client` mod was removed. Clients should now use `awc` directly.
[871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7)
* Integration testing was moved to new `actix-test` crate. Namely these items from the `test`
module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112]
[#2067]: https://github.com/actix/actix-web/pull/2067
[#2093]: https://github.com/actix/actix-web/pull/2093
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2097]: https://github.com/actix/actix-web/pull/2097
[#2112]: https://github.com/actix/actix-web/pull/2112
## 4.0.0-beta.4 - 2021-03-09
### Changed
* Feature `cookies` is now optional and enabled by default. [#1981]
* `JsonBody::new` returns a default limit of 32kB to be consistent with `JsonConfig` and the default
behaviour of the `web::Json<T>` extractor. [#2010]
[#1981]: https://github.com/actix/actix-web/pull/1981
[#2010]: https://github.com/actix/actix-web/pull/2010
## 4.0.0-beta.3 - 2021-02-10
* Update `actix-web-codegen` to `0.5.0-beta.1`.
## 4.0.0-beta.2 - 2021-02-10
### 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]
### 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]
### Fixed
* Multiple calls to `App::data` with the same type now keeps the latest call's data. [#1906]
### 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 ## 3.3.2 - 2020-12-01
### Fixed ### Fixed
@ -196,7 +93,7 @@
## 3.0.0-beta.4 - 2020-09-09 ## 3.0.0-beta.4 - 2020-09-09
### Added ### Added
* `middleware::NormalizePath` now has configurable behavior for either always having a trailing * `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
slash, or as the new addition, always trimming trailing slashes. [#1639] slash, or as the new addition, always trimming trailing slashes. [#1639]
### Changed ### Changed
@ -524,7 +421,7 @@
## [1.0.0-rc] - 2019-05-18 ## [1.0.0-rc] - 2019-05-18
### Added ### Add
* Add `Query<T>::from_query()` to extract parameters from a query string. #846 * Add `Query<T>::from_query()` to extract parameters from a query string. #846
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors. * `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
@ -540,7 +437,7 @@
## [1.0.0-beta.4] - 2019-05-12 ## [1.0.0-beta.4] - 2019-05-12
### Added ### Add
* Allow to set/override app data on scope level * Allow to set/override app data on scope level
@ -566,7 +463,7 @@
* CORS handling without headers #702 * CORS handling without headers #702
* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types. * Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
### Fixed ### Fixed
@ -630,7 +527,7 @@
### Changed ### Changed
* Allow using any service as default service. * Allow to use any service as default service.
* Remove generic type for request payload, always use default. * Remove generic type for request payload, always use default.
@ -693,13 +590,13 @@
### Added ### Added
* Rustls support * rustls support
### Changed ### Changed
* Use forked cookie * use forked cookie
* Multipart::Field renamed to MultipartField * multipart::Field renamed to MultipartField
## [1.0.0-alpha.1] - 2019-03-28 ## [1.0.0-alpha.1] - 2019-03-28

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.5" version = "3.3.3"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
readme = "README.md" readme = "README.md"
@ -15,7 +15,6 @@ license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with
features = ["openssl", "rustls", "compress", "secure-cookies"] features = ["openssl", "rustls", "compress", "secure-cookies"]
[badges] [badges]
@ -35,27 +34,23 @@ members = [
"actix-multipart", "actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-http-test", "test-server",
"actix-test",
] ]
[features] [features]
default = ["compress", "cookies"] default = ["compress"]
# content-encoding support # content-encoding support
compress = ["actix-http/compress"] compress = ["actix-http/compress", "awc/compress"]
# support for cookies # sessions feature
cookies = ["actix-http/cookies"]
# secure cookies feature
secure-cookies = ["actix-http/secure-cookies"] secure-cookies = ["actix-http/secure-cookies"]
# openssl # openssl
openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"] openssl = ["actix-tls/openssl", "awc/openssl", "open-ssl"]
# rustls # rustls
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] rustls = ["actix-tls/rustls", "awc/rustls", "rust-tls"]
[[example]] [[example]]
name = "basic" name = "basic"
@ -67,59 +62,62 @@ required-features = ["compress"]
[[test]] [[test]]
name = "test_server" name = "test_server"
required-features = ["compress", "cookies"] required-features = ["compress"]
[[example]] [[example]]
name = "on_connect" name = "on_connect"
required-features = [] required-features = []
[[example]]
name = "client"
required-features = ["rustls"]
[dependencies] [dependencies]
actix-codec = "0.4.0-beta.1" actix-codec = "0.3.0"
actix-macros = "0.2.0" actix-service = "1.0.6"
actix-router = "0.2.7" actix-utils = "2.0.0"
actix-rt = "2.2" actix-router = "0.2.4"
actix-server = "2.0.0-beta.3" actix-rt = "1.1.1"
actix-service = "2.0.0-beta.4" actix-server = "1.0.0"
actix-utils = "3.0.0-beta.4" actix-testing = "1.0.0"
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-macros = "0.1.0"
actix-threadpool = "0.3.1"
actix-tls = "2.0.0"
actix-web-codegen = "0.5.0-beta.2" actix-web-codegen = "0.4.0"
actix-http = "3.0.0-beta.5" actix-http = "2.2.0"
awc = { version = "2.0.3", default-features = false }
ahash = "0.7" bytes = "0.5.3"
bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false } futures-channel = { version = "0.3.5", default-features = false }
futures-util = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.5", default-features = false }
language-tags = "0.2" futures-util = { version = "0.3.5", default-features = false }
once_cell = "1.5" fxhash = "0.2.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
socket2 = "0.3.16"
pin-project = "1.0.0" pin-project = "1.0.0"
regex = "1.4" regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
smallvec = "1.6" time = { version = "0.2.7", default-features = false, features = ["std"] }
socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] }
url = "2.1" url = "2.1"
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] [dev-dependencies]
actix-test = { version = "0.0.1", features = ["openssl", "rustls"] } actix = "0.10.0"
awc = { version = "3.0.0-beta.4", features = ["openssl"] } actix-http = { version = "2.1.0", features = ["actors"] }
rand = "0.7"
brotli2 = "0.3.2"
criterion = "0.3"
env_logger = "0.8" env_logger = "0.8"
flate2 = "1.0.13"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0" serde_derive = "1.0"
tls-openssl = { package = "openssl", version = "0.10.9" } brotli = "3.3.3"
tls-rustls = { package = "rustls", version = "0.19.0" } flate2 = "1.0.13"
criterion = "0.3"
[profile.release] [profile.release]
lto = true lto = true
@ -127,14 +125,12 @@ opt-level = 3
codegen-units = 1 codegen-units = 1
[patch.crates-io] [patch.crates-io]
actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-multipart = { path = "actix-multipart" }
actix-test = { path = "actix-test" }
actix-web = { path = "." } actix-web = { path = "." }
actix-web-actors = { path = "actix-web-actors" } actix-http = { path = "actix-http" }
actix-http-test = { path = "test-server" }
actix-web-codegen = { path = "actix-web-codegen" } actix-web-codegen = { path = "actix-web-codegen" }
actix-files = { path = "actix-files" }
actix-multipart = { path = "actix-multipart" }
awc = { path = "awc" } awc = { path = "awc" }
[[bench]] [[bench]]
@ -144,7 +140,3 @@ harness = false
[[bench]] [[bench]]
name = "service" name = "service"
harness = false harness = false
[[bench]]
name = "responder"
harness = false

View File

@ -1,15 +1,5 @@
## Unreleased ## 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 ## 3.0.0

View File

@ -1,19 +1,20 @@
<div align="center"> <div align="center">
<h1>Actix Web</h1> <h1>Actix web</h1>
<p> <p>
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong> <strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
</p> </p>
<p> <p>
[![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web) [![crates.io](https://img.shields.io/crates/v/actix-web?label=latest)](https://crates.io/crates/actix-web)
[![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.5)](https://docs.rs/actix-web/4.0.0-beta.5) [![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.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.42+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.42.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![License](https://img.shields.io/crates/l/actix-web.svg)
[![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.5) [![Dependency Status](https://deps.rs/crate/actix-web/3.3.3/status.svg)](https://deps.rs/crate/actix-web/3.3.3)
<br /> <br />
[![build status](https://github.com/actix/actix-web/workflows/CI%20%28Linux%29/badge.svg?branch=master&event=push)](https://github.com/actix/actix-web/actions) [![Build Status](https://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) [![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) [![Chat on Discord](https://img.shields.io/discord/771444961383153695?label=chat&logo=discord)](https://discord.gg/NWpN5mmg3x)
</p> </p>
@ -31,8 +32,9 @@
* Static assets * Static assets
* SSL support using OpenSSL or Rustls * SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://docs.rs/actix-web/latest/actix_web/client/index.html) * Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
* Runs on stable Rust 1.46+ * Supports [Actix actor framework](https://github.com/actix/actix)
* Runs on stable Rust 1.42+
## Documentation ## Documentation
@ -71,18 +73,18 @@ async fn main() -> std::io::Result<()> {
### More examples ### More examples
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/) * [Basic Setup](https://github.com/actix/examples/tree/master/basics/)
* [Application State](https://github.com/actix/examples/tree/master/basics/state/) * [Application State](https://github.com/actix/examples/tree/master/state/)
* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/) * [JSON Handling](https://github.com/actix/examples/tree/master/json/)
* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/) * [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/)
* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/) * [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/)
* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/) * [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/)
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/) * [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/)
* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/) * [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/)
* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/) * [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/)
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/) * [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/)
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/) * [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/) * [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/)
You may consider checking out You may consider checking out
[this directory](https://github.com/actix/examples/tree/master/) for more examples. [this directory](https://github.com/actix/examples/tree/master/) for more examples.
@ -97,13 +99,13 @@ One of the fastest web frameworks available according to the
This project is licensed under either of This project is licensed under either of
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or * 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 * 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. at your option.
## Code of Conduct ## Code of Conduct
Contribution to the actix-web repo is organized under the terms of the Contributor Covenant. Contribution to the actix-web crate is organized under the terms of the Contributor Covenant, the
The Actix team promises to intervene to uphold that code of conduct. maintainers of Actix web, promises to intervene to uphold that code of conduct.

View File

@ -1,31 +1,6 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2020-xx-xx
## 0.6.0-beta.3 - 2021-03-09
* No notable changes.
## 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
## 0.4.1 - 2020-11-24 ## 0.4.1 - 2020-11-24

View File

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

View File

@ -3,17 +3,17 @@
> Static file serving for Actix Web > Static file serving for Actix Web
[![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files) [![crates.io](https://img.shields.io/crates/v/actix-files?label=latest)](https://crates.io/crates/actix-files)
[![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.3)](https://docs.rs/actix-files/0.6.0-beta.3) [![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.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![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) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.3/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.3) [![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) [![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) [![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 & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-files/) - [API Documentation](https://docs.rs/actix-files/)
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index) - [Example Project](https://github.com/actix/examples/tree/master/static_index)
- [Chat on Gitter](https://gitter.im/actix/actix-web) - [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::{ use actix_web::{
error::{BlockingError, Error}, error::{BlockingError, Error},
rt::task::{spawn_blocking, JoinHandle}, web,
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; 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)] #[doc(hidden)]
/// A helper created from a `std::fs::File` which reads the file /// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`. /// chunk-by-chunk on a `ThreadPool`.
pub struct ChunkedReadFile { pub struct ChunkedReadFile {
size: u64, pub(crate) size: u64,
offset: u64, pub(crate) offset: u64,
state: ChunkedReadFileState, pub(crate) file: Option<File>,
counter: u64, pub(crate) fut: Option<ChunkedBoxFuture>,
} pub(crate) 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,
}
}
} }
impl fmt::Debug for ChunkedReadFile { impl fmt::Debug for ChunkedReadFile {
@ -49,50 +40,55 @@ impl fmt::Debug for ChunkedReadFile {
impl Stream for ChunkedReadFile { impl Stream for ChunkedReadFile {
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(
let this = self.as_mut().get_mut(); mut self: Pin<&mut Self>,
match this.state { cx: &mut Context<'_>,
ChunkedReadFileState::File(ref mut file) => { ) -> Poll<Option<Self::Item>> {
let size = this.size; if let Some(ref mut fut) = self.fut {
let offset = this.offset; return match ready!(Pin::new(fut).poll(cx)) {
let counter = this.counter; Ok((file, bytes)) => {
self.fut.take();
self.file = Some(file);
if size == counter { self.offset += bytes.len() as u64;
Poll::Ready(None) self.counter += bytes.len() as u64;
} else {
let mut file = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = spawn_blocking(move || { Poll::Ready(Some(Ok(bytes)))
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)
} }
} Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
ChunkedReadFileState::Future(ref mut fut) => { };
let (file, bytes) = }
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
this.state = ChunkedReadFileState::File(Some(file));
this.offset += bytes.len() as u64; let size = self.size;
this.counter += bytes.len() as u64; 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 std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse}; use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
use askama_escape::{escape as escape_html_entity, Html};
use percent_encoding::{utf8_percent_encode, CONTROLS}; use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity;
/// A directory; responds with the generated directory listing. /// A directory; responds with the generated directory listing.
#[derive(Debug)] #[derive(Debug)]
@ -50,7 +50,7 @@ macro_rules! encode_file_url {
// " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f; // " -- &quot; & -- &amp; ' -- &#x27; < -- &lt; > -- &gt; / -- &#x2f;
macro_rules! encode_file_name { macro_rules! encode_file_name {
($entry:ident) => { ($entry:ident) => {
escape_html_entity(&$entry.file_name().to_string_lossy(), Html) escape_html_entity(&$entry.file_name().to_string_lossy())
}; };
} }
@ -66,7 +66,9 @@ pub(crate) fn directory_listing(
if dir.is_visible(&entry) { if dir.is_visible(&entry) {
let entry = entry.unwrap(); let entry = entry.unwrap();
let p = match entry.path().strip_prefix(&dir.path) { let p = match entry.path().strip_prefix(&dir.path) {
Ok(p) if cfg!(windows) => base.join(p).to_string_lossy().replace("\\", "/"), Ok(p) if cfg!(windows) => {
base.join(p).to_string_lossy().replace("\\", "/")
}
Ok(p) => base.join(p).to_string_lossy().into_owned(), Ok(p) => base.join(p).to_string_lossy().into_owned(),
Err(_) => continue, Err(_) => continue,
}; };

View File

@ -1,26 +1,27 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; 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_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, dev::{
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
},
error::Error, error::Error,
guard::Guard, guard::Guard,
http::header::DispositionType, http::header::DispositionType,
HttpRequest, HttpRequest,
}; };
use futures_core::future::LocalBoxFuture; use futures_util::future::{ok, FutureExt, LocalBoxFuture};
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, directory_listing, named, Directory, DirectoryRenderer, FilesService,
MimeOverride, HttpNewService, MimeOverride,
}; };
/// Static files handling service. /// Static files handling service.
/// ///
/// `Files` service must be registered with `App::service()` method. /// `Files` service must be registered with `App::service()` method.
/// ///
/// ``` /// ```rust
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::Files; /// use actix_files::Files;
/// ///
@ -38,7 +39,6 @@ pub struct Files {
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>, guards: Option<Rc<dyn Guard>>,
hidden_files: bool,
} }
impl fmt::Debug for Files { impl fmt::Debug for Files {
@ -60,7 +60,6 @@ impl Clone for Files {
path: self.path.clone(), path: self.path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files,
} }
} }
} }
@ -81,9 +80,8 @@ impl Files {
/// be inaccessible. Register more specific handlers and services first. /// be inaccessible. Register more specific handlers and services first.
/// ///
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a /// `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 /// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
/// adjusted with work load. More threads would spawn when need and threads goes idle for a /// by setting ACTIX_THREADPOOL environment variable.
/// period of time would be de-spawned.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files { pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into(); let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() { let dir = match orig_dir.canonicalize() {
@ -105,7 +103,6 @@ impl Files {
mime_override: None, mime_override: None,
file_flags: named::Flags::default(), file_flags: named::Flags::default(),
guards: None, guards: None,
hidden_files: false,
} }
} }
@ -128,8 +125,8 @@ impl Files {
/// Set custom directory renderer /// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
for<'r, 's> F: for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static, + 'static,
{ {
self.renderer = Rc::new(f); self.renderer = Rc::new(f);
self self
@ -201,10 +198,10 @@ impl Files {
/// Sets default handler which is used when no matched file could be found. /// Sets default handler which is used when no matched file could be found.
pub fn default_handler<F, U>(mut self, f: F) -> Self pub fn default_handler<F, U>(mut self, f: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U>,
U: ServiceFactory< U: ServiceFactory<
ServiceRequest,
Config = (), Config = (),
Request = ServiceRequest,
Response = ServiceResponse, Response = ServiceResponse,
Error = Error, Error = Error,
> + 'static, > + 'static,
@ -216,13 +213,6 @@ impl Files {
self 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 { impl HttpServiceFactory for Files {
@ -241,7 +231,8 @@ impl HttpServiceFactory for Files {
} }
} }
impl ServiceFactory<ServiceRequest> for Files { impl ServiceFactory for Files {
type Request = ServiceRequest;
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Config = (); type Config = ();
@ -260,22 +251,21 @@ impl ServiceFactory<ServiceRequest> for Files {
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files,
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {
let fut = default.new_service(()); default
Box::pin(async { .new_service(())
match fut.await { .map(move |result| match result {
Ok(default) => { Ok(default) => {
srv.default = Some(default); srv.default = Some(default);
Ok(srv) Ok(srv)
} }
Err(_) => Err(()), Err(_) => Err(()),
} })
}) .boxed_local()
} else { } else {
Box::pin(ok(srv)) ok(srv).boxed_local()
} }
} }
} }

View File

@ -3,7 +3,7 @@
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
//! # Example //! # Example
//! ``` //! ```rust
//! use actix_web::App; //! use actix_web::App;
//! use actix_files::Files; //! use actix_files::Files;
//! //!
@ -14,10 +14,12 @@
#![deny(rust_2018_idioms)] #![deny(rust_2018_idioms)]
#![warn(missing_docs, missing_debug_implementations)] #![warn(missing_docs, missing_debug_implementations)]
use std::io;
use actix_service::boxed::{BoxService, BoxServiceFactory}; use actix_service::boxed::{BoxService, BoxServiceFactory};
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::{BlockingError, Error, ErrorInternalServerError},
http::header::DispositionType, http::header::DispositionType,
}; };
use mime_guess::from_ext; 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() 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; type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
#[cfg(test)] #[cfg(test)]
@ -65,7 +74,6 @@ mod tests {
}; };
use actix_service::ServiceFactory; use actix_service::ServiceFactory;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
guard, guard,
http::{ http::{
@ -74,9 +82,9 @@ mod tests {
}, },
middleware::Compress, middleware::Compress,
test::{self, TestRequest}, test::{self, TestRequest},
web::{self, Bytes}, web, App, HttpResponse, Responder,
App, HttpResponse, Responder,
}; };
use futures_util::future::ok;
use super::*; use super::*;
@ -98,22 +106,11 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match() { async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open("Cargo.toml").unwrap();
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let since =
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() 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);
}
#[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))
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED); assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
@ -122,40 +119,17 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_with_if_none_match() { async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open("Cargo.toml").unwrap();
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let since =
header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
.insert_header((header::IF_NONE_MATCH, "miss_etag")) .header(header::IF_NONE_MATCH, "miss_etag")
.insert_header((header::IF_MODIFIED_SINCE, since)) .header(header::IF_MODIFIED_SINCE, since)
.to_http_request(); .to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED); 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] #[actix_rt::test]
async fn test_named_file_text() { async fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open("test--").is_err());
@ -210,7 +184,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_non_ascii_file_name() { async fn test_named_file_non_ascii_file_name() {
let mut file = let mut file =
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap(); NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@ -363,7 +338,7 @@ mod tests {
DispositionType::Attachment DispositionType::Attachment
} }
let srv = test::init_service( let mut srv = test::init_service(
App::new().service( App::new().service(
Files::new("/", ".") Files::new("/", ".")
.mime_override(all_attachment) .mime_override(all_attachment)
@ -373,7 +348,7 @@ mod tests {
.await; .await;
let request = TestRequest::get().uri("/").to_request(); 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); assert_eq!(response.status(), StatusCode::OK);
let content_disposition = response let content_disposition = response
@ -388,7 +363,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_ranges_status_code() { 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")), App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
) )
.await; .await;
@ -396,29 +371,29 @@ mod tests {
// Valid range header // Valid range header
let request = TestRequest::get() let request = TestRequest::get()
.uri("/t%65st/Cargo.toml") .uri("/t%65st/Cargo.toml")
.insert_header((header::RANGE, "bytes=10-20")) .header(header::RANGE, "bytes=10-20")
.to_request(); .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); assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
// Invalid range header // Invalid range header
let request = TestRequest::get() let request = TestRequest::get()
.uri("/t%65st/Cargo.toml") .uri("/t%65st/Cargo.toml")
.insert_header((header::RANGE, "bytes=1-0")) .header(header::RANGE, "bytes=1-0")
.to_request(); .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); assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_range_headers() { async fn test_named_file_content_range_headers() {
let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); let srv = test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=10-20")) .header(header::RANGE, "bytes=10-20")
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -428,7 +403,7 @@ mod tests {
// Invalid range header // Invalid range header
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=10-5")) .header(header::RANGE, "bytes=10-5")
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -438,12 +413,12 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_length_headers() { async fn test_named_file_content_length_headers() {
let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); let srv = test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=10-20")) .header(header::RANGE, "bytes=10-20")
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -453,7 +428,7 @@ mod tests {
// Valid range header, starting from 0 // Valid range header, starting from 0
let response = srv let response = srv
.get("/tests/test.binary") .get("/tests/test.binary")
.insert_header((header::RANGE, "bytes=0-20")) .header(header::RANGE, "bytes=0-20")
.send() .send()
.await .await
.unwrap(); .unwrap();
@ -477,7 +452,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_head_content_length_headers() { async fn test_head_content_length_headers() {
let srv = actix_test::start(|| App::new().service(Files::new("/", "."))); let srv = test::start(|| App::new().service(Files::new("/", ".")));
let response = srv.head("/tests/test.binary").send().await.unwrap(); let response = srv.head("/tests/test.binary").send().await.unwrap();
@ -493,14 +468,14 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files_with_spaces() { 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")), App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
) )
.await; .await;
let request = TestRequest::get() let request = TestRequest::get()
.uri("/tests/test%20space.binary") .uri("/tests/test%20space.binary")
.to_request(); .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); assert_eq!(response.status(), StatusCode::OK);
let bytes = test::read_body(response).await; let bytes = test::read_body(response).await;
@ -510,28 +485,28 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_files_not_allowed() { 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() let req = TestRequest::default()
.uri("/Cargo.toml") .uri("/Cargo.toml")
.method(Method::POST) .method(Method::POST)
.to_request(); .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); 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() let req = TestRequest::default()
.method(Method::PUT) .method(Method::PUT)
.uri("/Cargo.toml") .uri("/Cargo.toml")
.to_request(); .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); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_files_guards() { 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())), App::new().service(Files::new("/", ".").use_guards(guard::Post())),
) )
.await; .await;
@ -541,13 +516,13 @@ mod tests {
.method(Method::POST) .method(Method::POST)
.to_request(); .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); assert_eq!(resp.status(), StatusCode::OK);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_encoding() { 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 { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open("Cargo.toml")
.unwrap() .unwrap()
@ -558,16 +533,16 @@ mod tests {
let request = TestRequest::get() let request = TestRequest::get()
.uri("/") .uri("/")
.insert_header((header::ACCEPT_ENCODING, "gzip")) .header(header::ACCEPT_ENCODING, "gzip")
.to_request(); .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.status(), StatusCode::OK);
assert!(!res.headers().contains_key(header::CONTENT_ENCODING)); assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_encoding_gzip() { 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 { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open("Cargo.toml")
.unwrap() .unwrap()
@ -578,9 +553,9 @@ mod tests {
let request = TestRequest::get() let request = TestRequest::get()
.uri("/") .uri("/")
.insert_header((header::ACCEPT_ENCODING, "gzip")) .header(header::ACCEPT_ENCODING, "gzip")
.to_request(); .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.status(), StatusCode::OK);
assert_eq!( assert_eq!(
res.headers() res.headers()
@ -602,25 +577,27 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files() { async fn test_static_files() {
let srv = let mut srv = test::init_service(
test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) App::new().service(Files::new("/", ".").show_files_listing()),
.await; )
.await;
let req = TestRequest::with_uri("/missing").to_request(); 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); 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 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); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
let srv = let mut srv = test::init_service(
test::init_service(App::new().service(Files::new("/", ".").show_files_listing())) App::new().service(Files::new("/", ".").show_files_listing()),
.await; )
.await;
let req = TestRequest::with_uri("/tests").to_request(); 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!( assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(), resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/html; charset=utf-8" "text/html; charset=utf-8"
@ -633,16 +610,16 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_redirect_to_slash_directory() { async fn test_redirect_to_slash_directory() {
// should not redirect if no index // 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()), App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
) )
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); 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); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// should redirect if index present // should redirect if index present
let srv = test::init_service( let mut srv = test::init_service(
App::new().service( App::new().service(
Files::new("/", ".") Files::new("/", ".")
.index_file("test.png") .index_file("test.png")
@ -651,28 +628,24 @@ mod tests {
) )
.await; .await;
let req = TestRequest::with_uri("/tests").to_request(); 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); assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong // should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request(); 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); assert_eq!(resp.status(), StatusCode::NOT_FOUND);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_static_files_bad_directory() { async fn test_static_files_bad_directory() {
let service = Files::new("/", "./missing").new_service(()).await.unwrap(); let _st: Files = Files::new("/", "missing");
let _st: Files = Files::new("/", "Cargo.toml");
let req = TestRequest::with_uri("/").to_srv_request();
let resp = test::call_service(&service, req).await;
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_file_missing() { async fn test_default_handler_file_missing() {
let st = Files::new("/", ".") let mut st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| { .default_handler(|req: ServiceRequest| {
ok(req.into_response(HttpResponse::Ok().body("default content"))) ok(req.into_response(HttpResponse::Ok().body("default content")))
}) })
@ -680,78 +653,124 @@ mod tests {
.await .await
.unwrap(); .unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request(); 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); assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await; let bytes = test::read_body(resp).await;
assert_eq!(bytes, web::Bytes::from_static(b"default content")); assert_eq!(bytes, web::Bytes::from_static(b"default content"));
} }
#[actix_rt::test] // #[actix_rt::test]
async fn test_serve_index_nested() { // async fn test_serve_index() {
let service = Files::new(".", ".") // let st = Files::new(".").index_file("test.binary");
.index_file("lib.rs") // let req = TestRequest::default().uri("/tests").finish();
.new_service(())
.await
.unwrap();
let req = TestRequest::default().uri("/src").to_srv_request(); // let resp = st.handle(&req).respond_to(&req).unwrap();
let resp = test::call_service(&service, req).await; // let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::OK);
// assert_eq!(
// resp.headers()
// .get(header::CONTENT_TYPE)
// .expect("content type"),
// "application/octet-stream"
// );
// assert_eq!(
// resp.headers()
// .get(header::CONTENT_DISPOSITION)
// .expect("content disposition"),
// "attachment; filename=\"test.binary\""
// );
assert_eq!(resp.status(), StatusCode::OK); // let req = TestRequest::default().uri("/tests/").finish();
assert_eq!( // let resp = st.handle(&req).respond_to(&req).unwrap();
resp.headers().get(header::CONTENT_TYPE).unwrap(), // let resp = resp.as_msg();
"text/x-rust" // assert_eq!(resp.status(), StatusCode::OK);
); // assert_eq!(
assert_eq!( // resp.headers().get(header::CONTENT_TYPE).unwrap(),
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(), // "application/octet-stream"
"inline; filename=\"lib.rs\"" // );
); // assert_eq!(
} // resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
// "attachment; filename=\"test.binary\""
// );
#[actix_rt::test] // // nonexistent index file
async fn integration_serve_index() { // let req = TestRequest::default().uri("/tests/unknown").finish();
let srv = test::init_service( // let resp = st.handle(&req).respond_to(&req).unwrap();
App::new().service(Files::new("test", ".").index_file("Cargo.toml")), // let resp = resp.as_msg();
) // assert_eq!(resp.status(), StatusCode::NOT_FOUND);
.await;
let req = TestRequest::get().uri("/test").to_request(); // let req = TestRequest::default().uri("/tests/unknown/").finish();
let res = test::call_service(&srv, req).await; // let resp = st.handle(&req).respond_to(&req).unwrap();
assert_eq!(res.status(), StatusCode::OK); // let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// }
let bytes = test::read_body(res).await; // #[actix_rt::test]
// async fn test_serve_index_nested() {
// let st = Files::new(".").index_file("mod.rs");
// let req = TestRequest::default().uri("/src/client").finish();
// let resp = st.handle(&req).respond_to(&req).unwrap();
// let resp = resp.as_msg();
// assert_eq!(resp.status(), StatusCode::OK);
// assert_eq!(
// resp.headers().get(header::CONTENT_TYPE).unwrap(),
// "text/x-rust"
// );
// assert_eq!(
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
// "inline; filename=\"mod.rs\""
// );
// }
let data = Bytes::from(fs::read("Cargo.toml").unwrap()); // #[actix_rt::test]
assert_eq!(bytes, data); // 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 request = srv.get().uri(srv.url("/test")).finish().unwrap();
let res = test::call_service(&srv, req).await; // let response = srv.execute(request.send()).unwrap();
assert_eq!(res.status(), StatusCode::OK); // 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 data = Bytes::from(fs::read("Cargo.toml").unwrap()); // let response = srv.execute(request.send()).unwrap();
assert_eq!(bytes, data); // 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);
// nonexistent index file // // nonexistent index file
let req = TestRequest::get().uri("/test/unknown").to_request(); // let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
let res = test::call_service(&srv, req).await; // let response = srv.execute(request.send()).unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND); // assert_eq!(response.status(), StatusCode::NOT_FOUND);
let req = TestRequest::get().uri("/test/unknown/").to_request(); // let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
let res = test::call_service(&srv, req).await; // let response = srv.execute(request.send()).unwrap();
assert_eq!(res.status(), StatusCode::NOT_FOUND); // assert_eq!(response.status(), StatusCode::NOT_FOUND);
} // }
#[actix_rt::test] // #[actix_rt::test]
async fn integration_percent_encoded() { // fn integration_percent_encoded() {
let srv = test::init_service( // let mut srv = test::TestServer::with_factory(|| {
App::new().service(Files::new("test", ".").index_file("Cargo.toml")), // App::new().handler(
) // "test",
.await; // Files::new(".").index_file("Cargo.toml"),
// )
// });
let req = TestRequest::get().uri("/test/%43argo.toml").to_request(); // let request = srv
let res = test::call_service(&srv, req).await; // .get()
assert_eq!(res.status(), StatusCode::OK); // .uri(srv.url("/test/%43argo.toml"))
} // .finish()
// .unwrap();
// let response = srv.execute(request.send()).unwrap();
// assert_eq!(response.status(), StatusCode::OK);
// }
} }

View File

@ -11,13 +11,15 @@ use actix_web::{
dev::{BodyEncoding, SizedStream}, dev::{BodyEncoding, SizedStream},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, self, Charset, ContentDisposition, DispositionParam, DispositionType,
ExtendedValue,
}, },
ContentEncoding, StatusCode, ContentEncoding, StatusCode,
}, },
HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use bitflags::bitflags; use bitflags::bitflags;
use futures_util::future::{ready, Ready};
use mime_guess::from_path; use mime_guess::from_path;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
@ -60,7 +62,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
@ -137,7 +139,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
@ -156,7 +158,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// # use std::io; /// # use std::io;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
@ -275,31 +277,37 @@ impl NamedFile {
} }
/// Creates an `HttpResponse` with file as a streaming body. /// 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 { if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
if self.flags.contains(Flags::PREFER_UTF8) { if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); 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 { } 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) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
res.insert_header(( res.header(
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
)); );
} }
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
res.encoding(current_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) { let etag = if self.flags.contains(Flags::ETAG) {
@ -324,7 +332,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into(); let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { 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, _ => false,
} }
} else { } else {
@ -343,7 +351,7 @@ impl NamedFile {
let t2: SystemTime = since.clone().into(); let t2: SystemTime = since.clone().into();
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) { 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, _ => false,
} }
} else { } else {
@ -354,16 +362,16 @@ impl NamedFile {
if self.flags.contains(Flags::PREFER_UTF8) { if self.flags.contains(Flags::PREFER_UTF8) {
let ct = equiv_utf8_text(self.content_type.clone()); 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 { } 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) { if self.flags.contains(Flags::CONTENT_DISPOSITION) {
resp.insert_header(( resp.header(
header::CONTENT_DISPOSITION, header::CONTENT_DISPOSITION,
self.content_disposition.to_string(), self.content_disposition.to_string(),
)); );
} }
// default compressing // default compressing
@ -372,14 +380,14 @@ impl NamedFile {
} }
if let Some(lm) = last_modified { 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 { 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 length = self.md.len();
let mut offset = 0; let mut offset = 0;
@ -392,32 +400,43 @@ impl NamedFile {
offset = ranges[0].start; offset = ranges[0].start;
resp.encoding(ContentEncoding::Identity); resp.encoding(ContentEncoding::Identity);
resp.insert_header(( resp.header(
header::CONTENT_RANGE, header::CONTENT_RANGE,
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()), format!(
)); "bytes {}-{}/{}",
offset,
offset + length - 1,
self.md.len()
),
);
} else { } else {
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length))); resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish(); return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
}; };
} else { } else {
return resp.status(StatusCode::BAD_REQUEST).finish(); return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
}; };
}; };
if precondition_failed { if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish(); return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
} else if not_modified { } 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() { if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT); resp.status(StatusCode::PARTIAL_CONTENT);
} }
resp.body(SizedStream::new(length, reader)) Ok(resp.body(SizedStream::new(length, reader)))
} }
} }
@ -476,7 +495,10 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
} }
impl Responder for NamedFile { impl Responder for NamedFile {
fn respond_to(self, req: &HttpRequest) -> HttpResponse { type Error = Error;
self.into_response(req) type Future = Ready<Result<HttpResponse, Error>>;
fn respond_to(self, req: &HttpRequest) -> Self::Future {
ready(self.into_response(req))
} }
} }

View File

@ -3,8 +3,8 @@ use std::{
str::FromStr, str::FromStr,
}; };
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest}; use actix_web::{dev::Payload, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;
@ -15,19 +15,12 @@ impl FromStr for PathBufWrap {
type Err = UriSegmentError; type Err = UriSegmentError;
fn from_str(path: &str) -> Result<Self, Self::Err> { 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(); let mut buf = PathBuf::new();
for segment in path.split('/') { for segment in path.split('/') {
if segment == ".." { if segment == ".." {
buf.pop(); buf.pop();
} else if !hidden_files && segment.starts_with('.') { } else if segment.starts_with('.') {
return Err(UriSegmentError::BadStart('.')); return Err(UriSegmentError::BadStart('.'));
} else if segment.starts_with('*') { } else if segment.starts_with('*') {
return Err(UriSegmentError::BadStart('*')); return Err(UriSegmentError::BadStart('*'));
@ -103,17 +96,4 @@ mod tests {
PathBuf::from_iter(vec!["seg2"]) 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. /// HTTP Range header representation.
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
pub struct HttpRange { pub struct HttpRange {
@ -10,26 +8,91 @@ pub struct HttpRange {
pub length: u64, pub length: u64,
} }
#[derive(Debug, Clone, Display, Error)] const PREFIX: &str = "bytes=";
#[display(fmt = "Parse HTTP Range failed")] const PREFIX_LEN: usize = 6;
pub struct ParseRangeErr(#[error(not(source))] ());
impl HttpRange { impl HttpRange {
/// Parses Range HTTP header string as per RFC 2616. /// Parses Range HTTP header string as per RFC 2616.
/// ///
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`). /// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
/// `size` is full size of response (file). /// `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>, ()> {
match http_range::HttpRange::parse(header, size) { if header.is_empty() {
Ok(ranges) => Ok(ranges return Ok(Vec::new());
.iter()
.map(|range| HttpRange {
start: range.start,
length: range.length,
})
.collect()),
Err(_) => Err(ParseRangeErr(())),
} }
if !header.starts_with(PREFIX) {
return Err(());
}
let size_sig = size as i64;
let mut no_overlap = false;
let all_ranges: Vec<Option<HttpRange>> = header[PREFIX_LEN..]
.split(',')
.map(|x| x.trim())
.filter(|x| !x.is_empty())
.map(|ra| {
let mut start_end_iter = ra.split('-');
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(|_| ())?;
if length > size_sig {
length = size_sig;
}
Ok(Some(HttpRange {
start: (size_sig - length) as u64,
length: length as u64,
}))
} else {
let start: i64 = start_str.parse().map_err(|_| ())?;
if start < 0 {
return Err(());
}
if start >= size_sig {
no_overlap = true;
return Ok(None);
}
let length = if end_str.is_empty() {
// 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(|_| ())?;
if start > end {
return Err(());
}
if end >= size_sig {
end = size_sig - 1;
}
end - start + 1
};
Ok(Some(HttpRange {
start: start as u64,
length: length as u64,
}))
}
})
.collect::<Result<_, _>>()?;
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
if no_overlap && ranges.is_empty() {
return Err(());
}
Ok(ranges)
} }
} }
@ -270,7 +333,8 @@ mod tests {
if expected.is_empty() { if expected.is_empty() {
continue; continue;
} else { } else {
panic!( assert!(
false,
"parse({}, {}) returned error {:?}", "parse({}, {}) returned error {:?}",
header, header,
size, size,
@ -282,24 +346,28 @@ mod tests {
let got = res.unwrap(); let got = res.unwrap();
if got.len() != expected.len() { if got.len() != expected.len() {
panic!( assert!(
false,
"len(parseRange({}, {})) = {}, want {}", "len(parseRange({}, {})) = {}, want {}",
header, header,
size, size,
got.len(), got.len(),
expected.len() expected.len()
); );
continue;
} }
for i in 0..expected.len() { for i in 0..expected.len() {
if got[i].start != expected[i].start { if got[i].start != expected[i].start {
panic!( assert!(
false,
"parseRange({}, {})[{}].start = {}, want {}", "parseRange({}, {})[{}].start = {}, want {}",
header, size, i, got[i].start, expected[i].start header, size, i, got[i].start, expected[i].start
) )
} }
if got[i].length != expected[i].length { if got[i].length != expected[i].length {
panic!( assert!(
false,
"parseRange({}, {})[{}].length = {}, want {}", "parseRange({}, {})[{}].length = {}, want {}",
header, size, i, got[i].length, expected[i].length header, size, i, got[i].length, expected[i].length
) )

View File

@ -1,7 +1,11 @@
use std::{fmt, io, path::PathBuf, rc::Rc}; use std::{
fmt, io,
path::PathBuf,
rc::Rc,
task::{Context, Poll},
};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::Error,
@ -9,11 +13,11 @@ use actix_web::{
http::{header, Method}, http::{header, Method},
HttpResponse, HttpResponse,
}; };
use futures_core::future::LocalBoxFuture; use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
use crate::{ use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
PathBufWrap, NamedFile, PathBufWrap,
}; };
/// Assembled file serving service. /// Assembled file serving service.
@ -27,21 +31,21 @@ pub struct FilesService {
pub(crate) mime_override: Option<Rc<MimeOverride>>, pub(crate) mime_override: Option<Rc<MimeOverride>>,
pub(crate) file_flags: named::Flags, pub(crate) file_flags: named::Flags,
pub(crate) guards: Option<Rc<dyn Guard>>, pub(crate) guards: Option<Rc<dyn Guard>>,
pub(crate) hidden_files: bool,
} }
impl FilesService { type FilesServiceFuture = Either<
fn handle_err( Ready<Result<ServiceResponse, Error>>,
&self, LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
err: io::Error, >;
req: ServiceRequest,
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> {
log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref default) = self.default { impl FilesService {
Box::pin(default.call(req)) fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
log::debug!("Failed to handle {}: {}", req.path(), e);
if let Some(ref mut default) = self.default {
Either::Right(default.call(req))
} else { } else {
Box::pin(ok(req.error_response(err))) 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 Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse, 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 { let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards // execute user defined guards
(**guard).check(req.head()) (**guard).check(req.head())
@ -69,23 +76,22 @@ impl Service<ServiceRequest> for FilesService {
}; };
if !is_method_valid { if !is_method_valid {
return Box::pin(ok(req.into_response( return Either::Left(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed() 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."), .body("Request did not meet this resource's requirements."),
))); )));
} }
let real_path = let real_path: PathBufWrap = match req.match_info().path().parse() {
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { Ok(item) => item,
Ok(item) => item, Err(e) => return Either::Left(ok(req.error_response(e))),
Err(e) => return Box::pin(ok(req.error_response(e))), };
};
// full file path // full file path
let path = match self.directory.join(&real_path).canonicalize() { let path = match self.directory.join(&real_path).canonicalize() {
Ok(path) => path, Ok(path) => path,
Err(err) => return Box::pin(self.handle_err(err, req)), Err(e) => return self.handle_err(e, req),
}; };
if path.is_dir() { if path.is_dir() {
@ -93,9 +99,9 @@ impl Service<ServiceRequest> for FilesService {
if self.redirect_to_slash && !req.path().ends_with('/') { if self.redirect_to_slash && !req.path().ends_with('/') {
let redirect_to = format!("{}/", req.path()); let redirect_to = format!("{}/", req.path());
return Box::pin(ok(req.into_response( return Either::Left(ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to)) .header(header::LOCATION, redirect_to)
.body("") .body("")
.into_body(), .into_body(),
))); )));
@ -113,10 +119,12 @@ impl Service<ServiceRequest> for FilesService {
named_file.flags = self.file_flags; named_file.flags = self.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); Either::Left(ok(match named_file.into_response(&req) {
Box::pin(ok(ServiceResponse::new(req, res))) Ok(item) => ServiceResponse::new(req, item),
Err(e) => ServiceResponse::from_err(e, req),
}))
} }
Err(err) => self.handle_err(err, req), Err(e) => self.handle_err(e, req),
} }
} else if self.show_index { } else if self.show_index {
let dir = Directory::new(self.directory.clone(), path); let dir = Directory::new(self.directory.clone(), path);
@ -124,12 +132,12 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req); let x = (self.renderer)(&dir, &req);
Box::pin(match x { match x {
Ok(resp) => ok(resp), Ok(resp) => Either::Left(ok(resp)),
Err(err) => ok(ServiceResponse::from_err(err, req)), Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}) }
} else { } else {
Box::pin(ok(ServiceResponse::from_err( Either::Left(ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
))) )))
@ -138,16 +146,21 @@ impl Service<ServiceRequest> for FilesService {
match NamedFile::open(path) { match NamedFile::open(path) {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = self.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_()); let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = self.file_flags; named_file.flags = self.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); match named_file.into_response(&req) {
Box::pin(ok(ServiceResponse::new(req, res))) Ok(item) => {
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
}
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
}
} }
Err(err) => self.handle_err(err, req), Err(e) => self.handle_err(e, req),
} }
} }
} }

View File

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

View File

@ -1,125 +1,18 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2020-xx-xx
## 3.0.0-beta.5 - 2021-04-02 ## 2.2.2 - 2022-01-21
### Added
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
* `client::ConnectionIo` trait alias [#2081]
### Changed ### Changed
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063] - Migrate to `brotli` crate. [ad7e3c06]
### Removed [ad7e3c06]: https://github.com/actix/actix-web/commit/ad7e3c06
* Common typed HTTP headers were moved to actix-web. [2094]
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
[#2063]: https://github.com/actix/actix-web/pull/2063
[#2081]: https://github.com/actix/actix-web/pull/2081
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2127]: https://github.com/actix/actix-web/pull/2127
## 3.0.0-beta.4 - 2021-03-08 ## 2.2.1 - 2021-08-09
### Changed ### Fixed
* Feature `cookies` is now optional and disabled by default. [#1981] - Potential HTTP request smuggling vulnerabilities. [RUSTSEC-2021-0081](https://github.com/rustsec/advisory-db/pull/977)
* `ws::hash_key` now returns array. [#2035]
* `ResponseBuilder::json` now takes `impl Serialize`. [#2052]
### Removed
* Re-export of `futures_channel::oneshot::Canceled` is removed from `error` mod. [#1994]
* `ResponseError` impl for `futures_channel::oneshot::Canceled` is removed. [#1994]
[#1981]: https://github.com/actix/actix-web/pull/1981
[#1994]: https://github.com/actix/actix-web/pull/1994
[#2035]: https://github.com/actix/actix-web/pull/2035
[#2052]: https://github.com/actix/actix-web/pull/2052
## 3.0.0-beta.3 - 2021-02-10
* No notable changes.
## 3.0.0-beta.2 - 2021-02-10
### 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]
### 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]
### 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
## 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.0 - 2020-11-25 ## 2.2.0 - 2020-11-25
@ -166,14 +59,15 @@
* Update actix-connect and actix-tls dependencies. * Update actix-connect and actix-tls dependencies.
## 2.0.0-beta.3 - 2020-08-14 ## [2.0.0-beta.3] - 2020-08-14
### Fixed ### Fixed
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626] * Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
[#1626]: https://github.com/actix/actix-web/pull/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 ### Fixed
* Potential UB in h1 decoder using uninitialized memory. [#1614] * Potential UB in h1 decoder using uninitialized memory. [#1614]
@ -184,8 +78,10 @@
[#1615]: https://github.com/actix/actix-web/pull/1615 [#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 ### Changed
* Migrate cookie handling to `cookie` crate. [#1558] * Migrate cookie handling to `cookie` crate. [#1558]
* Update `sha-1` to 0.9. [#1586] * Update `sha-1` to 0.9. [#1586]
* Fix leak in client pool. [#1580] * Fix leak in client pool. [#1580]
@ -195,30 +91,33 @@
[#1586]: https://github.com/actix/actix-web/pull/1586 [#1586]: https://github.com/actix/actix-web/pull/1586
[#1580]: https://github.com/actix/actix-web/pull/1580 [#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 ### Changed
* Bump minimum supported Rust version to 1.40 * Bump minimum supported Rust version to 1.40
* content_length function is removed, and you can set Content-Length by calling * content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439]
no_chunking function [#1439]
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a * `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
`u64` instead of a `usize`. `u64` instead of a `usize`.
* Update `base64` dependency to 0.12 * Update `base64` dependency to 0.12
### Fixed ### Fixed
* Support parsing of `SameSite=None` [#1503] * Support parsing of `SameSite=None` [#1503]
[#1439]: https://github.com/actix/actix-web/pull/1439 [#1439]: https://github.com/actix/actix-web/pull/1439
[#1503]: https://github.com/actix/actix-web/pull/1503 [#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 ### Fixed
* Correct spelling of ConnectError::Unresolved [#1487] * Correct spelling of ConnectError::Unresolved [#1487]
* Fix a mistake in the encoding of websocket continuation messages wherein * Fix a mistake in the encoding of websocket continuation messages wherein
Item::FirstText and Item::FirstBinary are each encoded as the other. Item::FirstText and Item::FirstBinary are each encoded as the other.
### Changed ### Changed
* Implement `std::error::Error` for our custom errors [#1422] * Implement `std::error::Error` for our custom errors [#1422]
* Remove `failure` support for `ResponseError` since that crate * Remove `failure` support for `ResponseError` since that crate
will be deprecated in the near future. will be deprecated in the near future.
@ -226,247 +125,338 @@
[#1422]: https://github.com/actix/actix-web/pull/1422 [#1422]: https://github.com/actix/actix-web/pull/1422
[#1487]: https://github.com/actix/actix-web/pull/1487 [#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 ### Changed
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395] * 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] * Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively
* client::Connector accepts initial_window_size and initial_connection_window_size to improve download speed for awc when downloading large objects. [#1394]
HTTP2 configuration. [#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] * 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 [#1394]: https://github.com/actix/actix-web/pull/1394
[#1395]: https://github.com/actix/actix-web/pull/1395 [#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 ### Changed
* Update the `time` dependency to 0.2.7. * Update the `time` dependency to 0.2.7.
* Moved actors messages support from actix crate, enabled with feature `actors`. * Moved actors messages support from actix crate, enabled with feature `actors`.
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of * Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next().
`&mut self` in the poll_next().
* MessageBody is not implemented for &'static [u8] anymore. * MessageBody is not implemented for &'static [u8] anymore.
### Fixed ### Fixed
* Allow `SameSite=None` cookies to be sent in a response. * Allow `SameSite=None` cookies to be sent in a response.
## [1.0.1] - 2019-12-20
## 1.0.1 - 2019-12-20
### Fixed ### Fixed
* Poll upgrade service's readiness from HTTP service handlers * Poll upgrade service's readiness from HTTP service handlers
* Replace brotli with brotli2 #1224 * Replace brotli with brotli2 #1224
## [1.0.0] - 2019-12-13
## 1.0.0 - 2019-12-13
### Added ### Added
* Add websockets continuation frame support * Add websockets continuation frame support
### Changed ### Changed
* Replace `flate2-xxx` features with `compress` * Replace `flate2-xxx` features with `compress`
## [1.0.0-alpha.5] - 2019-12-09
## 1.0.0-alpha.5 - 2019-12-09
### Fixed ### Fixed
* Check `Upgrade` service readiness before calling it * Check `Upgrade` service readiness before calling it
* Fix buffer remaining capacity calculation
* Fix buffer remaining capacity calcualtion
### Changed ### Changed
* Websockets: Ping and Pong should have binary data #1049 * 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 ### Added
* Add impl ResponseBuilder for Error * Add impl ResponseBuilder for Error
### Changed ### Changed
* Use rust based brotli compression library * Use rust based brotli compression library
## 1.0.0-alpha.3 - 2019-12-07 ## [1.0.0-alpha.3] - 2019-12-07
### Changed ### Changed
* Migrate to tokio 0.2 * Migrate to tokio 0.2
* Migrate to `std::future` * Migrate to `std::future`
## 0.2.11 - 2019-11-06 ## [0.2.11] - 2019-11-06
### Added ### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() * 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` * Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed ### 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 ### 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 ### Fixed
* h2 will use error response #1080 * h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009 * 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 ### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation * Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1 * Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1 * Update serde_urlencoded to 0.6.1
### Fixed ### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031) * 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 ### Added
* Add `rustls` support * Add `rustls` support
* Add `Clone` impl for `HeaderMap` * Add `Clone` impl for `HeaderMap`
### Fixed ### Fixed
* awc client panic #1016 * 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 ### Added
* Add support for downcasting response errors #986 * Add support for downcasting response errors #986
## 0.2.6 - 2019-07-17 ## [0.2.6] - 2019-07-17
### Changed ### Changed
* Replace `ClonableService` with local copy * Replace `ClonableService` with local copy
* Upgrade `rand` dependency version to 0.7 * Upgrade `rand` dependency version to 0.7
## 0.2.5 - 2019-06-28 ## [0.2.5] - 2019-06-28
### Added ### Added
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946 * Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
### Changed ### Changed
* Use `encoding_rs` crate instead of unmaintained `encoding` crate * Use `encoding_rs` crate instead of unmaintained `encoding` crate
* Add `Copy` and `Clone` impls for `ws::Codec` * Add `Copy` and `Clone` impls for `ws::Codec`
## 0.2.4 - 2019-06-16 ## [0.2.4] - 2019-06-16
### Fixed ### Fixed
* Do not compress NoContent (204) responses #918 * Do not compress NoContent (204) responses #918
## 0.2.3 - 2019-06-02 ## [0.2.3] - 2019-06-02
### Added ### Added
* Debug impl for ResponseBuilder * Debug impl for ResponseBuilder
* From SizedStream and BodyStream for Body * From SizedStream and BodyStream for Body
### Changed ### Changed
* SizedStream uses u64 * SizedStream uses u64
## 0.2.2 - 2019-05-29 ## [0.2.2] - 2019-05-29
### Fixed ### Fixed
* Parse incoming stream before closing stream on disconnect #868 * Parse incoming stream before closing stream on disconnect #868
## 0.2.1 - 2019-05-25 ## [0.2.1] - 2019-05-25
### Fixed ### Fixed
* Handle socket read disconnect * Handle socket read disconnect
## 0.2.0 - 2019-05-12 ## [0.2.0] - 2019-05-12
### Changed ### Changed
* Update actix-service to 0.4 * Update actix-service to 0.4
* Expect and upgrade services accept `ServerConfig` config. * Expect and upgrade services accept `ServerConfig` config.
### Deleted ### Deleted
* `OneRequest` service * `OneRequest` service
## 0.1.5 - 2019-05-04 ## [0.1.5] - 2019-05-04
### Fixed ### Fixed
* Clean up response extensions in response pool #817 * Clean up response extensions in response pool #817
## 0.1.4 - 2019-04-24 ## [0.1.4] - 2019-04-24
### Added ### Added
* Allow to render h1 request headers in `Camel-Case` * Allow to render h1 request headers in `Camel-Case`
### Fixed ### Fixed
* Read until eof for http/1.0 responses #771 * Read until eof for http/1.0 responses #771
## 0.1.3 - 2019-04-23 ## [0.1.3] - 2019-04-23
### Fixed ### Fixed
* Fix http client pool management * Fix http client pool management
* Fix http client wait queue management #794 * Fix http client wait queue management #794
## 0.1.2 - 2019-04-23 ## [0.1.2] - 2019-04-23
### Fixed ### Fixed
* Fix BorrowMutError panic in client connector #793 * Fix BorrowMutError panic in client connector #793
## 0.1.1 - 2019-04-19 ## [0.1.1] - 2019-04-19
### Changed ### Changed
* Cookie::max_age() accepts value in seconds * Cookie::max_age() accepts value in seconds
* Cookie::max_age_time() accepts value in time::Duration * Cookie::max_age_time() accepts value in time::Duration
* Allow to specify server address for client connector * Allow to specify server address for client connector
## 0.1.0 - 2019-04-16 ## [0.1.0] - 2019-04-16
### Added ### Added
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr` * Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
### Changed ### Changed
* `actix_http::encoding` always available * `actix_http::encoding` always available
* use trust-dns-resolver 0.11.0 * use trust-dns-resolver 0.11.0
## 0.1.0-alpha.5 - 2019-04-12 ## [0.1.0-alpha.5] - 2019-04-12
### Added ### Added
* Allow to use custom service for upgrade requests * Allow to use custom service for upgrade requests
* Added `h1::SendResponse` future. * Added `h1::SendResponse` future.
### Changed ### Changed
* MessageBody::length() renamed to MessageBody::size() for consistency * MessageBody::length() renamed to MessageBody::size() for consistency
* ws handshake verification functions take RequestHead instead of Request * 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 ### Added
* Allow to use custom `Expect` handler * Allow to use custom `Expect` handler
* Add minimal `std::error::Error` impl for `Error` * Add minimal `std::error::Error` impl for `Error`
### Changed ### Changed
* Export IntoHeaderValue * Export IntoHeaderValue
* Render error and return as response body * Render error and return as response body
* Use thread pool for response body compression
* Use thread pool for response body comression
### Deleted ### Deleted
* Removed PayloadBuffer * Removed PayloadBuffer
## 0.1.0-alpha.3 - 2019-04-02 ## [0.1.0-alpha.3] - 2019-04-02
### Added ### Added
* Warn when an unsealed private cookie isn't valid UTF-8 * Warn when an unsealed private cookie isn't valid UTF-8
### Fixed ### Fixed
* Rust 1.31.0 compatibility * Rust 1.31.0 compatibility
* Preallocate read buffer for h1 codec * Preallocate read buffer for h1 codec
* Detect socket disconnection during protocol selection * Detect socket disconnection during protocol selection
## 0.1.0-alpha.2 - 2019-03-29 ## [0.1.0-alpha.2] - 2019-03-29
### Added ### Added
* Added ws::Message::Nop, no-op websockets message * Added ws::Message::Nop, no-op websockets message
### Changed ### 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 * Initial impl

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.5" version = "2.2.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
readme = "README.md" readme = "README.md"
@ -15,8 +15,7 @@ license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with features = ["openssl", "rustls", "compress", "secure-cookies", "actors"]
features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -26,85 +25,79 @@ path = "src/lib.rs"
default = [] default = []
# openssl # openssl
openssl = ["actix-tls/openssl"] openssl = ["actix-tls/openssl", "actix-connect/openssl"]
# rustls support # rustls support
rustls = ["actix-tls/rustls"] rustls = ["actix-tls/rustls", "actix-connect/rustls"]
# enable compression support # enable compressison support
compress = ["flate2", "brotli2"] compress = ["flate2", "brotli"]
# support for cookies
cookies = ["cookie"]
# support for secure cookies # support for secure cookies
secure-cookies = ["cookies", "cookie/secure"] secure-cookies = ["cookie/secure"]
# trust-dns as client dns resolver # support for actix Actor messages
trust-dns = ["trust-dns-resolver"] actors = ["actix"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.4" actix-service = "1.0.6"
actix-codec = "0.4.0-beta.1" actix-codec = "0.3.0"
actix-utils = "3.0.0-beta.4" actix-connect = "2.0.0"
actix-rt = "2.2" actix-utils = "2.0.0"
actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] } 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 }
ahash = "0.7"
base64 = "0.13" base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
bytes = "1" bytes = "0.5.3"
bytestring = "1" cookie = { version = "0.14.1", features = ["percent-encode"] }
cookie = { version = "0.14.1", features = ["percent-encode"], optional = true } copyless = "0.1.4"
derive_more = "0.99.5" derive_more = "0.99.2"
either = "1.5.3"
encoding_rs = "0.8" encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-channel = { version = "0.3.5", default-features = false }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } futures-core = { version = "0.3.5", default-features = false }
h2 = "0.3.1" futures-util = { version = "0.3.5", default-features = false }
http = "0.2.2" fxhash = "0.2.1"
h2 = "0.2.1"
http = "0.2.0"
httparse = "1.3" httparse = "1.3"
indexmap = "1.3"
itoa = "0.4" itoa = "0.4"
lazy_static = "1.4"
language-tags = "0.2" language-tags = "0.2"
local-channel = "0.1"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2" rand = "0.7"
rand = "0.8"
regex = "1.3" regex = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7"
sha-1 = "0.9" sha-1 = "0.9"
smallvec = "1.6" slab = "0.4"
time = { version = "0.2.23", default-features = false, features = ["std"] } serde_urlencoded = "0.7"
tokio = { version = "1.2", features = ["sync"] } time = { version = "0.2.7", default-features = false, features = ["std"] }
# compression # compression
brotli2 = { version="0.3.2", optional = true } brotli = { version = "3.3.3", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.3" actix-server = "1.0.1"
actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] } actix-connect = { version = "2.0.0", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } actix-http-test = { version = "2.0.0", features = ["openssl"] }
actix-tls = { version = "2.0.0", features = ["openssl"] }
criterion = "0.3" criterion = "0.3"
env_logger = "0.8" env_logger = "0.7"
rcgen = "0.8"
serde_derive = "1.0" serde_derive = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } open-ssl = { version="0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } rust-tls = { version="0.18", package = "rustls" }
[[example]]
name = "ws"
required-features = ["rustls"]
[[bench]] [[bench]]
name = "write-camel-case" name = "content-length"
harness = false harness = false
[[bench]] [[bench]]

View File

@ -3,19 +3,16 @@
> HTTP primitives for the Actix ecosystem. > HTTP primitives for the Actix ecosystem.
[![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http) [![crates.io](https://img.shields.io/crates/v/actix-http?label=latest)](https://crates.io/crates/actix-http)
[![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.5)](https://docs.rs/actix-http/3.0.0-beta.5) [![Documentation](https://docs.rs/actix-http/badge.svg?version=2.2.2)](https://docs.rs/actix-http/2.2.2)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) ![Apache 2.0 or MIT licensed](https://img.shields.io/crates/l/actix-http)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) [![Dependency Status](https://deps.rs/crate/actix-http/2.2.2/status.svg)](https://deps.rs/crate/actix-http/2.2.2)
<br /> [![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)
[![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.5/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.5)
[![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 & Resources ## Documentation & Resources
- [API Documentation](https://docs.rs/actix-http) - [API Documentation](https://docs.rs/actix-http)
- [Chat on Gitter](https://gitter.im/actix/actix-web) - [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 ## 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[5] = b'0';
buf[7] = b'9'; buf[7] = b'9';
} }
_ => {} _ => (),
} }
let mut curr: isize = 12; 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

@ -3,7 +3,7 @@ use std::{env, io};
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt as _; use futures_util::StreamExt;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info; use log::info;
@ -26,10 +26,7 @@ async fn main() -> io::Result<()> {
info!("request body: {:?}", body); info!("request body: {:?}", body);
Ok::<_, Error>( Ok::<_, Error>(
Response::Ok() Response::Ok()
.insert_header(( .header("x-head", HeaderValue::from_static("dummy value!"))
"x-head",
HeaderValue::from_static("dummy value!"),
))
.body(body), .body(body),
) )
}) })

View File

@ -4,7 +4,7 @@ use actix_http::http::HeaderValue;
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt as _; use futures_util::StreamExt;
use log::info; use log::info;
async fn handle_request(mut req: Request) -> Result<Response, Error> { async fn handle_request(mut req: Request) -> Result<Response, Error> {
@ -15,7 +15,7 @@ async fn handle_request(mut req: Request) -> Result<Response, Error> {
info!("request body: {:?}", body); info!("request body: {:?}", body);
Ok(Response::Ok() Ok(Response::Ok()
.insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .header("x-head", HeaderValue::from_static("dummy value!"))
.body(body)) .body(body))
} }

View File

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

View File

@ -1,107 +0,0 @@
//! Sets up a WebSocket server over TCP and TLS.
//! Sends a heartbeat message every 4 seconds but does not respond to any incoming frames.
extern crate tls_rustls as rustls;
use std::{
env, io,
pin::Pin,
task::{Context, Poll},
time::Duration,
};
use actix_codec::Encoder;
use actix_http::{error::Error, ws, HttpService, Request, Response};
use actix_rt::time::{interval, Interval};
use actix_server::Server;
use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use futures_core::{ready, Stream};
#[actix_rt::main]
async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix=info,h2_ws=info");
env_logger::init();
Server::build()
.bind("tcp", ("127.0.0.1", 8080), || {
HttpService::build().h1(handler).tcp()
})?
.bind("tls", ("127.0.0.1", 8443), || {
HttpService::build().finish(handler).rustls(tls_config())
})?
.run()
.await
}
async fn handler(req: Request) -> Result<Response, Error> {
log::info!("handshaking");
let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2
log::info!("responding");
Ok(res.streaming(Heartbeat::new(ws::Codec::new())))
}
struct Heartbeat {
codec: ws::Codec,
interval: Interval,
}
impl Heartbeat {
fn new(codec: ws::Codec) -> Self {
Self {
codec,
interval: interval(Duration::from_secs(4)),
}
}
}
impl Stream for Heartbeat {
type Item = Result<Bytes, Error>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
log::trace!("poll");
ready!(self.as_mut().interval.poll_tick(cx));
let mut buffer = BytesMut::new();
self.as_mut()
.codec
.encode(
ws::Message::Text(ByteString::from_static("hello world")),
&mut buffer,
)
.unwrap();
Poll::Ready(Some(Ok(buffer.freeze())))
}
}
fn tls_config() -> rustls::ServerConfig {
use std::io::BufReader;
use rustls::{
internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig,
};
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_pem();
let mut config = ServerConfig::new(NoClientAuth::new());
let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).unwrap();
let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
config
}

723
actix-http/src/body.rs Normal file
View File

@ -0,0 +1,723 @@
use std::marker::PhantomData;
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{fmt, mem};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use futures_util::ready;
use pin_project::pin_project;
use crate::error::Error;
#[derive(Debug, PartialEq, Copy, Clone)]
/// Body size hint
pub enum BodySize {
None,
Empty,
Sized(u64),
Stream,
}
impl BodySize {
pub fn is_eof(&self) -> bool {
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
}
}
/// Type that provides this trait can be streamed to a peer.
pub trait MessageBody {
fn size(&self) -> BodySize;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>>;
downcast_get_type_id!();
}
downcast!(MessageBody);
impl MessageBody for () {
fn size(&self) -> BodySize {
BodySize::Empty
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Poll::Ready(None)
}
}
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx)
}
}
#[pin_project(project = ResponseBodyProj)]
pub enum ResponseBody<B> {
Body(#[pin] B),
Other(#[pin] Body),
}
impl ResponseBody<Body> {
pub fn into_body<B>(self) -> ResponseBody<B> {
match self {
ResponseBody::Body(b) => ResponseBody::Other(b),
ResponseBody::Other(b) => ResponseBody::Other(b),
}
}
}
impl<B> ResponseBody<B> {
pub fn take_body(&mut self) -> ResponseBody<B> {
std::mem::replace(self, ResponseBody::Other(Body::None))
}
}
impl<B: MessageBody> ResponseBody<B> {
pub fn as_ref(&self) -> Option<&B> {
if let ResponseBody::Body(ref b) = self {
Some(b)
} else {
None
}
}
}
impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn size(&self) -> BodySize {
match self {
ResponseBody::Body(ref body) => body.size(),
ResponseBody::Other(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => body.poll_next(cx),
}
}
}
impl<B: MessageBody> Stream for ResponseBody<B> {
type Item = Result<Bytes, Error>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
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.
None,
/// Zero sized response body. `Content-Length` header is set to `0`.
Empty,
/// Specific response body.
Bytes(Bytes),
/// Generic message body.
Message(Box<dyn MessageBody + Unpin>),
}
impl Body {
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body {
Body::Bytes(Bytes::copy_from_slice(s))
}
/// Create body from generic message body.
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
Body::Message(Box::new(body))
}
}
impl MessageBody for Body {
fn size(&self) -> BodySize {
match self {
Body::None => BodySize::None,
Body::Empty => BodySize::Empty,
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
Body::Message(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
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)
} else {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
BodyProj::Message(ref mut body) => Pin::new(body.as_mut()).poll_next(cx),
}
}
}
impl PartialEq for Body {
fn eq(&self, other: &Body) -> bool {
match *self {
Body::None => matches!(*other, Body::None),
Body::Empty => matches!(*other, Body::Empty),
Body::Bytes(ref b) => match *other {
Body::Bytes(ref b2) => b == b2,
_ => false,
},
Body::Message(_) => false,
}
}
}
impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Body::None => write!(f, "Body::None"),
Body::Empty => write!(f, "Body::Empty"),
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
Body::Message(_) => write!(f, "Body::Message(_)"),
}
}
}
impl From<&'static str> for Body {
fn from(s: &'static str) -> Body {
Body::Bytes(Bytes::from_static(s.as_ref()))
}
}
impl From<&'static [u8]> for Body {
fn from(s: &'static [u8]) -> Body {
Body::Bytes(Bytes::from_static(s))
}
}
impl From<Vec<u8>> for Body {
fn from(vec: Vec<u8>) -> Body {
Body::Bytes(Bytes::from(vec))
}
}
impl From<String> for Body {
fn from(s: String) -> Body {
s.into_bytes().into()
}
}
impl<'a> From<&'a String> for Body {
fn from(s: &'a String) -> Body {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Bytes> for Body {
fn from(s: Bytes) -> Body {
Body::Bytes(s)
}
}
impl From<BytesMut> for Body {
fn from(s: BytesMut) -> Body {
Body::Bytes(s.freeze())
}
}
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body
where
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
{
fn from(s: SizedStream<S>) -> Body {
Body::from_message(s)
}
}
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, E>) -> Body {
Body::from_message(s)
}
}
impl MessageBody for Bytes {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
}
}
}
impl MessageBody for BytesMut {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
}
}
}
impl MessageBody for &'static str {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from_static(
mem::take(self.get_mut()).as_ref(),
))))
}
}
}
impl MessageBody for Vec<u8> {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
}
}
}
impl MessageBody for String {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(
mem::take(self.get_mut()).into_bytes(),
))))
}
}
}
/// Type represent streaming body.
/// Response does not contain `content-length` header and appropriate transfer encoding is used.
#[pin_project]
pub struct BodyStream<S: Unpin, E> {
#[pin]
stream: S,
_t: PhantomData<E>,
}
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,
_t: PhantomData,
}
}
}
impl<S, E> MessageBody for BodyStream<S, E>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
fn size(&self) -> BodySize {
BodySize::Stream
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut stream = self.project().stream;
loop {
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)),
});
}
}
}
/// 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,
}
impl<S> SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream }
}
}
impl<S> MessageBody for SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64)
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
let mut stream: Pin<&mut S> = self.project().stream;
loop {
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,
});
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use futures_util::future::poll_fn;
use futures_util::pin_mut;
use futures_util::stream;
impl Body {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
Body::Bytes(ref bin) => &bin,
_ => panic!(),
}
}
}
impl ResponseBody<Body> {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
ResponseBody::Body(ref b) => b.get_ref(),
ResponseBody::Other(ref b) => b.get_ref(),
}
}
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!(
Body::from_slice(b"test".as_ref()).size(),
BodySize::Sized(4)
);
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
let sb = Bytes::from(&b"test"[..]);
pin_mut!(sb);
assert_eq!(sb.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test");
pin_mut!(test_vec);
assert_eq!(test_vec.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes() {
let b = Bytes::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin_mut!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin_mut!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test");
pin_mut!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_unit() {
assert_eq!(().size(), BodySize::Empty);
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await
.is_none());
}
#[actix_rt::test]
async fn test_box() {
let val = Box::new(());
pin_mut!(val);
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(
Body::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1"))
);
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
}
#[actix_rt::test]
async fn test_body_debug() {
assert!(format!("{:?}", Body::None).contains("Body::None"));
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
}
#[actix_rt::test]
async fn test_serde_json() {
use serde_json::json;
assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(),
BodySize::Sized(25)
);
}
mod body_stream {
use super::*;
//use futures::task::noop_waker;
//use futures::stream::once;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin_mut!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
/* Now it does not compile as it should
#[actix_rt::test]
async fn move_pinned_pointer() {
let (sender, receiver) = futures::channel::oneshot::channel();
let mut body_stream = Ok(BodyStream::new(once(async {
let x = Box::new(0i32);
let y = &x;
receiver.await.unwrap();
let _z = **y;
Ok::<_, ()>(Bytes::new())
})));
let waker = noop_waker();
let mut context = Context::from_waker(&waker);
pin_mut!(body_stream);
let _ = body_stream.as_mut().unwrap().poll_next(&mut context);
sender.send(()).unwrap();
let _ = std::mem::replace(&mut body_stream, Err([0; 32])).unwrap().poll_next(&mut context);
}*/
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin_mut!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
}
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
let resp_body: &mut dyn MessageBody = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

View File

@ -1,158 +0,0 @@
use std::{
fmt, mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, SizedStream};
/// Represents various types of HTTP message body.
pub enum Body {
/// Empty response. `Content-Length` header is not set.
None,
/// Zero sized response body. `Content-Length` header is set to `0`.
Empty,
/// Specific response body.
Bytes(Bytes),
/// Generic message body.
Message(Box<dyn MessageBody + Unpin>),
}
impl Body {
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Body {
Body::Bytes(Bytes::copy_from_slice(s))
}
/// Create body from generic message body.
pub fn from_message<B: MessageBody + Unpin + 'static>(body: B) -> Body {
Body::Message(Box::new(body))
}
}
impl MessageBody for Body {
fn size(&self) -> BodySize {
match self {
Body::None => BodySize::None,
Body::Empty => BodySize::Empty,
Body::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
Body::Message(ref body) => body.size(),
}
}
fn poll_next(
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) => {
let len = bin.len();
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
Body::Message(body) => Pin::new(&mut **body).poll_next(cx),
}
}
}
impl PartialEq for Body {
fn eq(&self, other: &Body) -> bool {
match *self {
Body::None => matches!(*other, Body::None),
Body::Empty => matches!(*other, Body::Empty),
Body::Bytes(ref b) => match *other {
Body::Bytes(ref b2) => b == b2,
_ => false,
},
Body::Message(_) => false,
}
}
}
impl fmt::Debug for Body {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
Body::None => write!(f, "Body::None"),
Body::Empty => write!(f, "Body::Empty"),
Body::Bytes(ref b) => write!(f, "Body::Bytes({:?})", b),
Body::Message(_) => write!(f, "Body::Message(_)"),
}
}
}
impl From<&'static str> for Body {
fn from(s: &'static str) -> Body {
Body::Bytes(Bytes::from_static(s.as_ref()))
}
}
impl From<&'static [u8]> for Body {
fn from(s: &'static [u8]) -> Body {
Body::Bytes(Bytes::from_static(s))
}
}
impl From<Vec<u8>> for Body {
fn from(vec: Vec<u8>) -> Body {
Body::Bytes(Bytes::from(vec))
}
}
impl From<String> for Body {
fn from(s: String) -> Body {
s.into_bytes().into()
}
}
impl<'a> From<&'a String> for Body {
fn from(s: &'a String) -> Body {
Body::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Bytes> for Body {
fn from(s: Bytes) -> Body {
Body::Bytes(s)
}
}
impl From<BytesMut> for Body {
fn from(s: BytesMut) -> Body {
Body::Bytes(s.freeze())
}
}
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body
where
S: Stream<Item = Result<Bytes, Error>> + Unpin + 'static,
{
fn from(s: SizedStream<S>) -> Body {
Body::from_message(s)
}
}
impl<S, E> From<BodyStream<S>> for Body
where
S: Stream<Item = Result<Bytes, E>> + Unpin + 'static,
E: Into<Error> + 'static,
{
fn from(s: BodyStream<S>) -> Body {
Body::from_message(s)
}
}

View File

@ -1,59 +0,0 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use crate::error::Error;
use super::{BodySize, MessageBody};
/// Streaming response wrapper.
///
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
pub struct BodyStream<S: Unpin> {
stream: S,
}
impl<S, E> BodyStream<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
pub fn new(stream: S) -> Self {
BodyStream { stream }
}
}
impl<S, E> MessageBody for BodyStream<S>
where
S: Stream<Item = Result<Bytes, E>> + Unpin,
E: Into<Error>,
{
fn size(&self) -> BodySize {
BodySize::Stream
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`BodyStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
loop {
let stream = &mut self.as_mut().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)),
};
return Poll::Ready(chunk);
}
}
}

View File

@ -1,142 +0,0 @@
//! [`MessageBody`] trait and foreign implementations.
use std::{
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use crate::error::Error;
use super::BodySize;
/// Type that implement this trait can be streamed to a peer.
pub trait MessageBody {
fn size(&self) -> BodySize;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>>;
downcast_get_type_id!();
}
downcast!(MessageBody);
impl MessageBody for () {
fn size(&self) -> BodySize {
BodySize::Empty
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Poll::Ready(None)
}
}
impl<T: MessageBody + Unpin> MessageBody for Box<T> {
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx)
}
}
impl MessageBody for Bytes {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()))))
}
}
}
impl MessageBody for BytesMut {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze())))
}
}
}
impl MessageBody for &'static str {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from_static(
mem::take(self.get_mut()).as_ref(),
))))
}
}
}
impl MessageBody for Vec<u8> {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut())))))
}
}
}
impl MessageBody for String {
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(Bytes::from(
mem::take(self.get_mut()).into_bytes(),
))))
}
}
}

View File

@ -1,253 +0,0 @@
//! Traits and structures to aid consuming and writing HTTP payloads.
#[allow(clippy::module_inception)]
mod body;
mod body_stream;
mod message_body;
mod response_body;
mod size;
mod sized_stream;
pub use self::body::Body;
pub use self::body_stream::BodyStream;
pub use self::message_body::MessageBody;
pub use self::response_body::ResponseBody;
pub use self::size::BodySize;
pub use self::sized_stream::SizedStream;
#[cfg(test)]
mod tests {
use std::pin::Pin;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_util::stream;
use super::*;
impl Body {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
Body::Bytes(ref bin) => &bin,
_ => panic!(),
}
}
}
impl ResponseBody<Body> {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
ResponseBody::Body(ref b) => b.get_ref(),
ResponseBody::Other(ref b) => b.get_ref(),
}
}
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!(
Body::from_slice(b"test".as_ref()).size(),
BodySize::Sized(4)
);
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
let sb = Bytes::from(&b"test"[..]);
pin!(sb);
assert_eq!(sb.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test");
pin!(test_vec);
assert_eq!(test_vec.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes() {
let b = Bytes::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_unit() {
assert_eq!(().size(), BodySize::Empty);
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await
.is_none());
}
#[actix_rt::test]
async fn test_box() {
let val = Box::new(());
pin!(val);
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(
Body::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1"))
);
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
}
#[actix_rt::test]
async fn test_body_debug() {
assert!(format!("{:?}", Body::None).contains("Body::None"));
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
}
#[actix_rt::test]
async fn test_serde_json() {
use serde_json::json;
assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(),
BodySize::Sized(25)
);
}
#[actix_rt::test]
async fn body_stream_skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
}
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
let resp_body: &mut dyn MessageBody = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

View File

@ -1,77 +0,0 @@
use std::{
mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::Stream;
use pin_project::pin_project;
use crate::error::Error;
use super::{Body, BodySize, MessageBody};
#[pin_project(project = ResponseBodyProj)]
pub enum ResponseBody<B> {
Body(#[pin] B),
Other(Body),
}
impl ResponseBody<Body> {
pub fn into_body<B>(self) -> ResponseBody<B> {
match self {
ResponseBody::Body(b) => ResponseBody::Other(b),
ResponseBody::Other(b) => ResponseBody::Other(b),
}
}
}
impl<B> ResponseBody<B> {
pub fn take_body(&mut self) -> ResponseBody<B> {
mem::replace(self, ResponseBody::Other(Body::None))
}
}
impl<B: MessageBody> ResponseBody<B> {
pub fn as_ref(&self) -> Option<&B> {
if let ResponseBody::Body(ref b) = self {
Some(b)
} else {
None
}
}
}
impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn size(&self) -> BodySize {
match self {
ResponseBody::Body(ref body) => body.size(),
ResponseBody::Other(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
}
}
impl<B: MessageBody> Stream for ResponseBody<B> {
type Item = Result<Bytes, Error>;
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
}
}

View File

@ -1,40 +0,0 @@
/// Body size hint.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BodySize {
/// Absence of body can be assumed from method or status code.
///
/// Will skip writing Content-Length header.
None,
/// Zero size body.
///
/// Will write `Content-Length: 0` header.
Empty,
/// Known size body.
///
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`.
Sized(u64),
/// Unknown size body.
///
/// Will not write Content-Length header. Can be used with chunked Transfer-Encoding.
Stream,
}
impl BodySize {
/// Returns true if size hint indicates no or empty body.
///
/// ```
/// # use actix_http::body::BodySize;
/// assert!(BodySize::None.is_eof());
/// assert!(BodySize::Empty.is_eof());
/// assert!(BodySize::Sized(0).is_eof());
///
/// assert!(!BodySize::Sized(64).is_eof());
/// assert!(!BodySize::Stream.is_eof());
/// ```
pub fn is_eof(&self) -> bool {
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0))
}
}

View File

@ -1,59 +0,0 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::{ready, Stream};
use crate::error::Error;
use super::{BodySize, MessageBody};
/// Known sized streaming response wrapper.
///
/// This body implementation should be used if total size of stream is known. Data get sent as is
/// without using transfer encoding.
pub struct SizedStream<S: Unpin> {
size: u64,
stream: S,
}
impl<S> SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream }
}
}
impl<S> MessageBody for SizedStream<S>
where
S: Stream<Item = Result<Bytes, Error>> + Unpin,
{
fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64)
}
/// Attempts to pull out the next value of the underlying [`Stream`].
///
/// Empty values are skipped to prevent [`SizedStream`]'s transmission being
/// ended on a zero-length chunk, but rather proceed until the underlying
/// [`Stream`] ends.
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> {
loop {
let stream = &mut self.as_mut().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val,
};
return Poll::Ready(chunk);
}
}
}

View File

@ -10,6 +10,7 @@ use crate::config::{KeepAlive, ServiceConfig};
use crate::error::Error; use crate::error::Error;
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler};
use crate::h2::H2Service; use crate::h2::H2Service;
use crate::helpers::{Data, DataFactory};
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::service::HttpService; 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 /// This type can be used to construct an instance of [`HttpService`] through a
/// builder-like pattern. /// 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, keep_alive: KeepAlive,
client_timeout: u64, client_timeout: u64,
client_disconnect: u64, client_disconnect: u64,
@ -27,16 +28,18 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
local_addr: Option<net::SocketAddr>, local_addr: Option<net::SocketAddr>,
expect: X, expect: X,
upgrade: Option<U>, 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>>>, 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 where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service>::Future: 'static,
{ {
/// Create instance of `ServiceConfigBuilder` /// Create instance of `ServiceConfigBuilder`
pub fn new() -> Self { pub fn new() -> Self {
@ -48,24 +51,27 @@ where
local_addr: None, local_addr: None,
expect: ExpectHandler, expect: ExpectHandler,
upgrade: None, upgrade: None,
on_connect: None,
on_connect_ext: None, on_connect_ext: None,
_phantom: PhantomData, _t: PhantomData,
} }
} }
} }
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U> impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service>::Future: 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
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::Error: fmt::Display,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service>::Future: 'static,
{ {
/// Set server keep-alive setting. /// Set server keep-alive setting.
/// ///
@ -121,10 +127,11 @@ where
/// request will be forwarded to main service. /// request will be forwarded to main service.
pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U> pub fn expect<F, X1>(self, expect: F) -> HttpServiceBuilder<T, S, X1, U>
where where
F: IntoServiceFactory<X1, Request>, F: IntoServiceFactory<X1>,
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Config = (), Request = Request, Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Error>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
<X1::Service as Service>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -134,8 +141,9 @@ where
local_addr: self.local_addr, local_addr: self.local_addr,
expect: expect.into_factory(), expect: expect.into_factory(),
upgrade: self.upgrade, upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext, on_connect_ext: self.on_connect_ext,
_phantom: PhantomData, _t: PhantomData,
} }
} }
@ -145,10 +153,15 @@ where
/// and this service get called with original request and framed object. /// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1> pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where where
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>, F: IntoServiceFactory<U1>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U1: ServiceFactory<
Config = (),
Request = (Request, Framed<T, Codec>),
Response = (),
>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
<U1::Service as Service>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -158,11 +171,26 @@ where
local_addr: self.local_addr, local_addr: self.local_addr,
expect: self.expect, expect: self.expect,
upgrade: Some(upgrade.into_factory()), upgrade: Some(upgrade.into_factory()),
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext, 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. /// Sets the callback to be run on connection establishment.
/// ///
/// Has mutable access to a data container that will be merged into request extensions. /// Has mutable access to a data container that will be merged into request extensions.
@ -180,7 +208,7 @@ where
pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U> pub fn h1<F, B>(self, service: F) -> H1Service<T, S, B, X, U>
where where
B: MessageBody, B: MessageBody,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S>,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
@ -196,6 +224,7 @@ where
H1Service::with_config(cfg, service.into_factory()) H1Service::with_config(cfg, service.into_factory())
.expect(self.expect) .expect(self.expect)
.upgrade(self.upgrade) .upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext) .on_connect_ext(self.on_connect_ext)
} }
@ -203,10 +232,11 @@ where
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
B: MessageBody + 'static, B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S>,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@ -217,6 +247,7 @@ where
); );
H2Service::with_config(cfg, service.into_factory()) H2Service::with_config(cfg, service.into_factory())
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext) .on_connect_ext(self.on_connect_ext)
} }
@ -224,10 +255,11 @@ where
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U> pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where where
B: MessageBody + 'static, B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S>,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service>::Future: 'static,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@ -240,6 +272,7 @@ where
HttpService::with_config(cfg, service.into_factory()) HttpService::with_config(cfg, service.into_factory())
.expect(self.expect) .expect(self.expect)
.upgrade(self.upgrade) .upgrade(self.upgrade)
.on_connect(self.on_connect)
.on_connect_ext(self.on_connect_ext) .on_connect_ext(self.on_connect_ext)
} }
} }

View File

@ -1,35 +1,31 @@
use std::net::IpAddr;
use std::time::Duration; use std::time::Duration;
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB // These values are taken from hyper/src/proto/h2/client.rs
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
/// Connector configuration /// Connector configuration
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ConnectorConfig { pub(crate) struct ConnectorConfig {
pub(crate) timeout: Duration, pub(crate) timeout: Duration,
pub(crate) handshake_timeout: Duration,
pub(crate) conn_lifetime: Duration, pub(crate) conn_lifetime: Duration,
pub(crate) conn_keep_alive: Duration, pub(crate) conn_keep_alive: Duration,
pub(crate) disconnect_timeout: Option<Duration>, pub(crate) disconnect_timeout: Option<Duration>,
pub(crate) limit: usize, pub(crate) limit: usize,
pub(crate) conn_window_size: u32, pub(crate) conn_window_size: u32,
pub(crate) stream_window_size: u32, pub(crate) stream_window_size: u32,
pub(crate) local_address: Option<IpAddr>,
} }
impl Default for ConnectorConfig { impl Default for ConnectorConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
timeout: Duration::from_secs(5), timeout: Duration::from_secs(1),
handshake_timeout: Duration::from_secs(5),
conn_lifetime: Duration::from_secs(75), conn_lifetime: Duration::from_secs(75),
conn_keep_alive: Duration::from_secs(15), conn_keep_alive: Duration::from_secs(15),
disconnect_timeout: Some(Duration::from_millis(3000)), disconnect_timeout: Some(Duration::from_millis(3000)),
limit: 100, limit: 100,
conn_window_size: DEFAULT_H2_CONN_WINDOW, conn_window_size: DEFAULT_H2_CONN_WINDOW,
stream_window_size: DEFAULT_H2_STREAM_WINDOW, stream_window_size: DEFAULT_H2_STREAM_WINDOW,
local_address: None,
} }
} }
} }

View File

@ -1,16 +1,13 @@
use std::{ use std::future::Future;
io, use std::pin::Pin;
ops::{Deref, DerefMut}, use std::task::{Context, Poll};
pin::Pin, use std::{fmt, io, mem, time};
task::{Context, Poll},
time,
};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::task::JoinHandle; use bytes::{Buf, Bytes};
use bytes::Bytes; use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready};
use futures_core::future::LocalBoxFuture;
use h2::client::SendRequest; use h2::client::SendRequest;
use pin_project::pin_project;
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::h1::ClientCodec; use crate::h1::ClientCodec;
@ -18,347 +15,253 @@ use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired; use super::pool::{Acquired, Protocol};
use super::{h1proto, h2proto}; use super::{h1proto, h2proto};
/// Trait alias for types impl [tokio::io::AsyncRead] and [tokio::io::AsyncWrite]. pub(crate) enum ConnectionType<Io> {
pub trait ConnectionIo: AsyncRead + AsyncWrite + Unpin + 'static {} H1(Io),
H2(SendRequest<Bytes>),
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> ConnectionIo for T {} pub trait Connection {
type Io: AsyncRead + AsyncWrite + Unpin;
type Future: Future<Output = Result<(ResponseHead, Payload), SendRequestError>>;
fn protocol(&self) -> Protocol;
/// Send request and body
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
self,
head: H,
body: B,
) -> Self::Future;
type TunnelFuture: Future<
Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
}
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
/// Close connection
fn close(self: Pin<&mut Self>);
/// Release connection to the connection pool
fn release(self: Pin<&mut Self>);
}
#[doc(hidden)]
/// HTTP client connection /// HTTP client connection
pub struct H1Connection<Io: ConnectionIo> { pub struct IoConnection<T> {
io: Option<Io>, io: Option<ConnectionType<T>>,
created: time::Instant, created: time::Instant,
acquired: Acquired<Io>, pool: Option<Acquired<T>>,
} }
impl<Io: ConnectionIo> H1Connection<Io> { impl<T> fmt::Debug for IoConnection<T>
/// close or release the connection to pool based on flag input where
pub(super) fn on_release(&mut self, keep_alive: bool) { T: fmt::Debug,
if keep_alive { {
self.release(); fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
} else { match self.io {
self.close(); Some(ConnectionType::H1(ref io)) => write!(f, "H1Connection({:?})", io),
Some(ConnectionType::H2(_)) => write!(f, "H2Connection"),
None => write!(f, "Connection(Empty)"),
} }
} }
/// Close connection
fn close(&mut self) {
let io = self.io.take().unwrap();
self.acquired.close(ConnectionInnerType::H1(io));
}
/// Release this connection to the connection pool
fn release(&mut self) {
let io = self.io.take().unwrap();
self.acquired
.release(ConnectionInnerType::H1(io), self.created);
}
fn io_pin_mut(self: Pin<&mut Self>) -> Pin<&mut Io> {
Pin::new(self.get_mut().io.as_mut().unwrap())
}
} }
impl<Io: ConnectionIo> AsyncRead for H1Connection<Io> { impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
fn poll_read( pub(crate) fn new(
self: Pin<&mut Self>, io: ConnectionType<T>,
cx: &mut Context<'_>, created: time::Instant,
buf: &mut ReadBuf<'_>, pool: Option<Acquired<T>>,
) -> Poll<io::Result<()>> {
self.io_pin_mut().poll_read(cx, buf)
}
}
impl<Io: ConnectionIo> AsyncWrite for H1Connection<Io> {
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
self.io_pin_mut().poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
self.io_pin_mut().poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
self.io_pin_mut().poll_shutdown(cx)
}
fn poll_write_vectored(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>],
) -> Poll<io::Result<usize>> {
self.io_pin_mut().poll_write_vectored(cx, bufs)
}
fn is_write_vectored(&self) -> bool {
self.io.as_ref().unwrap().is_write_vectored()
}
}
/// HTTP2 client connection
pub struct H2Connection<Io: ConnectionIo> {
io: Option<H2ConnectionInner>,
created: time::Instant,
acquired: Acquired<Io>,
}
impl<Io: ConnectionIo> Deref for H2Connection<Io> {
type Target = SendRequest<Bytes>;
fn deref(&self) -> &Self::Target {
&self.io.as_ref().unwrap().sender
}
}
impl<Io: ConnectionIo> DerefMut for H2Connection<Io> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.io.as_mut().unwrap().sender
}
}
impl<Io: ConnectionIo> H2Connection<Io> {
/// close or release the connection to pool based on flag input
pub(super) fn on_release(&mut self, close: bool) {
if close {
self.close();
} else {
self.release();
}
}
/// Close connection
fn close(&mut self) {
let io = self.io.take().unwrap();
self.acquired.close(ConnectionInnerType::H2(io));
}
/// Release this connection to the connection pool
fn release(&mut self) {
let io = self.io.take().unwrap();
self.acquired
.release(ConnectionInnerType::H2(io), self.created);
}
}
/// `H2ConnectionInner` has two parts: `SendRequest` and `Connection`.
///
/// `Connection` is spawned as an async task on runtime and `H2ConnectionInner` holds a handle
/// for this task. Therefore, it can wake up and quit the task when SendRequest is dropped.
pub(super) struct H2ConnectionInner {
handle: JoinHandle<()>,
sender: SendRequest<Bytes>,
}
impl H2ConnectionInner {
pub(super) fn new<Io: ConnectionIo>(
sender: SendRequest<Bytes>,
connection: h2::client::Connection<Io>,
) -> Self { ) -> Self {
let handle = actix_rt::spawn(async move { IoConnection {
let _ = connection.await; pool,
}); created,
io: Some(io),
}
}
Self { handle, sender } pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
(self.io.unwrap(), self.created)
} }
} }
/// Cancel spawned connection task on drop. impl<T> Connection for IoConnection<T>
impl Drop for H2ConnectionInner { where
fn drop(&mut self) { T: AsyncRead + AsyncWrite + Unpin + 'static,
if self {
.sender type Io = T;
.send_request(http::Request::new(()), true) type Future =
.is_err() LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
{
self.handle.abort(); fn protocol(&self) -> Protocol {
match self.io {
Some(ConnectionType::H1(_)) => Protocol::Http1,
Some(ConnectionType::H2(_)) => Protocol::Http2,
None => Protocol::Http1,
}
}
fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
mut self,
head: H,
body: B,
) -> Self::Future {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
h1proto::send_request(io, head.into(), body, self.created, self.pool)
.boxed_local()
}
ConnectionType::H2(io) => {
h2proto::send_request(io, head.into(), body, self.created, self.pool)
.boxed_local()
}
}
}
type TunnelFuture = Either<
LocalBoxFuture<
'static,
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
>,
Ready<Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>>,
>;
/// Send request, returns Response and Framed
fn open_tunnel<H: Into<RequestHeadType>>(mut self, head: H) -> Self::TunnelFuture {
match self.io.take().unwrap() {
ConnectionType::H1(io) => {
Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
}
ConnectionType::H2(io) => {
if let Some(mut pool) = self.pool.take() {
pool.release(IoConnection::new(
ConnectionType::H2(io),
self.created,
None,
));
}
Either::Right(err(SendRequestError::TunnelNotSupported))
}
} }
} }
} }
#[allow(dead_code)] #[allow(dead_code)]
/// Unified connection type cover Http1 Plain/Tls and Http2 protocols pub(crate) enum EitherConnection<A, B> {
pub enum Connection<A, B = Box<dyn ConnectionIo>> A(IoConnection<A>),
B(IoConnection<B>),
}
impl<A, B> Connection for EitherConnection<A, B>
where where
A: ConnectionIo, A: AsyncRead + AsyncWrite + Unpin + 'static,
B: ConnectionIo, B: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
Tcp(ConnectionType<A>), type Io = EitherIo<A, B>;
Tls(ConnectionType<B>), type Future =
} LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
/// Unified connection type cover Http1/2 protocols fn protocol(&self) -> Protocol {
pub enum ConnectionType<Io: ConnectionIo> { match self {
H1(H1Connection<Io>), EitherConnection::A(con) => con.protocol(),
H2(H2Connection<Io>), EitherConnection::B(con) => con.protocol(),
}
/// Helper type for storing connection types in pool.
pub(super) enum ConnectionInnerType<Io> {
H1(Io),
H2(H2ConnectionInner),
}
impl<Io: ConnectionIo> ConnectionType<Io> {
pub(super) fn from_pool(
inner: ConnectionInnerType<Io>,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
match inner {
ConnectionInnerType::H1(io) => Self::from_h1(io, created, acquired),
ConnectionInnerType::H2(io) => Self::from_h2(io, created, acquired),
} }
} }
pub(super) fn from_h1( fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>(
io: Io,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H1(H1Connection {
io: Some(io),
created,
acquired,
})
}
pub(super) fn from_h2(
io: H2ConnectionInner,
created: time::Instant,
acquired: Acquired<Io>,
) -> Self {
Self::H2(H2Connection {
io: Some(io),
created,
acquired,
})
}
}
impl<A, B> Connection<A, B>
where
A: ConnectionIo,
B: ConnectionIo,
{
/// Send a request through connection.
pub fn send_request<RB, H>(
self, self,
head: H, head: H,
body: RB, body: RB,
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>> ) -> Self::Future {
where match self {
RB: MessageBody + 'static, EitherConnection::A(con) => con.send_request(head, body),
H: Into<RequestHeadType> + 'static, EitherConnection::B(con) => con.send_request(head, body),
{ }
Box::pin(async move {
match self {
Connection::Tcp(ConnectionType::H1(conn)) => {
h1proto::send_request(conn, head.into(), body).await
}
Connection::Tls(ConnectionType::H1(conn)) => {
h1proto::send_request(conn, head.into(), body).await
}
Connection::Tls(ConnectionType::H2(conn)) => {
h2proto::send_request(conn, head.into(), body).await
}
_ => unreachable!(
"Plain Tcp connection can be used only in Http1 protocol"
),
}
})
} }
/// Send request, returns Response and Framed tunnel. type TunnelFuture = LocalBoxFuture<
pub fn open_tunnel<H: Into<RequestHeadType> + 'static>(
self,
head: H,
) -> LocalBoxFuture<
'static, 'static,
Result<(ResponseHead, Framed<Connection<A, B>, ClientCodec>), SendRequestError>, Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
> { >;
Box::pin(async move {
match self { /// Send request, returns Response and Framed
Connection::Tcp(ConnectionType::H1(ref _conn)) => { fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
let (head, framed) = h1proto::open_tunnel(self, head.into()).await?; match self {
Ok((head, framed)) EitherConnection::A(con) => con
} .open_tunnel(head)
Connection::Tls(ConnectionType::H1(ref _conn)) => { .map(|res| {
let (head, framed) = h1proto::open_tunnel(self, head.into()).await?; res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A)))
Ok((head, framed)) })
} .boxed_local(),
Connection::Tls(ConnectionType::H2(mut conn)) => { EitherConnection::B(con) => con
conn.release(); .open_tunnel(head)
Err(SendRequestError::TunnelNotSupported) .map(|res| {
} res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B)))
Connection::Tcp(ConnectionType::H2(_)) => { })
unreachable!( .boxed_local(),
"Plain Tcp connection can be used only in Http1 protocol" }
)
}
}
})
} }
} }
impl<A, B> AsyncRead for Connection<A, B> #[pin_project(project = EitherIoProj)]
pub enum EitherIo<A, B> {
A(#[pin] A),
B(#[pin] B),
}
impl<A, B> AsyncRead for EitherIo<A, B>
where where
A: ConnectionIo, A: AsyncRead,
B: ConnectionIo, B: AsyncRead,
{ {
fn poll_read( fn poll_read(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>, buf: &mut [u8],
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<usize>> {
match self.get_mut() { match self.project() {
Connection::Tcp(ConnectionType::H1(conn)) => { EitherIoProj::A(val) => val.poll_read(cx, buf),
Pin::new(conn).poll_read(cx, buf) EitherIoProj::B(val) => val.poll_read(cx, buf),
} }
Connection::Tls(ConnectionType::H1(conn)) => { }
Pin::new(conn).poll_read(cx, buf)
} unsafe fn prepare_uninitialized_buffer(
_ => unreachable!("H2Connection can not impl AsyncRead trait"), &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),
} }
} }
} }
const H2_UNREACHABLE_WRITE: &str = "H2Connection can not impl AsyncWrite trait"; impl<A, B> AsyncWrite for EitherIo<A, B>
impl<A, B> AsyncWrite for Connection<A, B>
where where
A: ConnectionIo, A: AsyncWrite,
B: ConnectionIo, B: AsyncWrite,
{ {
fn poll_write( fn poll_write(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
buf: &[u8], buf: &[u8],
) -> Poll<io::Result<usize>> { ) -> Poll<io::Result<usize>> {
match self.get_mut() { match self.project() {
Connection::Tcp(ConnectionType::H1(conn)) => { EitherIoProj::A(val) => val.poll_write(cx, buf),
Pin::new(conn).poll_write(cx, buf) EitherIoProj::B(val) => val.poll_write(cx, buf),
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_write(cx, buf)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> { fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<io::Result<()>> {
match self.get_mut() { match self.project() {
Connection::Tcp(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), EitherIoProj::A(val) => val.poll_flush(cx),
Connection::Tls(ConnectionType::H1(conn)) => Pin::new(conn).poll_flush(cx), EitherIoProj::B(val) => val.poll_flush(cx),
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
@ -366,109 +269,23 @@ where
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<io::Result<()>> { ) -> Poll<io::Result<()>> {
match self.get_mut() { match self.project() {
Connection::Tcp(ConnectionType::H1(conn)) => { EitherIoProj::A(val) => val.poll_shutdown(cx),
Pin::new(conn).poll_shutdown(cx) EitherIoProj::B(val) => val.poll_shutdown(cx),
}
Connection::Tls(ConnectionType::H1(conn)) => {
Pin::new(conn).poll_shutdown(cx)
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
fn poll_write_vectored( fn poll_write_buf<U: Buf>(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
bufs: &[io::IoSlice<'_>], buf: &mut U,
) -> Poll<io::Result<usize>> { ) -> Poll<Result<usize, io::Error>>
match self.get_mut() { where
Connection::Tcp(ConnectionType::H1(conn)) => { Self: Sized,
Pin::new(conn).poll_write_vectored(cx, bufs) {
} match self.project() {
Connection::Tls(ConnectionType::H1(conn)) => { EitherIoProj::A(val) => val.poll_write_buf(cx, buf),
Pin::new(conn).poll_write_vectored(cx, bufs) EitherIoProj::B(val) => val.poll_write_buf(cx, buf),
}
_ => unreachable!(H2_UNREACHABLE_WRITE),
}
}
fn is_write_vectored(&self) -> bool {
match *self {
Connection::Tcp(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
Connection::Tls(ConnectionType::H1(ref conn)) => conn.is_write_vectored(),
_ => unreachable!(H2_UNREACHABLE_WRITE),
} }
} }
} }
#[cfg(test)]
mod test {
use std::{
future::Future,
net,
pin::Pin,
task::{Context, Poll},
time::{Duration, Instant},
};
use actix_rt::{
net::TcpStream,
time::{interval, Interval},
};
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 = H2ConnectionInner::new(sender.clone(), connection);
assert!(sender.clone().ready().await.is_ok());
assert!(h2::client::SendRequest::clone(&conn.sender)
.ready()
.await
.is_ok());
drop(conn);
struct DropCheck {
sender: h2::client::SendRequest<Bytes>,
interval: Interval,
start_from: Instant,
}
impl Future for DropCheck {
type Output = ();
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match futures_core::ready!(this.sender.poll_ready(cx)) {
Ok(()) => {
if this.start_from.elapsed() > Duration::from_secs(10) {
panic!("connection should be gone and can not be ready");
} else {
let _ = this.interval.poll_tick(cx);
Poll::Pending
}
}
Err(_) => Poll::Ready(()),
}
}
}
DropCheck {
sender,
interval: interval(Duration::from_millis(100)),
start_from: Instant::now(),
}
.await;
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,9 +1,10 @@
use std::io; use std::io;
use actix_connect::resolver::ResolveError;
use derive_more::{Display, From}; use derive_more::{Display, From};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::SslError; use actix_connect::ssl::openssl::{HandshakeError, SslError};
use crate::error::{Error, ParseError, ResponseError}; use crate::error::{Error, ParseError, ResponseError};
use crate::http::{Error as HttpError, StatusCode}; use crate::http::{Error as HttpError, StatusCode};
@ -20,12 +21,17 @@ pub enum ConnectError {
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
SslError(SslError), SslError(SslError),
/// SSL Handshake error
#[cfg(feature = "openssl")]
#[display(fmt = "{}", _0)]
SslHandshakeError(String),
/// Failed to resolve the hostname /// Failed to resolve the hostname
#[display(fmt = "Failed resolving hostname: {}", _0)] #[display(fmt = "Failed resolving hostname: {}", _0)]
Resolver(Box<dyn std::error::Error>), Resolver(ResolveError),
/// No dns records /// No dns records
#[display(fmt = "No DNS records found for the input")] #[display(fmt = "No dns records found for the input")]
NoRecords, NoRecords,
/// Http2 error /// Http2 error
@ -51,30 +57,34 @@ pub enum ConnectError {
impl std::error::Error for ConnectError {} impl std::error::Error for ConnectError {}
impl From<actix_tls::connect::ConnectError> for ConnectError { impl From<actix_connect::ConnectError> for ConnectError {
fn from(err: actix_tls::connect::ConnectError) -> ConnectError { fn from(err: actix_connect::ConnectError) -> ConnectError {
match err { match err {
actix_tls::connect::ConnectError::Resolver(e) => ConnectError::Resolver(e), actix_connect::ConnectError::Resolver(e) => ConnectError::Resolver(e),
actix_tls::connect::ConnectError::NoRecords => ConnectError::NoRecords, actix_connect::ConnectError::NoRecords => ConnectError::NoRecords,
actix_tls::connect::ConnectError::InvalidInput => panic!(), actix_connect::ConnectError::InvalidInput => panic!(),
actix_tls::connect::ConnectError::Unresolved => ConnectError::Unresolved, actix_connect::ConnectError::Unresolved => ConnectError::Unresolved,
actix_tls::connect::ConnectError::Io(e) => ConnectError::Io(e), 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)] #[derive(Debug, Display, From)]
pub enum InvalidUrl { pub enum InvalidUrl {
#[display(fmt = "Missing URL scheme")] #[display(fmt = "Missing url scheme")]
MissingScheme, MissingScheme,
#[display(fmt = "Unknown url scheme")]
#[display(fmt = "Unknown URL scheme")]
UnknownScheme, UnknownScheme,
#[display(fmt = "Missing host name")] #[display(fmt = "Missing host name")]
MissingHost, MissingHost,
#[display(fmt = "Url parse error: {}", _0)]
#[display(fmt = "URL parse error: {}", _0)]
HttpError(http::Error), HttpError(http::Error),
} }
@ -86,33 +96,25 @@ pub enum SendRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl), Url(InvalidUrl),
/// Failed to connect to host /// Failed to connect to host
#[display(fmt = "Failed to connect to host: {}", _0)] #[display(fmt = "Failed to connect to host: {}", _0)]
Connect(ConnectError), Connect(ConnectError),
/// Error sending request /// Error sending request
Send(io::Error), Send(io::Error),
/// Error parsing response /// Error parsing response
Response(ParseError), Response(ParseError),
/// Http error /// Http error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
/// Http2 error /// Http2 error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
H2(h2::Error), H2(h2::Error),
/// Response took too long /// Response took too long
#[display(fmt = "Timeout while waiting for response")] #[display(fmt = "Timeout while waiting for response")]
Timeout, Timeout,
/// Tunnels are not supported for http2 connection
/// Tunnels are not supported for HTTP/2 connection
#[display(fmt = "Tunnels are not supported for http2 connection")] #[display(fmt = "Tunnels are not supported for http2 connection")]
TunnelNotSupported, TunnelNotSupported,
/// Error sending request body /// Error sending request body
Body(Error), Body(Error),
} }
@ -138,8 +140,7 @@ pub enum FreezeRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl), Url(InvalidUrl),
/// Http error
/// HTTP error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
} }

View File

@ -1,36 +1,36 @@
use std::{ use std::io::Write;
io::Write, use std::pin::Pin;
pin::Pin, use std::task::{Context, Poll};
task::{Context, Poll}, use std::{io, mem, time};
};
use actix_codec::Framed; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_utils::future::poll_fn; use bytes::buf::BufMutExt;
use bytes::buf::BufMut;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::{ready, Stream}; use futures_core::Stream;
use futures_util::SinkExt as _; use futures_util::future::poll_fn;
use futures_util::{pin_mut, SinkExt, StreamExt};
use crate::error::PayloadError; use crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::http::{ use crate::header::HeaderMap;
header::{HeaderMap, IntoHeaderValue, EXPECT, HOST}, use crate::http::header::{IntoHeaderValue, HOST};
StatusCode,
};
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::{Payload, PayloadStream};
use super::connection::{ConnectionIo, H1Connection}; use super::connection::{ConnectionLifetime, ConnectionType, IoConnection};
use super::error::{ConnectError, SendRequestError}; use super::error::{ConnectError, SendRequestError};
use super::pool::Acquired;
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
pub(crate) async fn send_request<Io, B>( pub(crate) async fn send_request<T, B>(
io: H1Connection<Io>, io: T,
mut head: RequestHeadType, mut head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
Io: ConnectionIo, T: AsyncRead + AsyncWrite + Unpin + 'static,
B: MessageBody, B: MessageBody,
{ {
// set request host header // set request host header
@ -40,19 +40,19 @@ where
if let Some(host) = head.as_ref().uri.host() { if let Some(host) = head.as_ref().uri.host() {
let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
match head.as_ref().uri.port_u16() { let _ = match head.as_ref().uri.port_u16() {
None | Some(80) | Some(443) => write!(wrt, "{}", host)?, None | Some(80) | Some(443) => write!(wrt, "{}", host),
Some(port) => write!(wrt, "{}:{}", host, port)?, 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 { Ok(value) => match head {
RequestHeadType::Owned(ref mut head) => { RequestHeadType::Owned(ref mut head) => {
head.headers.insert(HOST, value); head.headers.insert(HOST, value)
} }
RequestHeadType::Rc(_, ref mut extra_headers) => { RequestHeadType::Rc(_, ref mut extra_headers) => {
let headers = extra_headers.get_or_insert(HeaderMap::new()); 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), Err(e) => log::error!("Can not set HOST header {}", e),
@ -60,102 +60,74 @@ where
} }
} }
// create Framed and prepare sending request let io = H1Connection {
let mut framed = Framed::new(io, h1::ClientCodec::default()); created,
pool,
// Check EXPECT header and enable expect handle flag accordingly. io: Some(io),
//
// RFC: https://tools.ietf.org/html/rfc7231#section-5.1.1
let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {
let keep_alive = framed.codec_ref().keepalive();
framed.io_mut().on_release(keep_alive);
// TODO: use a new variant or a new type better describing error violate
// `Requirements for clients` session of above RFC
return Err(SendRequestError::Connect(ConnectError::Disconnected));
}
_ => true,
}
} else {
false
}; };
framed.send((head, body.size()).into()).await?; // create Framed and send request
let mut framed_inner = Framed::new(io, h1::ClientCodec::default());
framed_inner.send((head, body.size()).into()).await?;
let mut pin_framed = Pin::new(&mut framed); // send request body
match body.size() {
// special handle for EXPECT request. BodySize::None | BodySize::Empty | BodySize::Sized(0) => (),
let (do_send, mut res_head) = if is_expect { _ => send_body(body, Pin::new(&mut framed_inner)).await?,
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
// return response head in case status code is not continue
// and current head would be used as final response head.
(head.status == StatusCode::CONTINUE, Some(head))
} else {
(true, None)
}; };
if do_send { // read response and init read body
// send request body let res = Pin::new(&mut framed_inner).into_future().await;
match body.size() { let (head, framed) = if let (Some(result), framed) = res {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {} let item = result.map_err(SendRequestError::from)?;
_ => send_body(body, pin_framed.as_mut()).await?, (item, framed)
}; } else {
return Err(SendRequestError::from(ConnectError::Disconnected));
};
// read response and init read body match framed.codec_ref().message_type() {
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
.await
.ok_or(ConnectError::Disconnected)??;
res_head = Some(head);
}
let head = res_head.unwrap();
match pin_framed.codec_ref().message_type() {
h1::MessageType::None => { h1::MessageType::None => {
let keep_alive = pin_framed.codec_ref().keepalive(); let force_close = !framed.codec_ref().keepalive();
pin_framed.io_mut().on_release(keep_alive); release_connection(framed, force_close);
Ok((head, Payload::None)) Ok((head, Payload::None))
} }
_ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))), _ => {
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
Ok((head, pl.into()))
}
} }
} }
pub(crate) async fn open_tunnel<Io>( pub(crate) async fn open_tunnel<T>(
io: Io, io: T,
head: RequestHeadType, head: RequestHeadType,
) -> Result<(ResponseHead, Framed<Io, h1::ClientCodec>), SendRequestError> ) -> Result<(ResponseHead, Framed<T, h1::ClientCodec>), SendRequestError>
where where
Io: ConnectionIo, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
// create Framed and send request. // create Framed and send request
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
framed.send((head, BodySize::None).into()).await?; framed.send((head, BodySize::None).into()).await?;
// read response head. // read response
let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx)) if let (Some(result), framed) = framed.into_future().await {
.await let head = result.map_err(SendRequestError::from)?;
.ok_or(ConnectError::Disconnected)??; Ok((head, framed))
} else {
Ok((head, framed)) Err(SendRequestError::from(ConnectError::Disconnected))
}
} }
/// send request body to the peer /// send request body to the peer
pub(crate) async fn send_body<Io, B>( pub(crate) async fn send_body<T, B>(
body: B, body: B,
mut framed: Pin<&mut Framed<Io, h1::ClientCodec>>, mut framed: Pin<&mut Framed<T, h1::ClientCodec>>,
) -> Result<(), SendRequestError> ) -> Result<(), SendRequestError>
where where
Io: ConnectionIo, T: ConnectionLifetime + Unpin,
B: MessageBody, B: MessageBody,
{ {
actix_rt::pin!(body); pin_mut!(body);
let mut eof = false; let mut eof = false;
while !eof { while !eof {
@ -187,25 +159,108 @@ where
} }
} }
framed.get_mut().flush().await?; SinkExt::flush(Pin::into_inner(framed)).await?;
Ok(()) Ok(())
} }
#[pin_project::pin_project] #[doc(hidden)]
pub(crate) struct PlStream<Io: ConnectionIo> { /// HTTP client connection
#[pin] pub struct H1Connection<T> {
framed: Framed<H1Connection<Io>, h1::ClientPayloadCodec>, /// T should be `Unpin`
io: Option<T>,
created: time::Instant,
pool: Option<Acquired<T>>,
} }
impl<Io: ConnectionIo> PlStream<Io> { impl<T> ConnectionLifetime for H1Connection<T>
fn new(framed: Framed<H1Connection<Io>, h1::ClientCodec>) -> Self { where
let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); T: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close connection
fn close(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.close(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
PlStream { framed } /// Release this connection to the connection pool
fn release(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.release(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
} }
} }
impl<Io: ConnectionIo> Stream for PlStream<Io> { 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 [u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Pin::new(self.io.as_mut().unwrap()).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx)
}
}
#[pin_project::pin_project]
pub(crate) struct PlStream<Io> {
#[pin]
framed: Option<Framed<Io, h1::ClientPayloadCodec>>,
}
impl<Io: ConnectionLifetime> PlStream<Io> {
fn new(framed: Framed<Io, h1::ClientCodec>) -> Self {
let framed = framed.into_map_codec(|codec| codec.into_payload_codec());
PlStream {
framed: Some(framed),
}
}
}
impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
type Item = Result<Bytes, PayloadError>; type Item = Result<Bytes, PayloadError>;
fn poll_next( fn poll_next(
@ -214,14 +269,30 @@ impl<Io: ConnectionIo> Stream for PlStream<Io> {
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
let mut this = self.project(); let mut this = self.project();
match ready!(this.framed.as_mut().next_item(cx)?) { match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? {
Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))), Poll::Pending => Poll::Pending,
Some(None) => { Poll::Ready(Some(chunk)) => {
let keep_alive = this.framed.codec_ref().keepalive(); if let Some(chunk) = chunk {
this.framed.io_mut().on_release(keep_alive); Poll::Ready(Some(Ok(chunk)))
Poll::Ready(None) } else {
let framed = this.framed.as_mut().as_pin_mut().unwrap();
let force_close = !framed.codec_ref().keepalive();
release_connection(framed, force_close);
Poll::Ready(None)
}
} }
None => Poll::Ready(None), Poll::Ready(None) => Poll::Ready(None),
} }
} }
} }
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
where
T: ConnectionLifetime,
{
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
framed.io_pin().release()
} else {
framed.io_pin().close()
}
}

View File

@ -1,7 +1,11 @@
use std::convert::TryFrom;
use std::future::Future; use std::future::Future;
use std::time;
use actix_utils::future::poll_fn; use actix_codec::{AsyncRead, AsyncWrite};
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::poll_fn;
use futures_util::pin_mut;
use h2::{ use h2::{
client::{Builder, Connection, SendRequest}, client::{Builder, Connection, SendRequest},
SendStream, SendStream,
@ -15,20 +19,22 @@ use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{ConnectionIo, H2Connection}; use super::connection::{ConnectionType, IoConnection};
use super::error::SendRequestError; use super::error::SendRequestError;
use super::pool::Acquired;
pub(crate) async fn send_request<Io, B>( pub(crate) async fn send_request<T, B>(
mut io: H2Connection<Io>, mut io: SendRequest<Bytes>,
head: RequestHeadType, head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
Io: ConnectionIo, T: AsyncRead + AsyncWrite + Unpin + 'static,
B: MessageBody, B: MessageBody,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
let head_req = head.as_ref().method == Method::HEAD; let head_req = head.as_ref().method == Method::HEAD;
let length = body.size(); let length = body.size();
let eof = matches!( let eof = matches!(
@ -54,14 +60,10 @@ where
BodySize::Empty => req BodySize::Empty => req
.headers_mut() .headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")), .insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => { BodySize::Sized(len) => req.headers_mut().insert(
let mut buf = itoa::Buffer::new(); CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(),
req.headers_mut().insert( ),
CONTENT_LENGTH,
HeaderValue::from_str(buf.format(len)).unwrap(),
)
}
}; };
// Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate. // Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
@ -84,26 +86,23 @@ where
// copy headers // copy headers
for (key, value) in headers { for (key, value) in headers {
match *key { match *key {
// TODO: consider skipping other headers according to: CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue, CONTENT_LENGTH if skip_len => continue,
// DATE => has_date = true, // DATE => has_date = true,
_ => {} _ => (),
} }
req.headers_mut().append(key, value.clone()); req.headers_mut().append(key, value.clone());
} }
let res = poll_fn(|cx| io.poll_ready(cx)).await; let res = poll_fn(|cx| io.poll_ready(cx)).await;
if let Err(e) = res { if let Err(e) = res {
io.on_release(e.is_io()); release(io, pool, created, e.is_io());
return Err(SendRequestError::from(e)); return Err(SendRequestError::from(e));
} }
let resp = match io.send_request(req, eof) { let resp = match io.send_request(req, eof) {
Ok((fut, send)) => { Ok((fut, send)) => {
io.on_release(false); release(io, pool, created, false);
if !eof { if !eof {
send_body(body, send).await?; send_body(body, send).await?;
@ -111,7 +110,7 @@ where
fut.await.map_err(SendRequestError::from)? fut.await.map_err(SendRequestError::from)?
} }
Err(e) => { Err(e) => {
io.on_release(e.is_io()); release(io, pool, created, e.is_io());
return Err(e.into()); return Err(e.into());
} }
}; };
@ -130,7 +129,7 @@ async fn send_body<B: MessageBody>(
mut send: SendStream<Bytes>, mut send: SendStream<Bytes>,
) -> Result<(), SendRequestError> { ) -> Result<(), SendRequestError> {
let mut buf = None; let mut buf = None;
actix_rt::pin!(body); pin_mut!(body);
loop { loop {
if buf.is_none() { if buf.is_none() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await { match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
@ -172,10 +171,28 @@ async fn send_body<B: MessageBody>(
} }
} }
pub(crate) fn handshake<Io: ConnectionIo>( // release SendRequest object
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: SendRequest<Bytes>,
pool: Option<Acquired<T>>,
created: time::Instant,
close: bool,
) {
if let Some(mut pool) = pool {
if close {
pool.close(IoConnection::new(ConnectionType::H2(io), created, None));
} else {
pool.release(IoConnection::new(ConnectionType::H2(io), created, None));
}
}
}
pub(crate) fn handshake<Io>(
io: Io, io: Io,
config: &ConnectorConfig, config: &ConnectorConfig,
) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> ) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
let mut builder = Builder::new(); let mut builder = Builder::new();
builder builder

View File

@ -1,5 +1,4 @@
//! HTTP client. //! Http client api
use http::Uri; use http::Uri;
mod config; mod config;
@ -10,14 +9,10 @@ mod h1proto;
mod h2proto; mod h2proto;
mod pool; mod pool;
pub use actix_tls::connect::{ pub use self::connection::Connection;
Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection, pub use self::connector::Connector;
};
pub use self::connection::{Connection, ConnectionIo};
pub use self::connector::{Connector, ConnectorService};
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use crate::Protocol; pub use self::pool::Protocol;
#[derive(Clone)] #[derive(Clone)]
pub struct Connect { pub struct Connect {

File diff suppressed because it is too large Load Diff

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,14 +4,12 @@ use std::rc::Rc;
use std::time::Duration; use std::time::Duration;
use std::{fmt, net}; use std::{fmt, net};
use actix_rt::{ use actix_rt::time::{delay_for, delay_until, Delay, Instant};
task::JoinHandle,
time::{interval, sleep_until, Instant, Sleep},
};
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::{future, FutureExt};
use time::OffsetDateTime; 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; const DATE_VALUE_LENGTH: usize = 29;
#[derive(Debug, PartialEq, Clone, Copy)] #[derive(Debug, PartialEq, Clone, Copy)]
@ -51,7 +49,7 @@ struct Inner {
ka_enabled: bool, ka_enabled: bool,
secure: bool, secure: bool,
local_addr: Option<std::net::SocketAddr>, local_addr: Option<std::net::SocketAddr>,
date_service: DateService, timer: DateService,
} }
impl Clone for ServiceConfig { impl Clone for ServiceConfig {
@ -93,40 +91,42 @@ impl ServiceConfig {
client_disconnect, client_disconnect,
secure, secure,
local_addr, local_addr,
date_service: DateService::new(), timer: DateService::new(),
})) }))
} }
/// Returns true if connection is secure (HTTPS)
#[inline] #[inline]
/// Returns true if connection is secure(https)
pub fn secure(&self) -> bool { pub fn secure(&self) -> bool {
self.0.secure self.0.secure
} }
/// Returns the local address that this server is bound to.
#[inline] #[inline]
/// Returns the local address that this server is bound to.
pub fn local_addr(&self) -> Option<net::SocketAddr> { pub fn local_addr(&self) -> Option<net::SocketAddr> {
self.0.local_addr self.0.local_addr
} }
/// Keep alive duration if configured.
#[inline] #[inline]
/// Keep alive duration if configured.
pub fn keep_alive(&self) -> Option<Duration> { pub fn keep_alive(&self) -> Option<Duration> {
self.0.keep_alive self.0.keep_alive
} }
/// Return state of connection keep-alive functionality
#[inline] #[inline]
/// Return state of connection keep-alive functionality
pub fn keep_alive_enabled(&self) -> bool { pub fn keep_alive_enabled(&self) -> bool {
self.0.ka_enabled self.0.ka_enabled
} }
/// Client timeout for first request.
#[inline] #[inline]
pub fn client_timer(&self) -> Option<Sleep> { /// Client timeout for first request.
pub fn client_timer(&self) -> Option<Delay> {
let delay_time = self.0.client_timeout; let delay_time = self.0.client_timeout;
if delay_time != 0 { if delay_time != 0 {
Some(sleep_until(self.now() + Duration::from_millis(delay_time))) Some(delay_until(
self.0.timer.now() + Duration::from_millis(delay_time),
))
} else { } else {
None None
} }
@ -136,7 +136,7 @@ impl ServiceConfig {
pub fn client_timer_expire(&self) -> Option<Instant> { pub fn client_timer_expire(&self) -> Option<Instant> {
let delay = self.0.client_timeout; let delay = self.0.client_timeout;
if delay != 0 { if delay != 0 {
Some(self.now() + Duration::from_millis(delay)) Some(self.0.timer.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -146,7 +146,7 @@ impl ServiceConfig {
pub fn client_disconnect_timer(&self) -> Option<Instant> { pub fn client_disconnect_timer(&self) -> Option<Instant> {
let delay = self.0.client_disconnect; let delay = self.0.client_disconnect;
if delay != 0 { if delay != 0 {
Some(self.now() + Duration::from_millis(delay)) Some(self.0.timer.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -154,18 +154,26 @@ impl ServiceConfig {
#[inline] #[inline]
/// Return keep-alive timer delay is configured. /// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Sleep> { pub fn keep_alive_timer(&self) -> Option<Delay> {
self.keep_alive().map(|ka| sleep_until(self.now() + ka)) if let Some(ka) = self.0.keep_alive {
Some(delay_until(self.0.timer.now() + ka))
} else {
None
}
} }
/// Keep-alive expire time /// Keep-alive expire time
pub fn keep_alive_expire(&self) -> Option<Instant> { pub fn keep_alive_expire(&self) -> Option<Instant> {
self.keep_alive().map(|ka| self.now() + ka) if let Some(ka) = self.0.keep_alive {
Some(self.0.timer.now() + ka)
} else {
None
}
} }
#[inline] #[inline]
pub(crate) fn now(&self) -> Instant { pub(crate) fn now(&self) -> Instant {
self.0.date_service.now() self.0.timer.now()
} }
#[doc(hidden)] #[doc(hidden)]
@ -173,7 +181,7 @@ impl ServiceConfig {
let mut buf: [u8; 39] = [0; 39]; let mut buf: [u8; 39] = [0; 39];
buf[..6].copy_from_slice(b"date: "); buf[..6].copy_from_slice(b"date: ");
self.0 self.0
.date_service .timer
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes)); .set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
buf[35..].copy_from_slice(b"\r\n\r\n"); buf[35..].copy_from_slice(b"\r\n\r\n");
dst.extend_from_slice(&buf); dst.extend_from_slice(&buf);
@ -181,7 +189,7 @@ impl ServiceConfig {
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) { pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
self.0 self.0
.date_service .timer
.set_date(|date| dst.extend_from_slice(&date.bytes)); .set_date(|date| dst.extend_from_slice(&date.bytes));
} }
} }
@ -222,103 +230,57 @@ impl fmt::Write for Date {
} }
} }
/// Service for update Date and Instant periodically at 500 millis interval. #[derive(Clone)]
struct DateService { struct DateService(Rc<DateServiceInner>);
current: Rc<Cell<(Date, Instant)>>,
handle: JoinHandle<()>, struct DateServiceInner {
current: Cell<Option<(Date, Instant)>>,
} }
impl Drop for DateService { impl DateServiceInner {
fn drop(&mut self) { fn new() -> Self {
// stop the timer update async task on drop. DateServiceInner {
self.handle.abort(); current: Cell::new(None),
}
}
fn reset(&self) {
self.current.take();
}
fn update(&self) {
let now = Instant::now();
let date = Date::new();
self.current.set(Some((date, now)));
} }
} }
impl DateService { impl DateService {
fn new() -> Self { fn new() -> Self {
// shared date and timer for DateService and update async task. DateService(Rc::new(DateServiceInner::new()))
let current = Rc::new(Cell::new((Date::new(), Instant::now()))); }
let current_clone = Rc::clone(&current);
// spawn an async task sleep for 500 milli and update current date/timer in a loop.
// handle is used to stop the task on DateService drop.
let handle = actix_rt::spawn(async move {
#[cfg(test)]
let _notify = notify_on_drop::NotifyOnDrop::new();
let mut interval = interval(Duration::from_millis(500)); fn check_date(&self) {
loop { if self.0.current.get().is_none() {
let now = interval.tick().await; self.0.update();
let date = Date::new();
current_clone.set((date, now));
}
});
DateService { current, handle } // periodic date update
let s = self.clone();
actix_rt::spawn(delay_for(Duration::from_millis(500)).then(move |_| {
s.0.reset();
future::ready(())
}));
}
} }
fn now(&self) -> Instant { fn now(&self) -> Instant {
self.current.get().1 self.check_date();
self.0.current.get().unwrap().1
} }
fn set_date<F: FnMut(&Date)>(&self, mut f: F) { fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
f(&self.current.get().0); self.check_date();
} f(&self.0.current.get().unwrap().0);
}
// TODO: move to a util module for testing all spawn handle drop style tasks.
#[cfg(test)]
/// Test Module for checking the drop state of certain async tasks that are spawned
/// with `actix_rt::spawn`
///
/// The target task must explicitly generate `NotifyOnDrop` when spawn the task
mod notify_on_drop {
use std::cell::RefCell;
thread_local! {
static NOTIFY_DROPPED: RefCell<Option<bool>> = RefCell::new(None);
}
/// Check if the spawned task is dropped.
///
/// # Panic:
///
/// When there was no `NotifyOnDrop` instance on current thread
pub(crate) fn is_dropped() -> bool {
NOTIFY_DROPPED.with(|bool| {
bool.borrow()
.expect("No NotifyOnDrop existed on current thread")
})
}
pub(crate) struct NotifyOnDrop;
impl NotifyOnDrop {
/// # Panic:
///
/// When construct multiple instances on any given thread.
pub(crate) fn new() -> Self {
NOTIFY_DROPPED.with(|bool| {
let mut bool = bool.borrow_mut();
if bool.is_some() {
panic!("NotifyOnDrop existed on current thread");
} else {
*bool = Some(false);
}
});
NotifyOnDrop
}
}
impl Drop for NotifyOnDrop {
fn drop(&mut self) {
NOTIFY_DROPPED.with(|bool| {
if let Some(b) = bool.borrow_mut().as_mut() {
*b = true;
}
});
}
} }
} }
@ -326,53 +288,14 @@ mod notify_on_drop {
mod tests { mod tests {
use super::*; use super::*;
use actix_rt::task::yield_now; // Test modifying the date from within the closure
// passed to `set_date`
#[actix_rt::test] #[test]
async fn test_date_service_update() { fn test_evil_date() {
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None); let service = DateService::new();
// Make sure that `check_date` doesn't try to spawn a task
yield_now().await; service.0.update();
service.set_date(|_| service.0.reset());
let mut buf1 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf1);
let now1 = settings.now();
sleep_until(Instant::now() + Duration::from_secs(2)).await;
yield_now().await;
let now2 = settings.now();
let mut buf2 = BytesMut::with_capacity(DATE_VALUE_LENGTH + 10);
settings.set_date(&mut buf2);
assert_ne!(now1, now2);
assert_ne!(buf1, buf2);
drop(settings);
assert!(notify_on_drop::is_dropped());
}
#[actix_rt::test]
async fn test_date_service_drop() {
let service = Rc::new(DateService::new());
// yield so date service have a chance to register the spawned timer update task.
yield_now().await;
let clone1 = service.clone();
let clone2 = service.clone();
let clone3 = service.clone();
drop(clone1);
assert_eq!(false, notify_on_drop::is_dropped());
drop(clone2);
assert_eq!(false, notify_on_drop::is_dropped());
drop(clone3);
assert_eq!(false, notify_on_drop::is_dropped());
drop(service);
assert!(notify_on_drop::is_dropped());
} }
#[test] #[test]

View File

@ -1,31 +1,25 @@
//! Stream decoders. use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{ use actix_threadpool::{run, CpuFuture};
future::Future, use brotli::DecompressorWriter as BrotliDecoder;
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliDecoder;
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use crate::{ use super::Writer;
encoding::Writer, use crate::error::PayloadError;
error::{BlockingError, PayloadError}, use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
};
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; const INPLACE: usize = 2049;
pub struct Decoder<S> { pub struct Decoder<S> {
decoder: Option<ContentDecoder>, decoder: Option<ContentDecoder>,
stream: S, stream: S,
eof: bool, eof: bool,
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>, fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>,
} }
impl<S> Decoder<S> impl<S> Decoder<S>
@ -37,7 +31,7 @@ where
pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> { pub fn new(stream: S, encoding: ContentEncoding) -> Decoder<S> {
let decoder = match encoding { let decoder = match encoding {
ContentEncoding::Br => Some(ContentDecoder::Br(Box::new( ContentEncoding::Br => Some(ContentDecoder::Br(Box::new(
BrotliDecoder::new(Writer::new()), BrotliDecoder::new(Writer::new(), 8 * 1024),
))), ))),
ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new( ContentEncoding::Deflate => Some(ContentDecoder::Deflate(Box::new(
ZlibDecoder::new(Writer::new()), ZlibDecoder::new(Writer::new()),
@ -47,7 +41,6 @@ where
))), ))),
_ => None, _ => None,
}; };
Decoder { Decoder {
decoder, decoder,
stream, stream,
@ -60,11 +53,15 @@ where
#[inline] #[inline]
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> { pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
// check content-encoding // check content-encoding
let encoding = headers let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) {
.get(&CONTENT_ENCODING) if let Ok(enc) = enc.to_str() {
.and_then(|val| val.to_str().ok()) ContentEncoding::from(enc)
.map(ContentEncoding::from) } else {
.unwrap_or(ContentEncoding::Identity); ContentEncoding::Identity
}
} else {
ContentEncoding::Identity
};
Self::new(stream, encoding) Self::new(stream, encoding)
} }
@ -82,12 +79,12 @@ where
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
loop { loop {
if let Some(ref mut fut) = self.fut { if let Some(ref mut fut) = self.fut {
let (chunk, decoder) = let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
self.decoder = Some(decoder); self.decoder = Some(decoder);
self.fut.take(); self.fut.take();
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
@ -97,34 +94,29 @@ where
return Poll::Ready(None); return Poll::Ready(None);
} }
match ready!(Pin::new(&mut self.stream).poll_next(cx)) { match Pin::new(&mut self.stream).poll_next(cx) {
Some(Err(err)) => return Poll::Ready(Some(Err(err))), Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
Poll::Ready(Some(Ok(chunk))) => {
Some(Ok(chunk)) => {
if let Some(mut decoder) = self.decoder.take() { if let Some(mut decoder) = self.decoder.take() {
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE { if chunk.len() < INPLACE {
let chunk = decoder.feed_data(chunk)?; let chunk = decoder.feed_data(chunk)?;
self.decoder = Some(decoder); self.decoder = Some(decoder);
if let Some(chunk) = chunk { if let Some(chunk) = chunk {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} else { } else {
self.fut = Some(spawn_blocking(move || { self.fut = Some(run(move || {
let chunk = decoder.feed_data(chunk)?; let chunk = decoder.feed_data(chunk)?;
Ok((chunk, decoder)) Ok((chunk, decoder))
})); }));
} }
continue; continue;
} else { } else {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Poll::Ready(None) => {
None => {
self.eof = true; self.eof = true;
return if let Some(mut decoder) = self.decoder.take() { return if let Some(mut decoder) = self.decoder.take() {
match decoder.feed_eof() { match decoder.feed_eof() {
Ok(Some(res)) => Poll::Ready(Some(Ok(res))), Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
@ -135,8 +127,10 @@ where
Poll::Ready(None) Poll::Ready(None)
}; };
} }
Poll::Pending => break,
} }
} }
Poll::Pending
} }
} }
@ -152,7 +146,6 @@ impl ContentDecoder {
ContentDecoder::Br(ref mut decoder) => match decoder.flush() { ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
Ok(()) => { Ok(()) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -161,11 +154,9 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -174,7 +165,6 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() { ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
Ok(_) => { Ok(_) => {
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
@ -195,7 +185,6 @@ impl ContentDecoder {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -204,12 +193,10 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))
} else { } else {
@ -218,11 +205,9 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) { ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => { Ok(_) => {
decoder.flush()?; decoder.flush()?;
let b = decoder.get_mut().take(); let b = decoder.get_mut().take();
if !b.is_empty() { if !b.is_empty() {
Ok(Some(b)) Ok(Some(b))

View File

@ -1,32 +1,24 @@
//! Stream encoders. //! Stream encoder
use std::future::Future;
use std::io::{self, Write};
use std::pin::Pin;
use std::task::{Context, Poll};
use std::{ use actix_threadpool::{run, CpuFuture};
future::Future, use brotli::CompressorWriter as BrotliEncoder;
io::{self, Write as _},
pin::Pin,
task::{Context, Poll},
};
use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliEncoder;
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project::pin_project;
use crate::{ use crate::body::{Body, BodySize, MessageBody, ResponseBody};
body::{Body, BodySize, MessageBody, ResponseBody}, use crate::http::header::{ContentEncoding, CONTENT_ENCODING};
http::{ use crate::http::{HeaderValue, StatusCode};
header::{ContentEncoding, CONTENT_ENCODING}, use crate::{Error, ResponseHead};
HeaderValue, StatusCode,
},
Error, ResponseHead,
};
use super::Writer; use super::Writer;
use crate::error::BlockingError;
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; const INPLACE: usize = 1024;
#[pin_project] #[pin_project]
pub struct Encoder<B> { pub struct Encoder<B> {
@ -34,7 +26,7 @@ pub struct Encoder<B> {
#[pin] #[pin]
body: EncoderBody<B>, body: EncoderBody<B>,
encoder: Option<ContentEncoder>, encoder: Option<ContentEncoder>,
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>, fut: Option<CpuFuture<ContentEncoder, io::Error>>,
} }
impl<B: MessageBody> Encoder<B> { impl<B: MessageBody> Encoder<B> {
@ -78,7 +70,6 @@ impl<B: MessageBody> Encoder<B> {
}); });
} }
} }
ResponseBody::Body(Encoder { ResponseBody::Body(Encoder {
body, body,
eof: false, eof: false,
@ -144,35 +135,32 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
} }
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; Ok(item) => item,
Err(e) => return Poll::Ready(Some(Err(e.into()))),
};
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
this.fut.take(); this.fut.take();
if !chunk.is_empty() { if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
let result = ready!(this.body.as_mut().poll_next(cx)); let result = this.body.as_mut().poll_next(cx);
match result { match result {
Some(Err(err)) => return Poll::Ready(Some(Err(err))), Poll::Ready(Some(Ok(chunk))) => {
Some(Ok(chunk)) => {
if let Some(mut encoder) = this.encoder.take() { if let Some(mut encoder) = this.encoder.take() {
if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE { if chunk.len() < INPLACE {
encoder.write(&chunk)?; encoder.write(&chunk)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
if !chunk.is_empty() { if !chunk.is_empty() {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} else { } else {
*this.fut = Some(spawn_blocking(move || { *this.fut = Some(run(move || {
encoder.write(&chunk)?; encoder.write(&chunk)?;
Ok(encoder) Ok(encoder)
})); }));
@ -181,8 +169,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(Some(Ok(chunk))); return Poll::Ready(Some(Ok(chunk)));
} }
} }
Poll::Ready(None) => {
None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish()?; let chunk = encoder.finish()?;
if chunk.is_empty() { if chunk.is_empty() {
@ -195,6 +182,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
return Poll::Ready(None); return Poll::Ready(None);
} }
} }
val => return val,
} }
} }
} }
@ -224,9 +212,12 @@ impl ContentEncoder {
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
ContentEncoding::Br => { ContentEncoding::Br => Some(ContentEncoder::Br(BrotliEncoder::new(
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Writer::new(),
} 32 * 1024,
3,
22,
))),
_ => None, _ => None,
} }
} }
@ -242,8 +233,8 @@ impl ContentEncoder {
fn finish(self) -> Result<Bytes, io::Error> { fn finish(self) -> Result<Bytes, io::Error> {
match self { match self {
ContentEncoder::Br(encoder) => match encoder.finish() { ContentEncoder::Br(mut encoder) => match encoder.flush() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(()) => Ok(encoder.into_inner().buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
ContentEncoder::Gzip(encoder) => match encoder.finish() { ContentEncoder::Gzip(encoder) => match encoder.finish() {

View File

@ -1,5 +1,4 @@
//! Content-Encoding support. //! Content-Encoding support
use std::io; use std::io;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};

View File

@ -6,22 +6,26 @@ use std::str::Utf8Error;
use std::string::FromUtf8Error; use std::string::FromUtf8Error;
use std::{fmt, io, result}; 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; use bytes::BytesMut;
use derive_more::{Display, From}; use derive_more::{Display, From};
pub use futures_channel::oneshot::Canceled;
use http::uri::InvalidUri; use http::uri::InvalidUri;
use http::{header, Error as HttpError, StatusCode}; use http::{header, Error as HttpError, StatusCode};
use serde::de::value::Error as DeError; use serde::de::value::Error as DeError;
use serde_json::error::Error as JsonError; use serde_json::error::Error as JsonError;
use serde_urlencoded::ser::Error as FormError; use serde_urlencoded::ser::Error as FormError;
// re-export for convenience
use crate::body::Body; use crate::body::Body;
pub use crate::cookie::ParseError as CookieParseError;
use crate::helpers::Writer; use crate::helpers::Writer;
use crate::response::{Response, ResponseBuilder}; use crate::response::{Response, ResponseBuilder};
#[cfg(feature = "cookies")] /// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
pub use crate::cookie::ParseError as CookieParseError;
/// A specialized [`std::result::Result`]
/// for actix web operations /// for actix web operations
/// ///
/// This typedef is generally used to avoid writing out /// This typedef is generally used to avoid writing out
@ -36,7 +40,7 @@ pub type Result<T, E = Error> = result::Result<T, E>;
/// converting errors with `into()`. /// converting errors with `into()`.
/// ///
/// Whenever it is created from an external object a response error is created /// Whenever it is created from an external object a response error is created
/// for it that can be used to create an HTTP response from it this means that /// for it that can be used to create an http response from it this means that
/// if you have access to an actix `Error` you can always get a /// if you have access to an actix `Error` you can always get a
/// `ResponseError` reference from it. /// `ResponseError` reference from it.
pub struct Error { pub struct Error {
@ -96,6 +100,10 @@ impl fmt::Debug for Error {
} }
impl std::error::Error 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)> { fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
None None
} }
@ -145,45 +153,62 @@ impl From<ResponseBuilder> for Error {
} }
} }
/// Return `GATEWAY_TIMEOUT` for `TimeoutError`
impl<E: ResponseError> ResponseError for TimeoutError<E> {
fn status_code(&self) -> StatusCode {
match self {
TimeoutError::Service(e) => e.status_code(),
TimeoutError::Timeout => StatusCode::GATEWAY_TIMEOUT,
}
}
}
#[derive(Debug, Display)] #[derive(Debug, Display)]
#[display(fmt = "UnknownError")] #[display(fmt = "UnknownError")]
struct UnitError; struct UnitError;
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`]. /// `InternalServerError` for `UnitError`
impl ResponseError for UnitError {} impl ResponseError for UnitError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`JsonError`]. /// `InternalServerError` for `JsonError`
impl ResponseError for JsonError {} impl ResponseError for JsonError {}
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`FormError`]. /// `InternalServerError` for `FormError`
impl ResponseError for FormError {} impl ResponseError for FormError {}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`]. /// `InternalServerError` for `openssl::ssl::Error`
impl ResponseError for actix_tls::accept::openssl::SslError {} 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 { impl ResponseError for DeError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
} }
} }
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`]. /// `InternalServerError` for `Canceled`
impl ResponseError for Canceled {}
/// `InternalServerError` for `BlockingError`
impl<E: fmt::Debug> ResponseError for BlockingError<E> {}
/// Return `BAD_REQUEST` for `Utf8Error`
impl ResponseError for Utf8Error { impl ResponseError for Utf8Error {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST 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 {} impl ResponseError for HttpError {}
/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code. /// Return `InternalServerError` for `io::Error`
///
/// 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.
impl ResponseError for io::Error { impl ResponseError for io::Error {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
match self.kind() { match self.kind() {
@ -194,7 +219,7 @@ impl ResponseError for io::Error {
} }
} }
/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`]. /// `BadRequest` for `InvalidHeaderValue`
impl ResponseError for header::InvalidHeaderValue { impl ResponseError for header::InvalidHeaderValue {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
@ -283,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)] #[derive(Display, Debug)]
/// A set of errors that can occur during payload parsing /// A set of errors that can occur during payload parsing
pub enum PayloadError { pub enum PayloadError {
/// A payload reached EOF, but is not complete. /// A payload reached EOF, but is not complete.
#[display( #[display(
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}", fmt = "A payload reached EOF, but is not complete. With error: {:?}",
_0 _0
)] )]
Incomplete(Option<io::Error>), Incomplete(Option<io::Error>),
/// Content encoding stream corruption
/// Content encoding stream corruption.
#[display(fmt = "Can not decode content-encoding.")] #[display(fmt = "Can not decode content-encoding.")]
EncodingCorrupted, EncodingCorrupted,
/// A payload reached size limit.
/// Payload reached size limit. #[display(fmt = "A payload reached size limit.")]
#[display(fmt = "Payload reached size limit.")]
Overflow, Overflow,
/// A payload length is unknown.
/// Payload length is unknown. #[display(fmt = "A payload length is unknown.")]
#[display(fmt = "Payload length is unknown.")]
UnknownLength, UnknownLength,
/// Http2 payload error
/// HTTP/2 payload error.
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http2Payload(h2::Error), Http2Payload(h2::Error),
/// Io error
/// Generic I/O error.
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Io(io::Error), Io(io::Error),
} }
impl std::error::Error for PayloadError { 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 From<h2::Error> for PayloadError { impl From<h2::Error> for PayloadError {
fn from(err: h2::Error) -> Self { fn from(err: h2::Error) -> Self {
@ -356,12 +354,15 @@ impl From<io::Error> for PayloadError {
} }
} }
impl From<BlockingError> for PayloadError { impl From<BlockingError<io::Error>> for PayloadError {
fn from(_: BlockingError) -> Self { fn from(err: BlockingError<io::Error>) -> Self {
PayloadError::Io(io::Error::new( match err {
io::ErrorKind::Other, BlockingError::Error(e) => PayloadError::Io(e),
"Operation is canceled", BlockingError::Canceled => PayloadError::Io(io::Error::new(
)) io::ErrorKind::Other,
"Operation is canceled",
)),
}
} }
} }
@ -379,7 +380,6 @@ impl ResponseError for PayloadError {
} }
/// Return `BadRequest` for `cookie::ParseError` /// Return `BadRequest` for `cookie::ParseError`
#[cfg(feature = "cookies")]
impl ResponseError for crate::cookie::ParseError { impl ResponseError for crate::cookie::ParseError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {
StatusCode::BAD_REQUEST StatusCode::BAD_REQUEST
@ -387,7 +387,7 @@ impl ResponseError for crate::cookie::ParseError {
} }
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
/// A set of errors that can occur during dispatching HTTP requests /// A set of errors that can occur during dispatching http requests
pub enum DispatchError { pub enum DispatchError {
/// Service error /// Service error
Service(Error), Service(Error),
@ -453,13 +453,21 @@ impl ResponseError for ContentTypeError {
} }
} }
impl<E, U: Encoder<I> + Decoder, I> ResponseError for FramedDispatcherError<E, U, I>
where
E: fmt::Debug + fmt::Display,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
}
/// Helper type that can wrap any error and generate custom response. /// Helper type that can wrap any error and generate custom response.
/// ///
/// In following example any `io::Error` will be converted into "BAD REQUEST" /// In following example any `io::Error` will be converted into "BAD REQUEST"
/// response as opposite to *INTERNAL SERVER ERROR* which is defined by /// response as opposite to *INTERNAL SERVER ERROR* which is defined by
/// default. /// default.
/// ///
/// ``` /// ```rust
/// # use std::io; /// # use std::io;
/// # use actix_http::*; /// # use actix_http::*;
/// ///
@ -943,6 +951,16 @@ where
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into() 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@ -959,7 +977,6 @@ mod tests {
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
#[cfg(feature = "cookies")]
#[test] #[test]
fn test_cookie_parse() { fn test_cookie_parse() {
let resp: Response = CookieParseError::EmptyName.error_response(); let resp: Response = CookieParseError::EmptyName.error_response();
@ -1001,22 +1018,22 @@ mod tests {
fn test_payload_error() { fn test_payload_error() {
let err: PayloadError = let err: PayloadError =
io::Error::new(io::ErrorKind::Other, "ParseError").into(); io::Error::new(io::ErrorKind::Other, "ParseError").into();
assert!(err.to_string().contains("ParseError")); assert!(format!("{}", err).contains("ParseError"));
let err = PayloadError::Incomplete(None); let err = PayloadError::Incomplete(None);
assert_eq!( assert_eq!(
err.to_string(), format!("{}", err),
"A payload reached EOF, but is not complete. Inner error: None" "A payload reached EOF, but is not complete. With error: None"
); );
} }
macro_rules! from { macro_rules! from {
($from:expr => $error:pat) => { ($from:expr => $error:pat) => {
match ParseError::from($from) { match ParseError::from($from) {
err @ $error => { e @ $error => {
assert!(err.to_string().len() >= 5); assert!(format!("{}", e).len() >= 5);
} }
err => unreachable!("{:?}", err), e => unreachable!("{:?}", e),
} }
}; };
} }
@ -1059,7 +1076,7 @@ mod tests {
let err = PayloadError::Overflow; let err = PayloadError::Overflow;
let resp_err: &dyn ResponseError = &err; let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap(); 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>(); let not_err = resp_err.downcast_ref::<ContentTypeError>();
assert!(not_err.is_none()); assert!(not_err.is_none());
} }

View File

@ -1,119 +1,62 @@
use std::{ use std::any::{Any, TypeId};
any::{Any, TypeId}, use std::{fmt, mem};
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)] #[derive(Default)]
/// A type map of request extensions.
pub struct Extensions { pub struct Extensions {
/// Use FxHasher with a std HashMap with for faster /// Use FxHasher with a std HashMap with for faster
/// lookups on the small `TypeId` (u64 equivalent) keys. /// lookups on the small `TypeId` (u64 equivalent) keys.
map: AHashMap<TypeId, Box<dyn Any>>, map: FxHashMap<TypeId, Box<dyn Any>>,
} }
impl Extensions { impl Extensions {
/// Creates an empty `Extensions`. /// Create an empty `Extensions`.
#[inline] #[inline]
pub fn new() -> Extensions { pub fn new() -> Extensions {
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. /// If a extension of this type already existed, it will
/// /// be returned.
/// ``` pub fn insert<T: 'static>(&mut self, val: T) {
/// # use actix_http::Extensions; self.map.insert(TypeId::of::<T>(), Box::new(val));
/// 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)
} }
/// Check if map contains an item of a given type. /// Check if container contains entry
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// assert!(!map.contains::<u32>());
///
/// assert_eq!(map.insert(1u32), None);
/// assert!(map.contains::<u32>());
/// ```
pub fn contains<T: 'static>(&self) -> bool { pub fn contains<T: 'static>(&self) -> bool {
self.map.contains_key(&TypeId::of::<T>()) self.map.contains_key(&TypeId::of::<T>())
} }
/// Get a reference to an item of a given type. /// Get a reference to a type previously inserted on this `Extensions`.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// map.insert(1u32);
/// assert_eq!(map.get::<u32>(), Some(&1u32));
/// ```
pub fn get<T: 'static>(&self) -> Option<&T> { pub fn get<T: 'static>(&self) -> Option<&T> {
self.map self.map
.get(&TypeId::of::<T>()) .get(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_ref()) .and_then(|boxed| boxed.downcast_ref())
} }
/// Get a mutable reference to an item of a given type. /// Get a mutable reference to a type previously inserted on this `Extensions`.
///
/// ```
/// # use actix_http::Extensions;
/// let mut map = Extensions::new();
/// map.insert(1u32);
/// assert_eq!(map.get_mut::<u32>(), Some(&mut 1u32));
/// ```
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> { pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
self.map self.map
.get_mut(&TypeId::of::<T>()) .get_mut(&TypeId::of::<T>())
.and_then(|boxed| boxed.downcast_mut()) .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. /// If a extension of this type existed, 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>());
/// ```
pub fn remove<T: 'static>(&mut self) -> Option<T> { 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. /// 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] #[inline]
pub fn clear(&mut self) { pub fn clear(&mut self) {
self.map.clear(); 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;

View File

@ -223,3 +223,15 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
Ok(()) 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 { } else {
Flags::empty() Flags::empty()
}; };
Codec { Codec {
config, config,
flags, flags,
@ -70,26 +69,26 @@ impl Codec {
} }
} }
/// Check if request is upgrade.
#[inline] #[inline]
/// Check if request is upgrade
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade self.ctype == ConnectionType::Upgrade
} }
/// Check if last response is keep-alive.
#[inline] #[inline]
/// Check if last response is keep-alive
pub fn keepalive(&self) -> bool { pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive self.ctype == ConnectionType::KeepAlive
} }
/// Check if keep-alive enabled on server level.
#[inline] #[inline]
/// Check if keep-alive enabled on server level
pub fn keepalive_enabled(&self) -> bool { pub fn keepalive_enabled(&self) -> bool {
self.flags.contains(Flags::KEEPALIVE_ENABLED) self.flags.contains(Flags::KEEPALIVE_ENABLED)
} }
/// Check last request's message type.
#[inline] #[inline]
/// Check last request's message type
pub fn message_type(&self) -> MessageType { pub fn message_type(&self) -> MessageType {
if self.flags.contains(Flags::STREAM) { if self.flags.contains(Flags::STREAM) {
MessageType::Stream MessageType::Stream
@ -111,8 +110,8 @@ impl Decoder for Codec {
type Error = ParseError; type Error = ParseError;
fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> { fn decode(&mut self, src: &mut BytesMut) -> Result<Option<Self::Item>, Self::Error> {
if let Some(ref mut payload) = self.payload { if self.payload.is_some() {
Ok(match payload.decode(src)? { Ok(match self.payload.as_mut().unwrap().decode(src)? {
Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))), Some(PayloadItem::Chunk(chunk)) => Some(Message::Chunk(Some(chunk))),
Some(PayloadItem::Eof) => { Some(PayloadItem::Eof) => {
self.payload.take(); self.payload.take();
@ -199,10 +198,10 @@ mod tests {
use http::Method; use http::Method;
use super::*; use super::*;
use crate::HttpMessage; use crate::httpmessage::HttpMessage;
#[actix_rt::test] #[test]
async fn test_http_request_chunked_payload_and_next_message() { fn test_http_request_chunked_payload_and_next_message() {
let mut codec = Codec::default(); let mut codec = Codec::default();
let mut buf = BytesMut::from( let mut buf = BytesMut::from(

View File

@ -14,7 +14,7 @@ use crate::header::HeaderMap;
use crate::message::{ConnectionType, ResponseHead}; use crate::message::{ConnectionType, ResponseHead};
use crate::request::Request; 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; const MAX_HEADERS: usize = 96;
/// Incoming message decoder /// Incoming message decoder
@ -67,6 +67,7 @@ pub(crate) trait MessageType: Sized {
let mut has_upgrade_websocket = false; let mut has_upgrade_websocket = false;
let mut expect = false; let mut expect = false;
let mut chunked = false; let mut chunked = false;
let mut seen_te = false;
let mut content_length = None; let mut content_length = None;
{ {
@ -85,8 +86,17 @@ pub(crate) trait MessageType: Sized {
}; };
match name { match name {
header::CONTENT_LENGTH => { header::CONTENT_LENGTH if content_length.is_some() => {
if let Ok(s) = value.to_str() { debug!("multiple Content-Length");
return Err(ParseError::Header);
}
header::CONTENT_LENGTH => match value.to_str() {
Ok(s) if s.trim().starts_with("+") => {
debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header);
}
Ok(s) => {
if let Ok(len) = s.parse::<u64>() { if let Ok(len) = s.parse::<u64>() {
if len != 0 { if len != 0 {
content_length = Some(len); content_length = Some(len);
@ -95,15 +105,31 @@ pub(crate) trait MessageType: Sized {
debug!("illegal Content-Length: {:?}", s); debug!("illegal Content-Length: {:?}", s);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} else { }
Err(_) => {
debug!("illegal Content-Length: {:?}", value); debug!("illegal Content-Length: {:?}", value);
return Err(ParseError::Header); return Err(ParseError::Header);
} }
} },
// transfer-encoding // transfer-encoding
header::TRANSFER_ENCODING if seen_te => {
debug!("multiple Transfer-Encoding not allowed");
return Err(ParseError::Header);
}
header::TRANSFER_ENCODING => { header::TRANSFER_ENCODING => {
seen_te = true;
if let Ok(s) = value.to_str().map(|s| s.trim()) { 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 { } else {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
@ -137,7 +163,7 @@ pub(crate) trait MessageType: Sized {
expect = true; expect = true;
} }
} }
_ => {} _ => (),
} }
headers.append(name, value); headers.append(name, value);
@ -203,15 +229,7 @@ impl MessageType for Request {
(len, method, uri, version, req.headers.len()) (len, method, uri, version, req.headers.len())
} }
httparse::Status::Partial => { httparse::Status::Partial => return Ok(None),
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)
};
}
} }
}; };
@ -224,12 +242,15 @@ impl MessageType for Request {
let decoder = match length { let decoder = match length {
PayloadLength::Payload(pl) => pl, PayloadLength::Payload(pl) => pl,
PayloadLength::UpgradeWebSocket => { PayloadLength::UpgradeWebSocket => {
// upgrade (WebSocket) // upgrade(websocket)
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} }
PayloadLength::None => { PayloadLength::None => {
if method == Method::CONNECT { if method == Method::CONNECT {
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else { } else {
PayloadType::None PayloadType::None
} }
@ -278,14 +299,7 @@ impl MessageType for ResponseHead {
(len, version, status, res.headers.len()) (len, version, status, res.headers.len())
} }
httparse::Status::Partial => { httparse::Status::Partial => return Ok(None),
return if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
Err(ParseError::TooLarge)
} else {
Ok(None)
}
}
} }
}; };
@ -301,6 +315,9 @@ impl MessageType for ResponseHead {
} else if status == StatusCode::SWITCHING_PROTOCOLS { } else if status == StatusCode::SWITCHING_PROTOCOLS {
// switching protocol or connect // switching protocol or connect
PayloadType::Stream(PayloadDecoder::eof()) PayloadType::Stream(PayloadDecoder::eof())
} else if src.len() >= MAX_BUFFER_SIZE {
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
return Err(ParseError::TooLarge);
} else { } else {
// for HTTP/1.0 read to eof and close connection // for HTTP/1.0 read to eof and close connection
if msg.version == Version::HTTP_10 { if msg.version == Version::HTTP_10 {
@ -519,19 +536,11 @@ impl ChunkedState {
size: &mut u64, size: &mut u64,
) -> Poll<Result<ChunkedState, io::Error>> { ) -> Poll<Result<ChunkedState, io::Error>> {
let radix = 16; let radix = 16;
match byte!(rdr) {
b @ b'0'..=b'9' => { let rem = match byte!(rdr) {
*size *= radix; b @ b'0'..=b'9' => b - b'0',
*size += u64::from(b - b'0'); b @ b'a'..=b'f' => b + 10 - b'a',
} b @ b'A'..=b'F' => b + 10 - b'A',
b @ b'a'..=b'f' => {
*size *= radix;
*size += u64::from(b + 10 - b'a');
}
b @ b'A'..=b'F' => {
*size *= radix;
*size += u64::from(b + 10 - b'A');
}
b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)), b'\t' | b' ' => return Poll::Ready(Ok(ChunkedState::SizeLws)),
b';' => return Poll::Ready(Ok(ChunkedState::Extension)), b';' => return Poll::Ready(Ok(ChunkedState::Extension)),
b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)), b'\r' => return Poll::Ready(Ok(ChunkedState::SizeLf)),
@ -541,8 +550,23 @@ impl ChunkedState {
"Invalid chunk size line: Invalid Size", "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>> { 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>> { fn read_extension(rdr: &mut BytesMut) -> Poll<Result<ChunkedState, io::Error>> {
match byte!(rdr) { match byte!(rdr) {
b'\r' => Poll::Ready(Ok(ChunkedState::SizeLf)), 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 _ => Poll::Ready(Ok(ChunkedState::Extension)), // no supported extensions
} }
} }
@ -652,7 +681,7 @@ mod tests {
use super::*; use super::*;
use crate::error::ParseError; use crate::error::ParseError;
use crate::http::header::{HeaderName, SET_COOKIE}; use crate::http::header::{HeaderName, SET_COOKIE};
use crate::HttpMessage; use crate::httpmessage::HttpMessage;
impl PayloadType { impl PayloadType {
fn unwrap(self) -> PayloadDecoder { fn unwrap(self) -> PayloadDecoder {
@ -694,7 +723,7 @@ mod tests {
match MessageDecoder::<Request>::default().decode($e) { match MessageDecoder::<Request>::default().decode($e) {
Err(err) => match err { Err(err) => match err {
ParseError::Io(_) => unreachable!("Parse error expected"), ParseError::Io(_) => unreachable!("Parse error expected"),
_ => {} _ => (),
}, },
_ => unreachable!("Error expected"), _ => unreachable!("Error expected"),
} }
@ -830,8 +859,8 @@ mod tests {
.get_all(SET_COOKIE) .get_all(SET_COOKIE)
.map(|v| v.to_str().unwrap().to_owned()) .map(|v| v.to_str().unwrap().to_owned())
.collect(); .collect();
assert_eq!(val[0], "c1=cookie1"); assert_eq!(val[1], "c1=cookie1");
assert_eq!(val[1], "c2=cookie2"); assert_eq!(val[0], "c2=cookie2");
} }
#[test] #[test]
@ -986,13 +1015,7 @@ mod tests {
"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
transfer-encoding: chnked\r\n\r\n", transfer-encoding: chnked\r\n\r\n",
); );
let req = parse_ready!(&mut buf); expect_parse_err!(&mut buf);
if let Ok(val) = req.chunked() {
assert!(!val);
} else {
unreachable!("Error");
}
} }
#[test] #[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::body::BodySize;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::header::{map::Value, HeaderName}; use crate::header::map;
use crate::helpers; use crate::helpers;
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use crate::http::{HeaderMap, StatusCode, Version}; use crate::http::{HeaderMap, StatusCode, Version};
@ -21,7 +21,7 @@ const AVERAGE_HEADER_SIZE: usize = 30;
pub(crate) struct MessageEncoder<T: MessageType> { pub(crate) struct MessageEncoder<T: MessageType> {
pub length: BodySize, pub length: BodySize,
pub te: TransferEncoding, pub te: TransferEncoding,
_phantom: PhantomData<T>, _t: PhantomData<T>,
} }
impl<T: MessageType> Default for MessageEncoder<T> { impl<T: MessageType> Default for MessageEncoder<T> {
@ -29,7 +29,7 @@ impl<T: MessageType> Default for MessageEncoder<T> {
MessageEncoder { MessageEncoder {
length: BodySize::None, length: BodySize::None,
te: TransferEncoding::empty(), te: TransferEncoding::empty(),
_phantom: PhantomData, _t: PhantomData,
} }
} }
} }
@ -80,6 +80,7 @@ pub(crate) trait MessageType: Sized {
match length { match length {
BodySize::Stream => { BodySize::Stream => {
if chunked { if chunked {
skip_len = true;
if camel_case { if camel_case {
dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n") dst.put_slice(b"\r\nTransfer-Encoding: chunked\r\n")
} else { } else {
@ -118,14 +119,24 @@ pub(crate) trait MessageType: Sized {
dst.put_slice(b"connection: close\r\n") 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 // write headers
let mut has_date = false; 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(); let mut remaining = dst.capacity() - dst.len();
// tracks bytes written since last buffer resize // 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 // container's knowledge, this is used to sync the containers cursor after data is written
let mut pos = 0; let mut pos = 0;
self.write_headers(|key, value| { for (key, value) in headers {
match *key { match *key {
CONNECTION => return, CONNECTION => continue,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => return, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true, DATE => has_date = true,
_ => {} _ => (),
} }
let k = key.as_str().as_bytes(); let k = key.as_str().as_bytes();
let k_len = k.len(); let k_len = k.len();
// TODO: drain? match value {
for val in value.iter() { map::Value::One(ref val) => {
let v = val.as_ref(); let v = val.as_ref();
let v_len = v.len(); let v_len = v.len();
// key length + value length + colon + space + \r\n // key length + value length + colon + space + \r\n
let len = k_len + v_len + 4; let len = k_len + v_len + 4;
if len > remaining { if len > remaining {
// SAFETY: all the bytes written up to position "pos" are initialized // not enough room in buffer for this header; reserve more space
// the written byte count and pointer advancement are kept in sync
unsafe { // SAFETY: all the bytes written up to position "pos" are initialized
dst.advance_mut(pos); // 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; // SAFETY: on each write, it is enough to ensure that the advancement of the
dst.reserve(len * 2); // cursor matches the number of bytes written
remaining = dst.capacity() - dst.len(); 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 buf = buf.add(k_len);
// reallocated and/or resized
buf = dst.chunk_mut().as_mut_ptr(); 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 map::Value::Multi(ref vec) => {
// the cursor matches the number of bytes written for val in vec {
unsafe { let v = val.as_ref();
if camel_case { let v_len = v.len();
// use Camel-Case headers let len = k_len + v_len + 4;
write_camel_case(k, from_raw_parts_mut(buf, k_len));
} else { if len > remaining {
write_data(k, buf, k_len); // 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 // final cursor synchronization with the bytes container
// //
@ -213,24 +274,6 @@ pub(crate) trait MessageType: Sized {
Ok(()) 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<()> { impl MessageType for Response<()> {
@ -287,7 +330,7 @@ impl MessageType for RequestHeadType {
let head = self.as_ref(); let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE); dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!( write!(
helpers::Writer(dst), Writer(dst),
"{} {} {}", "{} {} {}",
head.method, head.method,
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
@ -420,7 +463,7 @@ impl TransferEncoding {
*eof = true; *eof = true;
buf.extend_from_slice(b"0\r\n\r\n"); buf.extend_from_slice(b"0\r\n\r\n");
} else { } 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))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2); 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 /// # Safety
/// Callers must ensure that the given length matches given value length. /// Callers must ensure that the given length matches given value length.
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) { 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]) { fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
// first copy entire (potentially wrong) slice to output let mut index = 0;
buffer[..value.len()].copy_from_slice(value); let key = value;
let mut key_iter = key.iter();
let mut iter = value.iter(); if let Some(c) = key_iter.next() {
if *c >= b'a' && *c <= b'z' {
// first character should be uppercase buffer[index] = *c ^ b' ';
if let Some(c @ b'a'..=b'z') = iter.next() { index += 1;
buffer[0] = c & 0b1101_1111; }
} else {
return;
} }
// track 1 ahead of the current position since that's the location being assigned to while let Some(c) = key_iter.next() {
let mut index = 2; buffer[index] = *c;
index += 1;
// remaining characters after hyphens should also be uppercase if *c == b'-' {
while let Some(&c) = iter.next() { if let Some(c) = key_iter.next() {
if c == b'-' { if *c >= b'a' && *c <= b'z' {
// advance iter by one and uppercase if needed buffer[index] = *c ^ b' ';
if let Some(c @ b'a'..=b'z') = iter.next() { index += 1;
buffer[index] = c & 0b1101_1111; }
} }
} }
index += 1;
} }
} }
@ -529,8 +585,8 @@ mod tests {
); );
} }
#[actix_rt::test] #[test]
async fn test_camel_case() { fn test_camel_case() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default(); let mut head = RequestHead::default();
head.set_camel_case_headers(true); head.set_camel_case_headers(true);
@ -549,7 +605,6 @@ mod tests {
); );
let data = let data =
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap(); String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
assert!(data.contains("Content-Length: 0\r\n")); assert!(data.contains("Content-Length: 0\r\n"));
assert!(data.contains("Connection: close\r\n")); assert!(data.contains("Connection: close\r\n"));
assert!(data.contains("Content-Type: plain/text\r\n")); assert!(data.contains("Content-Type: plain/text\r\n"));
@ -592,8 +647,8 @@ mod tests {
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[actix_rt::test] #[test]
async fn test_extra_headers() { fn test_extra_headers() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default(); let mut head = RequestHead::default();
@ -626,15 +681,16 @@ mod tests {
assert!(data.contains("date: date\r\n")); assert!(data.contains("date: date\r\n"));
} }
#[actix_rt::test] #[test]
async fn test_no_content_length() { fn test_no_content_length() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut res: Response<()> = let mut res: Response<()> =
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>(); Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
res.headers_mut().insert(DATE, HeaderValue::from_static(""));
res.headers_mut() res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); .insert(DATE, HeaderValue::from_static(&""));
res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
let _ = res.encode_headers( let _ = res.encode_headers(
&mut bytes, &mut bytes,

View File

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

View File

@ -1,4 +1,4 @@
//! HTTP/1 protocol implementation. //! HTTP/1 implementation
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
mod client; mod client;
@ -17,7 +17,7 @@ pub use self::codec::Codec;
pub use self::dispatcher::Dispatcher; pub use self::dispatcher::Dispatcher;
pub use self::expect::ExpectHandler; pub use self::expect::ExpectHandler;
pub use self::payload::Payload; 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::upgrade::UpgradeHandler;
pub use self::utils::SendResponse; pub use self::utils::SendResponse;

View File

@ -3,8 +3,9 @@ use std::cell::RefCell;
use std::collections::VecDeque; use std::collections::VecDeque;
use std::pin::Pin; use std::pin::Pin;
use std::rc::{Rc, Weak}; use std::rc::{Rc, Weak};
use std::task::{Context, Poll, Waker}; use std::task::{Context, Poll};
use actix_utils::task::LocalWaker;
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::Stream;
@ -133,7 +134,7 @@ impl PayloadSender {
if shared.borrow().need_read { if shared.borrow().need_read {
PayloadStatus::Read PayloadStatus::Read
} else { } else {
shared.borrow_mut().register_io(cx); shared.borrow_mut().io_task.register(cx.waker());
PayloadStatus::Pause PayloadStatus::Pause
} }
} else { } else {
@ -149,8 +150,8 @@ struct Inner {
err: Option<PayloadError>, err: Option<PayloadError>,
need_read: bool, need_read: bool,
items: VecDeque<Bytes>, items: VecDeque<Bytes>,
task: Option<Waker>, task: LocalWaker,
io_task: Option<Waker>, io_task: LocalWaker,
} }
impl Inner { impl Inner {
@ -161,48 +162,8 @@ impl Inner {
err: None, err: None,
items: VecDeque::new(), items: VecDeque::new(),
need_read: true, need_read: true,
task: None, task: LocalWaker::new(),
io_task: None, io_task: LocalWaker::new(),
}
}
/// Wake up future waiting for payload data to be available.
fn wake(&mut self) {
if let Some(waker) = self.task.take() {
waker.wake();
}
}
/// Wake up future feeding data to Payload.
fn wake_io(&mut self) {
if let Some(waker) = self.io_task.take() {
waker.wake();
}
}
/// Register future waiting data from payload.
/// Waker would be used in `Inner::wake`
fn register(&mut self, cx: &mut Context<'_>) {
if self
.task
.as_ref()
.map(|w| !cx.waker().will_wake(w))
.unwrap_or(true)
{
self.task = Some(cx.waker().clone());
}
}
// Register future feeding data to payload.
/// Waker would be used in `Inner::wake_io`
fn register_io(&mut self, cx: &mut Context<'_>) {
if self
.io_task
.as_ref()
.map(|w| !cx.waker().will_wake(w))
.unwrap_or(true)
{
self.io_task = Some(cx.waker().clone());
} }
} }
@ -221,7 +182,9 @@ impl Inner {
self.len += data.len(); self.len += data.len();
self.items.push_back(data); self.items.push_back(data);
self.need_read = self.len < MAX_BUFFER_SIZE; self.need_read = self.len < MAX_BUFFER_SIZE;
self.wake(); if let Some(task) = self.task.take() {
task.wake()
}
} }
#[cfg(test)] #[cfg(test)]
@ -238,9 +201,9 @@ impl Inner {
self.need_read = self.len < MAX_BUFFER_SIZE; self.need_read = self.len < MAX_BUFFER_SIZE;
if self.need_read && !self.eof { if self.need_read && !self.eof {
self.register(cx); self.task.register(cx.waker());
} }
self.wake_io(); self.io_task.wake();
Poll::Ready(Some(Ok(data))) Poll::Ready(Some(Ok(data)))
} else if let Some(err) = self.err.take() { } else if let Some(err) = self.err.take() {
Poll::Ready(Some(Err(err))) Poll::Ready(Some(Err(err)))
@ -248,8 +211,8 @@ impl Inner {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
self.need_read = true; self.need_read = true;
self.register(cx); self.task.register(cx.waker());
self.wake_io(); self.io_task.wake();
Poll::Pending Poll::Pending
} }
} }
@ -263,7 +226,7 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use actix_utils::future::poll_fn; use futures_util::future::poll_fn;
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -1,4 +1,6 @@
use std::future::Future;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::pin::Pin;
use std::rc::Rc; use std::rc::Rc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{fmt, net}; use std::{fmt, net};
@ -6,41 +8,43 @@ use std::{fmt, net};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory};
use actix_utils::future::ready; use futures_core::ready;
use futures_core::future::LocalBoxFuture; use futures_util::future::{ok, Ready};
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig; 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::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::service::HttpServiceHandler; use crate::{ConnectCallback, Extensions};
use crate::{ConnectCallback, OnConnectData};
use super::codec::Codec; use super::codec::Codec;
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
use super::{ExpectHandler, UpgradeHandler}; use super::{ExpectHandler, Message, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport /// `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, srv: S,
cfg: ServiceConfig, cfg: ServiceConfig,
expect: X, expect: X,
upgrade: Option<U>, upgrade: Option<U>,
on_connect: Option<Rc<dyn Fn(&T) -> Box<dyn DataFactory>>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>, on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>, _t: PhantomData<(T, B)>,
} }
impl<T, S, B> H1Service<T, S, B> impl<T, S, B> H1Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
{ {
/// Create new `HttpService` instance with config. /// 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, cfg: ServiceConfig,
service: F, service: F,
) -> Self { ) -> Self {
@ -49,26 +53,28 @@ where
srv: service.into_factory(), srv: service.into_factory(),
expect: ExpectHandler, expect: ExpectHandler,
upgrade: None, upgrade: None,
on_connect: None,
on_connect_ext: None, on_connect_ext: None,
_phantom: PhantomData, _t: PhantomData,
} }
} }
} }
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U> impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>, U: ServiceFactory<
U::Future: 'static, Config = (),
Request = (Request, Framed<TcpStream, Codec>),
Response = (),
>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -76,15 +82,15 @@ where
pub fn tcp( pub fn tcp(
self, self,
) -> impl ServiceFactory< ) -> impl ServiceFactory<
TcpStream,
Config = (), Config = (),
Request = TcpStream,
Response = (), Response = (),
Error = DispatchError, Error = DispatchError,
InitError = (), InitError = (),
> { > {
pipeline_factory(|io: TcpStream| { pipeline_factory(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr))) ok((io, peer_addr))
}) })
.and_then(self) .and_then(self)
} }
@ -94,30 +100,24 @@ where
mod openssl { mod openssl {
use super::*; use super::*;
use actix_service::ServiceFactoryExt; use actix_tls::openssl::{Acceptor, SslAcceptor, SslStream};
use actix_tls::accept::{ use actix_tls::{openssl::HandshakeError, TlsError};
openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Request = (Request, Framed<SslStream<TcpStream>, Codec>),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -126,10 +126,10 @@ mod openssl {
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
) -> impl ServiceFactory< ) -> impl ServiceFactory<
TcpStream,
Config = (), Config = (),
Request = TcpStream,
Response = (), Response = (),
Error = TlsError<SslError, DispatchError>, Error = TlsError<HandshakeError<TcpStream>, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( pipeline_factory(
@ -137,9 +137,9 @@ mod openssl {
.map_err(TlsError::Tls) .map_err(TlsError::Tls)
.map_init_err(|_| panic!()), .map_init_err(|_| panic!()),
) )
.and_then(|io: TlsStream<TcpStream>| { .and_then(|io: SslStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok(); 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)) .and_then(self.map_err(TlsError::Service))
} }
@ -149,33 +149,25 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*; use super::*;
use actix_tls::rustls::{Acceptor, ServerConfig, TlsStream};
use std::io; use actix_tls::TlsError;
use std::{fmt, io};
use actix_service::ServiceFactoryExt;
use actix_tls::accept::{
rustls::{Acceptor, ServerConfig, TlsStream},
TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Request = (Request, Framed<TlsStream<TcpStream>, Codec>),
Response = (), Response = (),
>, >,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
@ -184,8 +176,8 @@ mod rustls {
self, self,
config: ServerConfig, config: ServerConfig,
) -> impl ServiceFactory< ) -> impl ServiceFactory<
TcpStream,
Config = (), Config = (),
Request = TcpStream,
Response = (), Response = (),
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
@ -197,7 +189,7 @@ mod rustls {
) )
.and_then(|io: TlsStream<TcpStream>| { .and_then(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok(); 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)) .and_then(self.map_err(TlsError::Service))
} }
@ -206,7 +198,7 @@ mod rustls {
impl<T, S, B, X, U> H1Service<T, S, B, X, U> impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Error: Into<Error>, S::Error: Into<Error>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
@ -214,7 +206,7 @@ where
{ {
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Response = Request>, X1: ServiceFactory<Request = Request, Response = Request>,
X1::Error: Into<Error>, X1::Error: Into<Error>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
@ -223,14 +215,15 @@ where
cfg: self.cfg, cfg: self.cfg,
srv: self.srv, srv: self.srv,
upgrade: self.upgrade, upgrade: self.upgrade,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext, 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> pub fn upgrade<U1>(self, upgrade: Option<U1>) -> H1Service<T, S, B, X, U1>
where where
U1: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>, U1: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
{ {
@ -239,11 +232,21 @@ where
cfg: self.cfg, cfg: self.cfg,
srv: self.srv, srv: self.srv,
expect: self.expect, expect: self.expect,
on_connect: self.on_connect,
on_connect_ext: self.on_connect_ext, 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. /// Set on connect callback.
pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self { pub(crate) fn on_connect_ext(mut self, f: Option<Rc<ConnectCallback<T>>>) -> Self {
self.on_connect_ext = f; self.on_connect_ext = f;
@ -251,106 +254,349 @@ where
} }
} }
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory for H1Service<T, S, B, X, U>
for H1Service<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Config = (), Request = Request>,
S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Error>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Config = (), Request = Request, Response = Request>,
X::Future: 'static,
X::Error: Into<Error>, X::Error: Into<Error>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<Config = (), Request = (Request, Framed<T, Codec>), Response = ()>,
U::Future: 'static,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Config = ();
type Request = (T, Option<net::SocketAddr>);
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Config = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = (); type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::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 { fn new_service(&self, _: ()) -> Self::Future {
let service = self.srv.new_service(()); H1ServiceResponse {
let expect = self.expect.new_service(()); fut: self.srv.new_service(()),
let upgrade = self.upgrade.as_ref().map(|s| s.new_service(())); fut_ex: Some(self.expect.new_service(())),
let on_connect_ext = self.on_connect_ext.clone(); fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())),
let cfg = self.cfg.clone(); expect: None,
upgrade: None,
on_connect: self.on_connect.clone(),
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_t: PhantomData,
}
}
}
Box::pin(async move { #[doc(hidden)]
let expect = expect #[pin_project::pin_project]
.await pub struct H1ServiceResponse<T, S, B, X, U>
.map_err(|e| log::error!("Init http expect service error: {:?}", e))?; where
S: ServiceFactory<Request = Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
#[pin]
fut: S::Future,
#[pin]
fut_ex: Option<X::Future>,
#[pin]
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>,
_t: PhantomData<(T, B)>,
}
let upgrade = match upgrade { impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U>
Some(upgrade) => { where
let upgrade = upgrade.await.map_err(|e| { T: AsyncRead + AsyncWrite + Unpin,
log::error!("Init http upgrade service error: {:?}", e) S: ServiceFactory<Request = Request>,
})?; S::Error: Into<Error>,
Some(upgrade) S::Response: Into<Response<B>>,
} S::InitError: fmt::Debug,
None => None, B: MessageBody,
}; X: ServiceFactory<Request = Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
type Output = Result<H1ServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
let service = service fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
.await let mut this = self.as_mut().project();
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
Ok(H1ServiceHandler::new( if let Some(fut) = this.fut_ex.as_pin_mut() {
cfg, let expect = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.expect = Some(expect);
this.fut_ex.set(None);
}
if let Some(fut) = this.fut_upg.as_pin_mut() {
let upgrade = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_ex.set(None);
}
let result = ready!(this
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
H1ServiceHandler::new(
this.cfg.take().unwrap(),
service, service,
expect, this.expect.take().unwrap(),
upgrade, this.upgrade.take(),
on_connect_ext, this.on_connect.clone(),
)) this.on_connect_ext.clone(),
}) )
}))
} }
} }
/// `Service` implementation for HTTP/1 transport /// `Service` implementation for HTTP/1 transport
pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, 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,
_t: PhantomData<(T, B)>,
}
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, S: Service<Request = Request>,
S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Error>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
X: Service<Request, Response = Request>, X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>, 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,
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 {
srv: CloneableService::new(srv),
expect: CloneableService::new(expect),
upgrade: upgrade.map(CloneableService::new),
cfg,
on_connect,
on_connect_ext,
_t: PhantomData,
}
}
}
impl<T, S, B, X, U> Service for H1ServiceHandler<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request = Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request = Request, Response = Request>,
X::Error: Into<Error>,
U: Service<Request = (Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Error>,
{ {
type Request = (T, Option<net::SocketAddr>);
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Future = Dispatcher<T, S, B, X, U>; 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>> {
self._poll_ready(cx).map_err(|e| { let ready = self
log::error!("HTTP/1 service readiness error: {:?}", e); .expect
DispatchError::Service(e) .poll_ready(cx)
}) .map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = self
.srv
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready;
let ready = if let Some(ref mut upg) = self.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready
} else {
ready
};
if ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
} }
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future { fn call(&mut self, (io, addr): Self::Request) -> Self::Future {
let on_connect_data = let deprecated_on_connect = self.on_connect.as_ref().map(|handler| handler(&io));
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
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( Dispatcher::new(
io, io,
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.srv.clone(),
on_connect_data, self.expect.clone(),
self.upgrade.clone(),
deprecated_on_connect,
connect_extensions,
addr, 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,34 +1,41 @@
use std::marker::PhantomData;
use std::task::{Context, Poll};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_core::future::LocalBoxFuture; use futures_util::future::Ready;
use crate::error::Error; use crate::error::Error;
use crate::h1::Codec; use crate::h1::Codec;
use crate::request::Request; 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 Response = ();
type Error = Error; type Error = Error;
type Config = (); type Service = UpgradeHandler<T>;
type Service = UpgradeHandler;
type InitError = Error; type InitError = Error;
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>; type Future = Ready<Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
unimplemented!() unimplemented!()
} }
} }
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler { impl<T> Service for UpgradeHandler<T> {
type Request = (Request, Framed<T, Codec>);
type Response = (); type Response = ();
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::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 { fn call(&mut self, _: Self::Request) -> Self::Future {
unimplemented!() unimplemented!()
} }
} }

View File

@ -1,75 +1,102 @@
use std::convert::TryFrom;
use std::future::Future;
use std::marker::PhantomData;
use std::net;
use std::pin::Pin;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{cmp, future::Future, marker::PhantomData, net, pin::Pin, rc::Rc};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{Delay, Instant};
use actix_service::Service; use actix_service::Service;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready; use h2::server::{Connection, SendResponse};
use h2::{ use h2::SendStream;
server::{Connection, SendResponse},
SendStream,
};
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace}; use log::{error, trace};
use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::body::{BodySize, MessageBody, ResponseBody};
use crate::cloneable::CloneableService;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error}; use crate::error::{DispatchError, Error};
use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage;
use crate::message::ResponseHead; use crate::message::ResponseHead;
use crate::payload::Payload; use crate::payload::Payload;
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
use crate::service::HttpFlow; use crate::Extensions;
use crate::OnConnectData;
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol. /// Dispatcher for HTTP/2 protocol
#[pin_project::pin_project] #[pin_project::pin_project]
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S: Service<Request = Request>, B: MessageBody>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>,
B: MessageBody,
{ {
flow: Rc<HttpFlow<S, X, U>>, service: CloneableService<S>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect_data: OnConnectData, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
_phantom: PhantomData<B>, ka_expire: Instant,
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 where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request = Request>,
S::Error: Into<Error>, S::Error: Into<Error>,
// S::Future: 'static,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
{ {
pub(crate) fn new( pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>, service: CloneableService<S>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
on_connect_data: OnConnectData, on_connect: Option<Box<dyn DataFactory>>,
on_connect_data: Extensions,
config: ServiceConfig, config: ServiceConfig,
timeout: Option<Delay>,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
// let keepalive = config.keep_alive_enabled();
// let flags = if keepalive {
// Flags::KEEPALIVE | Flags::KEEPALIVE_ENABLED
// } else {
// Flags::empty()
// };
// keep-alive timer
let (ka_expire, ka_timer) = if let Some(delay) = timeout {
(delay.deadline(), Some(delay))
} else if let Some(delay) = config.keep_alive_timer() {
(delay.deadline(), Some(delay))
} else {
(config.now(), None)
};
Dispatcher { Dispatcher {
flow, service,
config, config,
peer_addr, peer_addr,
connection, connection,
on_connect,
on_connect_data, on_connect_data,
_phantom: PhantomData, ka_expire,
ka_timer,
_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 where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request = Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Error> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
@ -82,39 +109,56 @@ where
let this = self.get_mut(); let this = self.get_mut();
loop { loop {
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) { match Pin::new(&mut this.connection).poll_accept(cx) {
None => return Poll::Ready(Ok(())), 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() {
this.ka_expire = expire;
}
}
Some(Err(err)) => return Poll::Ready(Err(err.into())),
Some(Ok((req, res))) => {
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body); let mut req = Request::with_payload(Payload::<
let pl = Payload::<crate::payload::PayloadStream>::H2(pl); crate::payload::PayloadStream,
let mut req = Request::with_payload(pl); >::H2(
crate::h2::Payload::new(body)
));
let head = req.head_mut(); let head = &mut req.head_mut();
head.uri = parts.uri; head.uri = parts.uri;
head.method = parts.method; head.method = parts.method;
head.version = parts.version; head.version = parts.version;
head.headers = parts.headers.into(); head.headers = parts.headers.into();
head.peer_addr = this.peer_addr; head.peer_addr = this.peer_addr;
// merge on_connect_ext data into request extensions // DEPRECATED
this.on_connect_data.merge_into(&mut req); // set on_connect data
if let Some(ref on_connect) = this.on_connect {
on_connect.set(&mut req.extensions_mut());
}
let svc = ServiceResponse { // 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( state: ServiceResponseState::ServiceCall(
this.flow.service.call(req), this.service.call(req),
Some(res), Some(res),
), ),
config: this.config.clone(), config: this.config.clone(),
buffer: None, buffer: None,
_phantom: PhantomData, _t: PhantomData,
}; });
actix_rt::spawn(svc);
} }
Poll::Pending => return Poll::Pending,
} }
} }
} }
@ -126,7 +170,7 @@ struct ServiceResponse<F, I, E, B> {
state: ServiceResponseState<F, B>, state: ServiceResponseState<F, B>,
config: ServiceConfig, config: ServiceConfig,
buffer: Option<Bytes>, buffer: Option<Bytes>,
_phantom: PhantomData<(I, E)>, _t: PhantomData<(I, E)>,
} }
#[pin_project::pin_project(project = ServiceResponseStateProj)] #[pin_project::pin_project(project = ServiceResponseStateProj)]
@ -163,36 +207,27 @@ where
skip_len = true; skip_len = true;
*size = BodySize::Stream; *size = BodySize::Stream;
} }
_ => {} _ => (),
} }
let _ = match size { let _ = match size {
BodySize::None | BodySize::Stream => None, BodySize::None | BodySize::Stream => None,
BodySize::Empty => res BodySize::Empty => res
.headers_mut() .headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")), .insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => { BodySize::Sized(len) => res.headers_mut().insert(
let mut buf = itoa::Buffer::new(); CONTENT_LENGTH,
HeaderValue::try_from(format!("{}", len)).unwrap(),
res.headers_mut().insert( ),
CONTENT_LENGTH,
HeaderValue::from_str(buf.format(*len)).unwrap(),
)
}
}; };
// copy headers // copy headers
for (key, value) in head.headers.iter() { for (key, value) in head.headers.iter() {
match *key { match *key {
// TODO: consider skipping other headers according to: CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue, CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true, DATE => has_date = true,
_ => {} _ => (),
} }
res.headers_mut().append(key, value.clone()); res.headers_mut().append(key, value.clone());
} }
@ -224,111 +259,110 @@ where
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
match this.state.project() { match this.state.project() {
ServiceResponseStateProj::ServiceCall(call, send) => { ServiceResponseStateProj::ServiceCall(call, send) => match call.poll(cx) {
match ready!(call.poll(cx)) { Poll::Ready(Ok(res)) => {
Ok(res) => { let (res, body) = res.into().replace_body(());
let (res, body) = res.into().replace_body(());
let mut send = send.take().unwrap(); let mut send = send.take().unwrap();
let mut size = body.size(); let mut size = body.size();
let h2_res = let h2_res = self.as_mut().prepare_response(res.head(), &mut size);
self.as_mut().prepare_response(res.head(), &mut size); this = self.as_mut().project();
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) { let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => { Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e); trace!("Error sending h2 response: {:?}", e);
return Poll::Ready(()); return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state
.set(ServiceResponseState::SendPayload(stream, body));
self.poll(cx)
} }
} Ok(stream) => stream,
};
Err(e) => { if size.is_eof() {
let res: Response = e.into().into(); Poll::Ready(())
let (res, body) = res.replace_body(()); } else {
this.state
let mut send = send.take().unwrap(); .set(ServiceResponseState::SendPayload(stream, body));
let mut size = body.size(); self.poll(cx)
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)
}
} }
} }
} 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) => { ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
loop { loop {
match this.buffer { loop {
Some(ref mut buffer) => match ready!(stream.poll_capacity(cx)) { if let Some(ref mut buffer) = this.buffer {
None => return Poll::Ready(()), 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)) => { if let Err(e) = stream.send_data(bytes, false) {
let len = buffer.len(); warn!("{:?}", e);
let bytes = buffer.split_to(cmp::min(cap, len)); return Poll::Ready(());
} else if !buffer.is_empty() {
if let Err(e) = stream.send_data(bytes, false) { let cap =
std::cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Poll::Ready(Some(Err(e))) => {
warn!("{:?}", e); warn!("{:?}", e);
return Poll::Ready(()); return Poll::Ready(());
} else if !buffer.is_empty() {
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
} }
} }
} else {
Some(Err(e)) => { match body.as_mut().poll_next(cx) {
warn!("{:?}", e); Poll::Pending => return Poll::Pending,
return Poll::Ready(()); Poll::Ready(None) => {
} if let Err(e) = stream.send_data(Bytes::new(), true)
}, {
warn!("{:?}", e);
None => match ready!(body.as_mut().poll_next(cx)) { }
None => { return Poll::Ready(());
if let Err(e) = stream.send_data(Bytes::new(), true) { }
warn!("{:?}", e); Poll::Ready(Some(Ok(chunk))) => {
stream.reserve_capacity(std::cmp::min(
chunk.len(),
CHUNK_SIZE,
));
*this.buffer = Some(chunk);
}
Poll::Ready(Some(Err(e))) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
} }
return Poll::Ready(());
} }
}
Some(Ok(chunk)) => {
stream
.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
*this.buffer = Some(chunk);
}
Some(Err(e)) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
}
},
} }
} }
} }

View File

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

View File

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

View File

@ -1,48 +0,0 @@
//! Helper trait for types that can be effectively borrowed as a [HeaderValue].
//!
//! [HeaderValue]: crate::http::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

@ -2,10 +2,10 @@ use std::cmp::Ordering;
use mime::Mime; use mime::Mime;
use super::{qitem, QualityItem}; use crate::header::{qitem, QualityItem};
use crate::http::header; use crate::http::header;
crate::header! { header! {
/// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2) /// `Accept` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.2)
/// ///
/// The `Accept` header field can be used by user agents to specify /// The `Accept` header field can be used by user agents to specify
@ -32,36 +32,50 @@ crate::header! {
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c` /// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
/// ///
/// # Examples /// # Examples
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// # extern crate actix_http;
/// use actix_web::http::header::{Accept, qitem}; /// extern crate mime;
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// builder.insert_header( /// let mut builder = Response::Ok();
///
/// builder.set(
/// Accept(vec![ /// Accept(vec![
/// qitem(mime::TEXT_HTML), /// qitem(mime::TEXT_HTML),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// # extern crate actix_http;
/// use actix_web::http::header::{Accept, qitem}; /// extern crate mime;
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// builder.insert_header( /// let mut builder = Response::Ok();
///
/// builder.set(
/// Accept(vec![ /// Accept(vec![
/// qitem(mime::APPLICATION_JSON), /// qitem(mime::APPLICATION_JSON),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// # extern crate actix_http;
/// use actix_web::http::header::{Accept, QualityItem, q, qitem}; /// extern crate mime;
/// use actix_http::Response;
/// use actix_http::http::header::{Accept, QualityItem, q, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// builder.insert_header( /// let mut builder = Response::Ok();
///
/// builder.set(
/// Accept(vec![ /// Accept(vec![
/// qitem(mime::TEXT_HTML), /// qitem(mime::TEXT_HTML),
/// qitem("application/xhtml+xml".parse().unwrap()), /// qitem("application/xhtml+xml".parse().unwrap()),
@ -76,6 +90,7 @@ crate::header! {
/// ), /// ),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
(Accept, header::ACCEPT) => (QualityItem<Mime>)+ (Accept, header::ACCEPT) => (QualityItem<Mime>)+
@ -116,8 +131,8 @@ crate::header! {
#[test] #[test]
fn test_fuzzing1() { fn test_fuzzing1() {
use actix_http::test::TestRequest; use crate::test::TestRequest;
let req = TestRequest::default().insert_header((crate::http::header::ACCEPT, "chunk#;e")).finish(); let req = TestRequest::with_header(crate::header::ACCEPT, "chunk#;e").finish();
let header = Accept::parse(&req); let header = Accept::parse(&req);
assert!(header.is_ok()); assert!(header.is_ok());
} }
@ -213,7 +228,7 @@ impl Accept {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::header::q; use crate::header::q;
#[test] #[test]
fn test_mime_precedence() { fn test_mime_precedence() {

View File

@ -1,6 +1,6 @@
use super::{Charset, QualityItem, ACCEPT_CHARSET}; use crate::header::{Charset, QualityItem, ACCEPT_CHARSET};
crate::header! { header! {
/// `Accept-Charset` header, defined in /// `Accept-Charset` header, defined in
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3) /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3)
/// ///
@ -21,37 +21,44 @@ crate::header! {
/// * `iso-8859-5, unicode-1-1;q=0.8` /// * `iso-8859-5, unicode-1-1;q=0.8`
/// ///
/// # Examples /// # Examples
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// # extern crate actix_http;
/// use actix_web::http::header::{AcceptCharset, Charset, qitem}; /// use actix_http::Response;
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// builder.insert_header( /// let mut builder = Response::Ok();
/// builder.set(
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)]) /// 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() {
/// use actix_web::HttpResponse; /// let mut builder = Response::Ok();
/// use actix_web::http::header::{AcceptCharset, Charset, q, QualityItem}; /// builder.set(
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptCharset(vec![ /// AcceptCharset(vec![
/// QualityItem::new(Charset::Us_Ascii, q(900)), /// QualityItem::new(Charset::Us_Ascii, q(900)),
/// QualityItem::new(Charset::Iso_8859_10, q(200)), /// 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() {
/// use actix_web::HttpResponse; /// let mut builder = Response::Ok();
/// use actix_web::http::header::{AcceptCharset, Charset, qitem}; /// builder.set(
///
/// let mut builder = HttpResponse::Ok();
/// builder.insert_header(
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))]) /// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
/// ); /// );
/// # }
/// ``` /// ```
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+ (AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+

View File

@ -26,20 +26,18 @@ header! {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_web::HttpResponse; /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
/// use actix_web::http::header::{AcceptEncoding, Encoding, qitem};
/// ///
/// let mut builder = HttpResponse::new(); /// let mut headers = Headers::new();
/// builder.insert_header( /// headers.set(
/// AcceptEncoding(vec![qitem(Encoding::Chunked)]) /// AcceptEncoding(vec![qitem(Encoding::Chunked)])
/// ); /// );
/// ``` /// ```
/// ``` /// ```
/// use actix_web::HttpResponse; /// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
/// use actix_web::http::header::{AcceptEncoding, Encoding, qitem};
/// ///
/// let mut builder = HttpResponse::new(); /// let mut headers = Headers::new();
/// builder.insert_header( /// headers.set(
/// AcceptEncoding(vec![ /// AcceptEncoding(vec![
/// qitem(Encoding::Chunked), /// qitem(Encoding::Chunked),
/// qitem(Encoding::Gzip), /// qitem(Encoding::Gzip),
@ -48,11 +46,10 @@ header! {
/// ); /// );
/// ``` /// ```
/// ``` /// ```
/// use actix_web::HttpResponse; /// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem};
/// use actix_web::http::header::{AcceptEncoding, Encoding, QualityItem, q, qitem};
/// ///
/// let mut builder = HttpResponse::new(); /// let mut headers = Headers::new();
/// builder.insert_header( /// headers.set(
/// AcceptEncoding(vec![ /// AcceptEncoding(vec![
/// qitem(Encoding::Chunked), /// qitem(Encoding::Chunked),
/// QualityItem::new(Encoding::Gzip, q(600)), /// QualityItem::new(Encoding::Gzip, q(600)),

View File

@ -1,7 +1,7 @@
use super::{QualityItem, ACCEPT_LANGUAGE}; use crate::header::{QualityItem, ACCEPT_LANGUAGE};
use language_tags::LanguageTag; use language_tags::LanguageTag;
crate::header! { header! {
/// `Accept-Language` header, defined in /// `Accept-Language` header, defined in
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5) /// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5)
/// ///
@ -22,35 +22,41 @@ crate::header! {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use language_tags::langtag; /// # extern crate actix_http;
/// use actix_web::HttpResponse; /// # extern crate language_tags;
/// use actix_web::http::header::{AcceptLanguage, LanguageTag, qitem}; /// use actix_http::Response;
/// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem};
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// let mut builder = Response::Ok();
/// let mut langtag: LanguageTag = Default::default(); /// let mut langtag: LanguageTag = Default::default();
/// langtag.language = Some("en".to_owned()); /// langtag.language = Some("en".to_owned());
/// langtag.region = Some("US".to_owned()); /// langtag.region = Some("US".to_owned());
/// builder.insert_header( /// builder.set(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// qitem(langtag), /// qitem(langtag),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use language_tags::langtag; /// # extern crate actix_http;
/// use actix_web::HttpResponse; /// # #[macro_use] extern crate language_tags;
/// use actix_web::http::header::{AcceptLanguage, QualityItem, q, qitem}; /// use actix_http::Response;
/// /// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem};
/// let mut builder = HttpResponse::Ok(); /// #
/// builder.insert_header( /// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// AcceptLanguage(vec![ /// AcceptLanguage(vec![
/// qitem(langtag!(da)), /// qitem(langtag!(da)),
/// QualityItem::new(langtag!(en;;;GB), q(800)), /// QualityItem::new(langtag!(en;;;GB), q(800)),
/// QualityItem::new(langtag!(en), q(700)), /// QualityItem::new(langtag!(en), q(700)),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
(AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+ (AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+

View File

@ -1,7 +1,7 @@
use actix_http::http::Method; use http::header;
use crate::http::header; use http::Method;
crate::header! { header! {
/// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1) /// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1)
/// ///
/// The `Allow` header field lists the set of methods advertised as /// The `Allow` header field lists the set of methods advertised as
@ -22,28 +22,38 @@ crate::header! {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// # extern crate http;
/// use actix_web::http::{header::Allow, Method}; /// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::header::Allow;
/// use http::Method;
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// builder.insert_header( /// let mut builder = Response::Ok();
/// builder.set(
/// Allow(vec![Method::GET]) /// Allow(vec![Method::GET])
/// ); /// );
/// # }
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// # extern crate http;
/// use actix_web::http::{header::Allow, Method}; /// # extern crate actix_http;
/// use actix_http::Response;
/// use actix_http::http::header::Allow;
/// use http::Method;
/// ///
/// let mut builder = HttpResponse::Ok(); /// # fn main() {
/// builder.insert_header( /// let mut builder = Response::Ok();
/// builder.set(
/// Allow(vec![ /// Allow(vec![
/// Method::GET, /// Method::GET,
/// Method::POST, /// Method::POST,
/// Method::PATCH, /// Method::PATCH,
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
(Allow, header::ALLOW) => (Method)* (Allow, header::ALLOW) => (Method)*

View File

@ -1,12 +1,12 @@
use std::fmt::{self, Write}; use std::fmt::{self, Write};
use std::str::FromStr; use std::str::FromStr;
use super::{ use http::header;
use crate::header::{
fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer, fmt_comma_delimited, from_comma_delimited, Header, IntoHeaderValue, Writer,
}; };
use crate::http::header;
/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2) /// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
/// ///
/// The `Cache-Control` header field is used to specify directives for /// The `Cache-Control` header field is used to specify directives for
@ -28,20 +28,20 @@ use crate::http::header;
/// * `max-age=30` /// * `max-age=30`
/// ///
/// # Examples /// # Examples
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{CacheControl, CacheDirective}; /// use actix_http::http::header::{CacheControl, CacheDirective};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)])); /// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{CacheControl, CacheDirective}; /// use actix_http::http::header::{CacheControl, CacheDirective};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header(CacheControl(vec![ /// builder.set(CacheControl(vec![
/// CacheDirective::NoCache, /// CacheDirective::NoCache,
/// CacheDirective::Private, /// CacheDirective::Private,
/// CacheDirective::MaxAge(360u32), /// CacheDirective::MaxAge(360u32),
@ -82,7 +82,7 @@ impl fmt::Display for CacheControl {
impl IntoHeaderValue for CacheControl { impl IntoHeaderValue for CacheControl {
type Error = header::InvalidHeaderValue; 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 mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take()) header::HeaderValue::from_maybe_shared(writer.take())
@ -191,13 +191,12 @@ impl FromStr for CacheDirective {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::header::Header; use crate::header::Header;
use actix_http::test::TestRequest; use crate::test::TestRequest;
#[test] #[test]
fn test_parse_multiple_headers() { fn test_parse_multiple_headers() {
let req = TestRequest::default() let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private")
.insert_header((header::CACHE_CONTROL, "no-cache, private"))
.finish(); .finish();
let cache = Header::parse(&req); let cache = Header::parse(&req);
assert_eq!( assert_eq!(
@ -211,9 +210,9 @@ mod tests {
#[test] #[test]
fn test_parse_argument() { fn test_parse_argument() {
let req = TestRequest::default() let req =
.insert_header((header::CACHE_CONTROL, "max-age=100, private")) TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private")
.finish(); .finish();
let cache = Header::parse(&req); let cache = Header::parse(&req);
assert_eq!( assert_eq!(
cache.ok(), cache.ok(),
@ -226,9 +225,8 @@ mod tests {
#[test] #[test]
fn test_parse_quote_form() { fn test_parse_quote_form() {
let req = TestRequest::default() let req =
.insert_header((header::CACHE_CONTROL, "max-age=\"200\"")) TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish();
.finish();
let cache = Header::parse(&req); let cache = Header::parse(&req);
assert_eq!( assert_eq!(
cache.ok(), cache.ok(),
@ -238,9 +236,8 @@ mod tests {
#[test] #[test]
fn test_parse_extension() { fn test_parse_extension() {
let req = TestRequest::default() let req =
.insert_header((header::CACHE_CONTROL, "foo, bar=baz")) TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish();
.finish();
let cache = Header::parse(&req); let cache = Header::parse(&req);
assert_eq!( assert_eq!(
cache.ok(), cache.ok(),
@ -253,9 +250,7 @@ mod tests {
#[test] #[test]
fn test_parse_bad_syntax() { fn test_parse_bad_syntax() {
let req = TestRequest::default() let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish();
.insert_header((header::CACHE_CONTROL, "foo="))
.finish();
let cache: Result<CacheControl, _> = Header::parse(&req); let cache: Result<CacheControl, _> = Header::parse(&req);
assert_eq!(cache.ok(), None) assert_eq!(cache.ok(), None)
} }

View File

@ -1,17 +1,16 @@
//! # References // # References
//! //
//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt // "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt // "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt // "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/ // Browser conformance tests at: http://greenbytes.de/tech/tc2231/
//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml // IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
use once_cell::sync::Lazy; use lazy_static::lazy_static;
use regex::Regex; use regex::Regex;
use std::fmt::{self, Write}; use std::fmt::{self, Write};
use crate::http::header; use crate::header::{self, ExtendedValue, Header, IntoHeaderValue, Writer};
use super::{ExtendedValue, Header, IntoHeaderValue, Writer};
/// Split at the index of the first `needle` if it exists or at the end. /// Split at the index of the first `needle` if it exists or at the end.
fn split_once(haystack: &str, needle: char) -> (&str, &str) { fn split_once(haystack: &str, needle: char) -> (&str, &str) {
@ -64,7 +63,7 @@ impl<'a> From<&'a str> for DispositionType {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_web::http::header::DispositionParam; /// use actix_http::http::header::DispositionParam;
/// ///
/// let param = DispositionParam::Filename(String::from("sample.txt")); /// let param = DispositionParam::Filename(String::from("sample.txt"));
/// assert!(param.is_filename()); /// assert!(param.is_filename());
@ -241,7 +240,7 @@ impl DispositionParam {
/// # Example /// # Example
/// ///
/// ``` /// ```
/// use actix_web::http::header::{ /// use actix_http::http::header::{
/// Charset, ContentDisposition, DispositionParam, DispositionType, /// Charset, ContentDisposition, DispositionParam, DispositionType,
/// ExtendedValue, /// ExtendedValue,
/// }; /// };
@ -319,8 +318,9 @@ impl ContentDisposition {
return Err(crate::error::ParseError::Header); return Err(crate::error::ParseError::Header);
} }
left = new_left; left = new_left;
if let Some(param_name) = param_name.strip_suffix('*') { if param_name.ends_with('*') {
// extended parameters // extended parameters
let param_name = &param_name[..param_name.len() - 1]; // trim asterisk
let (ext_value, new_left) = split_once_and_trim(left, ';'); let (ext_value, new_left) = split_once_and_trim(left, ';');
left = new_left; left = new_left;
let ext_value = header::parse_extended_value(ext_value)?; let ext_value = header::parse_extended_value(ext_value)?;
@ -455,7 +455,7 @@ impl ContentDisposition {
impl IntoHeaderValue for ContentDisposition { impl IntoHeaderValue for ContentDisposition {
type Error = header::InvalidHeaderValue; 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 mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
header::HeaderValue::from_maybe_shared(writer.take()) header::HeaderValue::from_maybe_shared(writer.take())
@ -521,7 +521,9 @@ impl fmt::Display for DispositionParam {
// //
// //
// See also comments in test_from_raw_unnecessary_percent_decode. // See also comments in test_from_raw_unnecessary_percent_decode.
static RE: Lazy<Regex> = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap()); lazy_static! {
static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
}
match self { match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Name(ref value) => write!(f, "name={}", value),
DispositionParam::Filename(ref value) => { DispositionParam::Filename(ref value) => {
@ -548,15 +550,16 @@ impl fmt::Display for ContentDisposition {
write!(f, "{}", self.disposition)?; write!(f, "{}", self.disposition)?;
self.parameters self.parameters
.iter() .iter()
.try_for_each(|param| write!(f, "; {}", param)) .map(|param| write!(f, "; {}", param))
.collect()
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::{ContentDisposition, DispositionParam, DispositionType}; use super::{ContentDisposition, DispositionParam, DispositionType};
use crate::http::header::Charset; use crate::header::shared::Charset;
use crate::http::header::{ExtendedValue, HeaderValue}; use crate::header::{ExtendedValue, HeaderValue};
#[test] #[test]
fn test_from_raw_basic() { fn test_from_raw_basic() {

View File

@ -1,7 +1,7 @@
use super::{QualityItem, CONTENT_LANGUAGE}; use crate::header::{QualityItem, CONTENT_LANGUAGE};
use language_tags::LanguageTag; use language_tags::LanguageTag;
crate::header! { header! {
/// `Content-Language` header, defined in /// `Content-Language` header, defined in
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2) /// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2)
/// ///
@ -23,31 +23,38 @@ crate::header! {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use language_tags::langtag; /// # extern crate actix_http;
/// use actix_web::HttpResponse; /// # #[macro_use] extern crate language_tags;
/// use actix_web::http::header::{ContentLanguage, qitem}; /// use actix_http::Response;
/// /// # use actix_http::http::header::{ContentLanguage, qitem};
/// let mut builder = HttpResponse::Ok(); /// #
/// builder.insert_header( /// # fn main() {
/// let mut builder = Response::Ok();
/// builder.set(
/// ContentLanguage(vec![ /// ContentLanguage(vec![
/// qitem(langtag!(en)), /// qitem(langtag!(en)),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use language_tags::langtag; /// # extern crate actix_http;
/// use actix_web::HttpResponse; /// # #[macro_use] extern crate language_tags;
/// use actix_web::http::header::{ContentLanguage, qitem}; /// use actix_http::Response;
/// # use actix_http::http::header::{ContentLanguage, qitem};
/// #
/// # fn main() {
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(
/// ContentLanguage(vec![ /// ContentLanguage(vec![
/// qitem(langtag!(da)), /// qitem(langtag!(da)),
/// qitem(langtag!(en;;;GB)), /// qitem(langtag!(en;;;GB)),
/// ]) /// ])
/// ); /// );
/// # }
/// ``` /// ```
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+ (ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+

View File

@ -2,11 +2,11 @@ use std::fmt::{self, Display, Write};
use std::str::FromStr; use std::str::FromStr;
use crate::error::ParseError; use crate::error::ParseError;
use super::{ use crate::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE, HeaderValue, IntoHeaderValue, InvalidHeaderValue, Writer, CONTENT_RANGE,
}; };
crate::header! { header! {
/// `Content-Range` header, defined in /// `Content-Range` header, defined in
/// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2) /// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2)
(ContentRange, CONTENT_RANGE) => [ContentRangeSpec] (ContentRange, CONTENT_RANGE) => [ContentRangeSpec]
@ -200,7 +200,7 @@ impl Display for ContentRangeSpec {
impl IntoHeaderValue for ContentRangeSpec { impl IntoHeaderValue for ContentRangeSpec {
type Error = InvalidHeaderValue; 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 mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take()) HeaderValue::from_maybe_shared(writer.take())

View File

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

View File

@ -1,7 +1,7 @@
use super::{HttpDate, DATE}; use crate::header::{HttpDate, DATE};
use std::time::SystemTime; use std::time::SystemTime;
crate::header! { header! {
/// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2) /// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2)
/// ///
/// The `Date` header field represents the date and time at which the /// The `Date` header field represents the date and time at which the
@ -19,15 +19,13 @@ crate::header! {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::Date;
/// use std::time::SystemTime; /// use std::time::SystemTime;
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Date;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(Date(SystemTime::now().into()));
/// Date(SystemTime::now().into())
/// );
/// ``` /// ```
(Date, DATE) => [HttpDate] (Date, DATE) => [HttpDate]

View File

@ -1,6 +1,6 @@
use super::{EntityTag, ETAG}; use crate::header::{EntityTag, ETAG};
crate::header! { header! {
/// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3) /// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3)
/// ///
/// The `ETag` header field in a response provides the current entity-tag /// The `ETag` header field in a response provides the current entity-tag
@ -27,24 +27,20 @@ crate::header! {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{ETag, EntityTag}; /// use actix_http::http::header::{ETag, EntityTag};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned())));
/// ETag(EntityTag::new(false, "xyzzy".to_owned()))
/// );
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{ETag, EntityTag}; /// use actix_http::http::header::{ETag, EntityTag};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned())));
/// ETag(EntityTag::new(true, "xyzzy".to_owned()))
/// );
/// ``` /// ```
(ETag, ETAG) => [EntityTag] (ETag, ETAG) => [EntityTag]

View File

@ -1,6 +1,6 @@
use super::{HttpDate, EXPIRES}; use crate::header::{HttpDate, EXPIRES};
crate::header! { header! {
/// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3) /// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3)
/// ///
/// The `Expires` header field gives the date/time after which the /// The `Expires` header field gives the date/time after which the
@ -21,16 +21,14 @@ crate::header! {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::Expires;
/// use std::time::{SystemTime, Duration}; /// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::Expires;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24); /// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
/// builder.insert_header( /// builder.set(Expires(expiration.into()));
/// Expires(expiration.into())
/// );
/// ``` /// ```
(Expires, EXPIRES) => [HttpDate] (Expires, EXPIRES) => [HttpDate]

View File

@ -1,6 +1,6 @@
use super::{EntityTag, IF_MATCH}; use crate::header::{EntityTag, IF_MATCH};
crate::header! { header! {
/// `If-Match` header, defined in /// `If-Match` header, defined in
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1) /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1)
/// ///
@ -29,20 +29,20 @@ crate::header! {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::IfMatch; /// use actix_http::http::header::IfMatch;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header(IfMatch::Any); /// builder.set(IfMatch::Any);
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{IfMatch, EntityTag}; /// use actix_http::http::header::{IfMatch, EntityTag};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(
/// IfMatch::Items(vec![ /// IfMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()), /// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()), /// EntityTag::new(false, "foobar".to_owned()),

View File

@ -1,6 +1,6 @@
use super::{HttpDate, IF_MODIFIED_SINCE}; use crate::header::{HttpDate, IF_MODIFIED_SINCE};
crate::header! { header! {
/// `If-Modified-Since` header, defined in /// `If-Modified-Since` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3) /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3)
/// ///
@ -21,16 +21,14 @@ crate::header! {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfModifiedSince;
/// use std::time::{SystemTime, Duration}; /// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfModifiedSince;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header( /// builder.set(IfModifiedSince(modified.into()));
/// IfModifiedSince(modified.into())
/// );
/// ``` /// ```
(IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate] (IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate]

View File

@ -1,6 +1,6 @@
use super::{EntityTag, IF_NONE_MATCH}; use crate::header::{EntityTag, IF_NONE_MATCH};
crate::header! { header! {
/// `If-None-Match` header, defined in /// `If-None-Match` header, defined in
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2) /// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2)
/// ///
@ -31,20 +31,20 @@ crate::header! {
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::IfNoneMatch; /// use actix_http::http::header::IfNoneMatch;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header(IfNoneMatch::Any); /// builder.set(IfNoneMatch::Any);
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{IfNoneMatch, EntityTag}; /// use actix_http::http::header::{IfNoneMatch, EntityTag};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(
/// IfNoneMatch::Items(vec![ /// IfNoneMatch::Items(vec![
/// EntityTag::new(false, "xyzzy".to_owned()), /// EntityTag::new(false, "xyzzy".to_owned()),
/// EntityTag::new(false, "foobar".to_owned()), /// EntityTag::new(false, "foobar".to_owned()),
@ -66,22 +66,20 @@ crate::header! {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::IfNoneMatch; use super::IfNoneMatch;
use crate::http::header::{EntityTag, Header, IF_NONE_MATCH}; use crate::header::{EntityTag, Header, IF_NONE_MATCH};
use actix_http::test::TestRequest; use crate::test::TestRequest;
#[test] #[test]
fn test_if_none_match() { fn test_if_none_match() {
let mut if_none_match: Result<IfNoneMatch, _>; let mut if_none_match: Result<IfNoneMatch, _>;
let req = TestRequest::default() let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish();
.insert_header((IF_NONE_MATCH, "*"))
.finish();
if_none_match = Header::parse(&req); if_none_match = Header::parse(&req);
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any)); assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
let req = TestRequest::default() let req =
.insert_header((IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])) TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])
.finish(); .finish();
if_none_match = Header::parse(&req); if_none_match = Header::parse(&req);
let mut entities: Vec<EntityTag> = Vec::new(); let mut entities: Vec<EntityTag> = Vec::new();

View File

@ -1,12 +1,11 @@
use std::fmt::{self, Display, Write}; use std::fmt::{self, Display, Write};
use crate::http::header;
use crate::error::ParseError; use crate::error::ParseError;
use super::{ use crate::header::{
from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate, self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate,
IntoHeaderValue, InvalidHeaderValue, Writer, IntoHeaderValue, InvalidHeaderValue, Writer,
}; };
use crate::HttpMessage; use crate::httpmessage::HttpMessage;
/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2) /// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2)
/// ///
@ -36,34 +35,31 @@ use crate::HttpMessage;
/// ///
/// # Examples /// # Examples
/// ///
/// ``` /// ```rust
/// use actix_web::HttpResponse; /// use actix_http::Response;
/// use actix_web::http::header::{EntityTag, IfRange}; /// use actix_http::http::header::{EntityTag, IfRange};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// builder.insert_header( /// builder.set(IfRange::EntityTag(EntityTag::new(
/// IfRange::EntityTag( /// false,
/// EntityTag::new(false, "abc".to_owned()) /// "xyzzy".to_owned(),
/// ) /// )));
/// );
/// ``` /// ```
/// ///
/// ``` /// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfRange;
/// use std::time::{Duration, SystemTime}; /// use std::time::{Duration, SystemTime};
/// use actix_web::{http::header::IfRange, HttpResponse};
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header( /// builder.set(IfRange::Date(fetched.into()));
/// IfRange::Date(fetched.into())
/// );
/// ``` /// ```
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub enum IfRange { pub enum IfRange {
/// The entity-tag the client has of the resource. /// The entity-tag the client has of the resource
EntityTag(EntityTag), EntityTag(EntityTag),
/// The date when the client retrieved the resource
/// The date when the client retrieved the resource.
Date(HttpDate), Date(HttpDate),
} }
@ -102,7 +98,7 @@ impl Display for IfRange {
impl IntoHeaderValue for IfRange { impl IntoHeaderValue for IfRange {
type Error = InvalidHeaderValue; 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 mut writer = Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
HeaderValue::from_maybe_shared(writer.take()) HeaderValue::from_maybe_shared(writer.take())
@ -112,10 +108,9 @@ impl IntoHeaderValue for IfRange {
#[cfg(test)] #[cfg(test)]
mod test_if_range { mod test_if_range {
use super::IfRange as HeaderField; use super::IfRange as HeaderField;
use crate::http::header::*; use crate::header::*;
use std::str; use std::str;
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]); 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>); test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
} }

View File

@ -1,6 +1,6 @@
use super::{HttpDate, IF_UNMODIFIED_SINCE}; use crate::header::{HttpDate, IF_UNMODIFIED_SINCE};
crate::header! { header! {
/// `If-Unmodified-Since` header, defined in /// `If-Unmodified-Since` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4) /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4)
/// ///
@ -22,16 +22,14 @@ crate::header! {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::IfUnmodifiedSince;
/// use std::time::{SystemTime, Duration}; /// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::IfUnmodifiedSince;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header( /// builder.set(IfUnmodifiedSince(modified.into()));
/// IfUnmodifiedSince(modified.into())
/// );
/// ``` /// ```
(IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate] (IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate]

View File

@ -1,6 +1,6 @@
use super::{HttpDate, LAST_MODIFIED}; use crate::header::{HttpDate, LAST_MODIFIED};
crate::header! { header! {
/// `Last-Modified` header, defined in /// `Last-Modified` header, defined in
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2) /// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2)
/// ///
@ -21,16 +21,14 @@ crate::header! {
/// ///
/// # Example /// # Example
/// ///
/// ``` /// ```rust
/// use actix_http::Response;
/// use actix_http::http::header::LastModified;
/// use std::time::{SystemTime, Duration}; /// use std::time::{SystemTime, Duration};
/// use actix_web::HttpResponse;
/// use actix_web::http::header::LastModified;
/// ///
/// let mut builder = HttpResponse::Ok(); /// let mut builder = Response::Ok();
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24); /// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
/// builder.insert_header( /// builder.set(LastModified(modified.into()));
/// LastModified(modified.into())
/// );
/// ``` /// ```
(LastModified, LAST_MODIFIED) => [HttpDate] (LastModified, LAST_MODIFIED) => [HttpDate]

View File

@ -3,14 +3,10 @@
//! ## Mime //! ## Mime
//! //!
//! Several header fields use MIME values for their contents. Keeping with the //! 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)`. //! is used, such as `ContentType(pub Mime)`.
#![cfg_attr(rustfmt, rustfmt_skip)] #![cfg_attr(rustfmt, rustfmt_skip)]
use std::fmt;
use bytes::{BytesMut, Bytes};
pub use actix_http::http::header::*;
pub use self::accept_charset::AcceptCharset; pub use self::accept_charset::AcceptCharset;
//pub use self::accept_encoding::AcceptEncoding; //pub use self::accept_encoding::AcceptEncoding;
pub use self::accept::Accept; pub use self::accept::Accept;
@ -32,39 +28,7 @@ pub use self::if_none_match::IfNoneMatch;
pub use self::if_range::IfRange; pub use self::if_range::IfRange;
pub use self::if_unmodified_since::IfUnmodifiedSince; pub use self::if_unmodified_since::IfUnmodifiedSince;
pub use self::last_modified::LastModified; pub use self::last_modified::LastModified;
pub use self::encoding::Encoding;
pub use self::entity::EntityTag;
//pub use self::range::{Range, ByteRangeSpec}; //pub use self::range::{Range, ByteRangeSpec};
pub(crate) use actix_http::http::header::{fmt_comma_delimited, from_comma_delimited, from_one_raw_str};
#[derive(Debug, Default)]
struct Writer {
buf: BytesMut,
}
impl Writer {
pub fn new() -> Writer {
Writer::default()
}
pub fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args)
}
}
#[doc(hidden)] #[doc(hidden)]
#[macro_export] #[macro_export]
@ -96,9 +60,9 @@ macro_rules! __hyper__tm {
#[cfg(test)] #[cfg(test)]
mod $tm{ mod $tm{
use std::str; use std::str;
use actix_http::http::Method; use http::Method;
use mime::*; use mime::*;
use $crate::http::header::*; use $crate::header::*;
use super::$id as HeaderField; use super::$id as HeaderField;
$($tf)* $($tf)*
} }
@ -112,13 +76,14 @@ macro_rules! test_header {
($id:ident, $raw:expr) => { ($id:ident, $raw:expr) => {
#[test] #[test]
fn $id() { fn $id() {
use actix_http::test; use super::*;
use $crate::test;
let raw = $raw; let raw = $raw;
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect(); let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default(); let mut req = test::TestRequest::default();
for item in a { for item in a {
req = req.insert_header((HeaderField::name(), item)).take(); req = req.header(HeaderField::name(), item).take();
} }
let req = req.finish(); let req = req.finish();
let value = HeaderField::parse(&req); let value = HeaderField::parse(&req);
@ -140,12 +105,12 @@ macro_rules! test_header {
($id:ident, $raw:expr, $typed:expr) => { ($id:ident, $raw:expr, $typed:expr) => {
#[test] #[test]
fn $id() { fn $id() {
use actix_http::test; use $crate::test;
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect(); let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
let mut req = test::TestRequest::default(); let mut req = test::TestRequest::default();
for item in a { for item in a {
req.insert_header((HeaderField::name(), item)); req.header(HeaderField::name(), item);
} }
let req = req.finish(); let req = req.finish();
let val = HeaderField::parse(&req); let val = HeaderField::parse(&req);
@ -168,7 +133,6 @@ macro_rules! test_header {
}; };
} }
#[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! header { macro_rules! header {
// $a:meta: Attributes associated with the header item (usually docs) // $a:meta: Attributes associated with the header item (usually docs)
@ -204,7 +168,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; 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; use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new(); let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
@ -240,7 +204,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; 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; use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new(); let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
@ -276,8 +240,8 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; 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> {
self.0.try_into_value() self.0.try_into()
} }
} }
}; };
@ -325,7 +289,7 @@ macro_rules! header {
impl $crate::http::header::IntoHeaderValue for $id { impl $crate::http::header::IntoHeaderValue for $id {
type Error = $crate::http::header::InvalidHeaderValue; 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; use std::fmt::Write;
let mut writer = $crate::http::header::Writer::new(); let mut writer = $crate::http::header::Writer::new();
let _ = write!(&mut writer, "{}", self); let _ = write!(&mut writer, "{}", self);
@ -369,7 +333,7 @@ macro_rules! header {
} }
mod accept_charset; mod accept_charset;
// mod accept_encoding; //mod accept_encoding;
mod accept; mod accept;
mod accept_language; mod accept_language;
mod allow; mod allow;
@ -387,5 +351,3 @@ mod if_none_match;
mod if_range; mod if_range;
mod if_unmodified_since; mod if_unmodified_since;
mod last_modified; mod last_modified;
mod encoding;
mod entity;

View File

@ -1,8 +1,8 @@
use std::fmt::{self, Display}; use std::fmt::{self, Display};
use std::str::FromStr; use std::str::FromStr;
use super::parsing::from_one_raw_str; use header::parsing::from_one_raw_str;
use super::{Header, Raw}; use header::{Header, Raw};
/// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1) /// `Range` header, defined in [RFC7233](https://tools.ietf.org/html/rfc7233#section-3.1)
/// ///

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,34 +1,35 @@
//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other //! Various http headers
//! header utility methods. // This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header)
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}; use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; pub use http::header::*;
use crate::error::ParseError; use crate::error::ParseError;
use crate::HttpMessage; use crate::httpmessage::HttpMessage;
mod as_name;
mod into_pair;
mod into_value;
mod utils;
mod common;
pub(crate) mod map; pub(crate) mod map;
mod shared; mod shared;
pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
pub use self::as_name::AsHeaderName;
pub use self::into_pair::IntoHeaderPair;
pub use self::into_value::IntoHeaderValue;
#[doc(hidden)] #[doc(hidden)]
pub use self::map::GetAll; pub use self::map::GetAll;
pub use self::map::HeaderMap; pub use self::map::HeaderMap;
pub use self::utils::*;
/// A trait for any object that already represents a valid header field and value. /// A trait for any object that will represent a header field and value.
pub trait Header: IntoHeaderValue { pub trait Header
where
Self: IntoHeaderValue,
{
/// Returns the name of the header field /// Returns the name of the header field
fn name() -> HeaderName; fn name() -> HeaderName;
@ -36,15 +37,358 @@ pub trait Header: IntoHeaderValue {
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
/// Convert `http::HeaderMap` to our `HeaderMap`. /// A trait for any object that can be Converted to a `HeaderValue`
impl From<http::HeaderMap> for HeaderMap { pub trait IntoHeaderValue: Sized {
fn from(mut map: http::HeaderMap) -> HeaderMap { /// The type returned in the event of a conversion error.
HeaderMap::from_drain(map.drain()) 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)
} }
} }
/// This encode set is used for HTTP header values and is defined at impl<'a> IntoHeaderValue for &'a str {
/// https://tools.ietf.org/html/rfc5987#section-3.2. 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 {
buf: BytesMut::new(),
}
}
fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args)
}
}
#[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)
}
}
}
/// 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 pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ') .add(b' ')
.add(b'"') .add(b'"')
@ -66,3 +410,91 @@ pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b']') .add(b']')
.add(b'{') .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

@ -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()))
}
}

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