mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-04 01:51:30 +02:00
Compare commits
120 Commits
files-v0.6
...
web-v4.0.0
Author | SHA1 | Date | |
---|---|---|---|
b62da7e86b | |||
5e9a3eb6ae | |||
3451d6874f | |||
18c3783a1c | |||
4b46351d36 | |||
b7c406637d | |||
c4e5651215 | |||
23b0e64199 | |||
fc31b091e4 | |||
effacf8fc8 | |||
95130fcfd0 | |||
5e81105317 | |||
5b4105e1e6 | |||
2d3a0d6038 | |||
fe0b3f459f | |||
ca69b6577e | |||
880b863f95 | |||
78384c3ff5 | |||
c1c4400c4a | |||
fc6f974617 | |||
14b249b804 | |||
0195824794 | |||
fb019f15b4 | |||
abc7fd374b | |||
cd652dca75 | |||
c836de44af | |||
badae2f8fd | |||
1f34718ecd | |||
ebda60fd6b | |||
d242f57758 | |||
b95e1dda34 | |||
8f2a97c6e3 | |||
ebaf25d55a | |||
42711c23d7 | |||
f6393728c7 | |||
d92ab7e8e0 | |||
5845b3965c | |||
aacec30ad1 | |||
2dbdf61c37 | |||
83365058ce | |||
3b93c62e23 | |||
946cccaa1a | |||
1838d9cd0f | |||
f62a982a51 | |||
dfd9dc40ea | |||
5efea652e3 | |||
dfa795ff9d | |||
2cc6b47fcf | |||
117025a96b | |||
3e0a9b99ff | |||
17b3e7e225 | |||
c065729468 | |||
55db3ec65c | |||
0404b78b54 | |||
68d1bd88b1 | |||
308b70b039 | |||
7fa6333a0c | |||
3279070f9f | |||
b37669cb3b | |||
1e538bf73e | |||
366c032c36 | |||
95113ad12f | |||
ce9b2770e2 | |||
4fc7d76759 | |||
81bef93e5e | |||
31d9ed81c5 | |||
c1af5089b9 | |||
77efc09362 | |||
871ca5e4ae | |||
ceace26ed4 | |||
75a9a72e78 | |||
d9d0d1d1a2 | |||
ea5ce3befb | |||
e18464b274 | |||
bd26083f33 | |||
991363a104 | |||
a290e58982 | |||
dcad9724bc | |||
949d14ae2b | |||
a6ed4aee84 | |||
519d7f2b8a | |||
dddb623a11 | |||
266cf0622c | |||
9604e249c9 | |||
dbc47c9122 | |||
4c243cbf89 | |||
deafb7c8b8 | |||
50309aa295 | |||
9eaea6a2fd | |||
830fb2cdb2 | |||
7cfed73be8 | |||
41bc04b1c4 | |||
20cf0094e5 | |||
83fb4978ad | |||
51e54dac8b | |||
c201c15f8c | |||
0c8196f8b0 | |||
ee10148444 | |||
1c95fc2654 | |||
da69bb4d12 | |||
0a506bf2e9 | |||
b2a9ba2ee4 | |||
f976150b67 | |||
b1dd8d28bc | |||
4edeb5ce47 | |||
d34a8689e5 | |||
a919d2de56 | |||
46a8f28b74 | |||
57398c6df1 | |||
7affc6878e | |||
46b2f7eaaf | |||
9e401b6ef7 | |||
fe392abeb4 | |||
f6cc829758 | |||
6575ee93f2 | |||
530d03791d | |||
d40ae8c8ca | |||
2204614134 | |||
188ee44f81 | |||
a4c9aaf337 |
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
10
.github/PULL_REQUEST_TEMPLATE.md
vendored
@ -1,21 +1,21 @@
|
||||
<!-- Thanks for considering contributing actix! -->
|
||||
<!-- Please fill out the following to make our reviews easy. -->
|
||||
<!-- Please fill out the following to get your PR reviewed quicker. -->
|
||||
|
||||
## PR Type
|
||||
<!-- What kind of change does this PR make? -->
|
||||
<!-- Bug Fix / Feature / Refactor / Code Style / Other -->
|
||||
INSERT_PR_TYPE
|
||||
PR_TYPE
|
||||
|
||||
|
||||
## PR Checklist
|
||||
Check your PR fulfills the following:
|
||||
|
||||
<!-- Check your PR fulfills the following items. ->>
|
||||
<!-- For draft PRs check the boxes as you complete them. -->
|
||||
|
||||
- [ ] Tests for the changes have been added / updated.
|
||||
- [ ] Documentation comments have been added / updated.
|
||||
- [ ] A changelog entry has been made for the appropriate packages.
|
||||
- [ ] Format code with the latest stable rustfmt
|
||||
- [ ] Format code with the latest stable rustfmt.
|
||||
- [ ] (Team) Label with affected crates and semver status.
|
||||
|
||||
|
||||
## Overview
|
||||
|
2
.github/workflows/bench.yml
vendored
2
.github/workflows/bench.yml
vendored
@ -1,4 +1,4 @@
|
||||
name: Benchmark (Linux)
|
||||
name: Benchmark
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
|
@ -1,24 +1,27 @@
|
||||
name: CI (Linux)
|
||||
name: CI
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
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.version }} - x86_64-unknown-linux-gnu
|
||||
runs-on: ubuntu-latest
|
||||
name: ${{ matrix.target.name }} / ${{ matrix.version }}
|
||||
runs-on: ${{ matrix.target.os }}
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -26,7 +29,7 @@ jobs:
|
||||
- name: Install ${{ matrix.version }}
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: ${{ matrix.version }}-x86_64-unknown-linux-gnu
|
||||
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
@ -35,20 +38,33 @@ jobs:
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
uses: Swatinem/rust-cache@v1.2.0
|
||||
|
||||
- name: check build
|
||||
- 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: --clean-per-run check --workspace --no-default-features --tests
|
||||
|
||||
- name: check full
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: check
|
||||
args: --all --bins --examples --tests
|
||||
args: --workspace --bins --examples --tests
|
||||
|
||||
- name: tests
|
||||
uses: actions-rs/cargo@v1
|
||||
timeout-minutes: 40
|
||||
with:
|
||||
command: test
|
||||
args: --all --all-features --no-fail-fast -- --nocapture
|
||||
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
|
||||
@ -65,12 +81,18 @@ jobs:
|
||||
args: --package=awc --no-default-features --features=rustls -- --nocapture
|
||||
|
||||
- name: Generate coverage file
|
||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
||||
if: >
|
||||
matrix.target.os == 'ubuntu-latest'
|
||||
&& matrix.version == 'stable'
|
||||
&& github.ref == 'refs/heads/master'
|
||||
run: |
|
||||
cargo install cargo-tarpaulin --vers "^0.13"
|
||||
cargo tarpaulin --out Xml
|
||||
cargo tarpaulin --out Xml --verbose
|
||||
- name: Upload to Codecov
|
||||
if: matrix.version == 'stable' && github.ref == 'refs/heads/master'
|
||||
if: >
|
||||
matrix.target.os == 'ubuntu-latest'
|
||||
&& matrix.version == 'stable'
|
||||
&& github.ref == 'refs/heads/master'
|
||||
uses: codecov/codecov-action@v1
|
||||
with:
|
||||
file: cobertura.xml
|
21
.github/workflows/clippy-fmt.yml
vendored
21
.github/workflows/clippy-fmt.yml
vendored
@ -1,32 +1,39 @@
|
||||
name: Lint
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
types: [opened, synchronize, reopened]
|
||||
|
||||
name: Clippy and rustfmt Check
|
||||
jobs:
|
||||
clippy_check:
|
||||
fmt:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: stable
|
||||
components: rustfmt
|
||||
override: true
|
||||
- name: Check with rustfmt
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: fmt
|
||||
args: --all -- --check
|
||||
|
||||
- uses: actions-rs/toolchain@v1
|
||||
clippy:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
|
||||
- name: Install Rust
|
||||
uses: actions-rs/toolchain@v1
|
||||
with:
|
||||
toolchain: nightly
|
||||
toolchain: stable
|
||||
components: clippy
|
||||
override: true
|
||||
- name: Check with Clippy
|
||||
uses: actions-rs/clippy-check@v1
|
||||
with:
|
||||
token: ${{ secrets.GITHUB_TOKEN }}
|
||||
args: --all-features --all --tests
|
||||
args: --workspace --tests --all-features
|
||||
|
56
.github/workflows/macos.yml
vendored
56
.github/workflows/macos.yml
vendored
@ -1,56 +0,0 @@
|
||||
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: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
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
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
12
.github/workflows/upload-doc.yml
vendored
12
.github/workflows/upload-doc.yml
vendored
@ -1,14 +1,12 @@
|
||||
name: Upload documentation
|
||||
name: Upload Documentation
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- master
|
||||
branches: [master]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
if: github.repository == 'actix/actix-web'
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
@ -20,14 +18,14 @@ jobs:
|
||||
profile: minimal
|
||||
override: true
|
||||
|
||||
- name: check build
|
||||
- name: Build Docs
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: doc
|
||||
args: --no-deps --workspace --all-features
|
||||
args: --workspace --all-features --no-deps
|
||||
|
||||
- name: Tweak HTML
|
||||
run: echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html
|
||||
run: echo '<meta http-equiv="refresh" content="0;url=actix_web/index.html">' > target/doc/index.html
|
||||
|
||||
- name: Deploy to GitHub Pages
|
||||
uses: JamesIves/github-pages-deploy-action@3.7.1
|
||||
|
76
.github/workflows/windows.yml
vendored
76
.github/workflows/windows.yml
vendored
@ -1,76 +0,0 @@
|
||||
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: Generate Cargo.lock
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
command: generate-lockfile
|
||||
- name: Cache Dependencies
|
||||
uses: Swatinem/rust-cache@v1.0.1
|
||||
|
||||
- name: check build
|
||||
uses: actions-rs/cargo@v1
|
||||
with:
|
||||
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
|
||||
|
||||
- name: Clear the cargo caches
|
||||
run: |
|
||||
cargo install cargo-cache --no-default-features --features ci-autoclean
|
||||
cargo-cache
|
74
CHANGES.md
74
CHANGES.md
@ -3,6 +3,60 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 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
|
||||
@ -24,13 +78,15 @@
|
||||
### Removed
|
||||
* Public modules `middleware::{normalize, err_handlers}`. All necessary middleware structs are now
|
||||
exposed directly by the `middleware` module.
|
||||
* Remove `actix-threadpool` as dependency. `actix_threadpool::BlockingError` error type can be imported
|
||||
from `actix_web::error` module. [#1878]
|
||||
|
||||
[#1812]: https://github.com/actix/actix-web/pull/1812
|
||||
[#1813]: https://github.com/actix/actix-web/pull/1813
|
||||
[#1852]: https://github.com/actix/actix-web/pull/1852
|
||||
[#1865]: https://github.com/actix/actix-web/pull/1865
|
||||
[#1875]: https://github.com/actix/actix-web/pull/1875
|
||||
|
||||
[#1878]: https://github.com/actix/actix-web/pull/1878
|
||||
|
||||
## 3.3.2 - 2020-12-01
|
||||
### Fixed
|
||||
@ -115,7 +171,7 @@
|
||||
|
||||
## 3.0.0-beta.4 - 2020-09-09
|
||||
### Added
|
||||
* `middleware::NormalizePath` now has configurable behaviour for either always having a trailing
|
||||
* `middleware::NormalizePath` now has configurable behavior for either always having a trailing
|
||||
slash, or as the new addition, always trimming trailing slashes. [#1639]
|
||||
|
||||
### Changed
|
||||
@ -443,7 +499,7 @@
|
||||
|
||||
## [1.0.0-rc] - 2019-05-18
|
||||
|
||||
### Add
|
||||
### Added
|
||||
|
||||
* Add `Query<T>::from_query()` to extract parameters from a query string. #846
|
||||
* `QueryConfig`, similar to `JsonConfig` for customizing error handling of query extractors.
|
||||
@ -459,7 +515,7 @@
|
||||
|
||||
## [1.0.0-beta.4] - 2019-05-12
|
||||
|
||||
### Add
|
||||
### Added
|
||||
|
||||
* Allow to set/override app data on scope level
|
||||
|
||||
@ -485,7 +541,7 @@
|
||||
|
||||
* CORS handling without headers #702
|
||||
|
||||
* Allow to construct `Data` instances to avoid double `Arc` for `Send + Sync` types.
|
||||
* Allow constructing `Data` instances to avoid double `Arc` for `Send + Sync` types.
|
||||
|
||||
### Fixed
|
||||
|
||||
@ -549,7 +605,7 @@
|
||||
|
||||
### Changed
|
||||
|
||||
* Allow to use any service as default service.
|
||||
* Allow using any service as default service.
|
||||
|
||||
* Remove generic type for request payload, always use default.
|
||||
|
||||
@ -612,13 +668,13 @@
|
||||
|
||||
### Added
|
||||
|
||||
* rustls support
|
||||
* Rustls support
|
||||
|
||||
### 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
|
||||
|
||||
|
71
Cargo.toml
71
Cargo.toml
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "4.0.0-beta.1"
|
||||
version = "4.0.0-beta.4"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
|
||||
readme = "README.md"
|
||||
@ -15,6 +15,7 @@ license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
# features that docs.rs will build with
|
||||
features = ["openssl", "rustls", "compress", "secure-cookies"]
|
||||
|
||||
[badges]
|
||||
@ -38,19 +39,22 @@ members = [
|
||||
]
|
||||
|
||||
[features]
|
||||
default = ["compress"]
|
||||
default = ["compress", "cookies"]
|
||||
|
||||
# content-encoding support
|
||||
compress = ["actix-http/compress", "awc/compress"]
|
||||
|
||||
# sessions feature
|
||||
# support for cookies
|
||||
cookies = ["actix-http/cookies", "awc/cookies"]
|
||||
|
||||
# secure cookies feature
|
||||
secure-cookies = ["actix-http/secure-cookies"]
|
||||
|
||||
# openssl
|
||||
openssl = ["actix-tls/accept", "actix-tls/openssl", "awc/openssl", "open-ssl"]
|
||||
openssl = ["tls-openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"]
|
||||
|
||||
# rustls
|
||||
rustls = ["actix-tls/accept", "actix-tls/rustls", "awc/rustls", "rust-tls"]
|
||||
rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"]
|
||||
|
||||
[[example]]
|
||||
name = "basic"
|
||||
@ -62,7 +66,7 @@ required-features = ["compress"]
|
||||
|
||||
[[test]]
|
||||
name = "test_server"
|
||||
required-features = ["compress"]
|
||||
required-features = ["compress", "cookies"]
|
||||
|
||||
[[example]]
|
||||
name = "on_connect"
|
||||
@ -74,48 +78,53 @@ required-features = ["rustls"]
|
||||
|
||||
[dependencies]
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-macros = "0.1.0"
|
||||
actix-router = "0.2.4"
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-server = "2.0.0-beta.2"
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-utils = "3.0.0-beta.1"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = { version = "3.0.0-beta.2", default-features = false, optional = true }
|
||||
actix-macros = "0.2.0"
|
||||
actix-router = "0.2.7"
|
||||
actix-rt = "2.1"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-service = "2.0.0-beta.4"
|
||||
actix-utils = "3.0.0-beta.2"
|
||||
actix-tls = { version = "3.0.0-beta.4", default-features = false, optional = true }
|
||||
|
||||
actix-web-codegen = "0.4.0"
|
||||
actix-http = "3.0.0-beta.1"
|
||||
awc = { version = "3.0.0-beta.1", default-features = false }
|
||||
actix-web-codegen = "0.5.0-beta.2"
|
||||
actix-http = "3.0.0-beta.4"
|
||||
awc = { version = "3.0.0-beta.3", default-features = false }
|
||||
|
||||
ahash = "0.6"
|
||||
ahash = "0.7"
|
||||
bytes = "1"
|
||||
derive_more = "0.99.5"
|
||||
either = "1.5.3"
|
||||
encoding_rs = "0.8"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
socket2 = "0.3.16"
|
||||
pin-project = "1.0.0"
|
||||
regex = "1.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
url = "2.1"
|
||||
open-ssl = { package = "openssl", version = "0.10", optional = true }
|
||||
rust-tls = { package = "rustls", version = "0.19.0", optional = true }
|
||||
smallvec = "1.6"
|
||||
socket2 = "0.3.16"
|
||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
|
||||
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
|
||||
url = "2.1"
|
||||
|
||||
[target.'cfg(windows)'.dependencies.tls-openssl]
|
||||
version = "0.10.9"
|
||||
package = "openssl"
|
||||
features = ["vendored"]
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
actix = "0.11.0-beta.1"
|
||||
actix-http = { version = "3.0.0-beta.1", features = ["actors"] }
|
||||
rand = "0.8"
|
||||
env_logger = "0.8"
|
||||
serde_derive = "1.0"
|
||||
brotli2 = "0.3.2"
|
||||
flate2 = "1.0.13"
|
||||
criterion = "0.3"
|
||||
env_logger = "0.8"
|
||||
flate2 = "1.0.13"
|
||||
rand = "0.8"
|
||||
rcgen = "0.8"
|
||||
serde_derive = "1.0"
|
||||
|
||||
[profile.release]
|
||||
lto = true
|
||||
@ -139,3 +148,7 @@ harness = false
|
||||
[[bench]]
|
||||
name = "service"
|
||||
harness = false
|
||||
|
||||
[[bench]]
|
||||
name = "responder"
|
||||
harness = false
|
||||
|
44
README.md
44
README.md
@ -1,20 +1,19 @@
|
||||
<div align="center">
|
||||
<h1>Actix web</h1>
|
||||
<h1>Actix Web</h1>
|
||||
<p>
|
||||
<strong>Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust</strong>
|
||||
</p>
|
||||
<p>
|
||||
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://docs.rs/actix-web/3.3.2)
|
||||
[](https://docs.rs/actix-web/4.0.0-beta.4)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/3.3.2)
|
||||

|
||||
[](https://deps.rs/crate/actix-web/4.0.0-beta.4)
|
||||
<br />
|
||||
[](https://travis-ci.org/actix/actix-web)
|
||||
[](https://github.com/actix/actix-web/actions)
|
||||
[](https://codecov.io/gh/actix/actix-web)
|
||||
[](https://crates.io/crates/actix-web)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||

|
||||
[](https://discord.gg/NWpN5mmg3x)
|
||||
|
||||
</p>
|
||||
@ -32,8 +31,7 @@
|
||||
* Static assets
|
||||
* SSL support using OpenSSL or Rustls
|
||||
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
|
||||
* Includes an async [HTTP client](https://actix.rs/actix-web/actix_web/client/index.html)
|
||||
* Supports [Actix actor framework](https://github.com/actix/actix)
|
||||
* Includes an async [HTTP client](https://docs.rs/actix-web/latest/actix_web/client/index.html)
|
||||
* Runs on stable Rust 1.46+
|
||||
|
||||
## Documentation
|
||||
@ -73,18 +71,18 @@ async fn main() -> std::io::Result<()> {
|
||||
|
||||
### More examples
|
||||
|
||||
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/)
|
||||
* [Application State](https://github.com/actix/examples/tree/master/state/)
|
||||
* [JSON Handling](https://github.com/actix/examples/tree/master/json/)
|
||||
* [Multipart Streams](https://github.com/actix/examples/tree/master/multipart/)
|
||||
* [Diesel Integration](https://github.com/actix/examples/tree/master/diesel/)
|
||||
* [r2d2 Integration](https://github.com/actix/examples/tree/master/r2d2/)
|
||||
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websocket/)
|
||||
* [Tera Templates](https://github.com/actix/examples/tree/master/template_tera/)
|
||||
* [Askama Templates](https://github.com/actix/examples/tree/master/template_askama/)
|
||||
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/rustls/)
|
||||
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/openssl/)
|
||||
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websocket-chat/)
|
||||
* [Basic Setup](https://github.com/actix/examples/tree/master/basics/basics/)
|
||||
* [Application State](https://github.com/actix/examples/tree/master/basics/state/)
|
||||
* [JSON Handling](https://github.com/actix/examples/tree/master/json/json/)
|
||||
* [Multipart Streams](https://github.com/actix/examples/tree/master/forms/multipart/)
|
||||
* [Diesel Integration](https://github.com/actix/examples/tree/master/database_interactions/diesel/)
|
||||
* [r2d2 Integration](https://github.com/actix/examples/tree/master/database_interactions/r2d2/)
|
||||
* [Simple WebSocket](https://github.com/actix/examples/tree/master/websockets/websocket/)
|
||||
* [Tera Templates](https://github.com/actix/examples/tree/master/template_engines/tera/)
|
||||
* [Askama Templates](https://github.com/actix/examples/tree/master/template_engines/askama/)
|
||||
* [HTTPS using Rustls](https://github.com/actix/examples/tree/master/security/rustls/)
|
||||
* [HTTPS using OpenSSL](https://github.com/actix/examples/tree/master/security/openssl/)
|
||||
* [WebSocket Chat](https://github.com/actix/examples/tree/master/websockets/chat/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/examples/tree/master/) for more examples.
|
||||
@ -99,9 +97,9 @@ One of the fastest web frameworks available according to the
|
||||
This project is licensed under either of
|
||||
|
||||
* Apache License, Version 2.0, ([LICENSE-APACHE](LICENSE-APACHE) or
|
||||
[http://www.apache.org/licenses/LICENSE-2.0](http://www.apache.org/licenses/LICENSE-2.0))
|
||||
[http://www.apache.org/licenses/LICENSE-2.0])
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or
|
||||
[http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
[http://opensource.org/licenses/MIT])
|
||||
|
||||
at your option.
|
||||
|
||||
|
@ -3,6 +3,18 @@
|
||||
## Unreleased - 2021-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]
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-files"
|
||||
version = "0.6.0-beta.1"
|
||||
version = "0.6.0-beta.3"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Static file serving for Actix Web"
|
||||
readme = "README.md"
|
||||
@ -17,19 +17,21 @@ name = "actix_files"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[dependencies]
|
||||
actix-web = { version = "4.0.0-beta.1", default-features = false }
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-web = { version = "4.0.0-beta.4", default-features = false }
|
||||
actix-service = "2.0.0-beta.4"
|
||||
|
||||
askama_escape = "0.10"
|
||||
bitflags = "1"
|
||||
bytes = "1"
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false }
|
||||
http-range = "0.1.4"
|
||||
derive_more = "0.99.5"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.1"
|
||||
percent-encoding = "2.1"
|
||||
v_htmlescape = "0.12"
|
||||
|
||||
[dev-dependencies]
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-web = "4.0.0-beta.1"
|
||||
actix-rt = "2.1"
|
||||
actix-web = "4.0.0-beta.4"
|
||||
|
@ -3,17 +3,17 @@
|
||||
> Static file serving for Actix Web
|
||||
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://docs.rs/actix-files/0.5.0)
|
||||
[](https://docs.rs/actix-files/0.6.0-beta.3)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-files/0.5.0)
|
||||
[](https://deps.rs/crate/actix-files/0.6.0-beta.3)
|
||||
[](https://crates.io/crates/actix-files)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
- [API Documentation](https://docs.rs/actix-files/)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/static_index)
|
||||
- [Example Project](https://github.com/actix/examples/tree/master/basics/static_index)
|
||||
- [Chat on Gitter](https://gitter.im/actix/actix-web)
|
||||
- Minimum supported Rust version: 1.46 or later
|
||||
|
@ -9,26 +9,35 @@ use std::{
|
||||
|
||||
use actix_web::{
|
||||
error::{BlockingError, Error},
|
||||
web,
|
||||
rt::task::{spawn_blocking, JoinHandle},
|
||||
};
|
||||
use bytes::Bytes;
|
||||
use futures_core::{ready, Stream};
|
||||
use futures_util::future::{FutureExt, LocalBoxFuture};
|
||||
|
||||
use crate::handle_error;
|
||||
|
||||
type ChunkedBoxFuture =
|
||||
LocalBoxFuture<'static, Result<(File, Bytes), BlockingError<io::Error>>>;
|
||||
|
||||
#[doc(hidden)]
|
||||
/// A helper created from a `std::fs::File` which reads the file
|
||||
/// chunk-by-chunk on a `ThreadPool`.
|
||||
pub struct ChunkedReadFile {
|
||||
pub(crate) size: u64,
|
||||
pub(crate) offset: u64,
|
||||
pub(crate) file: Option<File>,
|
||||
pub(crate) fut: Option<ChunkedBoxFuture>,
|
||||
pub(crate) counter: u64,
|
||||
size: u64,
|
||||
offset: u64,
|
||||
state: ChunkedReadFileState,
|
||||
counter: u64,
|
||||
}
|
||||
|
||||
enum ChunkedReadFileState {
|
||||
File(Option<File>),
|
||||
Future(JoinHandle<Result<(File, Bytes), io::Error>>),
|
||||
}
|
||||
|
||||
impl ChunkedReadFile {
|
||||
pub(crate) fn new(size: u64, offset: u64, file: File) -> Self {
|
||||
Self {
|
||||
size,
|
||||
offset,
|
||||
state: ChunkedReadFileState::File(Some(file)),
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ChunkedReadFile {
|
||||
@ -40,38 +49,23 @@ impl fmt::Debug for ChunkedReadFile {
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Result<Bytes, Error>;
|
||||
|
||||
fn poll_next(
|
||||
mut self: Pin<&mut Self>,
|
||||
cx: &mut Context<'_>,
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
if let Some(ref mut fut) = self.fut {
|
||||
return match ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok((file, bytes)) => {
|
||||
self.fut.take();
|
||||
self.file = Some(file);
|
||||
|
||||
self.offset += bytes.len() as u64;
|
||||
self.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
Err(e) => Poll::Ready(Some(Err(handle_error(e)))),
|
||||
};
|
||||
}
|
||||
|
||||
let size = self.size;
|
||||
let offset = self.offset;
|
||||
let counter = self.counter;
|
||||
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
||||
let this = self.as_mut().get_mut();
|
||||
match this.state {
|
||||
ChunkedReadFileState::File(ref mut file) => {
|
||||
let size = this.size;
|
||||
let offset = this.offset;
|
||||
let counter = this.counter;
|
||||
|
||||
if size == counter {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
let mut file = self.file.take().expect("Use after completion");
|
||||
let mut file = file
|
||||
.take()
|
||||
.expect("ChunkedReadFile polled after completion");
|
||||
|
||||
self.fut = Some(
|
||||
web::block(move || {
|
||||
let max_bytes =
|
||||
cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
let fut = spawn_blocking(move || {
|
||||
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
|
||||
|
||||
let mut buf = Vec::with_capacity(max_bytes);
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
@ -84,11 +78,21 @@ impl Stream for ChunkedReadFile {
|
||||
}
|
||||
|
||||
Ok((file, Bytes::from(buf)))
|
||||
})
|
||||
.boxed_local(),
|
||||
);
|
||||
|
||||
});
|
||||
this.state = ChunkedReadFileState::Future(fut);
|
||||
self.poll_next(cx)
|
||||
}
|
||||
}
|
||||
ChunkedReadFileState::Future(ref mut fut) => {
|
||||
let (file, bytes) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
this.state = ChunkedReadFileState::File(Some(file));
|
||||
|
||||
this.offset += bytes.len() as u64;
|
||||
this.counter += bytes.len() as u64;
|
||||
|
||||
Poll::Ready(Some(Ok(bytes)))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,8 +1,8 @@
|
||||
use std::{fmt::Write, fs::DirEntry, io, path::Path, path::PathBuf};
|
||||
|
||||
use actix_web::{dev::ServiceResponse, HttpRequest, HttpResponse};
|
||||
use askama_escape::{escape as escape_html_entity, Html};
|
||||
use percent_encoding::{utf8_percent_encode, CONTROLS};
|
||||
use v_htmlescape::escape as escape_html_entity;
|
||||
|
||||
/// A directory; responds with the generated directory listing.
|
||||
#[derive(Debug)]
|
||||
@ -50,7 +50,7 @@ macro_rules! encode_file_url {
|
||||
// " -- " & -- & ' -- ' < -- < > -- > / -- /
|
||||
macro_rules! encode_file_name {
|
||||
($entry:ident) => {
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy())
|
||||
escape_html_entity(&$entry.file_name().to_string_lossy(), Html)
|
||||
};
|
||||
}
|
||||
|
||||
@ -66,9 +66,7 @@ pub(crate) fn directory_listing(
|
||||
if dir.is_visible(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
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(),
|
||||
Err(_) => continue,
|
||||
};
|
||||
|
@ -2,9 +2,7 @@ use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
|
||||
|
||||
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
|
||||
use actix_web::{
|
||||
dev::{
|
||||
AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse,
|
||||
},
|
||||
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
|
||||
error::Error,
|
||||
guard::Guard,
|
||||
http::header::DispositionType,
|
||||
@ -13,8 +11,8 @@ use actix_web::{
|
||||
use futures_util::future::{ok, FutureExt, LocalBoxFuture};
|
||||
|
||||
use crate::{
|
||||
directory_listing, named, Directory, DirectoryRenderer, FilesService,
|
||||
HttpNewService, MimeOverride,
|
||||
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
|
||||
MimeOverride,
|
||||
};
|
||||
|
||||
/// Static files handling service.
|
||||
@ -82,8 +80,9 @@ impl Files {
|
||||
/// be inaccessible. Register more specific handlers and services first.
|
||||
///
|
||||
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a
|
||||
/// number of threads equal to 5x the number of available logical CPUs. Pool size can be changed
|
||||
/// by setting ACTIX_THREADPOOL environment variable.
|
||||
/// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are
|
||||
/// adjusted with work load. More threads would spawn when need and threads goes idle for a
|
||||
/// period of time would be de-spawned.
|
||||
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
|
||||
let orig_dir = serve_from.into();
|
||||
let dir = match orig_dir.canonicalize() {
|
||||
@ -128,8 +127,8 @@ impl Files {
|
||||
/// Set custom directory renderer
|
||||
pub fn files_listing_renderer<F>(mut self, f: F) -> Self
|
||||
where
|
||||
for<'r, 's> F: Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error>
|
||||
+ 'static,
|
||||
for<'r, 's> F:
|
||||
Fn(&'r Directory, &'s HttpRequest) -> Result<ServiceResponse, io::Error> + 'static,
|
||||
{
|
||||
self.renderer = Rc::new(f);
|
||||
self
|
||||
|
@ -14,12 +14,10 @@
|
||||
#![deny(rust_2018_idioms)]
|
||||
#![warn(missing_docs, missing_debug_implementations)]
|
||||
|
||||
use std::io;
|
||||
|
||||
use actix_service::boxed::{BoxService, BoxServiceFactory};
|
||||
use actix_web::{
|
||||
dev::{ServiceRequest, ServiceResponse},
|
||||
error::{BlockingError, Error, ErrorInternalServerError},
|
||||
error::Error,
|
||||
http::header::DispositionType,
|
||||
};
|
||||
use mime_guess::from_ext;
|
||||
@ -56,13 +54,6 @@ pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
|
||||
from_ext(ext).first_or_octet_stream()
|
||||
}
|
||||
|
||||
pub(crate) fn handle_error(err: BlockingError<io::Error>) -> Error {
|
||||
match err {
|
||||
BlockingError::Error(err) => err.into(),
|
||||
BlockingError::Canceled => ErrorInternalServerError("Unexpected error"),
|
||||
}
|
||||
}
|
||||
|
||||
type MimeOverride = dyn Fn(&mime::Name<'_>) -> DispositionType;
|
||||
|
||||
#[cfg(test)]
|
||||
@ -82,7 +73,8 @@ mod tests {
|
||||
},
|
||||
middleware::Compress,
|
||||
test::{self, TestRequest},
|
||||
web, App, HttpResponse, Responder,
|
||||
web::{self, Bytes},
|
||||
App, HttpResponse, Responder,
|
||||
};
|
||||
use futures_util::future::ok;
|
||||
|
||||
@ -106,11 +98,22 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_if_modified_since_without_if_none_match() {
|
||||
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()
|
||||
.header(header::IF_MODIFIED_SINCE, since)
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_modified_since_without_if_none_match_same() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let since = file.last_modified().unwrap();
|
||||
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
@ -119,17 +122,40 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_if_modified_since_with_if_none_match() {
|
||||
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()
|
||||
.header(header::IF_NONE_MATCH, "miss_etag")
|
||||
.header(header::IF_MODIFIED_SINCE, since)
|
||||
.insert_header((header::IF_NONE_MATCH, "miss_etag"))
|
||||
.insert_header((header::IF_MODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_ne!(resp.status(), StatusCode::NOT_MODIFIED);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_unmodified_since() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let since = file.last_modified().unwrap();
|
||||
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_if_unmodified_since_failed() {
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
|
||||
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::IF_UNMODIFIED_SINCE, since))
|
||||
.to_http_request();
|
||||
let resp = file.respond_to(&req).await.unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::PRECONDITION_FAILED);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_text() {
|
||||
assert!(NamedFile::open("test--").is_err());
|
||||
@ -184,8 +210,7 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_non_ascii_file_name() {
|
||||
let mut file =
|
||||
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
|
||||
.unwrap();
|
||||
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap();
|
||||
{
|
||||
file.file();
|
||||
let _f: &File = &file;
|
||||
@ -338,7 +363,7 @@ mod tests {
|
||||
DispositionType::Attachment
|
||||
}
|
||||
|
||||
let mut srv = test::init_service(
|
||||
let srv = test::init_service(
|
||||
App::new().service(
|
||||
Files::new("/", ".")
|
||||
.mime_override(all_attachment)
|
||||
@ -348,7 +373,7 @@ mod tests {
|
||||
.await;
|
||||
|
||||
let request = TestRequest::get().uri("/").to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
let response = test::call_service(&srv, request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let content_disposition = response
|
||||
@ -363,7 +388,7 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_ranges_status_code() {
|
||||
let mut srv = test::init_service(
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("/test", ".").index_file("Cargo.toml")),
|
||||
)
|
||||
.await;
|
||||
@ -371,17 +396,17 @@ mod tests {
|
||||
// Valid range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/Cargo.toml")
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.insert_header((header::RANGE, "bytes=10-20"))
|
||||
.to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
let response = test::call_service(&srv, request).await;
|
||||
assert_eq!(response.status(), StatusCode::PARTIAL_CONTENT);
|
||||
|
||||
// Invalid range header
|
||||
let request = TestRequest::get()
|
||||
.uri("/t%65st/Cargo.toml")
|
||||
.header(header::RANGE, "bytes=1-0")
|
||||
.insert_header((header::RANGE, "bytes=1-0"))
|
||||
.to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
let response = test::call_service(&srv, request).await;
|
||||
|
||||
assert_eq!(response.status(), StatusCode::RANGE_NOT_SATISFIABLE);
|
||||
}
|
||||
@ -393,7 +418,7 @@ mod tests {
|
||||
// Valid range header
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.insert_header((header::RANGE, "bytes=10-20"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@ -403,7 +428,7 @@ mod tests {
|
||||
// Invalid range header
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-5")
|
||||
.insert_header((header::RANGE, "bytes=10-5"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@ -418,7 +443,7 @@ mod tests {
|
||||
// Valid range header
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=10-20")
|
||||
.insert_header((header::RANGE, "bytes=10-20"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@ -428,7 +453,7 @@ mod tests {
|
||||
// Valid range header, starting from 0
|
||||
let response = srv
|
||||
.get("/tests/test.binary")
|
||||
.header(header::RANGE, "bytes=0-20")
|
||||
.insert_header((header::RANGE, "bytes=0-20"))
|
||||
.send()
|
||||
.await
|
||||
.unwrap();
|
||||
@ -468,14 +493,14 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_with_spaces() {
|
||||
let mut srv = test::init_service(
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("/", ".").index_file("Cargo.toml")),
|
||||
)
|
||||
.await;
|
||||
let request = TestRequest::get()
|
||||
.uri("/tests/test%20space.binary")
|
||||
.to_request();
|
||||
let response = test::call_service(&mut srv, request).await;
|
||||
let response = test::call_service(&srv, request).await;
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
|
||||
let bytes = test::read_body(response).await;
|
||||
@ -485,28 +510,28 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_files_not_allowed() {
|
||||
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||
|
||||
let req = TestRequest::default()
|
||||
.uri("/Cargo.toml")
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
|
||||
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||
let req = TestRequest::default()
|
||||
.method(Method::PUT)
|
||||
.uri("/Cargo.toml")
|
||||
.to_request();
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_files_guards() {
|
||||
let mut srv = test::init_service(
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("/", ".").use_guards(guard::Post())),
|
||||
)
|
||||
.await;
|
||||
@ -516,13 +541,13 @@ mod tests {
|
||||
.method(Method::POST)
|
||||
.to_request();
|
||||
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_encoding() {
|
||||
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
web::resource("/").to(|| async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
.unwrap()
|
||||
@ -533,16 +558,16 @@ mod tests {
|
||||
|
||||
let request = TestRequest::get()
|
||||
.uri("/")
|
||||
.header(header::ACCEPT_ENCODING, "gzip")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.to_request();
|
||||
let res = test::call_service(&mut srv, request).await;
|
||||
let res = test::call_service(&srv, request).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert!(!res.headers().contains_key(header::CONTENT_ENCODING));
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_named_file_content_encoding_gzip() {
|
||||
let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
let srv = test::init_service(App::new().wrap(Compress::default()).service(
|
||||
web::resource("/").to(|| async {
|
||||
NamedFile::open("Cargo.toml")
|
||||
.unwrap()
|
||||
@ -553,9 +578,9 @@ mod tests {
|
||||
|
||||
let request = TestRequest::get()
|
||||
.uri("/")
|
||||
.header(header::ACCEPT_ENCODING, "gzip")
|
||||
.insert_header((header::ACCEPT_ENCODING, "gzip"))
|
||||
.to_request();
|
||||
let res = test::call_service(&mut srv, request).await;
|
||||
let res = test::call_service(&srv, request).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
res.headers()
|
||||
@ -577,27 +602,25 @@ mod tests {
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files() {
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("/", ".").show_files_listing()),
|
||||
)
|
||||
let srv =
|
||||
test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
|
||||
.await;
|
||||
let req = TestRequest::with_uri("/missing").to_request();
|
||||
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let mut srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "."))).await;
|
||||
|
||||
let req = TestRequest::default().to_request();
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("/", ".").show_files_listing()),
|
||||
)
|
||||
let srv =
|
||||
test::init_service(App::new().service(Files::new("/", ".").show_files_listing()))
|
||||
.await;
|
||||
let req = TestRequest::with_uri("/tests").to_request();
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(
|
||||
resp.headers().get(header::CONTENT_TYPE).unwrap(),
|
||||
"text/html; charset=utf-8"
|
||||
@ -610,16 +633,16 @@ mod tests {
|
||||
#[actix_rt::test]
|
||||
async fn test_redirect_to_slash_directory() {
|
||||
// should not redirect if no index
|
||||
let mut srv = test::init_service(
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
|
||||
)
|
||||
.await;
|
||||
let req = TestRequest::with_uri("/tests").to_request();
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
// should redirect if index present
|
||||
let mut srv = test::init_service(
|
||||
let srv = test::init_service(
|
||||
App::new().service(
|
||||
Files::new("/", ".")
|
||||
.index_file("test.png")
|
||||
@ -628,24 +651,28 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
let req = TestRequest::with_uri("/tests").to_request();
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
|
||||
// should not redirect if the path is wrong
|
||||
let req = TestRequest::with_uri("/not_existing").to_request();
|
||||
let resp = test::call_service(&mut srv, req).await;
|
||||
let resp = test::call_service(&srv, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_static_files_bad_directory() {
|
||||
let _st: Files = Files::new("/", "missing");
|
||||
let _st: Files = Files::new("/", "Cargo.toml");
|
||||
let service = Files::new("/", "./missing").new_service(()).await.unwrap();
|
||||
|
||||
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]
|
||||
async fn test_default_handler_file_missing() {
|
||||
let mut st = Files::new("/", ".")
|
||||
let st = Files::new("/", ".")
|
||||
.default_handler(|req: ServiceRequest| {
|
||||
ok(req.into_response(HttpResponse::Ok().body("default content")))
|
||||
})
|
||||
@ -653,124 +680,78 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
let req = TestRequest::with_uri("/missing").to_srv_request();
|
||||
let resp = test::call_service(&st, req).await;
|
||||
|
||||
let resp = test::call_service(&mut st, req).await;
|
||||
assert_eq!(resp.status(), StatusCode::OK);
|
||||
let bytes = test::read_body(resp).await;
|
||||
assert_eq!(bytes, web::Bytes::from_static(b"default content"));
|
||||
}
|
||||
|
||||
// #[actix_rt::test]
|
||||
// async fn test_serve_index() {
|
||||
// let st = Files::new(".").index_file("test.binary");
|
||||
// let req = TestRequest::default().uri("/tests").finish();
|
||||
#[actix_rt::test]
|
||||
async fn test_serve_index_nested() {
|
||||
let service = Files::new(".", ".")
|
||||
.index_file("lib.rs")
|
||||
.new_service(())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
// 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)
|
||||
// .expect("content type"),
|
||||
// "application/octet-stream"
|
||||
// );
|
||||
// assert_eq!(
|
||||
// resp.headers()
|
||||
// .get(header::CONTENT_DISPOSITION)
|
||||
// .expect("content disposition"),
|
||||
// "attachment; filename=\"test.binary\""
|
||||
// );
|
||||
let req = TestRequest::default().uri("/src").to_srv_request();
|
||||
let resp = test::call_service(&service, req).await;
|
||||
|
||||
// let req = TestRequest::default().uri("/tests/").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(),
|
||||
// "application/octet-stream"
|
||||
// );
|
||||
// assert_eq!(
|
||||
// resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
|
||||
// "attachment; filename=\"test.binary\""
|
||||
// );
|
||||
|
||||
// // nonexistent index file
|
||||
// let req = TestRequest::default().uri("/tests/unknown").finish();
|
||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
||||
// let resp = resp.as_msg();
|
||||
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
// let req = TestRequest::default().uri("/tests/unknown/").finish();
|
||||
// let resp = st.handle(&req).respond_to(&req).unwrap();
|
||||
// let resp = resp.as_msg();
|
||||
// assert_eq!(resp.status(), StatusCode::NOT_FOUND);
|
||||
// }
|
||||
|
||||
// #[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\""
|
||||
// );
|
||||
// }
|
||||
|
||||
// #[actix_rt::test]
|
||||
// fn integration_serve_index() {
|
||||
// let mut srv = test::TestServer::with_factory(|| {
|
||||
// App::new().handler(
|
||||
// "test",
|
||||
// Files::new(".").index_file("Cargo.toml"),
|
||||
// )
|
||||
// });
|
||||
|
||||
// let request = srv.get().uri(srv.url("/test")).finish().unwrap();
|
||||
// let response = srv.execute(request.send()).unwrap();
|
||||
// assert_eq!(response.status(), StatusCode::OK);
|
||||
// let bytes = srv.execute(response.body()).unwrap();
|
||||
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||
// assert_eq!(bytes, data);
|
||||
|
||||
// let request = srv.get().uri(srv.url("/test/")).finish().unwrap();
|
||||
// let response = srv.execute(request.send()).unwrap();
|
||||
// assert_eq!(response.status(), StatusCode::OK);
|
||||
// let bytes = srv.execute(response.body()).unwrap();
|
||||
// let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||
// assert_eq!(bytes, data);
|
||||
|
||||
// // nonexistent index file
|
||||
// let request = srv.get().uri(srv.url("/test/unknown")).finish().unwrap();
|
||||
// let response = srv.execute(request.send()).unwrap();
|
||||
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
// let request = srv.get().uri(srv.url("/test/unknown/")).finish().unwrap();
|
||||
// let response = srv.execute(request.send()).unwrap();
|
||||
// assert_eq!(response.status(), StatusCode::NOT_FOUND);
|
||||
// }
|
||||
|
||||
// #[actix_rt::test]
|
||||
// fn integration_percent_encoded() {
|
||||
// let mut srv = test::TestServer::with_factory(|| {
|
||||
// App::new().handler(
|
||||
// "test",
|
||||
// Files::new(".").index_file("Cargo.toml"),
|
||||
// )
|
||||
// });
|
||||
|
||||
// let request = srv
|
||||
// .get()
|
||||
// .uri(srv.url("/test/%43argo.toml"))
|
||||
// .finish()
|
||||
// .unwrap();
|
||||
// let response = srv.execute(request.send()).unwrap();
|
||||
// assert_eq!(response.status(), StatusCode::OK);
|
||||
// }
|
||||
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=\"lib.rs\""
|
||||
);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn integration_serve_index() {
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::get().uri("/test").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let bytes = test::read_body(res).await;
|
||||
|
||||
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||
assert_eq!(bytes, data);
|
||||
|
||||
let req = TestRequest::get().uri("/test/").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
|
||||
let bytes = test::read_body(res).await;
|
||||
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
|
||||
assert_eq!(bytes, data);
|
||||
|
||||
// nonexistent index file
|
||||
let req = TestRequest::get().uri("/test/unknown").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let req = TestRequest::get().uri("/test/unknown/").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::NOT_FOUND);
|
||||
}
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn integration_percent_encoded() {
|
||||
let srv = test::init_service(
|
||||
App::new().service(Files::new("test", ".").index_file("Cargo.toml")),
|
||||
)
|
||||
.await;
|
||||
|
||||
let req = TestRequest::get().uri("/test/%43argo.toml").to_request();
|
||||
let res = test::call_service(&srv, req).await;
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
}
|
||||
|
@ -11,15 +11,13 @@ use actix_web::{
|
||||
dev::{BodyEncoding, SizedStream},
|
||||
http::{
|
||||
header::{
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType,
|
||||
ExtendedValue,
|
||||
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
|
||||
},
|
||||
ContentEncoding, StatusCode,
|
||||
},
|
||||
Error, HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
HttpMessage, HttpRequest, HttpResponse, Responder,
|
||||
};
|
||||
use bitflags::bitflags;
|
||||
use futures_util::future::{ready, Ready};
|
||||
use mime_guess::from_path;
|
||||
|
||||
use crate::ChunkedReadFile;
|
||||
@ -277,37 +275,31 @@ impl NamedFile {
|
||||
}
|
||||
|
||||
/// Creates an `HttpResponse` with file as a streaming body.
|
||||
pub fn into_response(self, req: &HttpRequest) -> Result<HttpResponse, Error> {
|
||||
pub fn into_response(self, req: &HttpRequest) -> HttpResponse {
|
||||
if self.status_code != StatusCode::OK {
|
||||
let mut res = HttpResponse::build(self.status_code);
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
res.header(header::CONTENT_TYPE, ct.to_string());
|
||||
res.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
} else {
|
||||
res.header(header::CONTENT_TYPE, self.content_type.to_string());
|
||||
res.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
res.header(
|
||||
res.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
if let Some(current_encoding) = self.encoding {
|
||||
res.encoding(current_encoding);
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file);
|
||||
|
||||
return Ok(res.streaming(reader));
|
||||
return res.streaming(reader);
|
||||
}
|
||||
|
||||
let etag = if self.flags.contains(Flags::ETAG) {
|
||||
@ -332,7 +324,7 @@ impl NamedFile {
|
||||
let t2: SystemTime = since.clone().into();
|
||||
|
||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||
(Ok(t1), Ok(t2)) => t1 > t2,
|
||||
(Ok(t1), Ok(t2)) => t1.as_secs() > t2.as_secs(),
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
@ -351,7 +343,7 @@ impl NamedFile {
|
||||
let t2: SystemTime = since.clone().into();
|
||||
|
||||
match (t1.duration_since(UNIX_EPOCH), t2.duration_since(UNIX_EPOCH)) {
|
||||
(Ok(t1), Ok(t2)) => t1 <= t2,
|
||||
(Ok(t1), Ok(t2)) => t1.as_secs() <= t2.as_secs(),
|
||||
_ => false,
|
||||
}
|
||||
} else {
|
||||
@ -362,16 +354,16 @@ impl NamedFile {
|
||||
|
||||
if self.flags.contains(Flags::PREFER_UTF8) {
|
||||
let ct = equiv_utf8_text(self.content_type.clone());
|
||||
resp.header(header::CONTENT_TYPE, ct.to_string());
|
||||
resp.insert_header((header::CONTENT_TYPE, ct.to_string()));
|
||||
} else {
|
||||
resp.header(header::CONTENT_TYPE, self.content_type.to_string());
|
||||
resp.insert_header((header::CONTENT_TYPE, self.content_type.to_string()));
|
||||
}
|
||||
|
||||
if self.flags.contains(Flags::CONTENT_DISPOSITION) {
|
||||
resp.header(
|
||||
resp.insert_header((
|
||||
header::CONTENT_DISPOSITION,
|
||||
self.content_disposition.to_string(),
|
||||
);
|
||||
));
|
||||
}
|
||||
|
||||
// default compressing
|
||||
@ -380,14 +372,14 @@ impl NamedFile {
|
||||
}
|
||||
|
||||
if let Some(lm) = last_modified {
|
||||
resp.header(header::LAST_MODIFIED, lm.to_string());
|
||||
resp.insert_header((header::LAST_MODIFIED, lm.to_string()));
|
||||
}
|
||||
|
||||
if let Some(etag) = etag {
|
||||
resp.header(header::ETAG, etag.to_string());
|
||||
resp.insert_header((header::ETAG, etag.to_string()));
|
||||
}
|
||||
|
||||
resp.header(header::ACCEPT_RANGES, "bytes");
|
||||
resp.insert_header((header::ACCEPT_RANGES, "bytes"));
|
||||
|
||||
let mut length = self.md.len();
|
||||
let mut offset = 0;
|
||||
@ -400,43 +392,32 @@ impl NamedFile {
|
||||
offset = ranges[0].start;
|
||||
|
||||
resp.encoding(ContentEncoding::Identity);
|
||||
resp.header(
|
||||
resp.insert_header((
|
||||
header::CONTENT_RANGE,
|
||||
format!(
|
||||
"bytes {}-{}/{}",
|
||||
offset,
|
||||
offset + length - 1,
|
||||
self.md.len()
|
||||
),
|
||||
);
|
||||
format!("bytes {}-{}/{}", offset, offset + length - 1, self.md.len()),
|
||||
));
|
||||
} else {
|
||||
resp.header(header::CONTENT_RANGE, format!("bytes */{}", length));
|
||||
return Ok(resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish());
|
||||
resp.insert_header((header::CONTENT_RANGE, format!("bytes */{}", length)));
|
||||
return resp.status(StatusCode::RANGE_NOT_SATISFIABLE).finish();
|
||||
};
|
||||
} else {
|
||||
return Ok(resp.status(StatusCode::BAD_REQUEST).finish());
|
||||
return resp.status(StatusCode::BAD_REQUEST).finish();
|
||||
};
|
||||
};
|
||||
|
||||
if precondition_failed {
|
||||
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish());
|
||||
return resp.status(StatusCode::PRECONDITION_FAILED).finish();
|
||||
} else if not_modified {
|
||||
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish());
|
||||
return resp.status(StatusCode::NOT_MODIFIED).finish();
|
||||
}
|
||||
|
||||
let reader = ChunkedReadFile {
|
||||
offset,
|
||||
size: length,
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
counter: 0,
|
||||
};
|
||||
let reader = ChunkedReadFile::new(length, offset, self.file);
|
||||
|
||||
if offset != 0 || length != self.md.len() {
|
||||
resp.status(StatusCode::PARTIAL_CONTENT);
|
||||
}
|
||||
|
||||
Ok(resp.body(SizedStream::new(length, reader)))
|
||||
resp.body(SizedStream::new(length, reader))
|
||||
}
|
||||
}
|
||||
|
||||
@ -495,10 +476,7 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
}
|
||||
|
||||
impl Responder for NamedFile {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<HttpResponse, Error>>;
|
||||
|
||||
fn respond_to(self, req: &HttpRequest) -> Self::Future {
|
||||
ready(self.into_response(req))
|
||||
fn respond_to(self, req: &HttpRequest) -> HttpResponse {
|
||||
self.into_response(req)
|
||||
}
|
||||
}
|
||||
|
@ -10,9 +10,6 @@ pub struct HttpRange {
|
||||
pub length: u64,
|
||||
}
|
||||
|
||||
const PREFIX: &str = "bytes=";
|
||||
const PREFIX_LEN: usize = 6;
|
||||
|
||||
#[derive(Debug, Clone, Display, Error)]
|
||||
#[display(fmt = "Parse HTTP Range failed")]
|
||||
pub struct ParseRangeErr(#[error(not(source))] ());
|
||||
@ -23,84 +20,16 @@ impl HttpRange {
|
||||
/// `header` is HTTP Range header (e.g. `bytes=bytes=0-9`).
|
||||
/// `size` is full size of response (file).
|
||||
pub fn parse(header: &str, size: u64) -> Result<Vec<HttpRange>, ParseRangeErr> {
|
||||
if header.is_empty() {
|
||||
return Ok(Vec::new());
|
||||
}
|
||||
if !header.starts_with(PREFIX) {
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
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(ParseRangeErr(()))?.trim();
|
||||
let end_str = start_end_iter.next().ok_or(ParseRangeErr(()))?.trim();
|
||||
|
||||
if start_str.is_empty() {
|
||||
// If no start is specified, end specifies the
|
||||
// range start relative to the end of the file.
|
||||
let mut length: i64 =
|
||||
end_str.parse().map_err(|_| ParseRangeErr(()))?;
|
||||
|
||||
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(|_| ParseRangeErr(()))?;
|
||||
|
||||
if start < 0 {
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
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(|_| ParseRangeErr(()))?;
|
||||
|
||||
if start > end {
|
||||
return Err(ParseRangeErr(()));
|
||||
}
|
||||
|
||||
if end >= size_sig {
|
||||
end = size_sig - 1;
|
||||
}
|
||||
|
||||
end - start + 1
|
||||
};
|
||||
|
||||
Ok(Some(HttpRange {
|
||||
start: start as u64,
|
||||
length: length as u64,
|
||||
}))
|
||||
}
|
||||
match http_range::HttpRange::parse(header, size) {
|
||||
Ok(ranges) => Ok(ranges
|
||||
.iter()
|
||||
.map(|range| HttpRange {
|
||||
start: range.start,
|
||||
length: range.length,
|
||||
})
|
||||
.collect::<Result<_, _>>()?;
|
||||
|
||||
let ranges: Vec<HttpRange> = all_ranges.into_iter().filter_map(|x| x).collect();
|
||||
|
||||
if no_overlap && ranges.is_empty() {
|
||||
return Err(ParseRangeErr(()));
|
||||
.collect()),
|
||||
Err(_) => Err(ParseRangeErr(())),
|
||||
}
|
||||
|
||||
Ok(ranges)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1,9 +1,4 @@
|
||||
use std::{
|
||||
fmt, io,
|
||||
path::PathBuf,
|
||||
rc::Rc,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll};
|
||||
|
||||
use actix_service::Service;
|
||||
use actix_web::{
|
||||
@ -16,8 +11,8 @@ use actix_web::{
|
||||
use futures_util::future::{ok, Either, LocalBoxFuture, Ready};
|
||||
|
||||
use crate::{
|
||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride,
|
||||
NamedFile, PathBufWrap,
|
||||
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
|
||||
PathBufWrap,
|
||||
};
|
||||
|
||||
/// Assembled file serving service.
|
||||
@ -40,10 +35,10 @@ type FilesServiceFuture = Either<
|
||||
>;
|
||||
|
||||
impl FilesService {
|
||||
fn handle_err(&mut self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
|
||||
fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture {
|
||||
log::debug!("Failed to handle {}: {}", req.path(), e);
|
||||
|
||||
if let Some(ref mut default) = self.default {
|
||||
if let Some(ref default) = self.default {
|
||||
Either::Right(default.call(req))
|
||||
} else {
|
||||
Either::Left(ok(req.error_response(e)))
|
||||
@ -62,11 +57,9 @@ impl Service<ServiceRequest> for FilesService {
|
||||
type Error = Error;
|
||||
type Future = FilesServiceFuture;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, req: ServiceRequest) -> Self::Future {
|
||||
fn call(&self, req: ServiceRequest) -> Self::Future {
|
||||
let is_method_valid = if let Some(guard) = &self.guards {
|
||||
// execute user defined guards
|
||||
(**guard).check(req.head())
|
||||
@ -78,7 +71,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
if !is_method_valid {
|
||||
return Either::Left(ok(req.into_response(
|
||||
actix_web::HttpResponse::MethodNotAllowed()
|
||||
.header(header::CONTENT_TYPE, "text/plain")
|
||||
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
|
||||
.body("Request did not meet this resource's requirements."),
|
||||
)));
|
||||
}
|
||||
@ -102,7 +95,7 @@ impl Service<ServiceRequest> for FilesService {
|
||||
|
||||
return Either::Left(ok(req.into_response(
|
||||
HttpResponse::Found()
|
||||
.header(header::LOCATION, redirect_to)
|
||||
.insert_header((header::LOCATION, redirect_to))
|
||||
.body("")
|
||||
.into_body(),
|
||||
)));
|
||||
@ -120,10 +113,8 @@ impl Service<ServiceRequest> for FilesService {
|
||||
named_file.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
Either::Left(ok(match named_file.into_response(&req) {
|
||||
Ok(item) => ServiceResponse::new(req, item),
|
||||
Err(e) => ServiceResponse::from_err(e, req),
|
||||
}))
|
||||
let res = named_file.into_response(&req);
|
||||
Either::Left(ok(ServiceResponse::new(req, res)))
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
@ -147,19 +138,14 @@ impl Service<ServiceRequest> for FilesService {
|
||||
match NamedFile::open(path) {
|
||||
Ok(mut named_file) => {
|
||||
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.flags = self.file_flags;
|
||||
|
||||
let (req, _) = req.into_parts();
|
||||
match named_file.into_response(&req) {
|
||||
Ok(item) => {
|
||||
Either::Left(ok(ServiceResponse::new(req.clone(), item)))
|
||||
}
|
||||
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))),
|
||||
}
|
||||
let res = named_file.into_response(&req);
|
||||
Either::Left(ok(ServiceResponse::new(req, res)))
|
||||
}
|
||||
Err(e) => self.handle_err(e, req),
|
||||
}
|
||||
|
@ -11,11 +11,10 @@ use actix_web::{
|
||||
#[actix_rt::test]
|
||||
async fn test_utf8_file_contents() {
|
||||
// use default ISO-8859-1 encoding
|
||||
let mut srv =
|
||||
test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
let res = test::call_service(&mut srv, req).await;
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
@ -24,13 +23,12 @@ async fn test_utf8_file_contents() {
|
||||
);
|
||||
|
||||
// prefer UTF-8 encoding
|
||||
let mut srv = test::init_service(
|
||||
App::new().service(Files::new("/", "./tests").prefer_utf8(true)),
|
||||
)
|
||||
let srv =
|
||||
test::init_service(App::new().service(Files::new("/", "./tests").prefer_utf8(true)))
|
||||
.await;
|
||||
|
||||
let req = TestRequest::with_uri("/utf8.txt").to_request();
|
||||
let res = test::call_service(&mut srv, req).await;
|
||||
let res = test::call_service(&srv, req).await;
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
assert_eq!(
|
||||
|
@ -3,6 +3,14 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.3 - 2021-03-09
|
||||
* No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.2 - 2021-02-10
|
||||
* No notable changes.
|
||||
|
||||
|
||||
## 3.0.0-beta.1 - 2021-01-07
|
||||
* Update `bytes` to `1.0`. [#1813]
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http-test"
|
||||
version = "3.0.0-beta.1"
|
||||
version = "3.0.0-beta.3"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Various helpers for Actix applications to use during testing"
|
||||
readme = "README.md"
|
||||
@ -26,16 +26,16 @@ path = "src/lib.rs"
|
||||
default = []
|
||||
|
||||
# openssl
|
||||
openssl = ["open-ssl", "awc/openssl"]
|
||||
openssl = ["tls-openssl", "awc/openssl"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-service = "2.0.0-beta.4"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-tls = "3.0.0-beta.2"
|
||||
actix-utils = "3.0.0-beta.1"
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-server = "2.0.0-beta.2"
|
||||
awc = "3.0.0-beta.1"
|
||||
actix-tls = "3.0.0-beta.4"
|
||||
actix-utils = "3.0.0-beta.2"
|
||||
actix-rt = "2.1"
|
||||
actix-server = "2.0.0-beta.3"
|
||||
awc = { version = "3.0.0-beta.3", default-features = false }
|
||||
|
||||
base64 = "0.13"
|
||||
bytes = "1"
|
||||
@ -47,9 +47,15 @@ serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
open-ssl = { version = "0.10", package = "openssl", optional = true }
|
||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||
tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
|
||||
|
||||
[target.'cfg(windows)'.dependencies.tls-openssl]
|
||||
version = "0.10.9"
|
||||
package = "openssl"
|
||||
features = ["vendored"]
|
||||
optional = true
|
||||
|
||||
[dev-dependencies]
|
||||
actix-web = "4.0.0-beta.1"
|
||||
actix-http = "3.0.0-beta.1"
|
||||
actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies"] }
|
||||
actix-http = "3.0.0-beta.4"
|
||||
|
@ -3,9 +3,9 @@
|
||||
> Various helpers for Actix applications to use during testing.
|
||||
|
||||
[](https://crates.io/crates/actix-http-test)
|
||||
[](https://docs.rs/actix-http-test/2.1.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http-test/2.1.0)
|
||||
[](https://docs.rs/actix-http-test/3.0.0-beta.3)
|
||||

|
||||
[](https://deps.rs/crate/actix-http-test/3.0.0-beta.3)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
@ -4,6 +4,9 @@
|
||||
#![doc(html_logo_url = "https://actix.rs/img/logo.png")]
|
||||
#![doc(html_favicon_url = "https://actix.rs/favicon.ico")]
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
extern crate tls_openssl as openssl;
|
||||
|
||||
use std::sync::mpsc;
|
||||
use std::{net, thread, time};
|
||||
|
||||
@ -60,7 +63,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
|
||||
// run server in separate thread
|
||||
thread::spawn(move || {
|
||||
let sys = System::new("actix-test-server");
|
||||
let sys = System::new();
|
||||
let local_addr = tcp.local_addr().unwrap();
|
||||
|
||||
let srv = Server::build()
|
||||
@ -69,7 +72,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
.disable_signals();
|
||||
|
||||
sys.block_on(async {
|
||||
srv.start();
|
||||
srv.run();
|
||||
tx.send((System::current(), local_addr)).unwrap();
|
||||
});
|
||||
|
||||
@ -82,7 +85,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
let connector = {
|
||||
#[cfg(feature = "openssl")]
|
||||
{
|
||||
use open_ssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
|
||||
|
||||
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
builder.set_verify(SslVerifyMode::NONE);
|
||||
@ -93,20 +96,17 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
.conn_lifetime(time::Duration::from_secs(0))
|
||||
.timeout(time::Duration::from_millis(30000))
|
||||
.ssl(builder.build())
|
||||
.finish()
|
||||
}
|
||||
#[cfg(not(feature = "openssl"))]
|
||||
{
|
||||
Connector::new()
|
||||
.conn_lifetime(time::Duration::from_secs(0))
|
||||
.timeout(time::Duration::from_millis(30000))
|
||||
.finish()
|
||||
}
|
||||
};
|
||||
|
||||
Client::builder().connector(connector).finish()
|
||||
};
|
||||
actix_tls::connect::start_default_resolver().await.unwrap();
|
||||
|
||||
TestServer {
|
||||
addr,
|
||||
@ -118,8 +118,7 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
|
||||
/// Get first available unused address
|
||||
pub fn unused_addr() -> net::SocketAddr {
|
||||
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
|
||||
let socket =
|
||||
Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
|
||||
let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
|
||||
socket.bind(&addr.into()).unwrap();
|
||||
socket.set_reuse_address(true).unwrap();
|
||||
let tcp = socket.into_tcp_listener();
|
||||
@ -148,7 +147,7 @@ impl TestServer {
|
||||
}
|
||||
}
|
||||
|
||||
/// Construct test https server url
|
||||
/// Construct test HTTPS server URL.
|
||||
pub fn surl(&self, uri: &str) -> String {
|
||||
if uri.starts_with('/') {
|
||||
format!("https://localhost:{}{}", self.addr.port(), uri)
|
||||
@ -162,7 +161,7 @@ impl TestServer {
|
||||
self.client.get(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `GET` request
|
||||
/// Create HTTPS `GET` request
|
||||
pub fn sget<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.get(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
@ -172,7 +171,7 @@ impl TestServer {
|
||||
self.client.post(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `POST` request
|
||||
/// Create HTTPS `POST` request
|
||||
pub fn spost<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.post(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
@ -182,7 +181,7 @@ impl TestServer {
|
||||
self.client.head(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `HEAD` request
|
||||
/// Create HTTPS `HEAD` request
|
||||
pub fn shead<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.head(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
@ -192,7 +191,7 @@ impl TestServer {
|
||||
self.client.put(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `PUT` request
|
||||
/// Create HTTPS `PUT` request
|
||||
pub fn sput<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.put(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
@ -202,7 +201,7 @@ impl TestServer {
|
||||
self.client.patch(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `PATCH` request
|
||||
/// Create HTTPS `PATCH` request
|
||||
pub fn spatch<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.patch(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
@ -212,7 +211,7 @@ impl TestServer {
|
||||
self.client.delete(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `DELETE` request
|
||||
/// Create HTTPS `DELETE` request
|
||||
pub fn sdelete<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.delete(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
@ -222,12 +221,12 @@ impl TestServer {
|
||||
self.client.options(self.url(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Create https `OPTIONS` request
|
||||
/// Create HTTPS `OPTIONS` request
|
||||
pub fn soptions<S: AsRef<str>>(&self, path: S) -> ClientRequest {
|
||||
self.client.options(self.surl(path.as_ref()).as_str())
|
||||
}
|
||||
|
||||
/// Connect to test http server
|
||||
/// Connect to test HTTP server
|
||||
pub fn request<S: AsRef<str>>(&self, method: Method, path: S) -> ClientRequest {
|
||||
self.client.request(method, path.as_ref())
|
||||
}
|
||||
@ -242,26 +241,24 @@ impl TestServer {
|
||||
response.body().limit(10_485_760).await
|
||||
}
|
||||
|
||||
/// Connect to websocket server at a given path
|
||||
/// Connect to WebSocket server at a given path.
|
||||
pub async fn ws_at(
|
||||
&mut self,
|
||||
path: &str,
|
||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError>
|
||||
{
|
||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
|
||||
let url = self.url(path);
|
||||
let connect = self.client.ws(url).connect();
|
||||
connect.await.map(|(_, framed)| framed)
|
||||
}
|
||||
|
||||
/// Connect to a websocket server
|
||||
/// Connect to a WebSocket server.
|
||||
pub async fn ws(
|
||||
&mut self,
|
||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError>
|
||||
{
|
||||
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
|
||||
self.ws_at("/").await
|
||||
}
|
||||
|
||||
/// Stop http server
|
||||
/// Stop HTTP server
|
||||
fn stop(&mut self) {
|
||||
self.system.stop();
|
||||
}
|
||||
|
@ -3,6 +3,77 @@
|
||||
## Unreleased - 2021-xx-xx
|
||||
|
||||
|
||||
## 3.0.0-beta.4 - 2021-03-08
|
||||
### Changed
|
||||
* Feature `cookies` is now optional and disabled by default. [#1981]
|
||||
* `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]`.
|
||||
@ -22,10 +93,14 @@
|
||||
* 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
|
||||
@ -72,15 +147,14 @@
|
||||
* Update actix-connect and actix-tls dependencies.
|
||||
|
||||
|
||||
## [2.0.0-beta.3] - 2020-08-14
|
||||
|
||||
## 2.0.0-beta.3 - 2020-08-14
|
||||
### Fixed
|
||||
* Memory leak of `client::pool::ConnectorPoolSupport`. [#1626]
|
||||
|
||||
[#1626]: https://github.com/actix/actix-web/pull/1626
|
||||
|
||||
|
||||
## [2.0.0-beta.2] - 2020-07-21
|
||||
## 2.0.0-beta.2 - 2020-07-21
|
||||
### Fixed
|
||||
* Potential UB in h1 decoder using uninitialized memory. [#1614]
|
||||
|
||||
@ -91,10 +165,8 @@
|
||||
[#1615]: https://github.com/actix/actix-web/pull/1615
|
||||
|
||||
|
||||
## [2.0.0-beta.1] - 2020-07-11
|
||||
|
||||
## 2.0.0-beta.1 - 2020-07-11
|
||||
### Changed
|
||||
|
||||
* Migrate cookie handling to `cookie` crate. [#1558]
|
||||
* Update `sha-1` to 0.9. [#1586]
|
||||
* Fix leak in client pool. [#1580]
|
||||
@ -104,33 +176,30 @@
|
||||
[#1586]: https://github.com/actix/actix-web/pull/1586
|
||||
[#1580]: https://github.com/actix/actix-web/pull/1580
|
||||
|
||||
## [2.0.0-alpha.4] - 2020-05-21
|
||||
|
||||
## 2.0.0-alpha.4 - 2020-05-21
|
||||
### Changed
|
||||
|
||||
* Bump minimum supported Rust version to 1.40
|
||||
* content_length function is removed, and you can set Content-Length by calling no_chunking function [#1439]
|
||||
* content_length function is removed, and you can set Content-Length by calling
|
||||
no_chunking function [#1439]
|
||||
* `BodySize::Sized64` variant has been removed. `BodySize::Sized` now receives a
|
||||
`u64` instead of a `usize`.
|
||||
* Update `base64` dependency to 0.12
|
||||
|
||||
### Fixed
|
||||
|
||||
* Support parsing of `SameSite=None` [#1503]
|
||||
|
||||
[#1439]: https://github.com/actix/actix-web/pull/1439
|
||||
[#1503]: https://github.com/actix/actix-web/pull/1503
|
||||
|
||||
## [2.0.0-alpha.3] - 2020-05-08
|
||||
|
||||
## 2.0.0-alpha.3 - 2020-05-08
|
||||
### Fixed
|
||||
|
||||
* Correct spelling of ConnectError::Unresolved [#1487]
|
||||
* Fix a mistake in the encoding of websocket continuation messages wherein
|
||||
Item::FirstText and Item::FirstBinary are each encoded as the other.
|
||||
|
||||
### Changed
|
||||
|
||||
* Implement `std::error::Error` for our custom errors [#1422]
|
||||
* Remove `failure` support for `ResponseError` since that crate
|
||||
will be deprecated in the near future.
|
||||
@ -138,338 +207,247 @@
|
||||
[#1422]: https://github.com/actix/actix-web/pull/1422
|
||||
[#1487]: https://github.com/actix/actix-web/pull/1487
|
||||
|
||||
## [2.0.0-alpha.2] - 2020-03-07
|
||||
|
||||
## 2.0.0-alpha.2 - 2020-03-07
|
||||
### Changed
|
||||
|
||||
* Update `actix-connect` and `actix-tls` dependency to 2.0.0-alpha.1. [#1395]
|
||||
|
||||
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB respectively
|
||||
to improve download speed for awc when downloading large objects. [#1394]
|
||||
|
||||
* client::Connector accepts initial_window_size and initial_connection_window_size HTTP2 configuration. [#1394]
|
||||
|
||||
* Change default initial window size and connection window size for HTTP2 to 2MB and 1MB
|
||||
respectively to improve download speed for awc when downloading large objects. [#1394]
|
||||
* client::Connector accepts initial_window_size and initial_connection_window_size
|
||||
HTTP2 configuration. [#1394]
|
||||
* client::Connector allowing to set max_http_version to limit HTTP version to be used. [#1394]
|
||||
|
||||
[#1394]: https://github.com/actix/actix-web/pull/1394
|
||||
[#1395]: https://github.com/actix/actix-web/pull/1395
|
||||
|
||||
## [2.0.0-alpha.1] - 2020-02-27
|
||||
|
||||
## 2.0.0-alpha.1 - 2020-02-27
|
||||
### Changed
|
||||
|
||||
* Update the `time` dependency to 0.2.7.
|
||||
* Moved actors messages support from actix crate, enabled with feature `actors`.
|
||||
* Breaking change: trait MessageBody requires Unpin and accepting Pin<&mut Self> instead of &mut self in the poll_next().
|
||||
* Breaking change: trait MessageBody requires Unpin and accepting `Pin<&mut Self>` instead of
|
||||
`&mut self` in the poll_next().
|
||||
* MessageBody is not implemented for &'static [u8] anymore.
|
||||
|
||||
### Fixed
|
||||
|
||||
* Allow `SameSite=None` cookies to be sent in a response.
|
||||
|
||||
## [1.0.1] - 2019-12-20
|
||||
|
||||
## 1.0.1 - 2019-12-20
|
||||
### Fixed
|
||||
|
||||
* Poll upgrade service's readiness from HTTP service handlers
|
||||
|
||||
* Replace brotli with brotli2 #1224
|
||||
|
||||
## [1.0.0] - 2019-12-13
|
||||
|
||||
## 1.0.0 - 2019-12-13
|
||||
### Added
|
||||
|
||||
* Add websockets continuation frame support
|
||||
|
||||
### Changed
|
||||
|
||||
* Replace `flate2-xxx` features with `compress`
|
||||
|
||||
## [1.0.0-alpha.5] - 2019-12-09
|
||||
|
||||
## 1.0.0-alpha.5 - 2019-12-09
|
||||
### Fixed
|
||||
|
||||
* Check `Upgrade` service readiness before calling it
|
||||
|
||||
* Fix buffer remaining capacity calcualtion
|
||||
* Fix buffer remaining capacity calculation
|
||||
|
||||
### Changed
|
||||
|
||||
* Websockets: Ping and Pong should have binary data #1049
|
||||
|
||||
## [1.0.0-alpha.4] - 2019-12-08
|
||||
|
||||
## 1.0.0-alpha.4 - 2019-12-08
|
||||
### Added
|
||||
|
||||
* Add impl ResponseBuilder for Error
|
||||
|
||||
### Changed
|
||||
|
||||
* Use rust based brotli compression library
|
||||
|
||||
## [1.0.0-alpha.3] - 2019-12-07
|
||||
|
||||
## 1.0.0-alpha.3 - 2019-12-07
|
||||
### Changed
|
||||
|
||||
* Migrate to tokio 0.2
|
||||
|
||||
* Migrate to `std::future`
|
||||
|
||||
|
||||
## [0.2.11] - 2019-11-06
|
||||
|
||||
## 0.2.11 - 2019-11-06
|
||||
### Added
|
||||
|
||||
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
|
||||
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
|
||||
|
||||
* Add an additional `filename*` param in the `Content-Disposition` header of
|
||||
`actix_files::NamedFile` to be more compatible. (#1151)
|
||||
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
|
||||
|
||||
### Fixed
|
||||
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain;
|
||||
charset=utf-8` header [#1118]
|
||||
|
||||
* 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
|
||||
|
||||
|
||||
## [0.2.10] - 2019-09-11
|
||||
|
||||
## 0.2.10 - 2019-09-11
|
||||
### Added
|
||||
|
||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
|
||||
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests
|
||||
with `RequestHead`
|
||||
|
||||
### Fixed
|
||||
|
||||
* h2 will use error response #1080
|
||||
|
||||
* on_connect result isn't added to request extensions for http2 requests #1009
|
||||
|
||||
|
||||
## [0.2.9] - 2019-08-13
|
||||
|
||||
## 0.2.9 - 2019-08-13
|
||||
### Changed
|
||||
|
||||
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
|
||||
|
||||
* Update percent-encoding to 2.1
|
||||
|
||||
* Update serde_urlencoded to 0.6.1
|
||||
|
||||
### Fixed
|
||||
|
||||
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
|
||||
|
||||
|
||||
## [0.2.8] - 2019-08-01
|
||||
|
||||
## 0.2.8 - 2019-08-01
|
||||
### Added
|
||||
|
||||
* Add `rustls` support
|
||||
|
||||
* Add `Clone` impl for `HeaderMap`
|
||||
|
||||
### Fixed
|
||||
|
||||
* awc client panic #1016
|
||||
|
||||
* Invalid response with compression middleware enabled, but compression-related features disabled #997
|
||||
* Invalid response with compression middleware enabled, but compression-related features
|
||||
disabled #997
|
||||
|
||||
|
||||
## [0.2.7] - 2019-07-18
|
||||
|
||||
## 0.2.7 - 2019-07-18
|
||||
### Added
|
||||
|
||||
* Add support for downcasting response errors #986
|
||||
|
||||
|
||||
## [0.2.6] - 2019-07-17
|
||||
|
||||
## 0.2.6 - 2019-07-17
|
||||
### Changed
|
||||
|
||||
* Replace `ClonableService` with local copy
|
||||
|
||||
* Upgrade `rand` dependency version to 0.7
|
||||
|
||||
|
||||
## [0.2.5] - 2019-06-28
|
||||
|
||||
## 0.2.5 - 2019-06-28
|
||||
### Added
|
||||
|
||||
* Add `on-connect` callback, `HttpServiceBuilder::on_connect()` #946
|
||||
|
||||
### Changed
|
||||
|
||||
* Use `encoding_rs` crate instead of unmaintained `encoding` crate
|
||||
|
||||
* Add `Copy` and `Clone` impls for `ws::Codec`
|
||||
|
||||
|
||||
## [0.2.4] - 2019-06-16
|
||||
|
||||
## 0.2.4 - 2019-06-16
|
||||
### Fixed
|
||||
|
||||
* Do not compress NoContent (204) responses #918
|
||||
|
||||
|
||||
## [0.2.3] - 2019-06-02
|
||||
|
||||
## 0.2.3 - 2019-06-02
|
||||
### Added
|
||||
|
||||
* Debug impl for ResponseBuilder
|
||||
|
||||
* From SizedStream and BodyStream for Body
|
||||
|
||||
### Changed
|
||||
|
||||
* SizedStream uses u64
|
||||
|
||||
|
||||
## [0.2.2] - 2019-05-29
|
||||
|
||||
## 0.2.2 - 2019-05-29
|
||||
### Fixed
|
||||
|
||||
* Parse incoming stream before closing stream on disconnect #868
|
||||
|
||||
|
||||
## [0.2.1] - 2019-05-25
|
||||
|
||||
## 0.2.1 - 2019-05-25
|
||||
### Fixed
|
||||
|
||||
* Handle socket read disconnect
|
||||
|
||||
|
||||
## [0.2.0] - 2019-05-12
|
||||
|
||||
## 0.2.0 - 2019-05-12
|
||||
### Changed
|
||||
|
||||
* Update actix-service to 0.4
|
||||
|
||||
* Expect and upgrade services accept `ServerConfig` config.
|
||||
|
||||
### Deleted
|
||||
|
||||
* `OneRequest` service
|
||||
|
||||
|
||||
## [0.1.5] - 2019-05-04
|
||||
|
||||
## 0.1.5 - 2019-05-04
|
||||
### Fixed
|
||||
|
||||
* Clean up response extensions in response pool #817
|
||||
|
||||
|
||||
## [0.1.4] - 2019-04-24
|
||||
|
||||
## 0.1.4 - 2019-04-24
|
||||
### Added
|
||||
|
||||
* Allow to render h1 request headers in `Camel-Case`
|
||||
|
||||
### Fixed
|
||||
|
||||
* Read until eof for http/1.0 responses #771
|
||||
|
||||
|
||||
## [0.1.3] - 2019-04-23
|
||||
|
||||
## 0.1.3 - 2019-04-23
|
||||
### Fixed
|
||||
|
||||
* Fix http client pool management
|
||||
|
||||
* Fix http client wait queue management #794
|
||||
|
||||
|
||||
## [0.1.2] - 2019-04-23
|
||||
|
||||
## 0.1.2 - 2019-04-23
|
||||
### Fixed
|
||||
|
||||
* Fix BorrowMutError panic in client connector #793
|
||||
|
||||
|
||||
## [0.1.1] - 2019-04-19
|
||||
|
||||
## 0.1.1 - 2019-04-19
|
||||
### Changed
|
||||
|
||||
* Cookie::max_age() accepts value in seconds
|
||||
|
||||
* Cookie::max_age_time() accepts value in time::Duration
|
||||
|
||||
* Allow to specify server address for client connector
|
||||
|
||||
|
||||
## [0.1.0] - 2019-04-16
|
||||
|
||||
## 0.1.0 - 2019-04-16
|
||||
### Added
|
||||
|
||||
* Expose peer addr via `Request::peer_addr()` and `RequestHead::peer_addr`
|
||||
|
||||
### Changed
|
||||
|
||||
* `actix_http::encoding` always available
|
||||
|
||||
* use trust-dns-resolver 0.11.0
|
||||
|
||||
|
||||
## [0.1.0-alpha.5] - 2019-04-12
|
||||
|
||||
## 0.1.0-alpha.5 - 2019-04-12
|
||||
### Added
|
||||
|
||||
* Allow to use custom service for upgrade requests
|
||||
|
||||
* Added `h1::SendResponse` future.
|
||||
|
||||
### Changed
|
||||
|
||||
* MessageBody::length() renamed to MessageBody::size() for consistency
|
||||
|
||||
* ws handshake verification functions take RequestHead instead of Request
|
||||
|
||||
|
||||
## [0.1.0-alpha.4] - 2019-04-08
|
||||
|
||||
## 0.1.0-alpha.4 - 2019-04-08
|
||||
### Added
|
||||
|
||||
* Allow to use custom `Expect` handler
|
||||
|
||||
* Add minimal `std::error::Error` impl for `Error`
|
||||
|
||||
### Changed
|
||||
|
||||
* Export IntoHeaderValue
|
||||
|
||||
* Render error and return as response body
|
||||
|
||||
* Use thread pool for response body comression
|
||||
* Use thread pool for response body compression
|
||||
|
||||
### Deleted
|
||||
|
||||
* Removed PayloadBuffer
|
||||
|
||||
|
||||
## [0.1.0-alpha.3] - 2019-04-02
|
||||
|
||||
## 0.1.0-alpha.3 - 2019-04-02
|
||||
### Added
|
||||
|
||||
* Warn when an unsealed private cookie isn't valid UTF-8
|
||||
|
||||
### Fixed
|
||||
|
||||
* Rust 1.31.0 compatibility
|
||||
|
||||
* Preallocate read buffer for h1 codec
|
||||
|
||||
* Detect socket disconnection during protocol selection
|
||||
|
||||
|
||||
## [0.1.0-alpha.2] - 2019-03-29
|
||||
|
||||
## 0.1.0-alpha.2 - 2019-03-29
|
||||
### Added
|
||||
|
||||
* Added ws::Message::Nop, no-op websockets message
|
||||
|
||||
### Changed
|
||||
|
||||
* Do not use thread pool for decomression if chunk size is smaller than 2048.
|
||||
* Do not use thread pool for decompression 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
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "actix-http"
|
||||
version = "3.0.0-beta.1"
|
||||
version = "3.0.0-beta.4"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "HTTP primitives for the Actix ecosystem"
|
||||
readme = "README.md"
|
||||
@ -15,7 +15,8 @@ license = "MIT OR Apache-2.0"
|
||||
edition = "2018"
|
||||
|
||||
[package.metadata.docs.rs]
|
||||
features = ["openssl", "rustls", "compress", "secure-cookies", "actors"]
|
||||
# features that docs.rs will build with
|
||||
features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
|
||||
|
||||
[lib]
|
||||
name = "actix_http"
|
||||
@ -30,44 +31,42 @@ openssl = ["actix-tls/openssl"]
|
||||
# rustls support
|
||||
rustls = ["actix-tls/rustls"]
|
||||
|
||||
# enable compressison support
|
||||
# enable compression support
|
||||
compress = ["flate2", "brotli2"]
|
||||
|
||||
# support for secure cookies
|
||||
secure-cookies = ["cookie/secure"]
|
||||
# support for cookies
|
||||
cookies = ["cookie"]
|
||||
|
||||
# support for actix Actor messages
|
||||
actors = ["actix"]
|
||||
# support for secure cookies
|
||||
secure-cookies = ["cookies", "cookie/secure"]
|
||||
|
||||
# trust-dns as client dns resolver
|
||||
trust-dns = ["trust-dns-resolver"]
|
||||
|
||||
[dependencies]
|
||||
actix-service = "2.0.0-beta.2"
|
||||
actix-service = "2.0.0-beta.4"
|
||||
actix-codec = "0.4.0-beta.1"
|
||||
actix-utils = "3.0.0-beta.1"
|
||||
actix-rt = "2.0.0-beta.1"
|
||||
actix-threadpool = "0.3.1"
|
||||
actix-tls = "3.0.0-beta.2"
|
||||
actix = { version = "0.11.0-beta.1", optional = true }
|
||||
actix-utils = "3.0.0-beta.2"
|
||||
actix-rt = "2.1"
|
||||
actix-tls = "3.0.0-beta.4"
|
||||
|
||||
ahash = "0.7"
|
||||
base64 = "0.13"
|
||||
bitflags = "1.2"
|
||||
bytes = "1"
|
||||
bytestring = "1"
|
||||
cookie = { version = "0.14.1", features = ["percent-encode"] }
|
||||
copyless = "0.1.4"
|
||||
cfg-if = "1"
|
||||
cookie = { version = "0.14.1", features = ["percent-encode"], optional = true }
|
||||
derive_more = "0.99.5"
|
||||
either = "1.5.3"
|
||||
encoding_rs = "0.8"
|
||||
futures-channel = { version = "0.3.7", default-features = false }
|
||||
futures-core = { version = "0.3.7", default-features = false }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["sink"] }
|
||||
fxhash = "0.2.1"
|
||||
h2 = "0.3.0"
|
||||
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
|
||||
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
|
||||
h2 = "0.3.1"
|
||||
http = "0.2.2"
|
||||
httparse = "1.3"
|
||||
indexmap = "1.3"
|
||||
itoa = "0.4"
|
||||
lazy_static = "1.4"
|
||||
language-tags = "0.2"
|
||||
once_cell = "1.5"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
percent-encoding = "2.1"
|
||||
@ -76,24 +75,37 @@ rand = "0.8"
|
||||
regex = "1.3"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
sha-1 = "0.9"
|
||||
slab = "0.4"
|
||||
serde_urlencoded = "0.7"
|
||||
time = { version = "0.2.7", default-features = false, features = ["std"] }
|
||||
sha-1 = "0.9"
|
||||
smallvec = "1.6"
|
||||
time = { version = "0.2.23", default-features = false, features = ["std"] }
|
||||
tokio = { version = "1.2", features = ["sync"] }
|
||||
|
||||
# compression
|
||||
brotli2 = { version="0.3.2", optional = true }
|
||||
flate2 = { version = "1.0.13", optional = true }
|
||||
|
||||
trust-dns-resolver = { version = "0.20.0", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
actix-server = "2.0.0-beta.2"
|
||||
actix-http-test = { version = "3.0.0-beta.1", features = ["openssl"] }
|
||||
actix-tls = { version = "3.0.0-beta.2", features = ["openssl"] }
|
||||
actix-server = "2.0.0-beta.3"
|
||||
actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] }
|
||||
actix-tls = { version = "3.0.0-beta.4", features = ["openssl"] }
|
||||
criterion = "0.3"
|
||||
env_logger = "0.7"
|
||||
env_logger = "0.8"
|
||||
rcgen = "0.8"
|
||||
serde_derive = "1.0"
|
||||
open-ssl = { version="0.10", package = "openssl" }
|
||||
rust-tls = { version="0.19", package = "rustls" }
|
||||
tls-openssl = { version = "0.10", package = "openssl" }
|
||||
tls-rustls = { version = "0.19", package = "rustls" }
|
||||
|
||||
[target.'cfg(windows)'.dev-dependencies.tls-openssl]
|
||||
version = "0.10.9"
|
||||
package = "openssl"
|
||||
features = ["vendored"]
|
||||
|
||||
[[example]]
|
||||
name = "ws"
|
||||
required-features = ["rustls"]
|
||||
|
||||
[[bench]]
|
||||
name = "write-camel-case"
|
||||
|
@ -3,10 +3,13 @@
|
||||
> HTTP primitives for the Actix ecosystem.
|
||||
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://docs.rs/actix-http/2.2.0)
|
||||

|
||||
[](https://deps.rs/crate/actix-http/2.2.0)
|
||||
[](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
[](https://docs.rs/actix-http/3.0.0-beta.4)
|
||||
[](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
|
||||

|
||||
<br />
|
||||
[](https://deps.rs/crate/actix-http/3.0.0-beta.4)
|
||||
[](https://crates.io/crates/actix-http)
|
||||
[](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
## Documentation & Resources
|
||||
|
||||
|
@ -26,7 +26,10 @@ async fn main() -> io::Result<()> {
|
||||
info!("request body: {:?}", body);
|
||||
Ok::<_, Error>(
|
||||
Response::Ok()
|
||||
.header("x-head", HeaderValue::from_static("dummy value!"))
|
||||
.insert_header((
|
||||
"x-head",
|
||||
HeaderValue::from_static("dummy value!"),
|
||||
))
|
||||
.body(body),
|
||||
)
|
||||
})
|
||||
|
@ -15,7 +15,7 @@ async fn handle_request(mut req: Request) -> Result<Response, Error> {
|
||||
|
||||
info!("request body: {:?}", body);
|
||||
Ok(Response::Ok()
|
||||
.header("x-head", HeaderValue::from_static("dummy value!"))
|
||||
.insert_header(("x-head", HeaderValue::from_static("dummy value!")))
|
||||
.body(body))
|
||||
}
|
||||
|
||||
|
@ -19,7 +19,10 @@ async fn main() -> io::Result<()> {
|
||||
.finish(|_req| {
|
||||
info!("{:?}", _req);
|
||||
let mut res = Response::Ok();
|
||||
res.header("x-head", HeaderValue::from_static("dummy value!"));
|
||||
res.insert_header((
|
||||
"x-head",
|
||||
HeaderValue::from_static("dummy value!"),
|
||||
));
|
||||
future::ok::<_, ()>(res.body("Hello world!"))
|
||||
})
|
||||
.tcp()
|
||||
|
107
actix-http/examples/ws.rs
Normal file
107
actix-http/examples/ws.rs
Normal file
@ -0,0 +1,107 @@
|
||||
//! 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
|
||||
}
|
@ -1,710 +0,0 @@
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, mem};
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::{ready, Stream};
|
||||
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(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) => 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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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)
|
||||
}
|
||||
}
|
||||
|
||||
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.
|
||||
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;
|
||||
return Poll::Ready(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)),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// 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.
|
||||
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;
|
||||
return Poll::Ready(match ready!(Pin::new(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());
|
||||
}
|
||||
}
|
158
actix-http/src/body/body.rs
Normal file
158
actix-http/src/body/body.rs
Normal file
@ -0,0 +1,158 @@
|
||||
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)
|
||||
}
|
||||
}
|
59
actix-http/src/body/body_stream.rs
Normal file
59
actix-http/src/body/body_stream.rs
Normal file
@ -0,0 +1,59 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
142
actix-http/src/body/message_body.rs
Normal file
142
actix-http/src/body/message_body.rs
Normal file
@ -0,0 +1,142 @@
|
||||
//! [`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(),
|
||||
))))
|
||||
}
|
||||
}
|
||||
}
|
252
actix-http/src/body/mod.rs
Normal file
252
actix-http/src/body/mod.rs
Normal file
@ -0,0 +1,252 @@
|
||||
//! 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 bytes::{Bytes, BytesMut};
|
||||
use futures_util::{future::poll_fn, 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());
|
||||
}
|
||||
}
|
77
actix-http/src/body/response_body.rs
Normal file
77
actix-http/src/body/response_body.rs
Normal file
@ -0,0 +1,77 @@
|
||||
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),
|
||||
}
|
||||
}
|
||||
}
|
40
actix-http/src/body/size.rs
Normal file
40
actix-http/src/body/size.rs
Normal file
@ -0,0 +1,40 @@
|
||||
/// 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))
|
||||
}
|
||||
}
|
59
actix-http/src/body/sized_stream.rs
Normal file
59
actix-http/src/body/sized_stream.rs
Normal file
@ -0,0 +1,59 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,8 +1,8 @@
|
||||
use std::net::IpAddr;
|
||||
use std::time::Duration;
|
||||
|
||||
// These values are taken from hyper/src/proto/h2/client.rs
|
||||
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2mb
|
||||
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1mb
|
||||
const DEFAULT_H2_CONN_WINDOW: u32 = 1024 * 1024 * 2; // 2MB
|
||||
const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
|
||||
|
||||
/// Connector configuration
|
||||
#[derive(Clone)]
|
||||
@ -14,18 +14,20 @@ pub(crate) struct ConnectorConfig {
|
||||
pub(crate) limit: usize,
|
||||
pub(crate) conn_window_size: u32,
|
||||
pub(crate) stream_window_size: u32,
|
||||
pub(crate) local_address: Option<IpAddr>,
|
||||
}
|
||||
|
||||
impl Default for ConnectorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
timeout: Duration::from_secs(1),
|
||||
timeout: Duration::from_secs(5),
|
||||
conn_lifetime: Duration::from_secs(75),
|
||||
conn_keep_alive: Duration::from_secs(15),
|
||||
disconnect_timeout: Some(Duration::from_millis(3000)),
|
||||
limit: 100,
|
||||
conn_window_size: DEFAULT_H2_CONN_WINDOW,
|
||||
stream_window_size: DEFAULT_H2_STREAM_WINDOW,
|
||||
local_address: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,11 +1,12 @@
|
||||
use std::future::Future;
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{fmt, io, time};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||
use actix_rt::task::JoinHandle;
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::{err, Either, FutureExt, LocalBoxFuture, Ready};
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use h2::client::SendRequest;
|
||||
use pin_project::pin_project;
|
||||
|
||||
@ -15,33 +16,82 @@ use crate::message::{RequestHeadType, ResponseHead};
|
||||
use crate::payload::Payload;
|
||||
|
||||
use super::error::SendRequestError;
|
||||
use super::pool::{Acquired, Protocol};
|
||||
use super::pool::Acquired;
|
||||
use super::{h1proto, h2proto};
|
||||
|
||||
pub(crate) enum ConnectionType<Io> {
|
||||
H1(Io),
|
||||
H2(SendRequest<Bytes>),
|
||||
H2(H2Connection),
|
||||
}
|
||||
|
||||
/// `H2Connection` has two parts: `SendRequest` and `Connection`.
|
||||
///
|
||||
/// `Connection` is spawned as an async task on runtime and `H2Connection` holds a handle for
|
||||
/// this task. Therefore, it can wake up and quit the task when SendRequest is dropped.
|
||||
pub(crate) struct H2Connection {
|
||||
handle: JoinHandle<()>,
|
||||
sender: SendRequest<Bytes>,
|
||||
}
|
||||
|
||||
impl H2Connection {
|
||||
pub(crate) fn new<Io>(
|
||||
sender: SendRequest<Bytes>,
|
||||
connection: h2::client::Connection<Io>,
|
||||
) -> Self
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
let handle = actix_rt::spawn(async move {
|
||||
let _ = connection.await;
|
||||
});
|
||||
|
||||
Self { handle, sender }
|
||||
}
|
||||
}
|
||||
|
||||
// cancel spawned connection task on drop.
|
||||
impl Drop for H2Connection {
|
||||
fn drop(&mut self) {
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
// only expose sender type to public.
|
||||
impl Deref for H2Connection {
|
||||
type Target = SendRequest<Bytes>;
|
||||
|
||||
fn deref(&self) -> &Self::Target {
|
||||
&self.sender
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for H2Connection {
|
||||
fn deref_mut(&mut self) -> &mut Self::Target {
|
||||
&mut self.sender
|
||||
}
|
||||
}
|
||||
|
||||
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>>(
|
||||
fn send_request<B, H>(
|
||||
self,
|
||||
head: H,
|
||||
body: B,
|
||||
) -> Self::Future;
|
||||
|
||||
type TunnelFuture: Future<
|
||||
Output = Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||
>;
|
||||
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
|
||||
where
|
||||
B: MessageBody + 'static,
|
||||
H: Into<RequestHeadType> + 'static;
|
||||
|
||||
/// Send request, returns Response and Framed
|
||||
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
|
||||
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
|
||||
self,
|
||||
head: H,
|
||||
) -> LocalBoxFuture<
|
||||
'static,
|
||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||
>;
|
||||
}
|
||||
|
||||
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
||||
@ -54,7 +104,10 @@ pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
|
||||
|
||||
#[doc(hidden)]
|
||||
/// HTTP client connection
|
||||
pub struct IoConnection<T> {
|
||||
pub struct IoConnection<T>
|
||||
where
|
||||
T: AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
io: Option<ConnectionType<T>>,
|
||||
created: time::Instant,
|
||||
pool: Option<Acquired<T>>,
|
||||
@ -62,7 +115,7 @@ pub struct IoConnection<T> {
|
||||
|
||||
impl<T> fmt::Debug for IoConnection<T>
|
||||
where
|
||||
T: fmt::Debug,
|
||||
T: AsyncWrite + Unpin + fmt::Debug + 'static,
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
match self.io {
|
||||
@ -89,55 +142,36 @@ impl<T: AsyncRead + AsyncWrite + Unpin> IoConnection<T> {
|
||||
pub(crate) fn into_inner(self) -> (ConnectionType<T>, time::Instant) {
|
||||
(self.io.unwrap(), self.created)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn into_parts(self) -> (ConnectionType<T>, time::Instant, Acquired<T>) {
|
||||
(self.io.unwrap(), self.created, self.pool.unwrap())
|
||||
}
|
||||
|
||||
impl<T> Connection for IoConnection<T>
|
||||
where
|
||||
T: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
type Io = T;
|
||||
type Future =
|
||||
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
||||
|
||||
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>>(
|
||||
async fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
|
||||
mut self,
|
||||
head: H,
|
||||
body: B,
|
||||
) -> Self::Future {
|
||||
) -> Result<(ResponseHead, Payload), SendRequestError> {
|
||||
match self.io.take().unwrap() {
|
||||
ConnectionType::H1(io) => {
|
||||
h1proto::send_request(io, head.into(), body, self.created, self.pool)
|
||||
.boxed_local()
|
||||
.await
|
||||
}
|
||||
ConnectionType::H2(io) => {
|
||||
h2proto::send_request(io, head.into(), body, self.created, self.pool)
|
||||
.boxed_local()
|
||||
.await
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
async fn open_tunnel<H: Into<RequestHeadType>>(
|
||||
mut self,
|
||||
head: H,
|
||||
) -> Result<(ResponseHead, Framed<T, ClientCodec>), SendRequestError> {
|
||||
match self.io.take().unwrap() {
|
||||
ConnectionType::H1(io) => {
|
||||
Either::Left(h1proto::open_tunnel(io, head.into()).boxed_local())
|
||||
}
|
||||
ConnectionType::H1(io) => h1proto::open_tunnel(io, head.into()).await,
|
||||
ConnectionType::H2(io) => {
|
||||
if let Some(mut pool) = self.pool.take() {
|
||||
pool.release(IoConnection::new(
|
||||
@ -146,65 +180,61 @@ where
|
||||
None,
|
||||
));
|
||||
}
|
||||
Either::Right(err(SendRequestError::TunnelNotSupported))
|
||||
Err(SendRequestError::TunnelNotSupported)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
pub(crate) enum EitherConnection<A, B> {
|
||||
pub(crate) enum EitherIoConnection<A, B>
|
||||
where
|
||||
A: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
B: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
A(IoConnection<A>),
|
||||
B(IoConnection<B>),
|
||||
}
|
||||
|
||||
impl<A, B> Connection for EitherConnection<A, B>
|
||||
impl<A, B> Connection for EitherIoConnection<A, B>
|
||||
where
|
||||
A: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
B: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
type Io = EitherIo<A, B>;
|
||||
type Future =
|
||||
LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>;
|
||||
|
||||
fn protocol(&self) -> Protocol {
|
||||
match self {
|
||||
EitherConnection::A(con) => con.protocol(),
|
||||
EitherConnection::B(con) => con.protocol(),
|
||||
}
|
||||
}
|
||||
|
||||
fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>(
|
||||
fn send_request<RB, H>(
|
||||
self,
|
||||
head: H,
|
||||
body: RB,
|
||||
) -> Self::Future {
|
||||
) -> LocalBoxFuture<'static, Result<(ResponseHead, Payload), SendRequestError>>
|
||||
where
|
||||
RB: MessageBody + 'static,
|
||||
H: Into<RequestHeadType> + 'static,
|
||||
{
|
||||
match self {
|
||||
EitherConnection::A(con) => con.send_request(head, body),
|
||||
EitherConnection::B(con) => con.send_request(head, body),
|
||||
EitherIoConnection::A(con) => Box::pin(con.send_request(head, body)),
|
||||
EitherIoConnection::B(con) => Box::pin(con.send_request(head, body)),
|
||||
}
|
||||
}
|
||||
|
||||
type TunnelFuture = LocalBoxFuture<
|
||||
'static,
|
||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||
>;
|
||||
|
||||
/// Send request, returns Response and Framed
|
||||
fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
|
||||
fn open_tunnel<H: Into<RequestHeadType> + 'static>(
|
||||
self,
|
||||
head: H,
|
||||
) -> LocalBoxFuture<
|
||||
'static,
|
||||
Result<(ResponseHead, Framed<Self::Io, ClientCodec>), SendRequestError>,
|
||||
> {
|
||||
match self {
|
||||
EitherConnection::A(con) => con
|
||||
.open_tunnel(head)
|
||||
.map(|res| {
|
||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::A)))
|
||||
})
|
||||
.boxed_local(),
|
||||
EitherConnection::B(con) => con
|
||||
.open_tunnel(head)
|
||||
.map(|res| {
|
||||
res.map(|(head, framed)| (head, framed.into_map_io(EitherIo::B)))
|
||||
})
|
||||
.boxed_local(),
|
||||
EitherIoConnection::A(con) => Box::pin(async {
|
||||
let (head, framed) = con.open_tunnel(head).await?;
|
||||
Ok((head, framed.into_map_io(EitherIo::A)))
|
||||
}),
|
||||
EitherIoConnection::B(con) => Box::pin(async {
|
||||
let (head, framed) = con.open_tunnel(head).await?;
|
||||
Ok((head, framed.into_map_io(EitherIo::B)))
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -265,3 +295,35 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use std::net;
|
||||
|
||||
use actix_rt::net::TcpStream;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_h2_connection_drop() {
|
||||
let addr = "127.0.0.1:0".parse::<net::SocketAddr>().unwrap();
|
||||
let listener = net::TcpListener::bind(addr).unwrap();
|
||||
let local = listener.local_addr().unwrap();
|
||||
|
||||
std::thread::spawn(move || while listener.accept().is_ok() {});
|
||||
|
||||
let tcp = TcpStream::connect(local).await.unwrap();
|
||||
let (sender, connection) = h2::client::handshake(tcp).await.unwrap();
|
||||
let conn = H2Connection::new(sender.clone(), connection);
|
||||
|
||||
assert!(sender.clone().ready().await.is_ok());
|
||||
assert!(h2::client::SendRequest::clone(&*conn).ready().await.is_ok());
|
||||
|
||||
drop(conn);
|
||||
|
||||
match sender.ready().await {
|
||||
Ok(_) => panic!("connection should be gone and can not be ready"),
|
||||
Err(e) => assert!(e.is_io()),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
@ -1,25 +1,30 @@
|
||||
use std::fmt;
|
||||
use std::marker::PhantomData;
|
||||
use std::time::Duration;
|
||||
use std::{
|
||||
fmt,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
net::IpAddr,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
time::Duration,
|
||||
};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::net::TcpStream;
|
||||
use actix_service::{apply_fn, Service, ServiceExt};
|
||||
use actix_tls::connect::{
|
||||
default_connector, Connect as TcpConnect, Connection as TcpConnection,
|
||||
new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
|
||||
};
|
||||
use actix_utils::timeout::{TimeoutError, TimeoutService};
|
||||
use http::Uri;
|
||||
|
||||
use super::config::ConnectorConfig;
|
||||
use super::connection::Connection;
|
||||
use super::connection::{Connection, EitherIoConnection};
|
||||
use super::error::ConnectError;
|
||||
use super::pool::{ConnectionPool, Protocol};
|
||||
use super::Connect;
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
|
||||
|
||||
#[cfg(feature = "rustls")]
|
||||
use actix_tls::connect::ssl::rustls::ClientConfig;
|
||||
#[cfg(feature = "rustls")]
|
||||
@ -35,7 +40,8 @@ enum SslConnector {
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
type SslConnector = ();
|
||||
|
||||
/// Manages http client network connectivity
|
||||
/// Manages HTTP client network connectivity.
|
||||
///
|
||||
/// The `Connector` type uses a builder-like combinator pattern for service
|
||||
/// construction that finishes by calling the `.finish()` method.
|
||||
///
|
||||
@ -55,7 +61,7 @@ pub struct Connector<T, U> {
|
||||
_phantom: PhantomData<U>,
|
||||
}
|
||||
|
||||
trait Io: AsyncRead + AsyncWrite + Unpin {}
|
||||
pub trait Io: AsyncRead + AsyncWrite + Unpin {}
|
||||
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
|
||||
|
||||
impl Connector<(), ()> {
|
||||
@ -70,7 +76,7 @@ impl Connector<(), ()> {
|
||||
> {
|
||||
Connector {
|
||||
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
|
||||
connector: default_connector(),
|
||||
connector: new_connector(resolver::resolver()),
|
||||
config: ConnectorConfig::default(),
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
@ -100,9 +106,9 @@ impl Connector<(), ()> {
|
||||
fn build_ssl(protocols: Vec<Vec<u8>>) -> SslConnector {
|
||||
let mut config = ClientConfig::new();
|
||||
config.set_protocols(&protocols);
|
||||
config
|
||||
.root_store
|
||||
.add_server_trust_anchors(&actix_tls::accept::rustls::TLS_SERVER_ROOTS);
|
||||
config.root_store.add_server_trust_anchors(
|
||||
&actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
|
||||
);
|
||||
SslConnector::Rustls(Arc::new(config))
|
||||
}
|
||||
|
||||
@ -161,8 +167,9 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Maximum supported http major version
|
||||
/// Supported versions http/1.1, http/2
|
||||
/// Maximum supported HTTP major version.
|
||||
///
|
||||
/// Supported versions are HTTP/1.1 and HTTP/2.
|
||||
pub fn max_http_version(mut self, val: http::Version) -> Self {
|
||||
let versions = match val {
|
||||
http::Version::HTTP_11 => vec![b"http/1.1".to_vec()],
|
||||
@ -236,6 +243,12 @@ where
|
||||
self
|
||||
}
|
||||
|
||||
/// Set local IP Address the connector would use for establishing connection.
|
||||
pub fn local_address(mut self, addr: IpAddr) -> Self {
|
||||
self.config.local_address = Some(addr);
|
||||
self
|
||||
}
|
||||
|
||||
/// Finish configuration process and create connector service.
|
||||
/// The Connector builder always concludes by calling `finish()` last in
|
||||
/// its combinator chain.
|
||||
@ -243,12 +256,19 @@ where
|
||||
self,
|
||||
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
|
||||
{
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
{
|
||||
let connector = TimeoutService::new(
|
||||
self.config.timeout,
|
||||
apply_fn(self.connector, |msg: Connect, srv| {
|
||||
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
|
||||
let local_address = self.config.local_address;
|
||||
let timeout = self.config.timeout;
|
||||
|
||||
let tcp_service = TimeoutService::new(
|
||||
timeout,
|
||||
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
|
||||
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
|
||||
|
||||
if let Some(local_addr) = local_address {
|
||||
req = req.set_local_addr(local_addr);
|
||||
}
|
||||
|
||||
srv.call(req)
|
||||
})
|
||||
.map_err(ConnectError::from)
|
||||
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
|
||||
@ -258,13 +278,30 @@ where
|
||||
TimeoutError::Timeout => ConnectError::Timeout,
|
||||
});
|
||||
|
||||
connect_impl::InnerConnector {
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
{
|
||||
// A dummy service for annotate tls pool's type signature.
|
||||
pub type DummyService = Box<
|
||||
dyn Service<
|
||||
Connect,
|
||||
Response = (Box<dyn Io>, Protocol),
|
||||
Error = ConnectError,
|
||||
Future = futures_core::future::LocalBoxFuture<
|
||||
'static,
|
||||
Result<(Box<dyn Io>, Protocol), ConnectError>,
|
||||
>,
|
||||
>,
|
||||
>;
|
||||
|
||||
InnerConnector::<_, DummyService, _, Box<dyn Io>> {
|
||||
tcp_pool: ConnectionPool::new(
|
||||
connector,
|
||||
tcp_service,
|
||||
self.config.no_disconnect_timeout(),
|
||||
),
|
||||
tls_pool: None,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
{
|
||||
const H2: &[u8] = b"h2";
|
||||
@ -275,10 +312,16 @@ where
|
||||
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session};
|
||||
|
||||
let ssl_service = TimeoutService::new(
|
||||
self.config.timeout,
|
||||
timeout,
|
||||
pipeline(
|
||||
apply_fn(self.connector.clone(), |msg: Connect, srv| {
|
||||
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
|
||||
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
|
||||
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
|
||||
|
||||
if let Some(local_addr) = local_address {
|
||||
req = req.set_local_addr(local_addr);
|
||||
}
|
||||
|
||||
srv.call(req)
|
||||
})
|
||||
.map_err(ConnectError::from),
|
||||
)
|
||||
@ -327,208 +370,177 @@ where
|
||||
TimeoutError::Timeout => ConnectError::Timeout,
|
||||
});
|
||||
|
||||
let tcp_service = TimeoutService::new(
|
||||
self.config.timeout,
|
||||
apply_fn(self.connector, |msg: Connect, srv| {
|
||||
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
|
||||
})
|
||||
.map_err(ConnectError::from)
|
||||
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
|
||||
)
|
||||
.map_err(|e| match e {
|
||||
TimeoutError::Service(e) => e,
|
||||
TimeoutError::Timeout => ConnectError::Timeout,
|
||||
});
|
||||
|
||||
connect_impl::InnerConnector {
|
||||
InnerConnector {
|
||||
tcp_pool: ConnectionPool::new(
|
||||
tcp_service,
|
||||
self.config.no_disconnect_timeout(),
|
||||
),
|
||||
ssl_pool: ConnectionPool::new(ssl_service, self.config),
|
||||
tls_pool: Some(ConnectionPool::new(ssl_service, self.config)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
|
||||
mod connect_impl {
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use futures_util::future::{err, Either, Ready};
|
||||
|
||||
use super::*;
|
||||
use crate::client::connection::IoConnection;
|
||||
|
||||
pub(crate) struct InnerConnector<T, Io>
|
||||
struct InnerConnector<S1, S2, Io1, Io2>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
pub(crate) tcp_pool: ConnectionPool<T, Io>,
|
||||
tcp_pool: ConnectionPool<S1, Io1>,
|
||||
tls_pool: Option<ConnectionPool<S2, Io2>>,
|
||||
}
|
||||
|
||||
impl<T, Io> Clone for InnerConnector<T, Io>
|
||||
impl<S1, S2, Io1, Io2> Clone for InnerConnector<S1, S2, Io1, Io2>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
InnerConnector {
|
||||
tcp_pool: self.tcp_pool.clone(),
|
||||
tls_pool: self.tls_pool.as_ref().cloned(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, Io> Service<Connect> for InnerConnector<T, Io>
|
||||
impl<S1, S2, Io1, Io2> Service<Connect> for InnerConnector<S1, S2, Io1, Io2>
|
||||
where
|
||||
Io: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static,
|
||||
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
type Response = IoConnection<Io>;
|
||||
type Response = EitherIoConnection<Io1, Io2>;
|
||||
type Error = ConnectError;
|
||||
type Future = Either<
|
||||
<ConnectionPool<T, Io> as Service<Connect>>::Future,
|
||||
Ready<Result<IoConnection<Io>, ConnectError>>,
|
||||
>;
|
||||
type Future = InnerConnectorResponse<S1, S2, Io1, Io2>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.tcp_pool.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Connect) -> Self::Future {
|
||||
fn call(&self, req: Connect) -> Self::Future {
|
||||
match req.uri.scheme_str() {
|
||||
Some("https") | Some("wss") => {
|
||||
Either::Right(err(ConnectError::SslIsNotSupported))
|
||||
Some("https") | Some("wss") => match self.tls_pool {
|
||||
None => InnerConnectorResponse::SslIsNotSupported,
|
||||
Some(ref pool) => InnerConnectorResponse::Io2(pool.call(req)),
|
||||
},
|
||||
_ => InnerConnectorResponse::Io1(self.tcp_pool.call(req)),
|
||||
}
|
||||
_ => Either::Left(self.tcp_pool.call(req)),
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project(project = InnerConnectorProj)]
|
||||
enum InnerConnectorResponse<S1, S2, Io1, Io2>
|
||||
where
|
||||
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
Io1(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future),
|
||||
Io2(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future),
|
||||
SslIsNotSupported,
|
||||
}
|
||||
|
||||
impl<S1, S2, Io1, Io2> Future for InnerConnectorResponse<S1, S2, Io1, Io2>
|
||||
where
|
||||
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
type Output = Result<EitherIoConnection<Io1, Io2>, ConnectError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.project() {
|
||||
InnerConnectorProj::Io1(fut) => fut.poll(cx).map_ok(EitherIoConnection::A),
|
||||
InnerConnectorProj::Io2(fut) => fut.poll(cx).map_ok(EitherIoConnection::B),
|
||||
InnerConnectorProj::SslIsNotSupported => {
|
||||
Poll::Ready(Err(ConnectError::SslIsNotSupported))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(feature = "openssl", feature = "rustls"))]
|
||||
mod connect_impl {
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
#[cfg(not(feature = "trust-dns"))]
|
||||
mod resolver {
|
||||
use super::*;
|
||||
|
||||
use futures_core::ready;
|
||||
use futures_util::future::Either;
|
||||
pub(super) fn resolver() -> Resolver {
|
||||
Resolver::Default
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "trust-dns")]
|
||||
mod resolver {
|
||||
use std::{cell::RefCell, net::SocketAddr};
|
||||
|
||||
use actix_tls::connect::Resolve;
|
||||
use futures_core::future::LocalBoxFuture;
|
||||
use trust_dns_resolver::{
|
||||
config::{ResolverConfig, ResolverOpts},
|
||||
system_conf::read_system_conf,
|
||||
TokioAsyncResolver,
|
||||
};
|
||||
|
||||
use super::*;
|
||||
use crate::client::connection::EitherConnection;
|
||||
|
||||
pub(crate) struct InnerConnector<T1, T2, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
|
||||
pub(super) fn resolver() -> Resolver {
|
||||
// new type for impl Resolve trait for TokioAsyncResolver.
|
||||
struct TrustDnsResolver(TokioAsyncResolver);
|
||||
|
||||
impl Resolve for TrustDnsResolver {
|
||||
fn lookup<'a>(
|
||||
&'a self,
|
||||
host: &'a str,
|
||||
port: u16,
|
||||
) -> LocalBoxFuture<'a, Result<Vec<SocketAddr>, Box<dyn std::error::Error>>>
|
||||
{
|
||||
pub(crate) tcp_pool: ConnectionPool<T1, Io1>,
|
||||
pub(crate) ssl_pool: ConnectionPool<T2, Io2>,
|
||||
}
|
||||
|
||||
impl<T1, T2, Io1, Io2> Clone for InnerConnector<T1, T2, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
fn clone(&self) -> Self {
|
||||
InnerConnector {
|
||||
tcp_pool: self.tcp_pool.clone(),
|
||||
ssl_pool: self.ssl_pool.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T1, T2, Io1, Io2> Service<Connect> for InnerConnector<T1, T2, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
T2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
type Response = EitherConnection<Io1, Io2>;
|
||||
type Error = ConnectError;
|
||||
type Future = Either<
|
||||
InnerConnectorResponseA<T1, Io1, Io2>,
|
||||
InnerConnectorResponseB<T2, Io1, Io2>,
|
||||
>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.tcp_pool.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Connect) -> Self::Future {
|
||||
match req.uri.scheme_str() {
|
||||
Some("https") | Some("wss") => Either::Right(InnerConnectorResponseB {
|
||||
fut: self.ssl_pool.call(req),
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
_ => Either::Left(InnerConnectorResponseA {
|
||||
fut: self.tcp_pool.call(req),
|
||||
_phantom: PhantomData,
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub(crate) struct InnerConnectorResponseA<T, Io1, Io2>
|
||||
where
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
#[pin]
|
||||
fut: <ConnectionPool<T, Io1> as Service<Connect>>::Future,
|
||||
_phantom: PhantomData<Io2>,
|
||||
}
|
||||
|
||||
impl<T, Io1, Io2> Future for InnerConnectorResponseA<T, Io1, Io2>
|
||||
where
|
||||
T: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Poll::Ready(
|
||||
ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
|
||||
.map(EitherConnection::A),
|
||||
)
|
||||
Box::pin(async move {
|
||||
let res = self
|
||||
.0
|
||||
.lookup_ip(host)
|
||||
.await?
|
||||
.iter()
|
||||
.map(|ip| SocketAddr::new(ip, port))
|
||||
.collect();
|
||||
Ok(res)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project::pin_project]
|
||||
pub(crate) struct InnerConnectorResponseB<T, Io1, Io2>
|
||||
where
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
{
|
||||
#[pin]
|
||||
fut: <ConnectionPool<T, Io2> as Service<Connect>>::Future,
|
||||
_phantom: PhantomData<Io1>,
|
||||
// dns struct is cached in thread local.
|
||||
// so new client constructor can reuse the existing dns resolver.
|
||||
thread_local! {
|
||||
static TRUST_DNS_RESOLVER: RefCell<Option<Resolver>> = RefCell::new(None);
|
||||
}
|
||||
|
||||
impl<T, Io1, Io2> Future for InnerConnectorResponseB<T, Io1, Io2>
|
||||
where
|
||||
T: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
|
||||
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
type Output = Result<EitherConnection<Io1, Io2>, ConnectError>;
|
||||
// get from thread local or construct a new trust-dns resolver.
|
||||
TRUST_DNS_RESOLVER.with(|local| {
|
||||
let resolver = local.borrow().as_ref().map(Clone::clone);
|
||||
match resolver {
|
||||
Some(resolver) => resolver,
|
||||
None => {
|
||||
let (cfg, opts) = match read_system_conf() {
|
||||
Ok((cfg, opts)) => (cfg, opts),
|
||||
Err(e) => {
|
||||
log::error!("TRust-DNS can not load system config: {}", e);
|
||||
(ResolverConfig::default(), ResolverOpts::default())
|
||||
}
|
||||
};
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
Poll::Ready(
|
||||
ready!(Pin::new(&mut self.get_mut().fut).poll(cx))
|
||||
.map(EitherConnection::B),
|
||||
)
|
||||
let resolver = TokioAsyncResolver::tokio(cfg, opts).unwrap();
|
||||
|
||||
// box trust dns resolver and put it in thread local.
|
||||
let resolver = Resolver::new_custom(TrustDnsResolver(resolver));
|
||||
*local.borrow_mut() = Some(resolver.clone());
|
||||
resolver
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use std::io;
|
||||
|
||||
use actix_tls::connect::resolver::ResolveError;
|
||||
use derive_more::{Display, From};
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
@ -23,10 +22,10 @@ pub enum ConnectError {
|
||||
|
||||
/// Failed to resolve the hostname
|
||||
#[display(fmt = "Failed resolving hostname: {}", _0)]
|
||||
Resolver(ResolveError),
|
||||
Resolver(Box<dyn std::error::Error>),
|
||||
|
||||
/// No dns records
|
||||
#[display(fmt = "No dns records found for the input")]
|
||||
#[display(fmt = "No DNS records found for the input")]
|
||||
NoRecords,
|
||||
|
||||
/// Http2 error
|
||||
@ -66,13 +65,16 @@ impl From<actix_tls::connect::ConnectError> for ConnectError {
|
||||
|
||||
#[derive(Debug, Display, From)]
|
||||
pub enum InvalidUrl {
|
||||
#[display(fmt = "Missing url scheme")]
|
||||
#[display(fmt = "Missing URL scheme")]
|
||||
MissingScheme,
|
||||
#[display(fmt = "Unknown url scheme")]
|
||||
|
||||
#[display(fmt = "Unknown URL scheme")]
|
||||
UnknownScheme,
|
||||
|
||||
#[display(fmt = "Missing host name")]
|
||||
MissingHost,
|
||||
#[display(fmt = "Url parse error: {}", _0)]
|
||||
|
||||
#[display(fmt = "URL parse error: {}", _0)]
|
||||
HttpError(http::Error),
|
||||
}
|
||||
|
||||
@ -84,25 +86,33 @@ pub enum SendRequestError {
|
||||
/// Invalid URL
|
||||
#[display(fmt = "Invalid URL: {}", _0)]
|
||||
Url(InvalidUrl),
|
||||
|
||||
/// Failed to connect to host
|
||||
#[display(fmt = "Failed to connect to host: {}", _0)]
|
||||
Connect(ConnectError),
|
||||
|
||||
/// Error sending request
|
||||
Send(io::Error),
|
||||
|
||||
/// Error parsing response
|
||||
Response(ParseError),
|
||||
|
||||
/// Http error
|
||||
#[display(fmt = "{}", _0)]
|
||||
Http(HttpError),
|
||||
|
||||
/// Http2 error
|
||||
#[display(fmt = "{}", _0)]
|
||||
H2(h2::Error),
|
||||
|
||||
/// Response took too long
|
||||
#[display(fmt = "Timeout while waiting for response")]
|
||||
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")]
|
||||
TunnelNotSupported,
|
||||
|
||||
/// Error sending request body
|
||||
Body(Error),
|
||||
}
|
||||
@ -128,7 +138,8 @@ pub enum FreezeRequestError {
|
||||
/// Invalid URL
|
||||
#[display(fmt = "Invalid URL: {}", _0)]
|
||||
Url(InvalidUrl),
|
||||
/// Http error
|
||||
|
||||
/// HTTP error
|
||||
#[display(fmt = "{}", _0)]
|
||||
Http(HttpError),
|
||||
}
|
||||
|
@ -7,13 +7,15 @@ use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf};
|
||||
use bytes::buf::BufMut;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
use futures_util::future::poll_fn;
|
||||
use futures_util::{pin_mut, SinkExt, StreamExt};
|
||||
use futures_util::{future::poll_fn, SinkExt, StreamExt};
|
||||
|
||||
use crate::error::PayloadError;
|
||||
use crate::h1;
|
||||
use crate::header::HeaderMap;
|
||||
use crate::http::header::{IntoHeaderValue, HOST};
|
||||
use crate::http::{
|
||||
header::{IntoHeaderValue, EXPECT, HOST},
|
||||
StatusCode,
|
||||
};
|
||||
use crate::message::{RequestHeadType, ResponseHead};
|
||||
use crate::payload::{Payload, PayloadStream};
|
||||
|
||||
@ -45,14 +47,14 @@ where
|
||||
Some(port) => write!(wrt, "{}:{}", host, port),
|
||||
};
|
||||
|
||||
match wrt.get_mut().split().freeze().try_into() {
|
||||
match wrt.get_mut().split().freeze().try_into_value() {
|
||||
Ok(value) => match head {
|
||||
RequestHeadType::Owned(ref mut head) => {
|
||||
head.headers.insert(HOST, value)
|
||||
head.headers.insert(HOST, value);
|
||||
}
|
||||
RequestHeadType::Rc(_, ref mut extra_headers) => {
|
||||
let headers = extra_headers.get_or_insert(HeaderMap::new());
|
||||
headers.insert(HOST, value)
|
||||
headers.insert(HOST, value);
|
||||
}
|
||||
},
|
||||
Err(e) => log::error!("Can not set HOST header {}", e),
|
||||
@ -66,33 +68,72 @@ where
|
||||
io: Some(io),
|
||||
};
|
||||
|
||||
// create Framed and send request
|
||||
let mut framed_inner = Framed::new(io, h1::ClientCodec::default());
|
||||
framed_inner.send((head, body.size()).into()).await?;
|
||||
// create Framed and prepare sending request
|
||||
let mut framed = Framed::new(io, h1::ClientCodec::default());
|
||||
|
||||
// Check EXPECT header and enable expect handle flag accordingly.
|
||||
//
|
||||
// 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 pin_framed = Pin::new(&mut framed);
|
||||
|
||||
let force_close = !pin_framed.codec_ref().keepalive();
|
||||
release_connection(pin_framed, force_close);
|
||||
|
||||
// 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?;
|
||||
|
||||
let mut pin_framed = Pin::new(&mut framed);
|
||||
|
||||
// special handle for EXPECT request.
|
||||
let (do_send, mut res_head) = if is_expect {
|
||||
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 {
|
||||
// send request body
|
||||
match body.size() {
|
||||
BodySize::None | BodySize::Empty | BodySize::Sized(0) => {}
|
||||
_ => send_body(body, Pin::new(&mut framed_inner)).await?,
|
||||
_ => send_body(body, pin_framed.as_mut()).await?,
|
||||
};
|
||||
|
||||
// read response and init read body
|
||||
let res = Pin::new(&mut framed_inner).into_future().await;
|
||||
let (head, framed) = if let (Some(result), framed) = res {
|
||||
let item = result.map_err(SendRequestError::from)?;
|
||||
(item, framed)
|
||||
} else {
|
||||
return Err(SendRequestError::from(ConnectError::Disconnected));
|
||||
};
|
||||
let head = poll_fn(|cx| pin_framed.as_mut().poll_next(cx))
|
||||
.await
|
||||
.ok_or(ConnectError::Disconnected)??;
|
||||
|
||||
match framed.codec_ref().message_type() {
|
||||
res_head = Some(head);
|
||||
}
|
||||
|
||||
let head = res_head.unwrap();
|
||||
|
||||
match pin_framed.codec_ref().message_type() {
|
||||
h1::MessageType::None => {
|
||||
let force_close = !framed.codec_ref().keepalive();
|
||||
release_connection(framed, force_close);
|
||||
let force_close = !pin_framed.codec_ref().keepalive();
|
||||
release_connection(pin_framed, force_close);
|
||||
Ok((head, Payload::None))
|
||||
}
|
||||
_ => {
|
||||
let pl: PayloadStream = PlStream::new(framed_inner).boxed_local();
|
||||
let pl: PayloadStream = Box::pin(PlStream::new(framed));
|
||||
Ok((head, pl.into()))
|
||||
}
|
||||
}
|
||||
@ -127,7 +168,7 @@ where
|
||||
T: ConnectionLifetime + Unpin,
|
||||
B: MessageBody,
|
||||
{
|
||||
pin_mut!(body);
|
||||
actix_rt::pin!(body);
|
||||
|
||||
let mut eof = false;
|
||||
while !eof {
|
||||
@ -165,7 +206,10 @@ where
|
||||
|
||||
#[doc(hidden)]
|
||||
/// HTTP client connection
|
||||
pub struct H1Connection<T> {
|
||||
pub struct H1Connection<T>
|
||||
where
|
||||
T: AsyncWrite + Unpin + 'static,
|
||||
{
|
||||
/// T should be `Unpin`
|
||||
io: Option<T>,
|
||||
created: time::Instant,
|
||||
|
@ -1,11 +1,9 @@
|
||||
use std::convert::TryFrom;
|
||||
use std::future::Future;
|
||||
use std::time;
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use bytes::Bytes;
|
||||
use futures_util::future::poll_fn;
|
||||
use futures_util::pin_mut;
|
||||
use h2::{
|
||||
client::{Builder, Connection, SendRequest},
|
||||
SendStream,
|
||||
@ -22,9 +20,10 @@ use super::config::ConnectorConfig;
|
||||
use super::connection::{ConnectionType, IoConnection};
|
||||
use super::error::SendRequestError;
|
||||
use super::pool::Acquired;
|
||||
use crate::client::connection::H2Connection;
|
||||
|
||||
pub(crate) async fn send_request<T, B>(
|
||||
mut io: SendRequest<Bytes>,
|
||||
mut io: H2Connection,
|
||||
head: RequestHeadType,
|
||||
body: B,
|
||||
created: time::Instant,
|
||||
@ -35,6 +34,7 @@ where
|
||||
B: MessageBody,
|
||||
{
|
||||
trace!("Sending client request: {:?} {:?}", head, body.size());
|
||||
|
||||
let head_req = head.as_ref().method == Method::HEAD;
|
||||
let length = body.size();
|
||||
let eof = matches!(
|
||||
@ -60,10 +60,14 @@ where
|
||||
BodySize::Empty => req
|
||||
.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
||||
BodySize::Sized(len) => req.headers_mut().insert(
|
||||
BodySize::Sized(len) => {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
|
||||
req.headers_mut().insert(
|
||||
CONTENT_LENGTH,
|
||||
HeaderValue::try_from(format!("{}", len)).unwrap(),
|
||||
),
|
||||
HeaderValue::from_str(buf.format(len)).unwrap(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
|
||||
@ -86,7 +90,10 @@ where
|
||||
// copy headers
|
||||
for (key, value) in headers {
|
||||
match *key {
|
||||
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
|
||||
// TODO: consider skipping other headers according to:
|
||||
// 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,
|
||||
// DATE => has_date = true,
|
||||
_ => {}
|
||||
@ -129,7 +136,7 @@ async fn send_body<B: MessageBody>(
|
||||
mut send: SendStream<Bytes>,
|
||||
) -> Result<(), SendRequestError> {
|
||||
let mut buf = None;
|
||||
pin_mut!(body);
|
||||
actix_rt::pin!(body);
|
||||
loop {
|
||||
if buf.is_none() {
|
||||
match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
|
||||
@ -171,9 +178,9 @@ async fn send_body<B: MessageBody>(
|
||||
}
|
||||
}
|
||||
|
||||
// release SendRequest object
|
||||
/// release SendRequest object
|
||||
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
|
||||
io: SendRequest<Bytes>,
|
||||
io: H2Connection,
|
||||
pool: Option<Acquired<T>>,
|
||||
created: time::Instant,
|
||||
close: bool,
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Http client api
|
||||
//! HTTP client.
|
||||
|
||||
use http::Uri;
|
||||
|
||||
mod config;
|
||||
@ -9,6 +10,10 @@ mod h1proto;
|
||||
mod h2proto;
|
||||
mod pool;
|
||||
|
||||
pub use actix_tls::connect::{
|
||||
Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
|
||||
};
|
||||
|
||||
pub use self::connection::Connection;
|
||||
pub use self::connector::Connector;
|
||||
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -4,12 +4,14 @@ use std::rc::Rc;
|
||||
use std::time::Duration;
|
||||
use std::{fmt, net};
|
||||
|
||||
use actix_rt::time::{sleep, sleep_until, Instant, Sleep};
|
||||
use actix_rt::{
|
||||
task::JoinHandle,
|
||||
time::{interval, sleep_until, Instant, Sleep},
|
||||
};
|
||||
use bytes::BytesMut;
|
||||
use futures_util::{future, FutureExt};
|
||||
use time::OffsetDateTime;
|
||||
|
||||
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
/// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
const DATE_VALUE_LENGTH: usize = 29;
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Copy)]
|
||||
@ -49,7 +51,7 @@ struct Inner {
|
||||
ka_enabled: bool,
|
||||
secure: bool,
|
||||
local_addr: Option<std::net::SocketAddr>,
|
||||
timer: DateService,
|
||||
date_service: DateService,
|
||||
}
|
||||
|
||||
impl Clone for ServiceConfig {
|
||||
@ -91,41 +93,41 @@ impl ServiceConfig {
|
||||
client_disconnect,
|
||||
secure,
|
||||
local_addr,
|
||||
timer: DateService::new(),
|
||||
date_service: DateService::new(),
|
||||
}))
|
||||
}
|
||||
|
||||
/// Returns true if connection is secure (HTTPS)
|
||||
#[inline]
|
||||
/// Returns true if connection is secure(https)
|
||||
pub fn secure(&self) -> bool {
|
||||
self.0.secure
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Returns the local address that this server is bound to.
|
||||
#[inline]
|
||||
pub fn local_addr(&self) -> Option<net::SocketAddr> {
|
||||
self.0.local_addr
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Keep alive duration if configured.
|
||||
#[inline]
|
||||
pub fn keep_alive(&self) -> Option<Duration> {
|
||||
self.0.keep_alive
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Return state of connection keep-alive functionality
|
||||
#[inline]
|
||||
pub fn keep_alive_enabled(&self) -> bool {
|
||||
self.0.ka_enabled
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Client timeout for first request.
|
||||
#[inline]
|
||||
pub fn client_timer(&self) -> Option<Sleep> {
|
||||
let delay_time = self.0.client_timeout;
|
||||
if delay_time != 0 {
|
||||
Some(sleep_until(
|
||||
self.0.timer.now() + Duration::from_millis(delay_time),
|
||||
self.0.date_service.now() + Duration::from_millis(delay_time),
|
||||
))
|
||||
} else {
|
||||
None
|
||||
@ -136,7 +138,7 @@ impl ServiceConfig {
|
||||
pub fn client_timer_expire(&self) -> Option<Instant> {
|
||||
let delay = self.0.client_timeout;
|
||||
if delay != 0 {
|
||||
Some(self.0.timer.now() + Duration::from_millis(delay))
|
||||
Some(self.0.date_service.now() + Duration::from_millis(delay))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -146,7 +148,7 @@ impl ServiceConfig {
|
||||
pub fn client_disconnect_timer(&self) -> Option<Instant> {
|
||||
let delay = self.0.client_disconnect;
|
||||
if delay != 0 {
|
||||
Some(self.0.timer.now() + Duration::from_millis(delay))
|
||||
Some(self.0.date_service.now() + Duration::from_millis(delay))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -156,7 +158,7 @@ impl ServiceConfig {
|
||||
/// Return keep-alive timer delay is configured.
|
||||
pub fn keep_alive_timer(&self) -> Option<Sleep> {
|
||||
if let Some(ka) = self.0.keep_alive {
|
||||
Some(sleep_until(self.0.timer.now() + ka))
|
||||
Some(sleep_until(self.0.date_service.now() + ka))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -165,7 +167,7 @@ impl ServiceConfig {
|
||||
/// Keep-alive expire time
|
||||
pub fn keep_alive_expire(&self) -> Option<Instant> {
|
||||
if let Some(ka) = self.0.keep_alive {
|
||||
Some(self.0.timer.now() + ka)
|
||||
Some(self.0.date_service.now() + ka)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
@ -173,7 +175,7 @@ impl ServiceConfig {
|
||||
|
||||
#[inline]
|
||||
pub(crate) fn now(&self) -> Instant {
|
||||
self.0.timer.now()
|
||||
self.0.date_service.now()
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
@ -181,7 +183,7 @@ impl ServiceConfig {
|
||||
let mut buf: [u8; 39] = [0; 39];
|
||||
buf[..6].copy_from_slice(b"date: ");
|
||||
self.0
|
||||
.timer
|
||||
.date_service
|
||||
.set_date(|date| buf[6..35].copy_from_slice(&date.bytes));
|
||||
buf[35..].copy_from_slice(b"\r\n\r\n");
|
||||
dst.extend_from_slice(&buf);
|
||||
@ -189,7 +191,7 @@ impl ServiceConfig {
|
||||
|
||||
pub(crate) fn set_date_header(&self, dst: &mut BytesMut) {
|
||||
self.0
|
||||
.timer
|
||||
.date_service
|
||||
.set_date(|date| dst.extend_from_slice(&date.bytes));
|
||||
}
|
||||
}
|
||||
@ -230,57 +232,103 @@ impl fmt::Write for Date {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct DateService(Rc<DateServiceInner>);
|
||||
|
||||
struct DateServiceInner {
|
||||
current: Cell<Option<(Date, Instant)>>,
|
||||
/// Service for update Date and Instant periodically at 500 millis interval.
|
||||
struct DateService {
|
||||
current: Rc<Cell<(Date, Instant)>>,
|
||||
handle: JoinHandle<()>,
|
||||
}
|
||||
|
||||
impl DateServiceInner {
|
||||
fn new() -> Self {
|
||||
DateServiceInner {
|
||||
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 Drop for DateService {
|
||||
fn drop(&mut self) {
|
||||
// stop the timer update async task on drop.
|
||||
self.handle.abort();
|
||||
}
|
||||
}
|
||||
|
||||
impl DateService {
|
||||
fn new() -> Self {
|
||||
DateService(Rc::new(DateServiceInner::new()))
|
||||
}
|
||||
// shared date and timer for DateService and update async task.
|
||||
let current = Rc::new(Cell::new((Date::new(), Instant::now())));
|
||||
let current_clone = Rc::clone(¤t);
|
||||
// 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();
|
||||
|
||||
fn check_date(&self) {
|
||||
if self.0.current.get().is_none() {
|
||||
self.0.update();
|
||||
|
||||
// periodic date update
|
||||
let s = self.clone();
|
||||
actix_rt::spawn(sleep(Duration::from_millis(500)).then(move |_| {
|
||||
s.0.reset();
|
||||
future::ready(())
|
||||
}));
|
||||
let mut interval = interval(Duration::from_millis(500));
|
||||
loop {
|
||||
let now = interval.tick().await;
|
||||
let date = Date::new();
|
||||
current_clone.set((date, now));
|
||||
}
|
||||
});
|
||||
|
||||
DateService { current, handle }
|
||||
}
|
||||
|
||||
fn now(&self) -> Instant {
|
||||
self.check_date();
|
||||
self.0.current.get().unwrap().1
|
||||
self.current.get().1
|
||||
}
|
||||
|
||||
fn set_date<F: FnMut(&Date)>(&self, mut f: F) {
|
||||
self.check_date();
|
||||
f(&self.0.current.get().unwrap().0);
|
||||
f(&self.current.get().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;
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -288,14 +336,53 @@ impl DateService {
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
// Test modifying the date from within the closure
|
||||
// passed to `set_date`
|
||||
#[test]
|
||||
fn test_evil_date() {
|
||||
let service = DateService::new();
|
||||
// Make sure that `check_date` doesn't try to spawn a task
|
||||
service.0.update();
|
||||
service.set_date(|_| service.0.reset());
|
||||
use actix_rt::task::yield_now;
|
||||
|
||||
#[actix_rt::test]
|
||||
async fn test_date_service_update() {
|
||||
let settings = ServiceConfig::new(KeepAlive::Os, 0, 0, false, None);
|
||||
|
||||
yield_now().await;
|
||||
|
||||
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]
|
||||
|
@ -1,25 +1,31 @@
|
||||
use std::future::Future;
|
||||
use std::io::{self, Write};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
//! Stream decoders.
|
||||
|
||||
use actix_threadpool::{run, CpuFuture};
|
||||
use std::{
|
||||
future::Future,
|
||||
io::{self, Write as _},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||
use brotli2::write::BrotliDecoder;
|
||||
use bytes::Bytes;
|
||||
use flate2::write::{GzDecoder, ZlibDecoder};
|
||||
use futures_core::{ready, Stream};
|
||||
|
||||
use super::Writer;
|
||||
use crate::error::PayloadError;
|
||||
use crate::http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING};
|
||||
use crate::{
|
||||
encoding::Writer,
|
||||
error::{BlockingError, PayloadError},
|
||||
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
|
||||
};
|
||||
|
||||
const INPLACE: usize = 2049;
|
||||
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;
|
||||
|
||||
pub struct Decoder<S> {
|
||||
decoder: Option<ContentDecoder>,
|
||||
stream: S,
|
||||
eof: bool,
|
||||
fut: Option<CpuFuture<(Option<Bytes>, ContentDecoder), io::Error>>,
|
||||
fut: Option<JoinHandle<Result<(Option<Bytes>, ContentDecoder), io::Error>>>,
|
||||
}
|
||||
|
||||
impl<S> Decoder<S>
|
||||
@ -41,6 +47,7 @@ where
|
||||
))),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
Decoder {
|
||||
decoder,
|
||||
stream,
|
||||
@ -53,15 +60,11 @@ where
|
||||
#[inline]
|
||||
pub fn from_headers(stream: S, headers: &HeaderMap) -> Decoder<S> {
|
||||
// check content-encoding
|
||||
let encoding = if let Some(enc) = headers.get(&CONTENT_ENCODING) {
|
||||
if let Ok(enc) = enc.to_str() {
|
||||
ContentEncoding::from(enc)
|
||||
} else {
|
||||
ContentEncoding::Identity
|
||||
}
|
||||
} else {
|
||||
ContentEncoding::Identity
|
||||
};
|
||||
let encoding = headers
|
||||
.get(&CONTENT_ENCODING)
|
||||
.and_then(|val| val.to_str().ok())
|
||||
.map(ContentEncoding::from)
|
||||
.unwrap_or(ContentEncoding::Identity);
|
||||
|
||||
Self::new(stream, encoding)
|
||||
}
|
||||
@ -79,12 +82,12 @@ where
|
||||
) -> Poll<Option<Self::Item>> {
|
||||
loop {
|
||||
if let Some(ref mut fut) = self.fut {
|
||||
let (chunk, decoder) = match ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
||||
};
|
||||
let (chunk, decoder) =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
|
||||
self.decoder = Some(decoder);
|
||||
self.fut.take();
|
||||
|
||||
if let Some(chunk) = chunk {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
@ -94,29 +97,34 @@ where
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
|
||||
match Pin::new(&mut self.stream).poll_next(cx) {
|
||||
Poll::Ready(Some(Err(err))) => return Poll::Ready(Some(Err(err))),
|
||||
Poll::Ready(Some(Ok(chunk))) => {
|
||||
match ready!(Pin::new(&mut self.stream).poll_next(cx)) {
|
||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
|
||||
Some(Ok(chunk)) => {
|
||||
if let Some(mut decoder) = self.decoder.take() {
|
||||
if chunk.len() < INPLACE {
|
||||
if chunk.len() < MAX_CHUNK_SIZE_DECODE_IN_PLACE {
|
||||
let chunk = decoder.feed_data(chunk)?;
|
||||
self.decoder = Some(decoder);
|
||||
|
||||
if let Some(chunk) = chunk {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
} else {
|
||||
self.fut = Some(run(move || {
|
||||
self.fut = Some(spawn_blocking(move || {
|
||||
let chunk = decoder.feed_data(chunk)?;
|
||||
Ok((chunk, decoder))
|
||||
}));
|
||||
}
|
||||
|
||||
continue;
|
||||
} else {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
|
||||
None => {
|
||||
self.eof = true;
|
||||
|
||||
return if let Some(mut decoder) = self.decoder.take() {
|
||||
match decoder.feed_eof() {
|
||||
Ok(Some(res)) => Poll::Ready(Some(Ok(res))),
|
||||
@ -127,10 +135,8 @@ where
|
||||
Poll::Ready(None)
|
||||
};
|
||||
}
|
||||
Poll::Pending => break,
|
||||
}
|
||||
}
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
||||
@ -146,6 +152,7 @@ impl ContentDecoder {
|
||||
ContentDecoder::Br(ref mut decoder) => match decoder.flush() {
|
||||
Ok(()) => {
|
||||
let b = decoder.get_mut().take();
|
||||
|
||||
if !b.is_empty() {
|
||||
Ok(Some(b))
|
||||
} else {
|
||||
@ -154,9 +161,11 @@ impl ContentDecoder {
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
|
||||
ContentDecoder::Gzip(ref mut decoder) => match decoder.try_finish() {
|
||||
Ok(_) => {
|
||||
let b = decoder.get_mut().take();
|
||||
|
||||
if !b.is_empty() {
|
||||
Ok(Some(b))
|
||||
} else {
|
||||
@ -165,6 +174,7 @@ impl ContentDecoder {
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
|
||||
ContentDecoder::Deflate(ref mut decoder) => match decoder.try_finish() {
|
||||
Ok(_) => {
|
||||
let b = decoder.get_mut().take();
|
||||
@ -185,6 +195,7 @@ impl ContentDecoder {
|
||||
Ok(_) => {
|
||||
decoder.flush()?;
|
||||
let b = decoder.get_mut().take();
|
||||
|
||||
if !b.is_empty() {
|
||||
Ok(Some(b))
|
||||
} else {
|
||||
@ -193,10 +204,12 @@ impl ContentDecoder {
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
|
||||
ContentDecoder::Gzip(ref mut decoder) => match decoder.write_all(&data) {
|
||||
Ok(_) => {
|
||||
decoder.flush()?;
|
||||
let b = decoder.get_mut().take();
|
||||
|
||||
if !b.is_empty() {
|
||||
Ok(Some(b))
|
||||
} else {
|
||||
@ -205,9 +218,11 @@ impl ContentDecoder {
|
||||
}
|
||||
Err(e) => Err(e),
|
||||
},
|
||||
|
||||
ContentDecoder::Deflate(ref mut decoder) => match decoder.write_all(&data) {
|
||||
Ok(_) => {
|
||||
decoder.flush()?;
|
||||
|
||||
let b = decoder.get_mut().take();
|
||||
if !b.is_empty() {
|
||||
Ok(Some(b))
|
||||
|
@ -1,24 +1,32 @@
|
||||
//! Stream encoder
|
||||
use std::future::Future;
|
||||
use std::io::{self, Write};
|
||||
use std::pin::Pin;
|
||||
use std::task::{Context, Poll};
|
||||
//! Stream encoders.
|
||||
|
||||
use actix_threadpool::{run, CpuFuture};
|
||||
use std::{
|
||||
future::Future,
|
||||
io::{self, Write as _},
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
|
||||
use actix_rt::task::{spawn_blocking, JoinHandle};
|
||||
use brotli2::write::BrotliEncoder;
|
||||
use bytes::Bytes;
|
||||
use flate2::write::{GzEncoder, ZlibEncoder};
|
||||
use futures_core::ready;
|
||||
use pin_project::pin_project;
|
||||
|
||||
use crate::body::{Body, BodySize, MessageBody, ResponseBody};
|
||||
use crate::http::header::{ContentEncoding, CONTENT_ENCODING};
|
||||
use crate::http::{HeaderValue, StatusCode};
|
||||
use crate::{Error, ResponseHead};
|
||||
use crate::{
|
||||
body::{Body, BodySize, MessageBody, ResponseBody},
|
||||
http::{
|
||||
header::{ContentEncoding, CONTENT_ENCODING},
|
||||
HeaderValue, StatusCode,
|
||||
},
|
||||
Error, ResponseHead,
|
||||
};
|
||||
|
||||
use super::Writer;
|
||||
use crate::error::BlockingError;
|
||||
|
||||
const INPLACE: usize = 1024;
|
||||
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
|
||||
|
||||
#[pin_project]
|
||||
pub struct Encoder<B> {
|
||||
@ -26,7 +34,7 @@ pub struct Encoder<B> {
|
||||
#[pin]
|
||||
body: EncoderBody<B>,
|
||||
encoder: Option<ContentEncoder>,
|
||||
fut: Option<CpuFuture<ContentEncoder, io::Error>>,
|
||||
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
|
||||
}
|
||||
|
||||
impl<B: MessageBody> Encoder<B> {
|
||||
@ -70,6 +78,7 @@ impl<B: MessageBody> Encoder<B> {
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
ResponseBody::Body(Encoder {
|
||||
body,
|
||||
eof: false,
|
||||
@ -135,32 +144,35 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
||||
}
|
||||
|
||||
if let Some(ref mut fut) = this.fut {
|
||||
let mut encoder = match ready!(Pin::new(fut).poll(cx)) {
|
||||
Ok(item) => item,
|
||||
Err(e) => return Poll::Ready(Some(Err(e.into()))),
|
||||
};
|
||||
let mut encoder =
|
||||
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
|
||||
|
||||
let chunk = encoder.take();
|
||||
*this.encoder = Some(encoder);
|
||||
this.fut.take();
|
||||
|
||||
if !chunk.is_empty() {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
}
|
||||
|
||||
let result = this.body.as_mut().poll_next(cx);
|
||||
let result = ready!(this.body.as_mut().poll_next(cx));
|
||||
|
||||
match result {
|
||||
Poll::Ready(Some(Ok(chunk))) => {
|
||||
Some(Err(err)) => return Poll::Ready(Some(Err(err))),
|
||||
|
||||
Some(Ok(chunk)) => {
|
||||
if let Some(mut encoder) = this.encoder.take() {
|
||||
if chunk.len() < INPLACE {
|
||||
if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
|
||||
encoder.write(&chunk)?;
|
||||
let chunk = encoder.take();
|
||||
*this.encoder = Some(encoder);
|
||||
|
||||
if !chunk.is_empty() {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
} else {
|
||||
*this.fut = Some(run(move || {
|
||||
*this.fut = Some(spawn_blocking(move || {
|
||||
encoder.write(&chunk)?;
|
||||
Ok(encoder)
|
||||
}));
|
||||
@ -169,7 +181,8 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
||||
return Poll::Ready(Some(Ok(chunk)));
|
||||
}
|
||||
}
|
||||
Poll::Ready(None) => {
|
||||
|
||||
None => {
|
||||
if let Some(encoder) = this.encoder.take() {
|
||||
let chunk = encoder.finish()?;
|
||||
if chunk.is_empty() {
|
||||
@ -182,7 +195,6 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
|
||||
return Poll::Ready(None);
|
||||
}
|
||||
}
|
||||
val => return val,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,5 @@
|
||||
//! Content-Encoding support
|
||||
//! Content-Encoding support.
|
||||
|
||||
use std::io;
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
@ -7,24 +7,23 @@ use std::string::FromUtf8Error;
|
||||
use std::{fmt, io, result};
|
||||
|
||||
use actix_codec::{Decoder, Encoder};
|
||||
pub use actix_threadpool::BlockingError;
|
||||
use actix_utils::dispatcher::DispatcherError as FramedDispatcherError;
|
||||
use actix_utils::timeout::TimeoutError;
|
||||
use bytes::BytesMut;
|
||||
use derive_more::{Display, From};
|
||||
pub use futures_channel::oneshot::Canceled;
|
||||
use http::uri::InvalidUri;
|
||||
use http::{header, Error as HttpError, StatusCode};
|
||||
use serde::de::value::Error as DeError;
|
||||
use serde_json::error::Error as JsonError;
|
||||
use serde_urlencoded::ser::Error as FormError;
|
||||
|
||||
// re-export for convenience
|
||||
use crate::body::Body;
|
||||
pub use crate::cookie::ParseError as CookieParseError;
|
||||
use crate::helpers::Writer;
|
||||
use crate::response::{Response, ResponseBuilder};
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
pub use crate::cookie::ParseError as CookieParseError;
|
||||
|
||||
/// A specialized [`std::result::Result`]
|
||||
/// for actix web operations
|
||||
///
|
||||
@ -40,7 +39,7 @@ pub type Result<T, E = Error> = result::Result<T, E>;
|
||||
/// converting errors with `into()`.
|
||||
///
|
||||
/// 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
|
||||
/// `ResponseError` reference from it.
|
||||
pub struct Error {
|
||||
@ -100,10 +99,6 @@ impl fmt::Debug for Error {
|
||||
}
|
||||
|
||||
impl std::error::Error for Error {
|
||||
fn cause(&self) -> Option<&dyn std::error::Error> {
|
||||
None
|
||||
}
|
||||
|
||||
fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
|
||||
None
|
||||
}
|
||||
@ -153,7 +148,10 @@ impl From<ResponseBuilder> for Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `GATEWAY_TIMEOUT` for `TimeoutError`
|
||||
/// Inspects the underlying enum and returns an appropriate status code.
|
||||
///
|
||||
/// If the variant is [`TimeoutError::Service`], the error code of the service is returned.
|
||||
/// Otherwise, [`StatusCode::GATEWAY_TIMEOUT`] is returned.
|
||||
impl<E: ResponseError> ResponseError for TimeoutError<E> {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self {
|
||||
@ -167,44 +165,41 @@ impl<E: ResponseError> ResponseError for TimeoutError<E> {
|
||||
#[display(fmt = "UnknownError")]
|
||||
struct UnitError;
|
||||
|
||||
/// `InternalServerError` for `UnitError`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`UnitError`].
|
||||
impl ResponseError for UnitError {}
|
||||
|
||||
/// `InternalServerError` for `JsonError`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`JsonError`].
|
||||
impl ResponseError for JsonError {}
|
||||
|
||||
/// `InternalServerError` for `FormError`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`FormError`].
|
||||
impl ResponseError for FormError {}
|
||||
|
||||
#[cfg(feature = "openssl")]
|
||||
/// `InternalServerError` for `openssl::ssl::Error`
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`actix_tls::accept::openssl::SslError`].
|
||||
impl ResponseError for actix_tls::accept::openssl::SslError {}
|
||||
|
||||
/// Return `BAD_REQUEST` for `de::value::Error`
|
||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`DeError`].
|
||||
impl ResponseError for DeError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `Canceled`
|
||||
impl ResponseError for Canceled {}
|
||||
|
||||
/// `InternalServerError` for `BlockingError`
|
||||
impl<E: fmt::Debug> ResponseError for BlockingError<E> {}
|
||||
|
||||
/// Return `BAD_REQUEST` for `Utf8Error`
|
||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`Utf8Error`].
|
||||
impl ResponseError for Utf8Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
}
|
||||
}
|
||||
|
||||
/// Return `InternalServerError` for `HttpError`,
|
||||
/// Response generation can return `HttpError`, so it is internal error
|
||||
/// Returns [`StatusCode::INTERNAL_SERVER_ERROR`] for [`HttpError`].
|
||||
impl ResponseError for HttpError {}
|
||||
|
||||
/// Return `InternalServerError` for `io::Error`
|
||||
/// Inspects the underlying [`io::ErrorKind`] and returns an appropriate status code.
|
||||
///
|
||||
/// If the error is [`io::ErrorKind::NotFound`], [`StatusCode::NOT_FOUND`] is returned. If the
|
||||
/// error is [`io::ErrorKind::PermissionDenied`], [`StatusCode::FORBIDDEN`] is returned. Otherwise,
|
||||
/// [`StatusCode::INTERNAL_SERVER_ERROR`] is returned.
|
||||
impl ResponseError for io::Error {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
match self.kind() {
|
||||
@ -215,7 +210,7 @@ impl ResponseError for io::Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// `BadRequest` for `InvalidHeaderValue`
|
||||
/// Returns [`StatusCode::BAD_REQUEST`] for [`header::InvalidHeaderValue`].
|
||||
impl ResponseError for header::InvalidHeaderValue {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
@ -304,33 +299,60 @@ impl From<httparse::Error> for ParseError {
|
||||
}
|
||||
}
|
||||
|
||||
/// A set of errors that can occur running blocking tasks in thread pool.
|
||||
#[derive(Debug, Display)]
|
||||
#[display(fmt = "Blocking thread pool is gone")]
|
||||
pub struct BlockingError;
|
||||
|
||||
impl std::error::Error for BlockingError {}
|
||||
|
||||
/// `InternalServerError` for `BlockingError`
|
||||
impl ResponseError for BlockingError {}
|
||||
|
||||
#[derive(Display, Debug)]
|
||||
/// A set of errors that can occur during payload parsing
|
||||
pub enum PayloadError {
|
||||
/// A payload reached EOF, but is not complete.
|
||||
#[display(
|
||||
fmt = "A payload reached EOF, but is not complete. With error: {:?}",
|
||||
fmt = "A payload reached EOF, but is not complete. Inner error: {:?}",
|
||||
_0
|
||||
)]
|
||||
Incomplete(Option<io::Error>),
|
||||
/// Content encoding stream corruption
|
||||
|
||||
/// Content encoding stream corruption.
|
||||
#[display(fmt = "Can not decode content-encoding.")]
|
||||
EncodingCorrupted,
|
||||
/// A payload reached size limit.
|
||||
#[display(fmt = "A payload reached size limit.")]
|
||||
|
||||
/// Payload reached size limit.
|
||||
#[display(fmt = "Payload reached size limit.")]
|
||||
Overflow,
|
||||
/// A payload length is unknown.
|
||||
#[display(fmt = "A payload length is unknown.")]
|
||||
|
||||
/// Payload length is unknown.
|
||||
#[display(fmt = "Payload length is unknown.")]
|
||||
UnknownLength,
|
||||
/// Http2 payload error
|
||||
|
||||
/// HTTP/2 payload error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
Http2Payload(h2::Error),
|
||||
/// Io error
|
||||
|
||||
/// Generic I/O error.
|
||||
#[display(fmt = "{}", _0)]
|
||||
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 {
|
||||
fn from(err: h2::Error) -> Self {
|
||||
@ -350,15 +372,12 @@ impl From<io::Error> for PayloadError {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<BlockingError<io::Error>> for PayloadError {
|
||||
fn from(err: BlockingError<io::Error>) -> Self {
|
||||
match err {
|
||||
BlockingError::Error(e) => PayloadError::Io(e),
|
||||
BlockingError::Canceled => PayloadError::Io(io::Error::new(
|
||||
impl From<BlockingError> for PayloadError {
|
||||
fn from(_: BlockingError) -> Self {
|
||||
PayloadError::Io(io::Error::new(
|
||||
io::ErrorKind::Other,
|
||||
"Operation is canceled",
|
||||
)),
|
||||
}
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
@ -376,6 +395,7 @@ impl ResponseError for PayloadError {
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `cookie::ParseError`
|
||||
#[cfg(feature = "cookies")]
|
||||
impl ResponseError for crate::cookie::ParseError {
|
||||
fn status_code(&self) -> StatusCode {
|
||||
StatusCode::BAD_REQUEST
|
||||
@ -383,7 +403,7 @@ impl ResponseError for crate::cookie::ParseError {
|
||||
}
|
||||
|
||||
#[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 {
|
||||
/// Service error
|
||||
Service(Error),
|
||||
@ -947,11 +967,6 @@ where
|
||||
InternalError::new(err, StatusCode::NETWORK_AUTHENTICATION_REQUIRED).into()
|
||||
}
|
||||
|
||||
#[cfg(feature = "actors")]
|
||||
/// `InternalServerError` for `actix::MailboxError`
|
||||
/// This is supported on feature=`actors` only
|
||||
impl ResponseError for actix::MailboxError {}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
@ -968,6 +983,7 @@ mod tests {
|
||||
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
|
||||
}
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
#[test]
|
||||
fn test_cookie_parse() {
|
||||
let resp: Response = CookieParseError::EmptyName.error_response();
|
||||
@ -1009,22 +1025,22 @@ mod tests {
|
||||
fn test_payload_error() {
|
||||
let err: PayloadError =
|
||||
io::Error::new(io::ErrorKind::Other, "ParseError").into();
|
||||
assert!(format!("{}", err).contains("ParseError"));
|
||||
assert!(err.to_string().contains("ParseError"));
|
||||
|
||||
let err = PayloadError::Incomplete(None);
|
||||
assert_eq!(
|
||||
format!("{}", err),
|
||||
"A payload reached EOF, but is not complete. With error: None"
|
||||
err.to_string(),
|
||||
"A payload reached EOF, but is not complete. Inner error: None"
|
||||
);
|
||||
}
|
||||
|
||||
macro_rules! from {
|
||||
($from:expr => $error:pat) => {
|
||||
match ParseError::from($from) {
|
||||
e @ $error => {
|
||||
assert!(format!("{}", e).len() >= 5);
|
||||
err @ $error => {
|
||||
assert!(err.to_string().len() >= 5);
|
||||
}
|
||||
e => unreachable!("{:?}", e),
|
||||
err => unreachable!("{:?}", err),
|
||||
}
|
||||
};
|
||||
}
|
||||
@ -1067,7 +1083,7 @@ mod tests {
|
||||
let err = PayloadError::Overflow;
|
||||
let resp_err: &dyn ResponseError = &err;
|
||||
let err = resp_err.downcast_ref::<PayloadError>().unwrap();
|
||||
assert_eq!(err.to_string(), "A payload reached size limit.");
|
||||
assert_eq!(err.to_string(), "Payload reached size limit.");
|
||||
let not_err = resp_err.downcast_ref::<ContentTypeError>();
|
||||
assert!(not_err.is_none());
|
||||
}
|
||||
|
@ -1,62 +1,119 @@
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{fmt, mem};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
fmt, mem,
|
||||
};
|
||||
|
||||
use fxhash::FxHashMap;
|
||||
use ahash::AHashMap;
|
||||
|
||||
/// A type map of request extensions.
|
||||
/// A type map for request extensions.
|
||||
///
|
||||
/// All entries into this map must be owned types (or static references).
|
||||
#[derive(Default)]
|
||||
pub struct Extensions {
|
||||
/// Use FxHasher with a std HashMap with for faster
|
||||
/// lookups on the small `TypeId` (u64 equivalent) keys.
|
||||
map: FxHashMap<TypeId, Box<dyn Any>>,
|
||||
map: AHashMap<TypeId, Box<dyn Any>>,
|
||||
}
|
||||
|
||||
impl Extensions {
|
||||
/// Create an empty `Extensions`.
|
||||
/// Creates an empty `Extensions`.
|
||||
#[inline]
|
||||
pub fn new() -> Extensions {
|
||||
Extensions {
|
||||
map: FxHashMap::default(),
|
||||
map: AHashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Insert a type into this `Extensions`.
|
||||
/// Insert an item into the map.
|
||||
///
|
||||
/// If a extension of this type already existed, it will
|
||||
/// be returned.
|
||||
pub fn insert<T: 'static>(&mut self, val: T) {
|
||||
self.map.insert(TypeId::of::<T>(), Box::new(val));
|
||||
/// If an item of this type was already stored, it will be replaced and returned.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::Extensions;
|
||||
/// let mut map = Extensions::new();
|
||||
/// assert_eq!(map.insert(""), None);
|
||||
/// assert_eq!(map.insert(1u32), None);
|
||||
/// assert_eq!(map.insert(2u32), Some(1u32));
|
||||
/// assert_eq!(*map.get::<u32>().unwrap(), 2u32);
|
||||
/// ```
|
||||
pub fn insert<T: 'static>(&mut self, val: T) -> Option<T> {
|
||||
self.map
|
||||
.insert(TypeId::of::<T>(), Box::new(val))
|
||||
.and_then(downcast_owned)
|
||||
}
|
||||
|
||||
/// Check if container contains entry
|
||||
/// Check if map contains an item of a given type.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::Extensions;
|
||||
/// let mut map = Extensions::new();
|
||||
/// assert!(!map.contains::<u32>());
|
||||
///
|
||||
/// assert_eq!(map.insert(1u32), None);
|
||||
/// assert!(map.contains::<u32>());
|
||||
/// ```
|
||||
pub fn contains<T: 'static>(&self) -> bool {
|
||||
self.map.contains_key(&TypeId::of::<T>())
|
||||
}
|
||||
|
||||
/// Get a reference to a type previously inserted on this `Extensions`.
|
||||
/// Get a reference to an item of a given type.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::Extensions;
|
||||
/// let mut map = Extensions::new();
|
||||
/// map.insert(1u32);
|
||||
/// assert_eq!(map.get::<u32>(), Some(&1u32));
|
||||
/// ```
|
||||
pub fn get<T: 'static>(&self) -> Option<&T> {
|
||||
self.map
|
||||
.get(&TypeId::of::<T>())
|
||||
.and_then(|boxed| boxed.downcast_ref())
|
||||
}
|
||||
|
||||
/// Get a mutable reference to a type previously inserted on this `Extensions`.
|
||||
/// Get a mutable reference to an item of a given type.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::Extensions;
|
||||
/// let mut map = Extensions::new();
|
||||
/// map.insert(1u32);
|
||||
/// assert_eq!(map.get_mut::<u32>(), Some(&mut 1u32));
|
||||
/// ```
|
||||
pub fn get_mut<T: 'static>(&mut self) -> Option<&mut T> {
|
||||
self.map
|
||||
.get_mut(&TypeId::of::<T>())
|
||||
.and_then(|boxed| boxed.downcast_mut())
|
||||
}
|
||||
|
||||
/// Remove a type from this `Extensions`.
|
||||
/// Remove an item from the map of a given type.
|
||||
///
|
||||
/// If a extension of this type existed, it will be returned.
|
||||
/// If an item of this type was already stored, it will be returned.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::Extensions;
|
||||
/// let mut map = Extensions::new();
|
||||
///
|
||||
/// map.insert(1u32);
|
||||
/// assert_eq!(map.get::<u32>(), Some(&1u32));
|
||||
///
|
||||
/// assert_eq!(map.remove::<u32>(), Some(1u32));
|
||||
/// assert!(!map.contains::<u32>());
|
||||
/// ```
|
||||
pub fn remove<T: 'static>(&mut self) -> Option<T> {
|
||||
self.map
|
||||
.remove(&TypeId::of::<T>())
|
||||
.and_then(|boxed| boxed.downcast().ok().map(|boxed| *boxed))
|
||||
self.map.remove(&TypeId::of::<T>()).and_then(downcast_owned)
|
||||
}
|
||||
|
||||
/// Clear the `Extensions` of all inserted extensions.
|
||||
///
|
||||
/// ```
|
||||
/// # use actix_http::Extensions;
|
||||
/// let mut map = Extensions::new();
|
||||
///
|
||||
/// map.insert(1u32);
|
||||
/// assert!(map.contains::<u32>());
|
||||
///
|
||||
/// map.clear();
|
||||
/// assert!(!map.contains::<u32>());
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn clear(&mut self) {
|
||||
self.map.clear();
|
||||
@ -79,6 +136,10 @@ impl fmt::Debug for Extensions {
|
||||
}
|
||||
}
|
||||
|
||||
fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
|
||||
boxed.downcast().ok().map(|boxed| *boxed)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
@ -223,15 +223,3 @@ impl Encoder<Message<(RequestHeadType, BodySize)>> for ClientCodec {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Writer<'a>(pub &'a mut BytesMut);
|
||||
|
||||
impl<'a> io::Write for Writer<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
@ -199,10 +199,10 @@ mod tests {
|
||||
use http::Method;
|
||||
|
||||
use super::*;
|
||||
use crate::httpmessage::HttpMessage;
|
||||
use crate::HttpMessage;
|
||||
|
||||
#[test]
|
||||
fn test_http_request_chunked_payload_and_next_message() {
|
||||
#[actix_rt::test]
|
||||
async fn test_http_request_chunked_payload_and_next_message() {
|
||||
let mut codec = Codec::default();
|
||||
|
||||
let mut buf = BytesMut::from(
|
||||
|
@ -14,7 +14,7 @@ use crate::header::HeaderMap;
|
||||
use crate::message::{ConnectionType, ResponseHead};
|
||||
use crate::request::Request;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 131_072;
|
||||
pub(crate) const MAX_BUFFER_SIZE: usize = 131_072;
|
||||
const MAX_HEADERS: usize = 96;
|
||||
|
||||
/// Incoming message decoder
|
||||
@ -203,7 +203,15 @@ impl MessageType for Request {
|
||||
|
||||
(len, method, uri, version, req.headers.len())
|
||||
}
|
||||
httparse::Status::Partial => return Ok(None),
|
||||
httparse::Status::Partial => {
|
||||
return if src.len() >= MAX_BUFFER_SIZE {
|
||||
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
Err(ParseError::TooLarge)
|
||||
} else {
|
||||
// Return None to notify more read are needed for parsing request
|
||||
Ok(None)
|
||||
};
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -216,15 +224,12 @@ impl MessageType for Request {
|
||||
let decoder = match length {
|
||||
PayloadLength::Payload(pl) => pl,
|
||||
PayloadLength::UpgradeWebSocket => {
|
||||
// upgrade(websocket)
|
||||
// upgrade (WebSocket)
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
}
|
||||
PayloadLength::None => {
|
||||
if method == Method::CONNECT {
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
} else if src.len() >= MAX_BUFFER_SIZE {
|
||||
trace!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
return Err(ParseError::TooLarge);
|
||||
} else {
|
||||
PayloadType::None
|
||||
}
|
||||
@ -273,7 +278,14 @@ impl MessageType for ResponseHead {
|
||||
|
||||
(len, version, status, res.headers.len())
|
||||
}
|
||||
httparse::Status::Partial => return Ok(None),
|
||||
httparse::Status::Partial => {
|
||||
return if src.len() >= MAX_BUFFER_SIZE {
|
||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
Err(ParseError::TooLarge)
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -289,9 +301,6 @@ impl MessageType for ResponseHead {
|
||||
} else if status == StatusCode::SWITCHING_PROTOCOLS {
|
||||
// switching protocol or connect
|
||||
PayloadType::Stream(PayloadDecoder::eof())
|
||||
} else if src.len() >= MAX_BUFFER_SIZE {
|
||||
error!("MAX_BUFFER_SIZE unprocessed data reached, closing");
|
||||
return Err(ParseError::TooLarge);
|
||||
} else {
|
||||
// for HTTP/1.0 read to eof and close connection
|
||||
if msg.version == Version::HTTP_10 {
|
||||
@ -643,7 +652,7 @@ mod tests {
|
||||
use super::*;
|
||||
use crate::error::ParseError;
|
||||
use crate::http::header::{HeaderName, SET_COOKIE};
|
||||
use crate::httpmessage::HttpMessage;
|
||||
use crate::HttpMessage;
|
||||
|
||||
impl PayloadType {
|
||||
fn unwrap(self) -> PayloadDecoder {
|
||||
@ -821,8 +830,8 @@ mod tests {
|
||||
.get_all(SET_COOKIE)
|
||||
.map(|v| v.to_str().unwrap().to_owned())
|
||||
.collect();
|
||||
assert_eq!(val[1], "c1=cookie1");
|
||||
assert_eq!(val[0], "c2=cookie2");
|
||||
assert_eq!(val[0], "c1=cookie1");
|
||||
assert_eq!(val[1], "c2=cookie2");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -8,7 +8,7 @@ use bytes::{BufMut, BytesMut};
|
||||
|
||||
use crate::body::BodySize;
|
||||
use crate::config::ServiceConfig;
|
||||
use crate::header::map;
|
||||
use crate::header::{map::Value, HeaderName};
|
||||
use crate::helpers;
|
||||
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
|
||||
use crate::http::{HeaderMap, StatusCode, Version};
|
||||
@ -121,21 +121,11 @@ pub(crate) trait MessageType: Sized {
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
|
||||
let empty_headers = HeaderMap::new();
|
||||
let extra_headers = self.extra_headers().unwrap_or(&empty_headers);
|
||||
let headers = self
|
||||
.headers()
|
||||
.inner
|
||||
.iter()
|
||||
.filter(|(name, _)| !extra_headers.contains_key(*name))
|
||||
.chain(extra_headers.inner.iter());
|
||||
|
||||
// write headers
|
||||
|
||||
let mut has_date = false;
|
||||
|
||||
let mut buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
||||
let mut buf = dst.chunk_mut().as_mut_ptr();
|
||||
let mut remaining = dst.capacity() - dst.len();
|
||||
|
||||
// tracks bytes written since last buffer resize
|
||||
@ -143,10 +133,10 @@ pub(crate) trait MessageType: Sized {
|
||||
// container's knowledge, this is used to sync the containers cursor after data is written
|
||||
let mut pos = 0;
|
||||
|
||||
for (key, value) in headers {
|
||||
self.write_headers(|key, value| {
|
||||
match *key {
|
||||
CONNECTION => continue,
|
||||
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
|
||||
CONNECTION => return,
|
||||
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => return,
|
||||
DATE => has_date = true,
|
||||
_ => {}
|
||||
}
|
||||
@ -154,8 +144,8 @@ pub(crate) trait MessageType: Sized {
|
||||
let k = key.as_str().as_bytes();
|
||||
let k_len = k.len();
|
||||
|
||||
match value {
|
||||
map::Value::One(ref val) => {
|
||||
// TODO: drain?
|
||||
for val in value.iter() {
|
||||
let v = val.as_ref();
|
||||
let v_len = v.len();
|
||||
|
||||
@ -163,8 +153,6 @@ pub(crate) trait MessageType: Sized {
|
||||
let len = k_len + v_len + 4;
|
||||
|
||||
if len > remaining {
|
||||
// not enough room in buffer for this header; reserve more space
|
||||
|
||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
||||
// the written byte count and pointer advancement are kept in sync
|
||||
unsafe {
|
||||
@ -177,60 +165,14 @@ pub(crate) trait MessageType: Sized {
|
||||
|
||||
// re-assign buf raw pointer since it's possible that the buffer was
|
||||
// reallocated and/or resized
|
||||
buf = dst.chunk_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 {
|
||||
// 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)
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
map::Value::Multi(ref vec) => {
|
||||
for val in vec {
|
||||
let v = val.as_ref();
|
||||
let v_len = v.len();
|
||||
let len = k_len + v_len + 4;
|
||||
|
||||
if len > remaining {
|
||||
// SAFETY: all the bytes written up to position "pos" are initialized
|
||||
// the written byte count and pointer advancement are kept in sync
|
||||
unsafe {
|
||||
dst.advance_mut(pos);
|
||||
}
|
||||
pos = 0;
|
||||
dst.reserve(len * 2);
|
||||
remaining = dst.capacity() - dst.len();
|
||||
|
||||
// re-assign buf raw pointer since it's possible that the buffer was
|
||||
// reallocated and/or resized
|
||||
buf = dst.chunk_mut().as_mut_ptr() as *mut u8;
|
||||
buf = dst.chunk_mut().as_mut_ptr();
|
||||
}
|
||||
|
||||
// SAFETY: on each write, it is enough to ensure that the advancement of
|
||||
// the cursor matches the number of bytes written
|
||||
unsafe {
|
||||
if camel_case {
|
||||
// use Camel-Case headers
|
||||
write_camel_case(k, from_raw_parts_mut(buf, k_len));
|
||||
} else {
|
||||
write_data(k, buf, k_len);
|
||||
@ -251,9 +193,7 @@ pub(crate) trait MessageType: Sized {
|
||||
pos += len;
|
||||
remaining -= len;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// final cursor synchronization with the bytes container
|
||||
//
|
||||
@ -273,6 +213,24 @@ pub(crate) trait MessageType: Sized {
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn write_headers<F>(&mut self, mut f: F)
|
||||
where
|
||||
F: FnMut(&HeaderName, &Value),
|
||||
{
|
||||
match self.extra_headers() {
|
||||
Some(headers) => {
|
||||
// merging headers from head and extra headers.
|
||||
self.headers()
|
||||
.inner
|
||||
.iter()
|
||||
.filter(|(name, _)| !headers.contains_key(*name))
|
||||
.chain(headers.inner.iter())
|
||||
.for_each(|(k, v)| f(k, v))
|
||||
}
|
||||
None => self.headers().inner.iter().for_each(|(k, v)| f(k, v)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl MessageType for Response<()> {
|
||||
@ -329,7 +287,7 @@ impl MessageType for RequestHeadType {
|
||||
let head = self.as_ref();
|
||||
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
|
||||
write!(
|
||||
Writer(dst),
|
||||
helpers::Writer(dst),
|
||||
"{} {} {}",
|
||||
head.method,
|
||||
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
|
||||
@ -462,7 +420,7 @@ impl TransferEncoding {
|
||||
*eof = true;
|
||||
buf.extend_from_slice(b"0\r\n\r\n");
|
||||
} else {
|
||||
writeln!(Writer(buf), "{:X}\r", msg.len())
|
||||
writeln!(helpers::Writer(buf), "{:X}\r", msg.len())
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
|
||||
|
||||
buf.reserve(msg.len() + 2);
|
||||
@ -512,18 +470,6 @@ impl TransferEncoding {
|
||||
}
|
||||
}
|
||||
|
||||
struct Writer<'a>(pub &'a mut BytesMut);
|
||||
|
||||
impl<'a> io::Write for Writer<'a> {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.0.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// # Safety
|
||||
/// Callers must ensure that the given length matches given value length.
|
||||
unsafe fn write_data(value: &[u8], buf: *mut u8, len: usize) {
|
||||
@ -583,8 +529,8 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_camel_case() {
|
||||
#[actix_rt::test]
|
||||
async fn test_camel_case() {
|
||||
let mut bytes = BytesMut::with_capacity(2048);
|
||||
let mut head = RequestHead::default();
|
||||
head.set_camel_case_headers(true);
|
||||
@ -603,7 +549,6 @@ mod tests {
|
||||
);
|
||||
let data =
|
||||
String::from_utf8(Vec::from(bytes.split().freeze().as_ref())).unwrap();
|
||||
eprintln!("{}", &data);
|
||||
|
||||
assert!(data.contains("Content-Length: 0\r\n"));
|
||||
assert!(data.contains("Connection: close\r\n"));
|
||||
@ -647,8 +592,8 @@ mod tests {
|
||||
assert!(data.contains("date: date\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_extra_headers() {
|
||||
#[actix_rt::test]
|
||||
async fn test_extra_headers() {
|
||||
let mut bytes = BytesMut::with_capacity(2048);
|
||||
|
||||
let mut head = RequestHead::default();
|
||||
@ -681,16 +626,15 @@ mod tests {
|
||||
assert!(data.contains("date: date\r\n"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_no_content_length() {
|
||||
#[actix_rt::test]
|
||||
async fn test_no_content_length() {
|
||||
let mut bytes = BytesMut::with_capacity(2048);
|
||||
|
||||
let mut res: Response<()> =
|
||||
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
|
||||
res.headers_mut().insert(DATE, HeaderValue::from_static(""));
|
||||
res.headers_mut()
|
||||
.insert(DATE, HeaderValue::from_static(&""));
|
||||
res.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static(&"0"));
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
|
||||
|
||||
let _ = res.encode_headers(
|
||||
&mut bytes,
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::task::{Context, Poll};
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
use futures_util::future::{ready, Ready};
|
||||
@ -26,11 +26,9 @@ impl Service<Request> for ExpectHandler {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, req: Request) -> Self::Future {
|
||||
fn call(&self, req: Request) -> Self::Future {
|
||||
ready(Ok(req))
|
||||
// TODO: add some way to trigger error
|
||||
// Err(error::ErrorExpectationFailed("test"))
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! HTTP/1 implementation
|
||||
//! HTTP/1 protocol implementation.
|
||||
use bytes::{Bytes, BytesMut};
|
||||
|
||||
mod client;
|
||||
|
@ -3,9 +3,8 @@ use std::cell::RefCell;
|
||||
use std::collections::VecDeque;
|
||||
use std::pin::Pin;
|
||||
use std::rc::{Rc, Weak};
|
||||
use std::task::{Context, Poll};
|
||||
use std::task::{Context, Poll, Waker};
|
||||
|
||||
use actix_utils::task::LocalWaker;
|
||||
use bytes::Bytes;
|
||||
use futures_core::Stream;
|
||||
|
||||
@ -134,7 +133,7 @@ impl PayloadSender {
|
||||
if shared.borrow().need_read {
|
||||
PayloadStatus::Read
|
||||
} else {
|
||||
shared.borrow_mut().io_task.register(cx.waker());
|
||||
shared.borrow_mut().register_io(cx);
|
||||
PayloadStatus::Pause
|
||||
}
|
||||
} else {
|
||||
@ -150,8 +149,8 @@ struct Inner {
|
||||
err: Option<PayloadError>,
|
||||
need_read: bool,
|
||||
items: VecDeque<Bytes>,
|
||||
task: LocalWaker,
|
||||
io_task: LocalWaker,
|
||||
task: Option<Waker>,
|
||||
io_task: Option<Waker>,
|
||||
}
|
||||
|
||||
impl Inner {
|
||||
@ -162,8 +161,48 @@ impl Inner {
|
||||
err: None,
|
||||
items: VecDeque::new(),
|
||||
need_read: true,
|
||||
task: LocalWaker::new(),
|
||||
io_task: LocalWaker::new(),
|
||||
task: None,
|
||||
io_task: None,
|
||||
}
|
||||
}
|
||||
|
||||
/// 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());
|
||||
}
|
||||
}
|
||||
|
||||
@ -182,7 +221,7 @@ impl Inner {
|
||||
self.len += data.len();
|
||||
self.items.push_back(data);
|
||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||
self.task.wake();
|
||||
self.wake();
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
@ -199,9 +238,9 @@ impl Inner {
|
||||
self.need_read = self.len < MAX_BUFFER_SIZE;
|
||||
|
||||
if self.need_read && !self.eof {
|
||||
self.task.register(cx.waker());
|
||||
self.register(cx);
|
||||
}
|
||||
self.io_task.wake();
|
||||
self.wake_io();
|
||||
Poll::Ready(Some(Ok(data)))
|
||||
} else if let Some(err) = self.err.take() {
|
||||
Poll::Ready(Some(Err(err)))
|
||||
@ -209,8 +248,8 @@ impl Inner {
|
||||
Poll::Ready(None)
|
||||
} else {
|
||||
self.need_read = true;
|
||||
self.task.register(cx.waker());
|
||||
self.io_task.wake();
|
||||
self.register(cx);
|
||||
self.wake_io();
|
||||
Poll::Pending
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,3 @@
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
@ -95,10 +94,10 @@ mod openssl {
|
||||
use super::*;
|
||||
|
||||
use actix_service::ServiceFactoryExt;
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
|
||||
impl<S, B, X, U> H1Service<SslStream<TcpStream>, S, B, X, U>
|
||||
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error>,
|
||||
@ -109,7 +108,7 @@ mod openssl {
|
||||
X::Error: Into<Error>,
|
||||
X::InitError: fmt::Debug,
|
||||
U: ServiceFactory<
|
||||
(Request, Framed<SslStream<TcpStream>, Codec>),
|
||||
(Request, Framed<TlsStream<TcpStream>, Codec>),
|
||||
Config = (),
|
||||
Response = (),
|
||||
>,
|
||||
@ -132,7 +131,7 @@ mod openssl {
|
||||
.map_err(TlsError::Tls)
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(|io: SslStream<TcpStream>| {
|
||||
.and_then(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ready(Ok((io, peer_addr)))
|
||||
})
|
||||
@ -367,7 +366,7 @@ where
|
||||
X: Service<Request>,
|
||||
U: Service<(Request, Framed<T, Codec>)>,
|
||||
{
|
||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
cfg: ServiceConfig,
|
||||
_phantom: PhantomData<B>,
|
||||
@ -417,9 +416,9 @@ where
|
||||
type Error = DispatchError;
|
||||
type Future = Dispatcher<T, S, B, X, U>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
let mut flow = self.flow.borrow_mut();
|
||||
let ready = flow
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
let ready = self
|
||||
.flow
|
||||
.expect
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
@ -429,7 +428,8 @@ where
|
||||
})?
|
||||
.is_ready();
|
||||
|
||||
let ready = flow
|
||||
let ready = self
|
||||
.flow
|
||||
.service
|
||||
.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
@ -440,7 +440,7 @@ where
|
||||
.is_ready()
|
||||
&& ready;
|
||||
|
||||
let ready = if let Some(ref mut upg) = flow.upgrade {
|
||||
let ready = if let Some(ref upg) = self.flow.upgrade {
|
||||
upg.poll_ready(cx)
|
||||
.map_err(|e| {
|
||||
let e = e.into();
|
||||
@ -460,7 +460,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
let on_connect_data =
|
||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
|
@ -1,4 +1,4 @@
|
||||
use std::task::{Context, Poll};
|
||||
use std::task::Poll;
|
||||
|
||||
use actix_codec::Framed;
|
||||
use actix_service::{Service, ServiceFactory};
|
||||
@ -28,11 +28,9 @@ impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
|
||||
type Error = Error;
|
||||
type Future = Ready<Result<Self::Response, Self::Error>>;
|
||||
|
||||
fn poll_ready(&mut self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
Poll::Ready(Ok(()))
|
||||
}
|
||||
actix_service::always_ready!();
|
||||
|
||||
fn call(&mut self, _: (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
|
||||
ready(Ok(()))
|
||||
}
|
||||
}
|
||||
|
@ -1,14 +1,7 @@
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::net;
|
||||
use std::pin::Pin;
|
||||
use std::rc::Rc;
|
||||
use std::task::{Context, Poll};
|
||||
use std::{cmp, convert::TryFrom};
|
||||
use std::{cmp, future::Future, marker::PhantomData, net, pin::Pin, rc::Rc};
|
||||
|
||||
use actix_codec::{AsyncRead, AsyncWrite};
|
||||
use actix_rt::time::{Instant, Sleep};
|
||||
use actix_service::Service;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::ready;
|
||||
@ -37,13 +30,11 @@ where
|
||||
S: Service<Request>,
|
||||
B: MessageBody,
|
||||
{
|
||||
flow: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
connection: Connection<T, Bytes>,
|
||||
on_connect_data: OnConnectData,
|
||||
config: ServiceConfig,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
ka_expire: Instant,
|
||||
ka_timer: Option<Sleep>,
|
||||
_phantom: PhantomData<B>,
|
||||
}
|
||||
|
||||
@ -56,37 +47,18 @@ where
|
||||
B: MessageBody,
|
||||
{
|
||||
pub(crate) fn new(
|
||||
services: Rc<RefCell<HttpFlow<S, X, U>>>,
|
||||
flow: Rc<HttpFlow<S, X, U>>,
|
||||
connection: Connection<T, Bytes>,
|
||||
on_connect_data: OnConnectData,
|
||||
config: ServiceConfig,
|
||||
timeout: Option<Sleep>,
|
||||
peer_addr: Option<net::SocketAddr>,
|
||||
) -> 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 {
|
||||
flow: services,
|
||||
flow,
|
||||
config,
|
||||
peer_addr,
|
||||
connection,
|
||||
on_connect_data,
|
||||
ka_expire,
|
||||
ka_timer,
|
||||
_phantom: PhantomData,
|
||||
}
|
||||
}
|
||||
@ -114,19 +86,12 @@ where
|
||||
Some(Err(err)) => return Poll::Ready(Err(err.into())),
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
let (parts, body) = req.into_parts();
|
||||
let pl = crate::h2::Payload::new(body);
|
||||
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
|
||||
let mut req = Request::with_payload(pl);
|
||||
|
||||
let head = &mut req.head_mut();
|
||||
let head = req.head_mut();
|
||||
head.uri = parts.uri;
|
||||
head.method = parts.method;
|
||||
head.version = parts.version;
|
||||
@ -136,9 +101,9 @@ where
|
||||
// merge on_connect_ext data into request extensions
|
||||
this.on_connect_data.merge_into(&mut req);
|
||||
|
||||
let svc = ServiceResponse::<S::Future, S::Response, S::Error, B> {
|
||||
let svc = ServiceResponse {
|
||||
state: ServiceResponseState::ServiceCall(
|
||||
this.flow.borrow_mut().service.call(req),
|
||||
this.flow.service.call(req),
|
||||
Some(res),
|
||||
),
|
||||
config: this.config.clone(),
|
||||
@ -204,16 +169,22 @@ where
|
||||
BodySize::Empty => res
|
||||
.headers_mut()
|
||||
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
|
||||
BodySize::Sized(len) => res.headers_mut().insert(
|
||||
BodySize::Sized(len) => {
|
||||
let mut buf = itoa::Buffer::new();
|
||||
|
||||
res.headers_mut().insert(
|
||||
CONTENT_LENGTH,
|
||||
HeaderValue::try_from(format!("{}", len)).unwrap(),
|
||||
),
|
||||
HeaderValue::from_str(buf.format(*len)).unwrap(),
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
// copy headers
|
||||
for (key, value) in head.headers.iter() {
|
||||
match *key {
|
||||
// omit HTTP/1 only headers
|
||||
// TODO: consider skipping other headers according to:
|
||||
// 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,
|
||||
DATE => has_date = true,
|
||||
@ -311,11 +282,9 @@ where
|
||||
}
|
||||
|
||||
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
|
||||
loop {
|
||||
loop {
|
||||
match this.buffer {
|
||||
Some(ref mut buffer) => {
|
||||
match ready!(stream.poll_capacity(cx)) {
|
||||
Some(ref mut buffer) => match ready!(stream.poll_capacity(cx)) {
|
||||
None => return Poll::Ready(()),
|
||||
|
||||
Some(Ok(cap)) => {
|
||||
@ -337,23 +306,19 @@ where
|
||||
warn!("{:?}", e);
|
||||
return Poll::Ready(());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
None => match ready!(body.as_mut().poll_next(cx)) {
|
||||
None => {
|
||||
if let Err(e) = stream.send_data(Bytes::new(), true)
|
||||
{
|
||||
if let Err(e) = stream.send_data(Bytes::new(), true) {
|
||||
warn!("{:?}", e);
|
||||
}
|
||||
return Poll::Ready(());
|
||||
}
|
||||
|
||||
Some(Ok(chunk)) => {
|
||||
stream.reserve_capacity(cmp::min(
|
||||
chunk.len(),
|
||||
CHUNK_SIZE,
|
||||
));
|
||||
stream
|
||||
.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
|
||||
*this.buffer = Some(chunk);
|
||||
}
|
||||
|
||||
@ -368,4 +333,3 @@ where
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,4 +1,4 @@
|
||||
//! HTTP/2 implementation.
|
||||
//! HTTP/2 protocol.
|
||||
|
||||
use std::{
|
||||
pin::Pin,
|
||||
|
@ -1,4 +1,3 @@
|
||||
use std::cell::RefCell;
|
||||
use std::future::Future;
|
||||
use std::marker::PhantomData;
|
||||
use std::pin::Pin;
|
||||
@ -94,12 +93,12 @@ where
|
||||
#[cfg(feature = "openssl")]
|
||||
mod openssl {
|
||||
use actix_service::{fn_factory, fn_service, ServiceFactoryExt};
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, SslStream};
|
||||
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
|
||||
use actix_tls::accept::TlsError;
|
||||
|
||||
use super::*;
|
||||
|
||||
impl<S, B> H2Service<SslStream<TcpStream>, S, B>
|
||||
impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
|
||||
where
|
||||
S: ServiceFactory<Request, Config = ()>,
|
||||
S::Error: Into<Error> + 'static,
|
||||
@ -124,7 +123,7 @@ mod openssl {
|
||||
.map_init_err(|_| panic!()),
|
||||
)
|
||||
.and_then(fn_factory(|| {
|
||||
ok::<_, S::InitError>(fn_service(|io: SslStream<TcpStream>| {
|
||||
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| {
|
||||
let peer_addr = io.get_ref().peer_addr().ok();
|
||||
ok((io, peer_addr))
|
||||
}))
|
||||
@ -244,12 +243,12 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// `Service` implementation for http/2 transport
|
||||
/// `Service` implementation for HTTP/2 transport
|
||||
pub struct H2ServiceHandler<T, S, B>
|
||||
where
|
||||
S: Service<Request>,
|
||||
{
|
||||
flow: Rc<RefCell<HttpFlow<S, (), ()>>>,
|
||||
flow: Rc<HttpFlow<S, (), ()>>,
|
||||
cfg: ServiceConfig,
|
||||
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
|
||||
_phantom: PhantomData<B>,
|
||||
@ -290,15 +289,15 @@ where
|
||||
type Error = DispatchError;
|
||||
type Future = H2ServiceHandlerResponse<T, S, B>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.flow.borrow_mut().service.poll_ready(cx).map_err(|e| {
|
||||
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.flow.service.poll_ready(cx).map_err(|e| {
|
||||
let e = e.into();
|
||||
error!("Service readiness error: {:?}", e);
|
||||
DispatchError::Service(e)
|
||||
})
|
||||
}
|
||||
|
||||
fn call(&mut self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {
|
||||
let on_connect_data =
|
||||
OnConnectData::from_io(&io, self.on_connect_ext.as_deref());
|
||||
|
||||
@ -321,7 +320,7 @@ where
|
||||
{
|
||||
Incoming(Dispatcher<T, S, B, (), ()>),
|
||||
Handshake(
|
||||
Option<Rc<RefCell<HttpFlow<S, (), ()>>>>,
|
||||
Option<Rc<HttpFlow<S, (), ()>>>,
|
||||
Option<ServiceConfig>,
|
||||
Option<net::SocketAddr>,
|
||||
OnConnectData,
|
||||
@ -369,7 +368,6 @@ where
|
||||
conn,
|
||||
on_connect_data,
|
||||
config.take().unwrap(),
|
||||
None,
|
||||
*peer_addr,
|
||||
));
|
||||
self.poll(cx)
|
||||
|
48
actix-http/src/header/as_name.rs
Normal file
48
actix-http/src/header/as_name.rs
Normal file
@ -0,0 +1,48 @@
|
||||
//! 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 {}
|
@ -32,50 +32,36 @@ header! {
|
||||
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// extern crate mime;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{Accept, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
///
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// Accept(vec![
|
||||
/// qitem(mime::TEXT_HTML),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// extern crate mime;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{Accept, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
///
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// Accept(vec![
|
||||
/// qitem(mime::APPLICATION_JSON),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// extern crate mime;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{Accept, QualityItem, q, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
///
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// Accept(vec![
|
||||
/// qitem(mime::TEXT_HTML),
|
||||
/// qitem("application/xhtml+xml".parse().unwrap()),
|
||||
@ -90,7 +76,6 @@ header! {
|
||||
/// ),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(Accept, header::ACCEPT) => (QualityItem<Mime>)+
|
||||
|
||||
@ -132,7 +117,7 @@ header! {
|
||||
#[test]
|
||||
fn test_fuzzing1() {
|
||||
use crate::test::TestRequest;
|
||||
let req = TestRequest::with_header(crate::header::ACCEPT, "chunk#;e").finish();
|
||||
let req = TestRequest::default().insert_header((crate::header::ACCEPT, "chunk#;e")).finish();
|
||||
let header = Accept::parse(&req);
|
||||
assert!(header.is_ok());
|
||||
}
|
||||
|
@ -21,44 +21,37 @@ header! {
|
||||
/// * `iso-8859-5, unicode-1-1;q=0.8`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
///
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{AcceptCharset, Charset, q, QualityItem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// AcceptCharset(vec![
|
||||
/// QualityItem::new(Charset::Us_Ascii, q(900)),
|
||||
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
///
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{AcceptCharset, Charset, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(AcceptCharset, ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
||||
|
||||
|
@ -22,41 +22,35 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// # extern crate language_tags;
|
||||
/// ```
|
||||
/// use language_tags::langtag;
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{AcceptLanguage, LanguageTag, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// let mut langtag: LanguageTag = Default::default();
|
||||
/// langtag.language = Some("en".to_owned());
|
||||
/// langtag.region = Some("US".to_owned());
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// AcceptLanguage(vec![
|
||||
/// qitem(langtag),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// # #[macro_use] extern crate language_tags;
|
||||
/// ```
|
||||
/// use language_tags::langtag;
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{AcceptLanguage, QualityItem, q, qitem};
|
||||
/// #
|
||||
/// # fn main() {
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// AcceptLanguage(vec![
|
||||
/// qitem(langtag!(da)),
|
||||
/// QualityItem::new(langtag!(en;;;GB), q(800)),
|
||||
/// QualityItem::new(langtag!(en), q(700)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(AcceptLanguage, ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+
|
||||
|
||||
|
@ -22,38 +22,28 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate http;
|
||||
/// # extern crate actix_http;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::Allow;
|
||||
/// use http::Method;
|
||||
/// use actix_http::http::{header::Allow, Method};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// Allow(vec![Method::GET])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate http;
|
||||
/// # extern crate actix_http;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::Allow;
|
||||
/// use http::Method;
|
||||
/// use actix_http::http::{header::Allow, Method};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// Allow(vec![
|
||||
/// Method::GET,
|
||||
/// Method::POST,
|
||||
/// Method::PATCH,
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(Allow, header::ALLOW) => (Method)*
|
||||
|
||||
|
@ -28,12 +28,12 @@ use crate::header::{
|
||||
/// * `max-age=30`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{CacheControl, CacheDirective};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
|
||||
/// builder.insert_header(CacheControl(vec![CacheDirective::MaxAge(86400u32)]));
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
@ -41,7 +41,7 @@ use crate::header::{
|
||||
/// use actix_http::http::header::{CacheControl, CacheDirective};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(CacheControl(vec![
|
||||
/// builder.insert_header(CacheControl(vec![
|
||||
/// CacheDirective::NoCache,
|
||||
/// CacheDirective::Private,
|
||||
/// CacheDirective::MaxAge(360u32),
|
||||
@ -82,7 +82,7 @@ impl fmt::Display for CacheControl {
|
||||
impl IntoHeaderValue for CacheControl {
|
||||
type Error = header::InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
header::HeaderValue::from_maybe_shared(writer.take())
|
||||
@ -196,7 +196,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_headers() {
|
||||
let req = TestRequest::with_header(header::CACHE_CONTROL, "no-cache, private")
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::CACHE_CONTROL, "no-cache, private"))
|
||||
.finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(
|
||||
@ -210,8 +211,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_argument() {
|
||||
let req =
|
||||
TestRequest::with_header(header::CACHE_CONTROL, "max-age=100, private")
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::CACHE_CONTROL, "max-age=100, private"))
|
||||
.finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(
|
||||
@ -225,8 +226,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_quote_form() {
|
||||
let req =
|
||||
TestRequest::with_header(header::CACHE_CONTROL, "max-age=\"200\"").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::CACHE_CONTROL, "max-age=\"200\""))
|
||||
.finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(
|
||||
cache.ok(),
|
||||
@ -236,8 +238,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_extension() {
|
||||
let req =
|
||||
TestRequest::with_header(header::CACHE_CONTROL, "foo, bar=baz").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::CACHE_CONTROL, "foo, bar=baz"))
|
||||
.finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(
|
||||
cache.ok(),
|
||||
@ -250,7 +253,9 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_parse_bad_syntax() {
|
||||
let req = TestRequest::with_header(header::CACHE_CONTROL, "foo=").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::CACHE_CONTROL, "foo="))
|
||||
.finish();
|
||||
let cache: Result<CacheControl, _> = Header::parse(&req);
|
||||
assert_eq!(cache.ok(), None)
|
||||
}
|
||||
|
@ -1,12 +1,12 @@
|
||||
// # References
|
||||
//
|
||||
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
|
||||
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
|
||||
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
|
||||
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/
|
||||
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
||||
//! # References
|
||||
//!
|
||||
//! "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
|
||||
//! "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
|
||||
//! "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc7578.txt
|
||||
//! Browser conformance tests at: http://greenbytes.de/tech/tc2231/
|
||||
//! IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
||||
|
||||
use lazy_static::lazy_static;
|
||||
use once_cell::sync::Lazy;
|
||||
use regex::Regex;
|
||||
use std::fmt::{self, Write};
|
||||
|
||||
@ -454,7 +454,7 @@ impl ContentDisposition {
|
||||
impl IntoHeaderValue for ContentDisposition {
|
||||
type Error = header::InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<header::HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<header::HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
header::HeaderValue::from_maybe_shared(writer.take())
|
||||
@ -520,9 +520,7 @@ impl fmt::Display for DispositionParam {
|
||||
//
|
||||
//
|
||||
// See also comments in test_from_raw_unnecessary_percent_decode.
|
||||
lazy_static! {
|
||||
static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
|
||||
}
|
||||
static RE: Lazy<Regex> = Lazy::new(|| Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap());
|
||||
match self {
|
||||
DispositionParam::Name(ref value) => write!(f, "name={}", value),
|
||||
DispositionParam::Filename(ref value) => {
|
||||
|
106
actix-http/src/header/common/content_encoding.rs
Normal file
106
actix-http/src/header/common/content_encoding.rs
Normal file
@ -0,0 +1,106 @@
|
||||
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()))
|
||||
}
|
||||
}
|
@ -23,38 +23,31 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// # #[macro_use] extern crate language_tags;
|
||||
/// ```
|
||||
/// use language_tags::langtag;
|
||||
/// use actix_http::Response;
|
||||
/// # use actix_http::http::header::{ContentLanguage, qitem};
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// use actix_http::http::header::{ContentLanguage, qitem};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// ContentLanguage(vec![
|
||||
/// qitem(langtag!(en)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_http;
|
||||
/// # #[macro_use] extern crate language_tags;
|
||||
/// ```
|
||||
/// use language_tags::langtag;
|
||||
/// use actix_http::Response;
|
||||
/// # use actix_http::http::header::{ContentLanguage, qitem};
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// use actix_http::http::header::{ContentLanguage, qitem};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// ContentLanguage(vec![
|
||||
/// qitem(langtag!(da)),
|
||||
/// qitem(langtag!(en;;;GB)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(ContentLanguage, CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+
|
||||
|
||||
|
@ -200,7 +200,7 @@ impl Display for ContentRangeSpec {
|
||||
impl IntoHeaderValue for ContentRangeSpec {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
HeaderValue::from_maybe_shared(writer.take())
|
||||
|
@ -30,31 +30,24 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::ContentType;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// ContentType::json()
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate mime;
|
||||
/// # extern crate actix_http;
|
||||
/// use mime::TEXT_HTML;
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::ContentType;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// ContentType(TEXT_HTML)
|
||||
/// builder.insert_header(
|
||||
/// ContentType(mime::TEXT_HTML)
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(ContentType, CONTENT_TYPE) => [Mime]
|
||||
|
||||
@ -99,6 +92,7 @@ impl ContentType {
|
||||
pub fn form_url_encoded() -> ContentType {
|
||||
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: image/jpeg` header.
|
||||
#[inline]
|
||||
pub fn jpeg() -> ContentType {
|
||||
|
@ -19,13 +19,15 @@ header! {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use std::time::SystemTime;
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::Date;
|
||||
/// use std::time::SystemTime;
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(Date(SystemTime::now().into()));
|
||||
/// builder.insert_header(
|
||||
/// Date(SystemTime::now().into())
|
||||
/// );
|
||||
/// ```
|
||||
(Date, DATE) => [HttpDate]
|
||||
|
||||
|
@ -27,20 +27,24 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{ETag, EntityTag};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned())));
|
||||
/// builder.insert_header(
|
||||
/// ETag(EntityTag::new(false, "xyzzy".to_owned()))
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{ETag, EntityTag};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned())));
|
||||
/// builder.insert_header(
|
||||
/// ETag(EntityTag::new(true, "xyzzy".to_owned()))
|
||||
/// );
|
||||
/// ```
|
||||
(ETag, ETAG) => [EntityTag]
|
||||
|
||||
|
@ -21,14 +21,16 @@ header! {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::Expires;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(Expires(expiration.into()));
|
||||
/// builder.insert_header(
|
||||
/// Expires(expiration.into())
|
||||
/// );
|
||||
/// ```
|
||||
(Expires, EXPIRES) => [HttpDate]
|
||||
|
||||
|
@ -29,20 +29,20 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::IfMatch;
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(IfMatch::Any);
|
||||
/// builder.insert_header(IfMatch::Any);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{IfMatch, EntityTag};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// IfMatch::Items(vec![
|
||||
/// EntityTag::new(false, "xyzzy".to_owned()),
|
||||
/// EntityTag::new(false, "foobar".to_owned()),
|
||||
|
@ -21,14 +21,16 @@ header! {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::IfModifiedSince;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(IfModifiedSince(modified.into()));
|
||||
/// builder.insert_header(
|
||||
/// IfModifiedSince(modified.into())
|
||||
/// );
|
||||
/// ```
|
||||
(IfModifiedSince, IF_MODIFIED_SINCE) => [HttpDate]
|
||||
|
||||
|
@ -31,20 +31,20 @@ header! {
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::IfNoneMatch;
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(IfNoneMatch::Any);
|
||||
/// builder.insert_header(IfNoneMatch::Any);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{IfNoneMatch, EntityTag};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(
|
||||
/// builder.insert_header(
|
||||
/// IfNoneMatch::Items(vec![
|
||||
/// EntityTag::new(false, "xyzzy".to_owned()),
|
||||
/// EntityTag::new(false, "foobar".to_owned()),
|
||||
@ -73,12 +73,14 @@ mod tests {
|
||||
fn test_if_none_match() {
|
||||
let mut if_none_match: Result<IfNoneMatch, _>;
|
||||
|
||||
let req = TestRequest::with_header(IF_NONE_MATCH, "*").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header((IF_NONE_MATCH, "*"))
|
||||
.finish();
|
||||
if_none_match = Header::parse(&req);
|
||||
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
|
||||
|
||||
let req =
|
||||
TestRequest::with_header(IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..])
|
||||
let req = TestRequest::default()
|
||||
.insert_header((IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]))
|
||||
.finish();
|
||||
|
||||
if_none_match = Header::parse(&req);
|
||||
|
@ -5,7 +5,7 @@ use crate::header::{
|
||||
self, from_one_raw_str, EntityTag, Header, HeaderName, HeaderValue, HttpDate,
|
||||
IntoHeaderValue, InvalidHeaderValue, Writer,
|
||||
};
|
||||
use crate::httpmessage::HttpMessage;
|
||||
use crate::HttpMessage;
|
||||
|
||||
/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2)
|
||||
///
|
||||
@ -35,31 +35,34 @@ use crate::httpmessage::HttpMessage;
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::{EntityTag, IfRange};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// builder.set(IfRange::EntityTag(EntityTag::new(
|
||||
/// false,
|
||||
/// "xyzzy".to_owned(),
|
||||
/// )));
|
||||
/// builder.insert_header(
|
||||
/// IfRange::EntityTag(
|
||||
/// EntityTag::new(false, "abc".to_owned())
|
||||
/// )
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::IfRange;
|
||||
/// ```
|
||||
/// use std::time::{Duration, SystemTime};
|
||||
/// use actix_http::{http::header::IfRange, Response};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(IfRange::Date(fetched.into()));
|
||||
/// builder.insert_header(
|
||||
/// IfRange::Date(fetched.into())
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum IfRange {
|
||||
/// The entity-tag the client has of the resource
|
||||
/// The entity-tag the client has of the resource.
|
||||
EntityTag(EntityTag),
|
||||
/// The date when the client retrieved the resource
|
||||
|
||||
/// The date when the client retrieved the resource.
|
||||
Date(HttpDate),
|
||||
}
|
||||
|
||||
@ -98,7 +101,7 @@ impl Display for IfRange {
|
||||
impl IntoHeaderValue for IfRange {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
HeaderValue::from_maybe_shared(writer.take())
|
||||
@ -110,7 +113,8 @@ mod test_if_range {
|
||||
use super::IfRange as HeaderField;
|
||||
use crate::header::*;
|
||||
use std::str;
|
||||
|
||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
|
||||
test_header!(test2, vec![b"\"xyzzy\""]);
|
||||
test_header!(test2, vec![b"\"abc\""]);
|
||||
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
|
||||
}
|
||||
|
@ -22,14 +22,16 @@ header! {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::IfUnmodifiedSince;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(IfUnmodifiedSince(modified.into()));
|
||||
/// builder.insert_header(
|
||||
/// IfUnmodifiedSince(modified.into())
|
||||
/// );
|
||||
/// ```
|
||||
(IfUnmodifiedSince, IF_UNMODIFIED_SINCE) => [HttpDate]
|
||||
|
||||
|
@ -21,14 +21,16 @@ header! {
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// ```
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
/// use actix_http::Response;
|
||||
/// use actix_http::http::header::LastModified;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = Response::Ok();
|
||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(LastModified(modified.into()));
|
||||
/// builder.insert_header(
|
||||
/// LastModified(modified.into())
|
||||
/// );
|
||||
/// ```
|
||||
(LastModified, LAST_MODIFIED) => [HttpDate]
|
||||
|
||||
|
@ -18,6 +18,7 @@ pub use self::content_disposition::{
|
||||
};
|
||||
pub use self::content_language::ContentLanguage;
|
||||
pub use self::content_range::{ContentRange, ContentRangeSpec};
|
||||
pub use self::content_encoding::{ContentEncoding};
|
||||
pub use self::content_type::ContentType;
|
||||
pub use self::date::Date;
|
||||
pub use self::etag::ETag;
|
||||
@ -83,7 +84,7 @@ macro_rules! test_header {
|
||||
let a: Vec<Vec<u8>> = raw.iter().map(|x| x.to_vec()).collect();
|
||||
let mut req = test::TestRequest::default();
|
||||
for item in a {
|
||||
req = req.header(HeaderField::name(), item).take();
|
||||
req = req.insert_header((HeaderField::name(), item)).take();
|
||||
}
|
||||
let req = req.finish();
|
||||
let value = HeaderField::parse(&req);
|
||||
@ -110,7 +111,7 @@ macro_rules! test_header {
|
||||
let a: Vec<Vec<u8>> = $raw.iter().map(|x| x.to_vec()).collect();
|
||||
let mut req = test::TestRequest::default();
|
||||
for item in a {
|
||||
req.header(HeaderField::name(), item);
|
||||
req.insert_header((HeaderField::name(), item));
|
||||
}
|
||||
let req = req.finish();
|
||||
let val = HeaderField::parse(&req);
|
||||
@ -168,7 +169,7 @@ macro_rules! header {
|
||||
impl $crate::http::header::IntoHeaderValue for $id {
|
||||
type Error = $crate::http::header::InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
use std::fmt::Write;
|
||||
let mut writer = $crate::http::header::Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
@ -204,7 +205,7 @@ macro_rules! header {
|
||||
impl $crate::http::header::IntoHeaderValue for $id {
|
||||
type Error = $crate::http::header::InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
use std::fmt::Write;
|
||||
let mut writer = $crate::http::header::Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
@ -240,8 +241,8 @@ macro_rules! header {
|
||||
impl $crate::http::header::IntoHeaderValue for $id {
|
||||
type Error = $crate::http::header::InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
self.0.try_into()
|
||||
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
self.0.try_into_value()
|
||||
}
|
||||
}
|
||||
};
|
||||
@ -289,7 +290,7 @@ macro_rules! header {
|
||||
impl $crate::http::header::IntoHeaderValue for $id {
|
||||
type Error = $crate::http::header::InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<$crate::http::header::HeaderValue, Self::Error> {
|
||||
use std::fmt::Write;
|
||||
let mut writer = $crate::http::header::Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
@ -340,6 +341,7 @@ mod allow;
|
||||
mod cache_control;
|
||||
mod content_disposition;
|
||||
mod content_language;
|
||||
mod content_encoding;
|
||||
mod content_range;
|
||||
mod content_type;
|
||||
mod date;
|
||||
|
117
actix-http/src/header/into_pair.rs
Normal file
117
actix-http/src/header/into_pair.rs
Normal file
@ -0,0 +1,117 @@
|
||||
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()?))
|
||||
}
|
||||
}
|
131
actix-http/src/header/into_value.rs
Normal file
131
actix-http/src/header/into_value.rs
Normal file
@ -0,0 +1,131 @@
|
||||
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
@ -1,35 +1,39 @@
|
||||
//! Various http headers
|
||||
// This is mostly copy of [hyper](https://github.com/hyperium/hyper/tree/master/src/header)
|
||||
//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other
|
||||
//! header utility methods.
|
||||
|
||||
use std::convert::TryFrom;
|
||||
use std::{fmt, str::FromStr};
|
||||
use std::fmt;
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::Error as HttpError;
|
||||
use mime::Mime;
|
||||
use percent_encoding::{AsciiSet, CONTROLS};
|
||||
|
||||
pub use http::header::*;
|
||||
|
||||
use crate::error::ParseError;
|
||||
use crate::httpmessage::HttpMessage;
|
||||
use crate::HttpMessage;
|
||||
|
||||
mod as_name;
|
||||
mod into_pair;
|
||||
mod into_value;
|
||||
mod utils;
|
||||
|
||||
mod common;
|
||||
pub(crate) mod map;
|
||||
mod shared;
|
||||
|
||||
pub use self::common::*;
|
||||
#[doc(hidden)]
|
||||
pub use self::shared::*;
|
||||
|
||||
pub use self::as_name::AsHeaderName;
|
||||
pub use self::into_pair::IntoHeaderPair;
|
||||
pub use self::into_value::IntoHeaderValue;
|
||||
#[doc(hidden)]
|
||||
pub use self::map::GetAll;
|
||||
pub use self::map::HeaderMap;
|
||||
pub use self::utils::*;
|
||||
|
||||
/// A trait for any object that will represent a header field and value.
|
||||
pub trait Header
|
||||
where
|
||||
Self: IntoHeaderValue,
|
||||
{
|
||||
/// A trait for any object that already represents a valid header field and value.
|
||||
pub trait Header: IntoHeaderValue {
|
||||
/// Returns the name of the header field
|
||||
fn name() -> HeaderName;
|
||||
|
||||
@ -37,170 +41,16 @@ where
|
||||
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
|
||||
}
|
||||
|
||||
/// A trait for any object that can be Converted to a `HeaderValue`
|
||||
pub trait IntoHeaderValue: Sized {
|
||||
/// The type returned in the event of a conversion error.
|
||||
type Error: Into<HttpError>;
|
||||
|
||||
/// Try to convert value to a Header value.
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error>;
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for HeaderValue {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
Ok(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoHeaderValue for &'a str {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
self.parse()
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> IntoHeaderValue for &'a [u8] {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
HeaderValue::from_bytes(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Bytes {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
HeaderValue::from_maybe_shared(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Vec<u8> {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
HeaderValue::try_from(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for String {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
HeaderValue::try_from(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for usize {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
let s = format!("{}", self);
|
||||
HeaderValue::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for u64 {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
let s = format!("{}", self);
|
||||
HeaderValue::try_from(s)
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for Mime {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
#[inline]
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
HeaderValue::try_from(format!("{}", self))
|
||||
}
|
||||
}
|
||||
|
||||
/// Represents supported types of content encodings
|
||||
#[derive(Copy, Clone, PartialEq, Debug)]
|
||||
pub enum ContentEncoding {
|
||||
/// Automatically select encoding based on encoding negotiation
|
||||
Auto,
|
||||
/// A format using the Brotli algorithm
|
||||
Br,
|
||||
/// A format using the zlib structure with deflate algorithm
|
||||
Deflate,
|
||||
/// Gzip algorithm
|
||||
Gzip,
|
||||
/// Indicates the identity function (i.e. no compression, nor modification)
|
||||
Identity,
|
||||
}
|
||||
|
||||
impl ContentEncoding {
|
||||
#[inline]
|
||||
/// Is the content compressed?
|
||||
pub fn is_compression(self) -> bool {
|
||||
matches!(self, ContentEncoding::Identity | ContentEncoding::Auto)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// Convert content encoding to string
|
||||
pub fn as_str(self) -> &'static str {
|
||||
match self {
|
||||
ContentEncoding::Br => "br",
|
||||
ContentEncoding::Gzip => "gzip",
|
||||
ContentEncoding::Deflate => "deflate",
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => "identity",
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
/// default quality value
|
||||
pub fn quality(self) -> f64 {
|
||||
match self {
|
||||
ContentEncoding::Br => 1.1,
|
||||
ContentEncoding::Gzip => 1.0,
|
||||
ContentEncoding::Deflate => 0.9,
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a str> for ContentEncoding {
|
||||
fn from(s: &'a str) -> ContentEncoding {
|
||||
let s = s.trim();
|
||||
|
||||
if s.eq_ignore_ascii_case("br") {
|
||||
ContentEncoding::Br
|
||||
} else if s.eq_ignore_ascii_case("gzip") {
|
||||
ContentEncoding::Gzip
|
||||
} else if s.eq_ignore_ascii_case("deflate") {
|
||||
ContentEncoding::Deflate
|
||||
} else {
|
||||
ContentEncoding::Identity
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[derive(Debug, Default)]
|
||||
pub(crate) struct Writer {
|
||||
buf: BytesMut,
|
||||
}
|
||||
|
||||
impl Writer {
|
||||
fn new() -> Writer {
|
||||
Writer {
|
||||
buf: BytesMut::new(),
|
||||
}
|
||||
Writer::default()
|
||||
}
|
||||
|
||||
fn take(&mut self) -> Bytes {
|
||||
self.buf.split().freeze()
|
||||
}
|
||||
@ -219,176 +69,15 @@ impl fmt::Write for Writer {
|
||||
}
|
||||
}
|
||||
|
||||
#[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
|
||||
/// Convert `http::HeaderMap` to our `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
|
||||
fn from(mut map: http::HeaderMap) -> HeaderMap {
|
||||
HeaderMap::from_drain(map.drain())
|
||||
}
|
||||
}
|
||||
|
||||
// This encode set is used for HTTP header values and is defined at
|
||||
// https://tools.ietf.org/html/rfc5987#section-3.2
|
||||
/// This encode set is used for HTTP header values and is defined at
|
||||
/// https://tools.ietf.org/html/rfc5987#section-3.2.
|
||||
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||
.add(b' ')
|
||||
.add(b'"')
|
||||
@ -410,91 +99,3 @@ pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
|
||||
.add(b']')
|
||||
.add(b'{')
|
||||
.add(b'}');
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::shared::Charset;
|
||||
use super::{parse_extended_value, ExtendedValue};
|
||||
use language_tags::LanguageTag;
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_with_encoding_and_language_tag() {
|
||||
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
|
||||
// RFC 5987, Section 3.2.2
|
||||
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
|
||||
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
|
||||
assert!(result.is_ok());
|
||||
let extended_value = result.unwrap();
|
||||
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
|
||||
assert!(extended_value.language_tag.is_some());
|
||||
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
|
||||
assert_eq!(
|
||||
vec![163, b' ', b'r', b'a', b't', b'e', b's'],
|
||||
extended_value.value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_with_encoding() {
|
||||
// RFC 5987, Section 3.2.2
|
||||
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
|
||||
// and U+20AC (EURO SIGN)
|
||||
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
|
||||
assert!(result.is_ok());
|
||||
let extended_value = result.unwrap();
|
||||
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
|
||||
assert!(extended_value.language_tag.is_none());
|
||||
assert_eq!(
|
||||
vec![
|
||||
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
||||
b't', b'e', b's',
|
||||
],
|
||||
extended_value.value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_missing_language_tag_and_encoding() {
|
||||
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
|
||||
let result = parse_extended_value("foo%20bar.html");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_partially_formatted() {
|
||||
let result = parse_extended_value("UTF-8'missing third part");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_partially_formatted_blank() {
|
||||
let result = parse_extended_value("blank second part'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt_extended_value_with_encoding_and_language_tag() {
|
||||
let extended_value = ExtendedValue {
|
||||
charset: Charset::Iso_8859_1,
|
||||
language_tag: Some("en".parse().expect("Could not parse language tag")),
|
||||
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
|
||||
};
|
||||
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt_extended_value_with_encoding() {
|
||||
let extended_value = ExtendedValue {
|
||||
charset: Charset::Ext("UTF-8".to_string()),
|
||||
language_tag: None,
|
||||
value: vec![
|
||||
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
||||
b't', b'e', b's',
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
"UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
|
||||
format!("{}", extended_value)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -161,7 +161,7 @@ impl FromStr for EntityTag {
|
||||
impl IntoHeaderValue for EntityTag {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||
let mut wrt = Writer::new();
|
||||
write!(wrt, "{}", self).unwrap();
|
||||
HeaderValue::from_maybe_shared(wrt.take())
|
||||
|
193
actix-http/src/header/shared/extended.rs
Normal file
193
actix-http/src/header/shared/extended.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
|
||||
use crate::header::{Charset, HTTP_VALUE};
|
||||
|
||||
// From hyper v0.11.27 src/header/parsing.rs
|
||||
|
||||
/// The value part of an extended parameter consisting of three parts:
|
||||
/// - The REQUIRED character set name (`charset`).
|
||||
/// - The OPTIONAL language information (`language_tag`).
|
||||
/// - A character sequence representing the actual value (`value`), separated by single quotes.
|
||||
///
|
||||
/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ExtendedValue {
|
||||
/// The character set that is used to encode the `value` to a string.
|
||||
pub charset: Charset,
|
||||
|
||||
/// The human language details of the `value`, if available.
|
||||
pub language_tag: Option<LanguageTag>,
|
||||
|
||||
/// The parameter value, as expressed in octets.
|
||||
pub value: Vec<u8>,
|
||||
}
|
||||
|
||||
/// Parses extended header parameter values (`ext-value`), as defined in
|
||||
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2).
|
||||
///
|
||||
/// Extended values are denoted by parameter names that end with `*`.
|
||||
///
|
||||
/// ## ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// ext-value = charset "'" [ language ] "'" value-chars
|
||||
/// ; like RFC 2231's <extended-initial-value>
|
||||
/// ; (see [RFC2231], Section 7)
|
||||
///
|
||||
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset
|
||||
///
|
||||
/// mime-charset = 1*mime-charsetc
|
||||
/// mime-charsetc = ALPHA / DIGIT
|
||||
/// / "!" / "#" / "$" / "%" / "&"
|
||||
/// / "+" / "-" / "^" / "_" / "`"
|
||||
/// / "{" / "}" / "~"
|
||||
/// ; as <mime-charset> in Section 2.3 of [RFC2978]
|
||||
/// ; except that the single quote is not included
|
||||
/// ; SHOULD be registered in the IANA charset registry
|
||||
///
|
||||
/// language = <Language-Tag, defined in [RFC5646], Section 2.1>
|
||||
///
|
||||
/// value-chars = *( pct-encoded / attr-char )
|
||||
///
|
||||
/// pct-encoded = "%" HEXDIG HEXDIG
|
||||
/// ; see [RFC3986], Section 2.1
|
||||
///
|
||||
/// attr-char = ALPHA / DIGIT
|
||||
/// / "!" / "#" / "$" / "&" / "+" / "-" / "."
|
||||
/// / "^" / "_" / "`" / "|" / "~"
|
||||
/// ; token except ( "*" / "'" / "%" )
|
||||
/// ```
|
||||
pub fn parse_extended_value(
|
||||
val: &str,
|
||||
) -> Result<ExtendedValue, crate::error::ParseError> {
|
||||
// Break into three pieces separated by the single-quote character
|
||||
let mut parts = val.splitn(3, '\'');
|
||||
|
||||
// Interpret the first piece as a Charset
|
||||
let charset: Charset = match parts.next() {
|
||||
None => return Err(crate::error::ParseError::Header),
|
||||
Some(n) => FromStr::from_str(n).map_err(|_| crate::error::ParseError::Header)?,
|
||||
};
|
||||
|
||||
// Interpret the second piece as a language tag
|
||||
let language_tag: Option<LanguageTag> = match parts.next() {
|
||||
None => return Err(crate::error::ParseError::Header),
|
||||
Some("") => None,
|
||||
Some(s) => match s.parse() {
|
||||
Ok(lt) => Some(lt),
|
||||
Err(_) => return Err(crate::error::ParseError::Header),
|
||||
},
|
||||
};
|
||||
|
||||
// Interpret the third piece as a sequence of value characters
|
||||
let value: Vec<u8> = match parts.next() {
|
||||
None => return Err(crate::error::ParseError::Header),
|
||||
Some(v) => percent_encoding::percent_decode(v.as_bytes()).collect(),
|
||||
};
|
||||
|
||||
Ok(ExtendedValue {
|
||||
value,
|
||||
charset,
|
||||
language_tag,
|
||||
})
|
||||
}
|
||||
|
||||
impl fmt::Display for ExtendedValue {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let encoded_value =
|
||||
percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
|
||||
if let Some(ref lang) = self.language_tag {
|
||||
write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
|
||||
} else {
|
||||
write!(f, "{}''{}", self.charset, encoded_value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_with_encoding_and_language_tag() {
|
||||
let expected_language_tag = "en".parse::<LanguageTag>().unwrap();
|
||||
// RFC 5987, Section 3.2.2
|
||||
// Extended notation, using the Unicode character U+00A3 (POUND SIGN)
|
||||
let result = parse_extended_value("iso-8859-1'en'%A3%20rates");
|
||||
assert!(result.is_ok());
|
||||
let extended_value = result.unwrap();
|
||||
assert_eq!(Charset::Iso_8859_1, extended_value.charset);
|
||||
assert!(extended_value.language_tag.is_some());
|
||||
assert_eq!(expected_language_tag, extended_value.language_tag.unwrap());
|
||||
assert_eq!(
|
||||
vec![163, b' ', b'r', b'a', b't', b'e', b's'],
|
||||
extended_value.value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_with_encoding() {
|
||||
// RFC 5987, Section 3.2.2
|
||||
// Extended notation, using the Unicode characters U+00A3 (POUND SIGN)
|
||||
// and U+20AC (EURO SIGN)
|
||||
let result = parse_extended_value("UTF-8''%c2%a3%20and%20%e2%82%ac%20rates");
|
||||
assert!(result.is_ok());
|
||||
let extended_value = result.unwrap();
|
||||
assert_eq!(Charset::Ext("UTF-8".to_string()), extended_value.charset);
|
||||
assert!(extended_value.language_tag.is_none());
|
||||
assert_eq!(
|
||||
vec![
|
||||
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
||||
b't', b'e', b's',
|
||||
],
|
||||
extended_value.value
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_missing_language_tag_and_encoding() {
|
||||
// From: https://greenbytes.de/tech/tc2231/#attwithfn2231quot2
|
||||
let result = parse_extended_value("foo%20bar.html");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_partially_formatted() {
|
||||
let result = parse_extended_value("UTF-8'missing third part");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extended_value_partially_formatted_blank() {
|
||||
let result = parse_extended_value("blank second part'");
|
||||
assert!(result.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt_extended_value_with_encoding_and_language_tag() {
|
||||
let extended_value = ExtendedValue {
|
||||
charset: Charset::Iso_8859_1,
|
||||
language_tag: Some("en".parse().expect("Could not parse language tag")),
|
||||
value: vec![163, b' ', b'r', b'a', b't', b'e', b's'],
|
||||
};
|
||||
assert_eq!("ISO-8859-1'en'%A3%20rates", format!("{}", extended_value));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_fmt_extended_value_with_encoding() {
|
||||
let extended_value = ExtendedValue {
|
||||
charset: Charset::Ext("UTF-8".to_string()),
|
||||
language_tag: None,
|
||||
value: vec![
|
||||
194, 163, b' ', b'a', b'n', b'd', b' ', 226, 130, 172, b' ', b'r', b'a',
|
||||
b't', b'e', b's',
|
||||
],
|
||||
};
|
||||
assert_eq!(
|
||||
"UTF-8''%C2%A3%20and%20%E2%82%AC%20rates",
|
||||
format!("{}", extended_value)
|
||||
);
|
||||
}
|
||||
}
|
@ -48,7 +48,7 @@ impl From<SystemTime> for HttpDate {
|
||||
impl IntoHeaderValue for HttpDate {
|
||||
type Error = InvalidHeaderValue;
|
||||
|
||||
fn try_into(self) -> Result<HeaderValue, Self::Error> {
|
||||
fn try_into_value(self) -> Result<HeaderValue, Self::Error> {
|
||||
let mut wrt = BytesMut::with_capacity(29).writer();
|
||||
write!(
|
||||
wrt,
|
||||
|
@ -1,14 +1,16 @@
|
||||
//! Copied for `hyper::header::shared`;
|
||||
|
||||
pub use self::charset::Charset;
|
||||
pub use self::encoding::Encoding;
|
||||
pub use self::entity::EntityTag;
|
||||
pub use self::httpdate::HttpDate;
|
||||
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
||||
pub use language_tags::LanguageTag;
|
||||
//! Originally taken from `hyper::header::shared`.
|
||||
|
||||
mod charset;
|
||||
mod encoding;
|
||||
mod entity;
|
||||
mod extended;
|
||||
mod httpdate;
|
||||
mod quality_item;
|
||||
|
||||
pub use self::charset::Charset;
|
||||
pub use self::encoding::Encoding;
|
||||
pub use self::entity::EntityTag;
|
||||
pub use self::extended::{parse_extended_value, ExtendedValue};
|
||||
pub use self::httpdate::HttpDate;
|
||||
pub use self::quality_item::{q, qitem, Quality, QualityItem};
|
||||
pub use language_tags::LanguageTag;
|
||||
|
63
actix-http/src/header/utils.rs
Normal file
63
actix-http/src/header/utils.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use std::{fmt, str::FromStr};
|
||||
|
||||
use http::HeaderValue;
|
||||
|
||||
use crate::{error::ParseError, header::HTTP_VALUE};
|
||||
|
||||
/// Reads a comma-delimited raw header into a Vec.
|
||||
#[inline]
|
||||
pub fn from_comma_delimited<'a, I, T>(all: I) -> Result<Vec<T>, ParseError>
|
||||
where
|
||||
I: Iterator<Item = &'a HeaderValue> + 'a,
|
||||
T: FromStr,
|
||||
{
|
||||
let mut result = Vec::new();
|
||||
for h in all {
|
||||
let s = h.to_str().map_err(|_| ParseError::Header)?;
|
||||
result.extend(
|
||||
s.split(',')
|
||||
.filter_map(|x| match x.trim() {
|
||||
"" => None,
|
||||
y => Some(y),
|
||||
})
|
||||
.filter_map(|x| x.trim().parse().ok()),
|
||||
)
|
||||
}
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
/// Reads a single string when parsing a header.
|
||||
#[inline]
|
||||
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
|
||||
if let Some(line) = val {
|
||||
let line = line.to_str().map_err(|_| ParseError::Header)?;
|
||||
if !line.is_empty() {
|
||||
return T::from_str(line).or(Err(ParseError::Header));
|
||||
}
|
||||
}
|
||||
Err(ParseError::Header)
|
||||
}
|
||||
|
||||
/// Format an array into a comma-delimited string.
|
||||
#[inline]
|
||||
pub fn fmt_comma_delimited<T>(f: &mut fmt::Formatter<'_>, parts: &[T]) -> fmt::Result
|
||||
where
|
||||
T: fmt::Display,
|
||||
{
|
||||
let mut iter = parts.iter();
|
||||
if let Some(part) = iter.next() {
|
||||
fmt::Display::fmt(part, f)?;
|
||||
}
|
||||
for part in iter {
|
||||
f.write_str(", ")?;
|
||||
fmt::Display::fmt(part, f)?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Percent encode a sequence of bytes with a character set defined in
|
||||
/// <https://tools.ietf.org/html/rfc5987#section-3.2>
|
||||
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
|
||||
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
|
||||
fmt::Display::fmt(&encoded, f)
|
||||
}
|
@ -67,6 +67,14 @@ impl Response {
|
||||
static_resp!(ExpectationFailed, StatusCode::EXPECTATION_FAILED);
|
||||
static_resp!(UnprocessableEntity, StatusCode::UNPROCESSABLE_ENTITY);
|
||||
static_resp!(TooManyRequests, StatusCode::TOO_MANY_REQUESTS);
|
||||
static_resp!(
|
||||
RequestHeaderFieldsTooLarge,
|
||||
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE
|
||||
);
|
||||
static_resp!(
|
||||
UnavailableForLegalReasons,
|
||||
StatusCode::UNAVAILABLE_FOR_LEGAL_REASONS
|
||||
);
|
||||
|
||||
static_resp!(InternalServerError, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
static_resp!(NotImplemented, StatusCode::NOT_IMPLEMENTED);
|
@ -5,15 +5,17 @@ use encoding_rs::{Encoding, UTF_8};
|
||||
use http::header;
|
||||
use mime::Mime;
|
||||
|
||||
use crate::cookie::Cookie;
|
||||
use crate::error::{ContentTypeError, CookieParseError, ParseError};
|
||||
use crate::error::{ContentTypeError, ParseError};
|
||||
use crate::extensions::Extensions;
|
||||
use crate::header::{Header, HeaderMap};
|
||||
use crate::payload::Payload;
|
||||
#[cfg(feature = "cookies")]
|
||||
use crate::{cookie::Cookie, error::CookieParseError};
|
||||
|
||||
#[cfg(feature = "cookies")]
|
||||
struct Cookies(Vec<Cookie<'static>>);
|
||||
|
||||
/// Trait that implements general purpose operations on http messages
|
||||
/// Trait that implements general purpose operations on HTTP messages.
|
||||
pub trait HttpMessage: Sized {
|
||||
/// Type of message payload stream
|
||||
type Stream;
|
||||
@ -30,8 +32,8 @@ pub trait HttpMessage: Sized {
|
||||
/// Mutable reference to a the request's extensions container
|
||||
fn extensions_mut(&self) -> RefMut<'_, Extensions>;
|
||||
|
||||
/// Get a header.
|
||||
#[doc(hidden)]
|
||||
/// Get a header
|
||||
fn get_header<H: Header>(&self) -> Option<H>
|
||||
where
|
||||
Self: Sized,
|
||||
@ -43,8 +45,8 @@ pub trait HttpMessage: Sized {
|
||||
}
|
||||
}
|
||||
|
||||
/// Read the request content type. If request does not contain
|
||||
/// *Content-Type* header, empty str get returned.
|
||||
/// Read the request content type. If request did not contain a *Content-Type* header, an empty
|
||||
/// string is returned.
|
||||
fn content_type(&self) -> &str {
|
||||
if let Some(content_type) = self.headers().get(header::CONTENT_TYPE) {
|
||||
if let Ok(content_type) = content_type.to_str() {
|
||||
@ -90,7 +92,7 @@ pub trait HttpMessage: Sized {
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
/// Check if request has chunked transfer encoding
|
||||
/// Check if request has chunked transfer encoding.
|
||||
fn chunked(&self) -> Result<bool, ParseError> {
|
||||
if let Some(encodings) = self.headers().get(header::TRANSFER_ENCODING) {
|
||||
if let Ok(s) = encodings.to_str() {
|
||||
@ -104,7 +106,7 @@ pub trait HttpMessage: Sized {
|
||||
}
|
||||
|
||||
/// Load request cookies.
|
||||
#[inline]
|
||||
#[cfg(feature = "cookies")]
|
||||
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
|
||||
if self.extensions().get::<Cookies>().is_none() {
|
||||
let mut cookies = Vec::new();
|
||||
@ -119,12 +121,14 @@ pub trait HttpMessage: Sized {
|
||||
}
|
||||
self.extensions_mut().insert(Cookies(cookies));
|
||||
}
|
||||
|
||||
Ok(Ref::map(self.extensions(), |ext| {
|
||||
&ext.get::<Cookies>().unwrap().0
|
||||
}))
|
||||
}
|
||||
|
||||
/// Return request cookie.
|
||||
#[cfg(feature = "cookies")]
|
||||
fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
|
||||
if let Ok(cookies) = self.cookies() {
|
||||
for cookie in cookies.iter() {
|
||||
@ -173,10 +177,12 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_content_type() {
|
||||
let req = TestRequest::with_header("content-type", "text/plain").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "text/plain"))
|
||||
.finish();
|
||||
assert_eq!(req.content_type(), "text/plain");
|
||||
let req =
|
||||
TestRequest::with_header("content-type", "application/json; charset=utf=8")
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "application/json; charset=utf=8"))
|
||||
.finish();
|
||||
assert_eq!(req.content_type(), "application/json");
|
||||
let req = TestRequest::default().finish();
|
||||
@ -185,12 +191,14 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_mime_type() {
|
||||
let req = TestRequest::with_header("content-type", "application/json").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "application/json"))
|
||||
.finish();
|
||||
assert_eq!(req.mime_type().unwrap(), Some(mime::APPLICATION_JSON));
|
||||
let req = TestRequest::default().finish();
|
||||
assert_eq!(req.mime_type().unwrap(), None);
|
||||
let req =
|
||||
TestRequest::with_header("content-type", "application/json; charset=utf-8")
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "application/json; charset=utf-8"))
|
||||
.finish();
|
||||
let mt = req.mime_type().unwrap().unwrap();
|
||||
assert_eq!(mt.get_param(mime::CHARSET), Some(mime::UTF_8));
|
||||
@ -200,10 +208,8 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_mime_type_error() {
|
||||
let req = TestRequest::with_header(
|
||||
"content-type",
|
||||
"applicationadfadsfasdflknadsfklnadsfjson",
|
||||
)
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "applicationadfadsfasdflknadsfklnadsfjson"))
|
||||
.finish();
|
||||
assert_eq!(Err(ContentTypeError::ParseError), req.mime_type());
|
||||
}
|
||||
@ -213,26 +219,26 @@ mod tests {
|
||||
let req = TestRequest::default().finish();
|
||||
assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
|
||||
|
||||
let req = TestRequest::with_header("content-type", "application/json").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "application/json"))
|
||||
.finish();
|
||||
assert_eq!(UTF_8.name(), req.encoding().unwrap().name());
|
||||
|
||||
let req = TestRequest::with_header(
|
||||
"content-type",
|
||||
"application/json; charset=ISO-8859-2",
|
||||
)
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "application/json; charset=ISO-8859-2"))
|
||||
.finish();
|
||||
assert_eq!(ISO_8859_2, req.encoding().unwrap());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_encoding_error() {
|
||||
let req = TestRequest::with_header("content-type", "applicatjson").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "applicatjson"))
|
||||
.finish();
|
||||
assert_eq!(Some(ContentTypeError::ParseError), req.encoding().err());
|
||||
|
||||
let req = TestRequest::with_header(
|
||||
"content-type",
|
||||
"application/json; charset=kkkttktk",
|
||||
)
|
||||
let req = TestRequest::default()
|
||||
.insert_header(("content-type", "application/json; charset=kkkttktk"))
|
||||
.finish();
|
||||
assert_eq!(
|
||||
Some(ContentTypeError::UnknownEncoding),
|
||||
@ -245,15 +251,16 @@ mod tests {
|
||||
let req = TestRequest::default().finish();
|
||||
assert!(!req.chunked().unwrap());
|
||||
|
||||
let req =
|
||||
TestRequest::with_header(header::TRANSFER_ENCODING, "chunked").finish();
|
||||
let req = TestRequest::default()
|
||||
.insert_header((header::TRANSFER_ENCODING, "chunked"))
|
||||
.finish();
|
||||
assert!(req.chunked().unwrap());
|
||||
|
||||
let req = TestRequest::default()
|
||||
.header(
|
||||
.insert_header((
|
||||
header::TRANSFER_ENCODING,
|
||||
Bytes::from_static(b"some va\xadscc\xacas0xsdasdlue"),
|
||||
)
|
||||
))
|
||||
.finish();
|
||||
assert!(req.chunked().is_err());
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user