1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-29 19:37:34 +02:00

Compare commits

...

95 Commits

Author SHA1 Message Date
Rob Ede
1aee8a1a58 Merge remote-tracking branch 'origin/master' into on-connect-fix 2021-12-05 23:24:44 +00:00
Rob Ede
bed72d9bb7 fix examples 2021-12-05 23:23:36 +00:00
Rob Ede
cca0593df1 Merge branch 'master' into on-connect-fix 2021-12-05 21:26:31 +00:00
Rob Ede
c596f573a6 bump actix-server to rc.1 2021-12-05 21:25:15 +00:00
Ali MJ Al-Nasrawy
627c0dc22f workaround rustdoc bug for Error (#2489) 2021-12-05 16:19:08 +00:00
Rob Ede
2d053b7036 remove actix_http::http module (#2488) 2021-12-05 14:37:20 +00:00
Rob Ede
efa68ec453 fix docs 2021-12-05 04:57:17 +00:00
Rob Ede
01885f9954 inline unsafe 2021-12-05 04:49:52 +00:00
Rob Ede
a86c831b89 Merge remote-tracking branch 'origin/master' into on-connect-fix 2021-12-05 04:46:19 +00:00
Ali MJ Al-Nasrawy
59be0c65c6 disallow query or fragements in url_for constructions (#2430)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-12-05 04:39:18 +00:00
Rob Ede
e1a2d9c606 Quality / QualityItem improvements (#2486) 2021-12-05 03:38:08 +00:00
Rob Ede
d89c706cd6 re-instate Range typed header (#2485)
Co-authored-by: RideWindX <ridewindx@gmail.com>
2021-12-05 00:02:25 +00:00
Mohammed Sazid Al Rashid
4c9ca7196d Add WsResponseBuilder to build web socket session response (#1920)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-12-04 22:32:44 +00:00
Rob Ede
fa7f3e6908 undeprecate App::data_factory (#2484) 2021-12-04 19:41:15 +00:00
Rob Ede
c7c02ef99d body ergonomics v3 (#2468) 2021-12-04 19:40:47 +00:00
fakeshadow
a2d5c5a058 Use cilent time out for h2 handshake timeout. (#2483) 2021-12-02 18:16:34 +00:00
Rob Ede
deece8d519 re-instate accept-encoding typed header (#2482) 2021-12-02 17:04:40 +00:00
Rob Ede
2a72bdae09 improve typed header macro (#2481) 2021-12-02 15:25:39 +00:00
Rob Ede
075d871e63 wrap LanguageTags type in new AnyOrSome type to support wildcards (#2480) 2021-12-02 13:59:25 +00:00
Rob Ede
c4b20df56a convert all remaining IETF RFC links to new format 2021-12-02 03:45:04 +00:00
Rob Ede
0df275c478 update all IETF RFC links to new URL format 2021-12-01 19:42:02 +00:00
Rob Ede
697238fadc prepare actix-multipart release 0.4.0-beta.9 2021-12-01 00:26:07 +00:00
Rob Ede
e045418038 prepare for actix-tls rc.1 (#2474) 2021-11-30 14:12:04 +00:00
Rob Ede
a978b417f3 use actix ready future in remaining return types 2021-11-30 13:11:41 +00:00
fakeshadow
fa82b698b7 remove pin-project from actix-web. (#2471) 2021-11-30 11:16:53 +00:00
Rob Ede
fc4cdf81eb expose header::map module (#2470) 2021-11-29 02:22:47 +00:00
Ali MJ Al-Nasrawy
654dc64a09 don't hang after dropping mutipart (#2463) 2021-11-29 02:00:24 +00:00
fakeshadow
cf54388534 re-work from request macro. (#2469) 2021-11-29 01:23:27 +00:00
Rob Ede
39243095b5 guarantee ordering of header map get_all (#2467) 2021-11-28 19:23:29 +00:00
fakeshadow
89c6d62656 clean up multipart and field stream trait impl (#2462) 2021-11-25 00:10:53 +00:00
fakeshadow
52bbbd1d73 Mnior cleanup of multipart API. (#2461) 2021-11-24 20:53:11 +00:00
Rob Ede
3e6e9779dc fix big5 charset parsing 2021-11-24 20:16:15 +00:00
Rob Ede
9bdd334bb4 add test for duplicate dynamic segent name 2021-11-23 15:57:18 +00:00
Rob Ede
bcbbc115aa fix awc changelog 2021-11-23 15:12:55 +00:00
Rob Ede
ab5eb7c1aa prepare actix-multipart release 0.4.0-beta.8 2021-11-22 18:48:14 +00:00
Rob Ede
18b8ef0765 prepare actix-test release 0.1.0-beta.7 2021-11-22 18:47:43 +00:00
Rob Ede
b806b4773c prepare actix-http-test release 3.0.0-beta.7 2021-11-22 18:46:58 +00:00
Rob Ede
0062d99b6f prepare actix-files release 0.6.0-beta.9 2021-11-22 18:46:19 +00:00
Rob Ede
99e6a9c26d prepare awc release 3.0.0-beta.11 2021-11-22 18:41:43 +00:00
Rob Ede
5f5bd2184e prepare actix-web release 4.0.0-beta.12 2021-11-22 18:20:55 +00:00
Rob Ede
88e074879d prepare actix-http release 3.0.0-beta.13 2021-11-22 18:19:09 +00:00
fakeshadow
e7987e7429 awc: support http2 over plain tcp with feature flag (#2439)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-11-22 18:16:56 +00:00
Rob Ede
a172f5968d prepare for actix-tls v3 beta 9 (#2456) 2021-11-22 15:37:23 +00:00
Rob Ede
a2a42ec152 use anybody in doc test 2021-11-22 01:35:33 +00:00
fakeshadow
dd347e0bd0 implement io-uring for actix-files (#2408)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-11-22 01:19:09 +00:00
Rob Ede
194a691537 files: 304 Not Modified responses omit Content-Length header (#2453) 2021-11-19 14:04:12 +00:00
Rob Ede
56ee97f722 add files path traversal tests 2021-11-18 18:14:34 +00:00
Ali MJ Al-Nasrawy
66620a1012 simplify handler.rs (#2450) 2021-11-17 20:11:35 +00:00
Rob Ede
e33618ed6d ensure content disposition header in multipart (#2451)
Co-authored-by: Craig Pastro <craig.pastro@gmail.com>
2021-11-17 17:44:50 +00:00
Rob Ede
1fe309bcc6 increase ci test timeout 2021-11-17 15:32:42 +00:00
fakeshadow
168a7284d3 fix actix_http::Error conversion. (#2449) 2021-11-17 13:13:05 +00:00
Rob Ede
68a3acb9c2 bump zstd dep 2021-11-16 23:22:29 +00:00
Rob Ede
84c6d25fd3 bump env logger dep 2021-11-16 23:07:08 +00:00
Rob Ede
0a135c7dc9 bump actix-codec to 0.4.1 2021-11-16 22:41:24 +00:00
Rob Ede
668a33c793 remove internal usage of Body 2021-11-16 22:10:30 +00:00
Rob Ede
d8cbb879dd make AnyBody generic on Body type (#2448) 2021-11-16 21:41:35 +00:00
Rob Ede
13cf5a9e44 remove chunked encoding header for websockets 2021-11-16 16:55:45 +00:00
Rob Ede
4df1cd78b7 simplify AnyBody and BodySize (#2446) 2021-11-16 09:21:10 +00:00
Rob Ede
e8a0e16863 run tarpaulin on workspace 2021-11-15 18:11:51 +00:00
Rob Ede
a2f59c02f7 bump actix-server to beta 9 (#2442) 2021-11-15 04:03:33 +00:00
Rob Ede
2754608f3c fix codegen tests 2021-11-08 02:46:43 +00:00
Ibraheem Ahmed
c020cedb63 Log internal server errors (#2387) 2021-11-07 17:02:23 +00:00
fakeshadow
5e554dca35 fix awc clippy warning (#2431) 2021-11-04 15:57:55 +00:00
fakeshadow
6ec2d7b909 add keep alive to h2 through ping pong (#2433) 2021-11-04 15:15:23 +00:00
Ali MJ Al-Nasrawy
ec6d284a8e improve "data no configured" message (#2429) 2021-10-31 13:19:21 +00:00
fakeshadow
be9530eb72 avoid building actix-tls with no-default-features (#2426) 2021-10-26 13:16:48 +01:00
Luca Palmieri
855e260fdb Add html_utf8 content type. (#2423) 2021-10-26 09:24:38 +01:00
fakeshadow
d13854505f move actix_http::client module to awc (#2425) 2021-10-26 00:37:40 +01:00
fakeshadow
d40b6748bc remove dead dep (#2420) 2021-10-22 00:22:58 +01:00
Rob Ede
c79b9a0df3 prepare actix-files release 0.6.0-beta.8 2021-10-20 23:32:46 +01:00
Rob Ede
4af414064b prepare actix-multipart release 0.4.0-beta.7 2021-10-20 23:31:46 +01:00
Rob Ede
9abe166d52 actix-web beta 10 releases (#2417) 2021-10-20 22:32:05 +01:00
Rob Ede
c09ec6af4c split off coverage ci job 2021-10-20 02:27:30 +01:00
Rob Ede
37f2bf5625 clippy 2021-10-20 02:06:51 +01:00
Rob Ede
4f6f0b0137 chore: Bump rustls to 0.20.0 (#2416)
Co-authored-by: Kirill Mironov <vetrokm@gmail.com>
2021-10-20 02:00:11 +01:00
Rob Ede
591abc37c3 add test runtime macro (#2409) 2021-10-19 17:30:32 +01:00
Rob Ede
ad22cc4e7f bump msrv to 1.52.1 2021-10-19 01:59:28 +01:00
Rob Ede
efdf3ab1c3 clippy 2021-10-19 01:32:58 +01:00
Rob Ede
6b3ea4fc61 copy original route macro input with compile errors (#2410) 2021-10-14 18:06:31 +01:00
James Rhodes
99985fc4ec web: implement into_inner for Data<T: ?Sized> (#2407) 2021-10-12 18:35:33 +01:00
Omid Rad
a6707fb7ee Remove checked_expr (#2401) 2021-10-11 18:28:09 +01:00
Rob Ede
a3806cde19 fix changelog 2021-09-12 22:41:08 +01:00
Jake
efefa0d0ce web: add option to not require content type header for Json (#2362)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-09-11 17:27:50 +01:00
Rob Ede
450ff5fa1d improve extract docs (#2384) 2021-09-11 16:48:47 +01:00
Arniu Tseng
8ae278cb68 Remove FromRequest::Config (#2233)
Co-authored-by: Jonas Platte <jplatte@users.noreply.github.com>
Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-09-11 01:11:16 +01:00
Rob Ede
46699e3429 remove time dep from actix-http (#2383) 2021-09-11 00:01:01 +01:00
Rob Ede
999c003aa8 clippy 2021-07-14 23:54:55 +01:00
Rob Ede
2bc7102e37 update changelog 2021-07-14 23:50:29 +01:00
Rob Ede
20752fd82e document unsafe 2021-07-14 23:45:58 +01:00
Rob Ede
e6290dfd09 fix test 2021-07-14 00:36:00 +01:00
Rob Ede
9e685fc5fb fix doc references 2021-07-14 00:27:33 +01:00
Rob Ede
cf63f5c755 remove dead code 2021-07-14 00:23:22 +01:00
Rob Ede
694cfc94c9 fmt 2021-07-14 00:21:05 +01:00
Rob Ede
6bb33ec5db use custom cloneany trait 2021-07-14 00:20:45 +01:00
Rob Ede
3b2e2acb6c fix connection data on keep alive connections 2021-07-12 18:37:41 +01:00
222 changed files with 8586 additions and 5241 deletions

View File

@@ -1,9 +1,14 @@
[alias] [alias]
chk = "check --workspace --all-features --tests --examples --bins" lint = "clippy --workspace --tests --examples --bins -- -Dclippy::todo"
lint = "clippy --workspace --all-features --tests --examples --bins" lint-all = "clippy --workspace --all-features --tests --examples --bins -- -Dclippy::todo"
ci-min = "hack check --workspace --no-default-features"
ci-min-test = "hack check --workspace --no-default-features --tests --examples" # lib checking
ci-default = "check --workspace --bins --tests --examples" ci-check-min = "hack --workspace check --no-default-features"
ci-full = "check --workspace --all-features --bins --tests --examples" ci-check-default = "hack --workspace check"
ci-test = "test --workspace --all-features --lib --tests --no-fail-fast -- --nocapture" ci-check-default-tests = "check --workspace --tests"
ci-check-all-feature-powerset="hack --workspace --feature-powerset --skip=__compress,io-uring check"
ci-check-all-feature-powerset-linux="hack --workspace --feature-powerset --skip=__compress check"
# testing
ci-doctest-default = "test --workspace --doc --no-fail-fast -- --nocapture"
ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture" ci-doctest = "test --workspace --all-features --doc --no-fail-fast -- --nocapture"

View File

@@ -14,9 +14,9 @@ jobs:
target: target:
- { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu } - { name: Linux, os: ubuntu-latest, triple: x86_64-unknown-linux-gnu }
- { name: macOS, os: macos-latest, triple: x86_64-apple-darwin } - { name: macOS, os: macos-latest, triple: x86_64-apple-darwin }
- { name: Windows, os: windows-latest, triple: x86_64-pc-windows-msvc } - { name: Windows, os: windows-2022, triple: x86_64-pc-windows-msvc }
version: version:
- 1.51.0 # MSRV - 1.52.0 # MSRV
- stable - stable
- nightly - nightly
@@ -32,6 +32,8 @@ jobs:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# install OpenSSL on Windows # install OpenSSL on Windows
# TODO: GitHub actions docs state that OpenSSL is
# already installed on these Windows machines somewhere
- name: Set vcpkg root - name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc' if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
@@ -48,8 +50,7 @@ jobs:
- name: Generate Cargo.lock - name: Generate Cargo.lock
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with: { command: generate-lockfile }
command: generate-lockfile
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0 uses: Swatinem/rust-cache@v1.2.0
@@ -61,53 +62,105 @@ jobs:
- name: check minimal - name: check minimal
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-min } with: { command: ci-check-min }
- name: check minimal + tests
uses: actions-rs/cargo@v1
with: { command: ci-min-test }
- name: check default - name: check default
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: { command: ci-default } with: { command: ci-check-default }
- name: check full
uses: actions-rs/cargo@v1
with: { command: ci-full }
- name: tests - name: tests
uses: actions-rs/cargo@v1 timeout-minutes: 60
timeout-minutes: 40
with:
command: ci-test
args: --skip=test_reading_deflate_encoding_large_random_rustls
- name: Generate coverage file
if: >
matrix.target.os == 'ubuntu-latest'
&& matrix.version == 'stable'
&& github.ref == 'refs/heads/master'
run: | run: |
cargo install cargo-tarpaulin --vers "^0.13" cargo test --lib --tests -p=actix-router --all-features
cargo tarpaulin --out Xml --verbose cargo test --lib --tests -p=actix-http --all-features
- name: Upload to Codecov cargo test --lib --tests -p=actix-web --features=rustls,openssl -- --skip=test_reading_deflate_encoding_large_random_rustls
if: > cargo test --lib --tests -p=actix-web-codegen --all-features
matrix.target.os == 'ubuntu-latest' cargo test --lib --tests -p=awc --all-features
&& matrix.version == 'stable' cargo test --lib --tests -p=actix-http-test --all-features
&& github.ref == 'refs/heads/master' cargo test --lib --tests -p=actix-test --all-features
uses: codecov/codecov-action@v1 cargo test --lib --tests -p=actix-files
with: cargo test --lib --tests -p=actix-multipart --all-features
file: cobertura.xml cargo test --lib --tests -p=actix-web-actors --all-features
- name: tests (io-uring)
if: matrix.target.os == 'ubuntu-latest'
timeout-minutes: 60
run: >
sudo bash -c "ulimit -Sl 512
&& ulimit -Hl 512
&& PATH=$PATH:/usr/share/rust/.cargo/bin
&& RUSTUP_TOOLCHAIN=${{ matrix.version }} cargo test --lib --tests -p=actix-files --all-features"
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.6.3 --no-default-features --features ci-autoclean
cargo-cache cargo-cache
rustdoc: ci_feature_powerset_check:
name: rustdoc name: Verify Feature Combinations
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: check feature combinations
uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset }
- name: check feature combinations
uses: actions-rs/cargo@v1
with: { command: ci-check-all-feature-powerset-linux }
coverage:
name: coverage
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Install stable
uses: actions-rs/toolchain@v1
with:
toolchain: stable-x86_64-unknown-linux-gnu
profile: minimal
override: true
- name: Generate Cargo.lock
uses: actions-rs/cargo@v1
with: { command: generate-lockfile }
- name: Cache Dependencies
uses: Swatinem/rust-cache@v1.2.0
- name: Generate coverage file
if: github.ref == 'refs/heads/master'
run: |
cargo install cargo-tarpaulin --vers "^0.13"
cargo tarpaulin --workspace --features=rustls,openssl --out Xml --verbose
- name: Upload to Codecov
if: github.ref == 'refs/heads/master'
uses: codecov/codecov-action@v1
with: { file: cobertura.xml }
rustdoc:
name: doc tests
runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
@@ -124,13 +177,7 @@ jobs:
- name: Cache Dependencies - name: Cache Dependencies
uses: Swatinem/rust-cache@v1.3.0 uses: Swatinem/rust-cache@v1.3.0
- name: Install cargo-hack
uses: actions-rs/cargo@v1
with:
command: install
args: cargo-hack
- name: doc tests - name: doc tests
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
timeout-minutes: 40 timeout-minutes: 60
with: { command: ci-doctest } with: { command: ci-doctest }

View File

@@ -1,6 +1,93 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Added
* Methods on `AcceptLanguage`: `ranked` and `preference`. [#2480]
* `AcceptEncoding` typed header. [#2482]
* `Range` typed header. [#2485]
* `HttpResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
* `ServiceResponse::map_into_{left,right}_body` and `HttpResponse::map_into_boxed_body`. [#2468]
* `HttpServer::on_connect` now receives a `CloneableExtensions` object. [#2327]
[#2325]: https://github.com/actix/actix-web/pull/2325
[#2327]: https://github.com/actix/actix-web/pull/2327
### Changed
* Rename `Accept::{mime_precedence => ranked}`. [#2480]
* Rename `Accept::{mime_preference => preference}`. [#2480]
* Un-deprecate `App::data_factory`. [#2484]
* `HttpRequest::url_for` no longer constructs URLs with query or fragment components. [#2430]
* `HttpServer::on_connect` now receives a `CloneableExtensions` object. [#2327]
### Fixed
* Accept wildcard `*` items in `AcceptLanguage`. [#2480]
* Re-exports `dev::{BodySize, MessageBody, SizedStream}`. They are exposed through the `body` module. [#2468]
* Typed headers containing lists that require one or more items now enforce this minimum. [#2482]
[#2327]: https://github.com/actix/actix-web/pull/2327
[#2430]: https://github.com/actix/actix-web/pull/2430
[#2468]: https://github.com/actix/actix-web/pull/2468
[#2480]: https://github.com/actix/actix-web/pull/2480
[#2482]: https://github.com/actix/actix-web/pull/2482
[#2484]: https://github.com/actix/actix-web/pull/2484
[#2485]: https://github.com/actix/actix-web/pull/2485
## 4.0.0-beta.13 - 2021-11-30
### Changed
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474
## 4.0.0-beta.12 - 2021-11-22
### Changed
* Compress middleware's response type is now `AnyBody<Encoder<B>>`. [#2448]
### Fixed
* Relax `Unpin` bound on `S` (stream) parameter of `HttpResponseBuilder::streaming`. [#2448]
### Removed
* `dev::ResponseBody` re-export; is function is replaced by the new `dev::AnyBody` enum. [#2446]
[#2446]: https://github.com/actix/actix-web/pull/2446
[#2448]: https://github.com/actix/actix-web/pull/2448
## 4.0.0-beta.11 - 2021-11-15
### Added
* Re-export `dev::ServerHandle` from `actix-server`. [#2442]
### Changed
* `ContentType::html` now produces `text/html; charset=utf-8` instead of `text/html`. [#2423]
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
[#2423]: https://github.com/actix/actix-web/pull/2423
[#2442]: https://github.com/actix/actix-web/pull/2442
## 4.0.0-beta.10 - 2021-10-20
### Added
* Option to allow `Json` extractor to work without a `Content-Type` header present. [#2362]
* `#[actix_web::test]` macro for setting up tests with a runtime. [#2409]
### Changed
* Associated type `FromRequest::Config` was removed. [#2233]
* Inner field made private on `web::Payload`. [#2384]
* `Data::into_inner` and `Data::get_ref` no longer requires `T: Sized`. [#2403]
* Updated rustls to v0.20. [#2414]
* Minimum supported Rust version (MSRV) is now 1.52.
### Removed
* Useless `ServiceResponse::checked_expr` method. [#2401]
[#2233]: https://github.com/actix/actix-web/pull/2233
[#2362]: https://github.com/actix/actix-web/pull/2362
[#2384]: https://github.com/actix/actix-web/pull/2384
[#2401]: https://github.com/actix/actix-web/pull/2401
[#2403]: https://github.com/actix/actix-web/pull/2403
[#2409]: https://github.com/actix/actix-web/pull/2409
[#2414]: https://github.com/actix/actix-web/pull/2414
## 4.0.0-beta.9 - 2021-09-09 ## 4.0.0-beta.9 - 2021-09-09

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.9" version = "4.0.0-beta.13"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
@@ -11,13 +11,14 @@ categories = [
"web-programming::websocket" "web-programming::websocket"
] ]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web.git"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
[package.metadata.docs.rs] [package.metadata.docs.rs]
# features that docs.rs will build with # features that docs.rs will build with
features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"] features = ["openssl", "rustls", "compress-brotli", "compress-gzip", "compress-zstd", "cookies", "secure-cookies"]
rustdoc-args = ["--cfg", "docsrs"]
[lib] [lib]
name = "actix_web" name = "actix_web"
@@ -37,8 +38,6 @@ members = [
"actix-test", "actix-test",
"actix-router", "actix-router",
] ]
# enable when MSRV is 1.51+
# resolver = "2"
[features] [features]
default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] default = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]
@@ -62,22 +61,25 @@ openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls # rustls
rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"] rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status. # Internal (PRIVATE!) features used to aid testing and checking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
__compress = [] __compress = []
# io-uring feature only avaiable for Linux OSes.
experimental-io-uring = ["actix-server/io-uring"]
[dependencies] [dependencies]
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-macros = "0.2.1" actix-macros = "0.2.3"
actix-router = "0.5.0-beta.2" actix-rt = "2.3"
actix-rt = "2.2" actix-server = "2.0.0-rc.1"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0" actix-service = "2.0.0"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true } actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.4" actix-http = "3.0.0-beta.14"
actix-http = "3.0.0-beta.10" actix-router = "0.5.0-beta.2"
actix-web-codegen = "0.5.0-beta.5"
ahash = "0.7" ahash = "0.7"
bytes = "1" bytes = "1"
@@ -94,29 +96,31 @@ once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
paste = "1" paste = "1"
pin-project = "1.0.0" pin-project-lite = "0.2.7"
regex = "1.4" regex = "1.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
smallvec = "1.6.1" smallvec = "1.6.1"
socket2 = "0.4.0" socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.3", default-features = false, features = ["formatting"] }
url = "2.1" url = "2.1"
[dev-dependencies] [dev-dependencies]
actix-test = { version = "0.1.0-beta.3", features = ["openssl", "rustls"] } actix-test = { version = "0.1.0-beta.7", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.8", features = ["openssl"] } awc = { version = "3.0.0-beta.11", features = ["openssl"] }
brotli2 = "0.3.2" brotli2 = "0.3.2"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.9"
flate2 = "1.0.13" flate2 = "1.0.13"
zstd = "0.7" futures-util = { version = "0.3.7", default-features = false, features = ["std"] }
rand = "0.8" rand = "0.8"
rcgen = "0.8" rcgen = "0.8"
rustls-pemfile = "0.2"
tls-openssl = { package = "openssl", version = "0.10.9" } tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.19.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
zstd = "0.9"
[profile.dev] [profile.dev]
# Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much. # Disabling debug info speeds up builds a bunch and we don't rely on it for debugging that much.
@@ -139,6 +143,15 @@ actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" } actix-web-codegen = { path = "actix-web-codegen" }
awc = { path = "awc" } awc = { path = "awc" }
# uncomment for quick testing against local actix-net repo
# actix-service = { path = "../actix-net/actix-service" }
# actix-macros = { path = "../actix-net/actix-macros" }
# actix-rt = { path = "../actix-net/actix-rt" }
# actix-codec = { path = "../actix-net/actix-codec" }
# actix-utils = { path = "../actix-net/actix-utils" }
# actix-tls = { path = "../actix-net/actix-tls" }
# actix-server = { path = "../actix-net/actix-server" }
[[test]] [[test]]
name = "test_server" name = "test_server"
required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"] required-features = ["compress-brotli", "compress-gzip", "compress-zstd", "cookies"]

View File

@@ -11,6 +11,8 @@
Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`. Alternatively, explicitly require trailing slashes: `NormalizePath::new(TrailingSlash::Always)`.
* The `type Config` of `FromRequest` was removed.
* Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd). * Feature flag `compress` has been split into its supported algorithm (brotli, gzip, zstd).
By default all compression algorithms are enabled. By default all compression algorithms are enabled.
To select algorithm you want to include with `middleware::Compress` use following flags: To select algorithm you want to include with `middleware::Compress` use following flags:

View File

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

View File

@@ -3,6 +3,22 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.9 - 2021-11-22
* Add crate feature `experimental-io-uring`, enabling async file I/O to be utilized. This feature is only available on Linux OSes with recent kernel versions. This feature is semver-exempt. [#2408]
* Add `NamedFile::open_async`. [#2408]
* Fix 304 Not Modified responses to omit the Content-Length header, as per the spec. [#2453]
* The `Responder` impl for `NamedFile` now has a boxed future associated type. [#2408]
* The `Service` impl for `NamedFileService` now has a boxed future associated type. [#2408]
* Add `impl Clone` for `FilesService`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
[#2453]: https://github.com/actix/actix-web/pull/2453
## 0.6.0-beta.8 - 2021-10-20
* Minimum supported Rust version (MSRV) is now 1.52.
## 0.6.0-beta.7 - 2021-09-09 ## 0.6.0-beta.7 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51. * Minimum supported Rust version (MSRV) is now 1.51.

View File

@@ -1,7 +1,11 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.6.0-beta.7" version = "0.6.0-beta.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"fakeshadow <24548779@qq.com>",
"Rob Ede <robjtede@icloud.com>",
]
description = "Static file serving for Actix Web" description = "Static file serving for Actix Web"
keywords = ["actix", "http", "async", "futures"] keywords = ["actix", "http", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
@@ -14,11 +18,14 @@ edition = "2018"
name = "actix_files" name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[features]
experimental-io-uring = ["actix-web/experimental-io-uring", "tokio-uring"]
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.9", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
actix-http = "3.0.0-beta.10" actix-http = "3.0.0-beta.14"
actix-service = "2.0.0" actix-service = "2"
actix-utils = "3.0.0" actix-utils = "3"
askama_escape = "0.10" askama_escape = "0.10"
bitflags = "1" bitflags = "1"
@@ -30,8 +37,11 @@ log = "0.4"
mime = "0.3" mime = "0.3"
mime_guess = "2.0.1" mime_guess = "2.0.1"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project-lite = "0.2.7"
tokio-uring = { version = "0.1", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-web = "4.0.0-beta.9" actix-web = "4.0.0-beta.11"
actix-test = "0.1.0-beta.3" actix-test = "0.1.0-beta.7"

View File

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

View File

@@ -1,98 +1,277 @@
use std::{ use std::{
cmp, fmt, cmp, fmt,
fs::File,
future::Future, future::Future,
io::{self, Read, Seek}, io,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_web::{ use actix_web::{error::Error, web::Bytes};
error::{BlockingError, Error},
rt::task::{spawn_blocking, JoinHandle},
};
use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
#[doc(hidden)] use super::named::File;
/// A helper created from a `std::fs::File` which reads the file
/// chunk-by-chunk on a `ThreadPool`. pin_project! {
pub struct ChunkedReadFile { /// Adapter to read a `std::file::File` in chunks.
#[doc(hidden)]
pub struct ChunkedReadFile<F, Fut> {
size: u64, size: u64,
offset: u64, offset: u64,
state: ChunkedReadFileState, #[pin]
state: ChunkedReadFileState<Fut>,
counter: u64, counter: u64,
} callback: F,
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 { #[cfg(not(feature = "experimental-io-uring"))]
pin_project! {
#[project = ChunkedReadFileStateProj]
#[project_replace = ChunkedReadFileStateProjReplace]
enum ChunkedReadFileState<Fut> {
File { file: Option<File>, },
Future { #[pin] fut: Fut },
}
}
#[cfg(feature = "experimental-io-uring")]
pin_project! {
#[project = ChunkedReadFileStateProj]
#[project_replace = ChunkedReadFileStateProjReplace]
enum ChunkedReadFileState<Fut> {
File { file: Option<(File, BytesMut)> },
Future { #[pin] fut: Fut },
}
}
impl<F, Fut> fmt::Debug for ChunkedReadFile<F, Fut> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("ChunkedReadFile") f.write_str("ChunkedReadFile")
} }
} }
impl Stream for ChunkedReadFile { pub(crate) fn new_chunked_read(
type Item = Result<Bytes, Error>; size: u64,
offset: u64,
file: File,
) -> impl Stream<Item = Result<Bytes, Error>> {
ChunkedReadFile {
size,
offset,
#[cfg(not(feature = "experimental-io-uring"))]
state: ChunkedReadFileState::File { file: Some(file) },
#[cfg(feature = "experimental-io-uring")]
state: ChunkedReadFileState::File {
file: Some((file, BytesMut::new())),
},
counter: 0,
callback: chunked_read_file_callback,
}
}
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { #[cfg(not(feature = "experimental-io-uring"))]
let this = self.as_mut().get_mut(); async fn chunked_read_file_callback(
match this.state { mut file: File,
ChunkedReadFileState::File(ref mut file) => { offset: u64,
let size = this.size; max_bytes: usize,
let offset = this.offset; ) -> Result<(File, Bytes), Error> {
let counter = this.counter; use io::{Read as _, Seek as _};
if size == counter {
Poll::Ready(None)
} else {
let mut file = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = spawn_blocking(move || {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let res = actix_web::rt::task::spawn_blocking(move || {
let mut buf = Vec::with_capacity(max_bytes); let mut buf = Vec::with_capacity(max_bytes);
file.seek(io::SeekFrom::Start(offset))?; file.seek(io::SeekFrom::Start(offset))?;
let n_bytes = let n_bytes = file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
file.by_ref().take(max_bytes as u64).read_to_end(&mut buf)?;
if n_bytes == 0 {
Err(io::Error::from(io::ErrorKind::UnexpectedEof))
} else {
Ok((file, Bytes::from(buf)))
}
})
.await
.map_err(|_| actix_web::error::BlockingError)??;
Ok(res)
}
#[cfg(feature = "experimental-io-uring")]
async fn chunked_read_file_callback(
file: File,
offset: u64,
max_bytes: usize,
mut bytes_mut: BytesMut,
) -> io::Result<(File, Bytes, BytesMut)> {
bytes_mut.reserve(max_bytes);
let (res, mut bytes_mut) = file.read_at(bytes_mut, offset).await;
let n_bytes = res?;
if n_bytes == 0 { if n_bytes == 0 {
return Err(io::ErrorKind::UnexpectedEof.into()); return Err(io::ErrorKind::UnexpectedEof.into());
} }
Ok((file, Bytes::from(buf))) let bytes = bytes_mut.split_to(n_bytes).freeze();
});
this.state = ChunkedReadFileState::Future(fut); Ok((file, bytes, bytes_mut))
}
#[cfg(feature = "experimental-io-uring")]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where
F: Fn(File, u64, usize, BytesMut) -> Fut,
Fut: Future<Output = io::Result<(File, Bytes, BytesMut)>>,
{
type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut().project();
match this.state.as_mut().project() {
ChunkedReadFileStateProj::File { file } => {
let size = *this.size;
let offset = *this.offset;
let counter = *this.counter;
if size == counter {
Poll::Ready(None)
} else {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let (file, bytes_mut) = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes, bytes_mut);
this.state
.project_replace(ChunkedReadFileState::Future { fut });
self.poll_next(cx) self.poll_next(cx)
} }
} }
ChunkedReadFileState::Future(ref mut fut) => { ChunkedReadFileStateProj::Future { fut } => {
let (file, bytes) = let (file, bytes, bytes_mut) = ready!(fut.poll(cx))?;
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??;
this.state = ChunkedReadFileState::File(Some(file));
this.offset += bytes.len() as u64; this.state.project_replace(ChunkedReadFileState::File {
this.counter += bytes.len() as u64; file: Some((file, bytes_mut)),
});
*this.offset += bytes.len() as u64;
*this.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes))) Poll::Ready(Some(Ok(bytes)))
} }
} }
} }
} }
#[cfg(not(feature = "experimental-io-uring"))]
impl<F, Fut> Stream for ChunkedReadFile<F, Fut>
where
F: Fn(File, u64, usize) -> Fut,
Fut: Future<Output = Result<(File, Bytes), Error>>,
{
type Item = Result<Bytes, Error>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
let mut this = self.as_mut().project();
match this.state.as_mut().project() {
ChunkedReadFileStateProj::File { file } => {
let size = *this.size;
let offset = *this.offset;
let counter = *this.counter;
if size == counter {
Poll::Ready(None)
} else {
let max_bytes = cmp::min(size.saturating_sub(counter), 65_536) as usize;
let file = file
.take()
.expect("ChunkedReadFile polled after completion");
let fut = (this.callback)(file, offset, max_bytes);
this.state
.project_replace(ChunkedReadFileState::Future { fut });
self.poll_next(cx)
}
}
ChunkedReadFileStateProj::Future { fut } => {
let (file, bytes) = ready!(fut.poll(cx))?;
this.state
.project_replace(ChunkedReadFileState::File { file: Some(file) });
*this.offset += bytes.len() as u64;
*this.counter += bytes.len() as u64;
Poll::Ready(Some(Ok(bytes)))
}
}
}
}
#[cfg(feature = "experimental-io-uring")]
use bytes_mut::BytesMut;
// TODO: remove new type and use bytes::BytesMut directly
#[doc(hidden)]
#[cfg(feature = "experimental-io-uring")]
mod bytes_mut {
use std::ops::{Deref, DerefMut};
use tokio_uring::buf::{IoBuf, IoBufMut};
#[derive(Debug)]
pub struct BytesMut(bytes::BytesMut);
impl BytesMut {
pub(super) fn new() -> Self {
Self(bytes::BytesMut::new())
}
}
impl Deref for BytesMut {
type Target = bytes::BytesMut;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for BytesMut {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
unsafe impl IoBuf for BytesMut {
fn stable_ptr(&self) -> *const u8 {
self.0.as_ptr()
}
fn bytes_init(&self) -> usize {
self.0.len()
}
fn bytes_total(&self) -> usize {
self.0.capacity()
}
}
unsafe impl IoBufMut for BytesMut {
fn stable_mut_ptr(&mut self) -> *mut u8 {
self.0.as_mut_ptr()
}
unsafe fn set_init(&mut self, init_len: usize) {
if self.len() < init_len {
self.0.set_len(init_len);
}
}
}
}

View File

@@ -6,7 +6,6 @@ use std::{
}; };
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ dev::{
AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest, AppService, HttpServiceFactory, RequestHead, ResourceDef, ServiceRequest,
@@ -20,8 +19,9 @@ use actix_web::{
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, directory_listing, named,
MimeOverride, PathFilter, service::{FilesService, FilesServiceInner},
Directory, DirectoryRenderer, HttpNewService, MimeOverride, PathFilter,
}; };
/// Static files handling service. /// Static files handling service.
@@ -283,11 +283,17 @@ impl Files {
/// Setting a fallback static file handler: /// Setting a fallback static file handler:
/// ``` /// ```
/// use actix_files::{Files, NamedFile}; /// use actix_files::{Files, NamedFile};
/// use actix_web::dev::{ServiceRequest, ServiceResponse, fn_service};
/// ///
/// # fn run() -> Result<(), actix_web::Error> { /// # fn run() -> Result<(), actix_web::Error> {
/// let files = Files::new("/", "./static") /// let files = Files::new("/", "./static")
/// .index_file("index.html") /// .index_file("index.html")
/// .default_handler(NamedFile::open("./static/404.html")?); /// .default_handler(fn_service(|req: ServiceRequest| async {
/// let (req, _) = req.into_parts();
/// let file = NamedFile::open_async("./static/404.html").await?;
/// let res = file.into_response(&req);
/// Ok(ServiceResponse::new(req, res))
/// }));
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -353,7 +359,7 @@ impl ServiceFactory<ServiceRequest> for Files {
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
let mut srv = FilesService { let mut inner = FilesServiceInner {
directory: self.directory.clone(), directory: self.directory.clone(),
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
@@ -372,14 +378,14 @@ impl ServiceFactory<ServiceRequest> for Files {
Box::pin(async { Box::pin(async {
match fut.await { match fut.await {
Ok(default) => { Ok(default) => {
srv.default = Some(default); inner.default = Some(default);
Ok(srv) Ok(FilesService(Rc::new(inner)))
} }
Err(_) => Err(()), Err(_) => Err(()),
} }
}) })
} else { } else {
Box::pin(ok(srv)) Box::pin(async move { Ok(FilesService(Rc::new(inner))) })
} }
} }
} }

View File

@@ -33,12 +33,12 @@ mod path_buf;
mod range; mod range;
mod service; mod service;
pub use crate::chunked::ChunkedReadFile; pub use self::chunked::ChunkedReadFile;
pub use crate::directory::Directory; pub use self::directory::Directory;
pub use crate::files::Files; pub use self::files::Files;
pub use crate::named::NamedFile; pub use self::named::NamedFile;
pub use crate::range::HttpRange; pub use self::range::HttpRange;
pub use crate::service::FilesService; pub use self::service::FilesService;
use self::directory::{directory_listing, DirectoryRenderer}; use self::directory::{directory_listing, DirectoryRenderer};
use self::error::FilesError; use self::error::FilesError;
@@ -62,13 +62,12 @@ type PathFilter = dyn Fn(&Path, &RequestHead) -> bool;
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::{ use std::{
fs::{self, File}, fs::{self},
ops::Add, ops::Add,
time::{Duration, SystemTime}, time::{Duration, SystemTime},
}; };
use actix_service::ServiceFactory; use actix_service::ServiceFactory;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
guard, guard,
http::{ http::{
@@ -82,8 +81,9 @@ mod tests {
}; };
use super::*; use super::*;
use crate::named::File;
#[actix_rt::test] #[actix_web::test]
async fn test_file_extension_to_mime() { async fn test_file_extension_to_mime() {
let m = file_extension_to_mime(""); let m = file_extension_to_mime("");
assert_eq!(m, mime::APPLICATION_OCTET_STREAM); assert_eq!(m, mime::APPLICATION_OCTET_STREAM);
@@ -100,7 +100,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match() { async fn test_if_modified_since_without_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
@@ -112,7 +112,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_without_if_none_match_same() { async fn test_if_modified_since_without_if_none_match_same() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = file.last_modified().unwrap(); let since = file.last_modified().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
@@ -124,7 +124,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_modified_since_with_if_none_match() { async fn test_if_modified_since_with_if_none_match() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60))); let since = header::HttpDate::from(SystemTime::now().add(Duration::from_secs(60)));
let req = TestRequest::default() let req = TestRequest::default()
@@ -137,7 +137,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_unmodified_since() { async fn test_if_unmodified_since() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = file.last_modified().unwrap(); let since = file.last_modified().unwrap();
let req = TestRequest::default() let req = TestRequest::default()
@@ -149,7 +149,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_if_unmodified_since_failed() { async fn test_if_unmodified_since_failed() {
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let since = header::HttpDate::from(SystemTime::UNIX_EPOCH); let since = header::HttpDate::from(SystemTime::UNIX_EPOCH);
let req = TestRequest::default() let req = TestRequest::default()
@@ -161,8 +161,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_text() { async fn test_named_file_text() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open_async("test--").await.is_err());
let mut file = NamedFile::open("Cargo.toml").unwrap(); let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@@ -185,8 +185,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_disposition() { async fn test_named_file_content_disposition() {
assert!(NamedFile::open("test--").is_err()); assert!(NamedFile::open_async("test--").await.is_err());
let mut file = NamedFile::open("Cargo.toml").unwrap(); let mut file = NamedFile::open_async("Cargo.toml").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@@ -202,7 +202,8 @@ mod tests {
"inline; filename=\"Cargo.toml\"" "inline; filename=\"Cargo.toml\""
); );
let file = NamedFile::open("Cargo.toml") let file = NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.disable_content_disposition(); .disable_content_disposition();
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
@@ -212,8 +213,19 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_non_ascii_file_name() { async fn test_named_file_non_ascii_file_name() {
let mut file = let file = {
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml").unwrap(); #[cfg(feature = "experimental-io-uring")]
{
crate::named::File::open("Cargo.toml").await.unwrap()
}
#[cfg(not(feature = "experimental-io-uring"))]
{
crate::named::File::open("Cargo.toml").unwrap()
}
};
let mut file = NamedFile::from_file(file, "貨物.toml").unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@@ -236,7 +248,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_set_content_type() { async fn test_named_file_set_content_type() {
let mut file = NamedFile::open("Cargo.toml") let mut file = NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_content_type(mime::TEXT_XML); .set_content_type(mime::TEXT_XML);
{ {
@@ -261,7 +274,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_image() { async fn test_named_file_image() {
let mut file = NamedFile::open("tests/test.png").unwrap(); let mut file = NamedFile::open_async("tests/test.png").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@@ -284,7 +297,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_javascript() { async fn test_named_file_javascript() {
let file = NamedFile::open("tests/test.js").unwrap(); let file = NamedFile::open_async("tests/test.js").await.unwrap();
let req = TestRequest::default().to_http_request(); let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
@@ -304,7 +317,8 @@ mod tests {
disposition: DispositionType::Attachment, disposition: DispositionType::Attachment,
parameters: vec![DispositionParam::Filename(String::from("test.png"))], parameters: vec![DispositionParam::Filename(String::from("test.png"))],
}; };
let mut file = NamedFile::open("tests/test.png") let mut file = NamedFile::open_async("tests/test.png")
.await
.unwrap() .unwrap()
.set_content_disposition(cd); .set_content_disposition(cd);
{ {
@@ -329,7 +343,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_binary() { async fn test_named_file_binary() {
let mut file = NamedFile::open("tests/test.binary").unwrap(); let mut file = NamedFile::open_async("tests/test.binary").await.unwrap();
{ {
file.file(); file.file();
let _f: &File = &file; let _f: &File = &file;
@@ -352,7 +366,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_status_code_text() { async fn test_named_file_status_code_text() {
let mut file = NamedFile::open("Cargo.toml") let mut file = NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_status_code(StatusCode::NOT_FOUND); .set_status_code(StatusCode::NOT_FOUND);
{ {
@@ -568,7 +583,8 @@ mod tests {
async fn test_named_file_content_encoding() { async fn test_named_file_content_encoding() {
let srv = test::init_service(App::new().wrap(Compress::default()).service( let srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_content_encoding(header::ContentEncoding::Identity) .set_content_encoding(header::ContentEncoding::Identity)
}), }),
@@ -588,7 +604,8 @@ mod tests {
async fn test_named_file_content_encoding_gzip() { async fn test_named_file_content_encoding_gzip() {
let srv = test::init_service(App::new().wrap(Compress::default()).service( let srv = test::init_service(App::new().wrap(Compress::default()).service(
web::resource("/").to(|| async { web::resource("/").to(|| async {
NamedFile::open("Cargo.toml") NamedFile::open_async("Cargo.toml")
.await
.unwrap() .unwrap()
.set_content_encoding(header::ContentEncoding::Gzip) .set_content_encoding(header::ContentEncoding::Gzip)
}), }),
@@ -614,7 +631,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_allowed_method() { async fn test_named_file_allowed_method() {
let req = TestRequest::default().method(Method::GET).to_http_request(); let req = TestRequest::default().method(Method::GET).to_http_request();
let file = NamedFile::open("Cargo.toml").unwrap(); let file = NamedFile::open_async("Cargo.toml").await.unwrap();
let resp = file.respond_to(&req).await.unwrap(); let resp = file.respond_to(&req).await.unwrap();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
@@ -705,8 +722,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_file_missing() { async fn test_default_handler_file_missing() {
let st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| { .default_handler(|req: ServiceRequest| async {
ok(req.into_response(HttpResponse::Ok().body("default content"))) Ok(req.into_response(HttpResponse::Ok().body("default content")))
}) })
.new_service(()) .new_service(())
.await .await
@@ -789,9 +806,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_serve_named_file() { async fn test_serve_named_file() {
let srv = let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap())) let srv = test::init_service(App::new().service(factory)).await;
.await;
let req = TestRequest::get().uri("/Cargo.toml").to_request(); let req = TestRequest::get().uri("/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
@@ -808,11 +824,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_serve_named_file_prefix() { async fn test_serve_named_file_prefix() {
let srv = test::init_service( let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
App::new() let srv =
.service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())), test::init_service(App::new().service(web::scope("/test").service(factory))).await;
)
.await;
let req = TestRequest::get().uri("/test/Cargo.toml").to_request(); let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
@@ -829,10 +843,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_default_service() { async fn test_named_file_default_service() {
let srv = test::init_service( let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
App::new().default_service(NamedFile::open("Cargo.toml").unwrap()), let srv = test::init_service(App::new().default_service(factory)).await;
)
.await;
for route in ["/foobar", "/baz", "/"].iter() { for route in ["/foobar", "/baz", "/"].iter() {
let req = TestRequest::get().uri(route).to_request(); let req = TestRequest::get().uri(route).to_request();
@@ -847,8 +859,9 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_named_file() { async fn test_default_handler_named_file() {
let factory = NamedFile::open_async("Cargo.toml").await.unwrap();
let st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(NamedFile::open("Cargo.toml").unwrap()) .default_handler(factory)
.new_service(()) .new_service(())
.await .await
.unwrap(); .unwrap();
@@ -926,8 +939,8 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_default_handler_filter() { async fn test_default_handler_filter() {
let st = Files::new("/", ".") let st = Files::new("/", ".")
.default_handler(|req: ServiceRequest| { .default_handler(|req: ServiceRequest| async {
ok(req.into_response(HttpResponse::Ok().body("default content"))) Ok(req.into_response(HttpResponse::Ok().body("default content")))
}) })
.path_filter(|path, _| path.extension() == Some("png".as_ref())) .path_filter(|path, _| path.extension() == Some("png".as_ref()))
.new_service(()) .new_service(())

View File

@@ -1,29 +1,35 @@
use actix_service::{Service, ServiceFactory}; use std::{
use actix_utils::future::{ok, ready, Ready}; fmt,
use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef}; fs::Metadata,
use std::fs::{File, Metadata}; io,
use std::io; ops::{Deref, DerefMut},
use std::ops::{Deref, DerefMut}; path::{Path, PathBuf},
use std::path::{Path, PathBuf}; time::{SystemTime, UNIX_EPOCH},
use std::time::{SystemTime, UNIX_EPOCH}; };
#[cfg(unix)] #[cfg(unix)]
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use actix_service::{Service, ServiceFactory};
use actix_web::{ use actix_web::{
dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream}, body::{self, BoxBody, SizedStream},
dev::{
AppService, BodyEncoding, HttpServiceFactory, ResourceDef, ServiceRequest,
ServiceResponse,
},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, self, Charset, ContentDisposition, ContentEncoding, DispositionParam,
DispositionType, ExtendedValue,
}, },
ContentEncoding, StatusCode, StatusCode,
}, },
Error, HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use bitflags::bitflags; use bitflags::bitflags;
use futures_core::future::LocalBoxFuture;
use mime_guess::from_path; use mime_guess::from_path;
use crate::ChunkedReadFile;
use crate::{encoding::equiv_utf8_text, range::HttpRange}; use crate::{encoding::equiv_utf8_text, range::HttpRange};
bitflags! { bitflags! {
@@ -48,9 +54,9 @@ impl Default for Flags {
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// # fn run() -> Result<(), Box<dyn std::error::Error>> { /// # async fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let app = App::new() /// let file = NamedFile::open_async("./static/index.html").await?;
/// .service(NamedFile::open("./static/index.html")?); /// let app = App::new().service(file);
/// # Ok(()) /// # Ok(())
/// # } /// # }
/// ``` /// ```
@@ -62,10 +68,9 @@ impl Default for Flags {
/// ///
/// #[get("/")] /// #[get("/")]
/// async fn index() -> impl Responder { /// async fn index() -> impl Responder {
/// NamedFile::open("./static/index.html") /// NamedFile::open_async("./static/index.html").await
/// } /// }
/// ``` /// ```
#[derive(Debug)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
file: File, file: File,
@@ -78,6 +83,39 @@ pub struct NamedFile {
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
} }
impl fmt::Debug for NamedFile {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("NamedFile")
.field("path", &self.path)
.field(
"file",
#[cfg(feature = "experimental-io-uring")]
{
&"tokio_uring::File"
},
#[cfg(not(feature = "experimental-io-uring"))]
{
&self.file
},
)
.field("modified", &self.modified)
.field("md", &self.md)
.field("flags", &self.flags)
.field("status_code", &self.status_code)
.field("content_type", &self.content_type)
.field("content_disposition", &self.content_disposition)
.field("encoding", &self.encoding)
.finish()
}
}
#[cfg(not(feature = "experimental-io-uring"))]
pub(crate) use std::fs::File;
#[cfg(feature = "experimental-io-uring")]
pub(crate) use tokio_uring::fs::File;
use super::chunked;
impl NamedFile { impl NamedFile {
/// Creates an instance from a previously opened file. /// Creates an instance from a previously opened file.
/// ///
@@ -85,8 +123,7 @@ impl NamedFile {
/// `ContentDisposition` headers. /// `ContentDisposition` headers.
/// ///
/// # Examples /// # Examples
/// /// ```ignore
/// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
@@ -147,7 +184,30 @@ impl NamedFile {
(ct, cd) (ct, cd)
}; };
let md = file.metadata()?; let md = {
#[cfg(not(feature = "experimental-io-uring"))]
{
file.metadata()?
}
#[cfg(feature = "experimental-io-uring")]
{
use std::os::unix::prelude::{AsRawFd, FromRawFd};
let fd = file.as_raw_fd();
// SAFETY: fd is borrowed and lives longer than the unsafe block
unsafe {
let file = std::fs::File::from_raw_fd(fd);
let md = file.metadata();
// SAFETY: forget the fd before exiting block in success or error case but don't
// run destructor (that would close file handle)
std::mem::forget(file);
md?
}
}
};
let modified = md.modified().ok(); let modified = md.modified().ok();
let encoding = None; let encoding = None;
@@ -164,17 +224,45 @@ impl NamedFile {
}) })
} }
#[cfg(not(feature = "experimental-io-uring"))]
/// Attempts to open a file in read-only mode. /// Attempts to open a file in read-only mode.
/// ///
/// # Examples /// # Examples
///
/// ``` /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
/// ``` /// ```
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> { pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
Self::from_file(File::open(&path)?, path) let file = File::open(&path)?;
Self::from_file(file, path)
}
/// Attempts to open a file asynchronously in read-only mode.
///
/// When the `experimental-io-uring` crate feature is enabled, this will be async.
/// Otherwise, it will be just like [`open`][Self::open].
///
/// # Examples
/// ```
/// use actix_files::NamedFile;
/// # async fn open() {
/// let file = NamedFile::open_async("foo.txt").await.unwrap();
/// # }
/// ```
pub async fn open_async<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
let file = {
#[cfg(not(feature = "experimental-io-uring"))]
{
File::open(&path)?
}
#[cfg(feature = "experimental-io-uring")]
{
File::open(&path).await?
}
};
Self::from_file(file, path)
} }
/// Returns reference to the underlying `File` object. /// Returns reference to the underlying `File` object.
@@ -186,13 +274,12 @@ impl NamedFile {
/// Retrieve the path of this file. /// Retrieve the path of this file.
/// ///
/// # Examples /// # Examples
///
/// ``` /// ```
/// # use std::io; /// # use std::io;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// # fn path() -> io::Result<()> { /// # async fn path() -> io::Result<()> {
/// let file = NamedFile::open("test.txt")?; /// let file = NamedFile::open_async("test.txt").await?;
/// assert_eq!(file.path().as_os_str(), "foo.txt"); /// assert_eq!(file.path().as_os_str(), "foo.txt");
/// # Ok(()) /// # Ok(())
/// # } /// # }
@@ -310,7 +397,7 @@ impl NamedFile {
} }
/// Creates an `HttpResponse` with file as a streaming body. /// Creates an `HttpResponse` with file as a streaming body.
pub fn into_response(self, req: &HttpRequest) -> HttpResponse { pub fn into_response(self, req: &HttpRequest) -> HttpResponse<BoxBody> {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut res = HttpResponse::build(self.status_code); let mut res = HttpResponse::build(self.status_code);
@@ -332,7 +419,7 @@ impl NamedFile {
res.encoding(current_encoding); res.encoding(current_encoding);
} }
let reader = ChunkedReadFile::new(self.md.len(), 0, self.file); let reader = chunked::new_chunked_read(self.md.len(), 0, self.file);
return res.streaming(reader); return res.streaming(reader);
} }
@@ -443,10 +530,13 @@ impl NamedFile {
if precondition_failed { if precondition_failed {
return resp.status(StatusCode::PRECONDITION_FAILED).finish(); return resp.status(StatusCode::PRECONDITION_FAILED).finish();
} else if not_modified { } else if not_modified {
return resp.status(StatusCode::NOT_MODIFIED).finish(); return resp
.status(StatusCode::NOT_MODIFIED)
.body(body::None::new())
.map_into_boxed_body();
} }
let reader = ChunkedReadFile::new(length, offset, self.file); let reader = chunked::new_chunked_read(length, offset, self.file);
if offset != 0 || length != self.md.len() { if offset != 0 || length != self.md.len() {
resp.status(StatusCode::PARTIAL_CONTENT); resp.status(StatusCode::PARTIAL_CONTENT);
@@ -456,20 +546,6 @@ impl NamedFile {
} }
} }
impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &File {
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut File {
&mut self.file
}
}
/// Returns true if `req` has no `If-Match` header or one which matches `etag`. /// Returns true if `req` has no `If-Match` header or one which matches `etag`.
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool { fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
match req.get_header::<header::IfMatch>() { match req.get_header::<header::IfMatch>() {
@@ -510,8 +586,24 @@ fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
} }
} }
impl Deref for NamedFile {
type Target = File;
fn deref(&self) -> &Self::Target {
&self.file
}
}
impl DerefMut for NamedFile {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.file
}
}
impl Responder for NamedFile { impl Responder for NamedFile {
fn respond_to(self, req: &HttpRequest) -> HttpResponse { type Body = BoxBody;
fn respond_to(self, req: &HttpRequest) -> HttpResponse<Self::Body> {
self.into_response(req) self.into_response(req)
} }
} }
@@ -520,14 +612,16 @@ impl ServiceFactory<ServiceRequest> for NamedFile {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Config = (); type Config = ();
type InitError = ();
type Service = NamedFileService; type Service = NamedFileService;
type Future = Ready<Result<Self::Service, ()>>; type InitError = ();
type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
ok(NamedFileService { let service = NamedFileService {
path: self.path.clone(), path: self.path.clone(),
}) };
Box::pin(async move { Ok(service) })
} }
} }
@@ -540,18 +634,19 @@ pub struct NamedFileService {
impl Service<ServiceRequest> for NamedFileService { impl Service<ServiceRequest> for NamedFileService {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future { fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
ready(
NamedFile::open(&self.path) let path = self.path.clone();
.map_err(|e| e.into()) Box::pin(async move {
.map(|f| f.into_response(&req)) let file = NamedFile::open_async(path).await?;
.map(|res| ServiceResponse::new(req, res)), let res = file.into_response(&req);
) Ok(ServiceResponse::new(req, res))
})
} }
} }

View File

@@ -8,7 +8,7 @@ use actix_web::{dev::Payload, FromRequest, HttpRequest};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;
#[derive(Debug)] #[derive(Debug, PartialEq, Eq)]
pub(crate) struct PathBufWrap(PathBuf); pub(crate) struct PathBufWrap(PathBuf);
impl FromStr for PathBufWrap { impl FromStr for PathBufWrap {
@@ -21,6 +21,8 @@ impl FromStr for PathBufWrap {
impl PathBufWrap { impl PathBufWrap {
/// Parse a path, giving the choice of allowing hidden files to be considered valid segments. /// Parse a path, giving the choice of allowing hidden files to be considered valid segments.
///
/// Path traversal is guarded by this method.
pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> { pub fn parse_path(path: &str, hidden_files: bool) -> Result<Self, UriSegmentError> {
let mut buf = PathBuf::new(); let mut buf = PathBuf::new();
@@ -59,7 +61,6 @@ impl AsRef<Path> for PathBufWrap {
impl FromRequest for PathBufWrap { impl FromRequest for PathBufWrap {
type Error = UriSegmentError; type Error = UriSegmentError;
type Future = Ready<Result<Self, Self::Error>>; type Future = Ready<Result<Self, Self::Error>>;
type Config = ();
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
ready(req.match_info().path().parse()) ready(req.match_info().path().parse())
@@ -116,4 +117,24 @@ mod tests {
PathBuf::from_iter(vec!["test", ".tt"]) PathBuf::from_iter(vec!["test", ".tt"])
); );
} }
#[test]
fn path_traversal() {
assert_eq!(
PathBufWrap::parse_path("/../README.md", false).unwrap().0,
PathBuf::from_iter(vec!["README.md"])
);
assert_eq!(
PathBufWrap::parse_path("/../README.md", true).unwrap().0,
PathBuf::from_iter(vec!["README.md"])
);
assert_eq!(
PathBufWrap::parse_path("/../../../../../../../../../../etc/passwd", false)
.unwrap()
.0,
PathBuf::from_iter(vec!["etc/passwd"])
);
}
} }

View File

@@ -1,7 +1,6 @@
use std::{fmt, io, path::PathBuf, rc::Rc}; use std::{fmt, io, ops::Deref, path::PathBuf, rc::Rc};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{ServiceRequest, ServiceResponse}, dev::{ServiceRequest, ServiceResponse},
error::Error, error::Error,
@@ -17,7 +16,18 @@ use crate::{
}; };
/// Assembled file serving service. /// Assembled file serving service.
pub struct FilesService { #[derive(Clone)]
pub struct FilesService(pub(crate) Rc<FilesServiceInner>);
impl Deref for FilesService {
type Target = FilesServiceInner;
fn deref(&self) -> &Self::Target {
&*self.0
}
}
pub struct FilesServiceInner {
pub(crate) directory: PathBuf, pub(crate) directory: PathBuf,
pub(crate) index: Option<String>, pub(crate) index: Option<String>,
pub(crate) show_index: bool, pub(crate) show_index: bool,
@@ -31,20 +41,50 @@ pub struct FilesService {
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
} }
impl fmt::Debug for FilesServiceInner {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("FilesServiceInner")
}
}
impl FilesService { impl FilesService {
fn handle_err( async fn handle_err(
&self, &self,
err: io::Error, err: io::Error,
req: ServiceRequest, req: ServiceRequest,
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> { ) -> Result<ServiceResponse, Error> {
log::debug!("error handling {}: {}", req.path(), err); log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref default) = self.default { if let Some(ref default) = self.default {
Box::pin(default.call(req)) default.call(req).await
} else { } else {
Box::pin(ok(req.error_response(err))) Ok(req.error_response(err))
} }
} }
fn serve_named_file(
&self,
req: ServiceRequest,
mut named_file: NamedFile,
) -> ServiceResponse {
if let Some(ref mime_override) = self.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition;
}
named_file.flags = self.file_flags;
let (req, _) = req.into_parts();
let res = named_file.into_response(&req);
ServiceResponse::new(req, res)
}
fn show_index(&self, req: ServiceRequest, path: PathBuf) -> ServiceResponse {
let dir = Directory::new(self.directory.clone(), path);
let (req, _) = req.into_parts();
(self.renderer)(&dir, &req).unwrap_or_else(|e| ServiceResponse::from_err(e, req))
}
} }
impl fmt::Debug for FilesService { impl fmt::Debug for FilesService {
@@ -56,7 +96,7 @@ impl fmt::Debug for FilesService {
impl Service<ServiceRequest> for FilesService { impl Service<ServiceRequest> for FilesService {
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
@@ -69,103 +109,87 @@ impl Service<ServiceRequest> for FilesService {
matches!(*req.method(), Method::HEAD | Method::GET) matches!(*req.method(), Method::HEAD | Method::GET)
}; };
let this = self.clone();
Box::pin(async move {
if !is_method_valid { if !is_method_valid {
return Box::pin(ok(req.into_response( return Ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed() actix_web::HttpResponse::MethodNotAllowed()
.insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8)) .insert_header(header::ContentType(mime::TEXT_PLAIN_UTF_8))
.body("Request did not meet this resource's requirements."), .body("Request did not meet this resource's requirements."),
))); ));
} }
let real_path = let real_path =
match PathBufWrap::parse_path(req.match_info().path(), self.hidden_files) { match PathBufWrap::parse_path(req.match_info().path(), this.hidden_files) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Box::pin(ok(req.error_response(e))), Err(e) => return Ok(req.error_response(e)),
}; };
if let Some(filter) = &self.path_filter { if let Some(filter) = &this.path_filter {
if !filter(real_path.as_ref(), req.head()) { if !filter(real_path.as_ref(), req.head()) {
if let Some(ref default) = self.default { if let Some(ref default) = this.default {
return Box::pin(default.call(req)); return default.call(req).await;
} else { } else {
return Box::pin(ok( return Ok(
req.into_response(actix_web::HttpResponse::NotFound().finish()) req.into_response(actix_web::HttpResponse::NotFound().finish())
)); );
} }
} }
} }
// full file path // full file path
let path = self.directory.join(&real_path); let path = this.directory.join(&real_path);
if let Err(err) = path.canonicalize() { if let Err(err) = path.canonicalize() {
return Box::pin(self.handle_err(err, req)); return this.handle_err(err, req).await;
} }
if path.is_dir() { if path.is_dir() {
if self.redirect_to_slash if this.redirect_to_slash
&& !req.path().ends_with('/') && !req.path().ends_with('/')
&& (self.index.is_some() || self.show_index) && (this.index.is_some() || this.show_index)
{ {
let redirect_to = format!("{}/", req.path()); let redirect_to = format!("{}/", req.path());
return Box::pin(ok(req.into_response( return Ok(req.into_response(
HttpResponse::Found() HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to)) .insert_header((header::LOCATION, redirect_to))
.finish(), .finish(),
))); ));
} }
let serve_named_file = |req: ServiceRequest, mut named_file: NamedFile| { match this.index {
if let Some(ref mime_override) = self.mime_override { Some(ref index) => {
let new_disposition = mime_override(&named_file.content_type.type_()); let named_path = path.join(index);
named_file.content_disposition.disposition = new_disposition; match NamedFile::open_async(named_path).await {
Ok(named_file) => Ok(this.serve_named_file(req, named_file)),
Err(_) if this.show_index => Ok(this.show_index(req, path)),
Err(err) => this.handle_err(err, req).await,
} }
named_file.flags = self.file_flags; }
None if this.show_index => Ok(this.show_index(req, path)),
let (req, _) = req.into_parts(); _ => Ok(ServiceResponse::from_err(
let res = named_file.into_response(&req);
Box::pin(ok(ServiceResponse::new(req, res)))
};
let show_index = |req: ServiceRequest| {
let dir = Directory::new(self.directory.clone(), path.clone());
let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req);
Box::pin(match x {
Ok(resp) => ok(resp),
Err(err) => ok(ServiceResponse::from_err(err, req)),
})
};
match self.index {
Some(ref index) => match NamedFile::open(path.join(index)) {
Ok(named_file) => serve_named_file(req, named_file),
Err(_) if self.show_index => show_index(req),
Err(err) => self.handle_err(err, req),
},
None if self.show_index => show_index(req),
_ => Box::pin(ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
))), )),
} }
} else { } else {
match NamedFile::open(path) { match NamedFile::open_async(&path).await {
Ok(mut named_file) => { Ok(mut named_file) => {
if let Some(ref mime_override) = self.mime_override { if let Some(ref mime_override) = this.mime_override {
let new_disposition = mime_override(&named_file.content_type.type_()); let new_disposition =
mime_override(&named_file.content_type.type_());
named_file.content_disposition.disposition = new_disposition; named_file.content_disposition.disposition = new_disposition;
} }
named_file.flags = self.file_flags; named_file.flags = this.file_flags;
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file.into_response(&req);
Box::pin(ok(ServiceResponse::new(req, res))) Ok(ServiceResponse::new(req, res))
} }
Err(err) => self.handle_err(err, req), Err(err) => this.handle_err(err, req).await,
} }
} }
})
} }
} }

View File

@@ -8,7 +8,7 @@ use actix_web::{
App, App,
}; };
#[actix_rt::test] #[actix_web::test]
async fn test_utf8_file_contents() { async fn test_utf8_file_contents() {
// use default ISO-8859-1 encoding // use default ISO-8859-1 encoding
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await; let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;

View File

@@ -7,7 +7,7 @@ use actix_web::{
}; };
use bytes::Bytes; use bytes::Bytes;
#[actix_rt::test] #[actix_web::test]
async fn test_guard_filter() { async fn test_guard_filter() {
let srv = test::init_service( let srv = test::init_service(
App::new() App::new()

View File

@@ -0,0 +1,27 @@
use actix_files::Files;
use actix_web::{
http::StatusCode,
test::{self, TestRequest},
App,
};
#[actix_rt::test]
async fn test_directory_traversal_prevention() {
let srv = test::init_service(App::new().service(Files::new("/", "./tests"))).await;
let req =
TestRequest::with_uri("/../../../../../../../../../../../etc/passwd").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = TestRequest::with_uri(
"/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/%2e%2e/etc/passwd",
)
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
let req = TestRequest::with_uri("/%00/etc/passwd%00").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}

View File

@@ -3,6 +3,26 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.8 - 2021-11-30
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.7 - 2021-11-22
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
## 3.0.0-beta.6 - 2021-11-15
* `TestServer::stop` is now async and will wait for the server and system to shutdown. [#2442]
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
* Minimum supported Rust version (MSRV) is now 1.52.
[#2442]: https://github.com/actix/actix-web/pull/2442
## 3.0.0-beta.5 - 2021-09-09 ## 3.0.0-beta.5 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51. * Minimum supported Rust version (MSRV) is now 1.51.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.5" version = "3.0.0-beta.8"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Various helpers for Actix applications to use during testing" description = "Various helpers for Actix applications to use during testing"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@@ -30,26 +30,26 @@ openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-tls = "3.0.0-beta.5" actix-tls = "3.0.0-rc.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-rc.1"
awc = { version = "3.0.0-beta.8", default-features = false } awc = { version = "3.0.0-beta.11", default-features = false }
base64 = "0.13" base64 = "0.13"
bytes = "1" bytes = "1"
futures-core = { version = "0.3.7", default-features = false } futures-core = { version = "0.3.7", default-features = false }
http = "0.2.2" http = "0.2.5"
log = "0.4" log = "0.4"
socket2 = "0.4" socket2 = "0.4"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.7" serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { version = "0.10.9", package = "openssl", optional = true } tls-openssl = { version = "0.10.9", package = "openssl", optional = true }
tokio = { version = "1.2", features = ["sync"] }
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.9", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.11", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.10" actix-http = "3.0.0-beta.14"

View File

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

View File

@@ -7,42 +7,41 @@
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::sync::mpsc; use std::{net, thread, time::Duration};
use std::{net, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::{net::TcpStream, System}; use actix_rt::{net::TcpStream, System};
use actix_server::{Server, ServiceFactory}; use actix_server::{Server, ServiceFactory};
use awc::{ use awc::{
error::PayloadError, http::HeaderMap, ws, Client, ClientRequest, ClientResponse, Connector, error::PayloadError, http::header::HeaderMap, ws, Client, ClientRequest, ClientResponse,
Connector,
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::stream::Stream; use futures_core::stream::Stream;
use http::Method; use http::Method;
use socket2::{Domain, Protocol, Socket, Type}; use socket2::{Domain, Protocol, Socket, Type};
use tokio::sync::mpsc;
/// Start test server /// Start test server.
/// ///
/// `TestServer` is very simple test server that simplify process of writing /// `TestServer` is very simple test server that simplify process of writing integration tests cases
/// integration tests cases for actix web applications. /// for HTTP applications.
/// ///
/// # Examples /// # Examples
/// /// ```no_run
/// ```
/// use actix_http::HttpService; /// use actix_http::HttpService;
/// use actix_http_test::TestServer; /// use actix_http_test::test_server;
/// use actix_web::{web, App, HttpResponse, Error}; /// use actix_web::{web, App, HttpResponse, Error};
/// ///
/// async fn my_handler() -> Result<HttpResponse, Error> { /// async fn my_handler() -> Result<HttpResponse, Error> {
/// Ok(HttpResponse::Ok().into()) /// Ok(HttpResponse::Ok().into())
/// } /// }
/// ///
/// #[actix_rt::test] /// #[actix_web::test]
/// async fn test_example() { /// async fn test_example() {
/// let mut srv = TestServer::start( /// let mut srv = TestServer::start(||
/// || HttpService::new( /// HttpService::new(
/// App::new().service( /// App::new().service(web::resource("/").to(my_handler))
/// web::resource("/").to(my_handler))
/// ) /// )
/// ); /// );
/// ///
@@ -56,72 +55,86 @@ pub async fn test_server<F: ServiceFactory<TcpStream>>(factory: F) -> TestServer
test_server_with_addr(tcp, factory).await test_server_with_addr(tcp, factory).await
} }
/// Start [`test server`](test_server()) on a concrete Address /// Start [`test server`](test_server()) on an existing address binding.
pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>( pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
tcp: net::TcpListener, tcp: net::TcpListener,
factory: F, factory: F,
) -> TestServer { ) -> TestServer {
let (tx, rx) = mpsc::channel(); let (started_tx, started_rx) = std::sync::mpsc::channel();
let (thread_stop_tx, thread_stop_rx) = mpsc::channel(1);
// run server in separate thread // run server in separate thread
thread::spawn(move || { thread::spawn(move || {
let sys = System::new(); System::new().block_on(async move {
let local_addr = tcp.local_addr().unwrap(); let local_addr = tcp.local_addr().unwrap();
let srv = Server::build() let srv = Server::build()
.listen("test", tcp, factory)?
.workers(1) .workers(1)
.disable_signals(); .disable_signals()
.system_exit()
.listen("test", tcp, factory)
.expect("test server could not be created");
sys.block_on(async { let srv = srv.run();
srv.run(); started_tx
tx.send((System::current(), local_addr)).unwrap(); .send((System::current(), srv.handle(), local_addr))
.unwrap();
// drive server loop
srv.await.unwrap();
}); });
sys.run() // notify TestServer that server and system have shut down
// all thread managed resources should be dropped at this point
let _ = thread_stop_tx.send(());
}); });
let (system, addr) = rx.recv().unwrap(); let (system, server, addr) = started_rx.recv().unwrap();
let client = { let client = {
let connector = {
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
{ let connector = {
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode}; use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap(); let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE); builder.set_verify(SslVerifyMode::NONE);
let _ = builder let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
.ssl(builder.build()) .ssl(builder.build())
} };
#[cfg(not(feature = "openssl"))] #[cfg(not(feature = "openssl"))]
{ let connector = {
Connector::new() Connector::new()
.conn_lifetime(time::Duration::from_secs(0)) .conn_lifetime(Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000)) .timeout(Duration::from_millis(30000))
}
}; };
Client::builder().connector(connector).finish() Client::builder().connector(connector).finish()
}; };
TestServer { TestServer {
addr, server,
client, client,
system, system,
addr,
thread_stop_rx,
} }
} }
/// Test server controller /// Test server controller
pub struct TestServer { pub struct TestServer {
server: actix_server::ServerHandle,
client: awc::Client,
system: actix_rt::System,
addr: net::SocketAddr, addr: net::SocketAddr,
client: Client, thread_stop_rx: mpsc::Receiver<()>,
system: System,
} }
impl TestServer { impl TestServer {
@@ -258,15 +271,32 @@ impl TestServer {
self.client.headers() self.client.headers()
} }
/// Stop HTTP server /// Stop HTTP server.
fn stop(&mut self) { ///
/// Waits for spawned `Server` and `System` to (force) shutdown.
pub async fn stop(&mut self) {
// signal server to stop
self.server.stop(false).await;
// also signal system to stop
// though this is handled by `ServerBuilder::exit_system` too
self.system.stop(); self.system.stop();
// wait for thread to be stopped but don't care about result
let _ = self.thread_stop_rx.recv().await;
} }
} }
impl Drop for TestServer { impl Drop for TestServer {
fn drop(&mut self) { fn drop(&mut self) {
self.stop() // calls in this Drop impl should be enough to shut down the server, system, and thread
// without needing to await anything
// signal server to stop
let _ = self.server.stop(true);
// signal system to stop
self.system.stop();
} }
} }

View File

@@ -1,6 +1,105 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
### Added
* Add timeout for canceling HTTP/2 server side connection handshake. Default to 5 seconds. [#2483]
* HTTP/2 handshake timeout can be configured with `ServiceConfig::client_timeout`. [#2483]
* `Response::map_into_boxed_body`. [#2468]
* `body::EitherBody` enum. [#2468]
* `body::None` struct. [#2468]
* Impl `MessageBody` for `bytestring::ByteString`. [#2468]
* `impl Clone for ws::HandshakeError`. [#2468]
* `#[must_use]` for `ws::Codec` to prevent subtle bugs. [#1920]
* `impl Default ` for `ws::Codec`. [#1920]
* `header::QualityItem::{max, min}`. [#2486]
* `header::Quality::{MAX, MIN}`. [#2486]
* `impl Display` for `header::Quality`. [#2486]
* `CloneableExtensions` object for use in `on_connect` handlers. [#2327]
### Changed
* Rename `body::BoxBody::{from_body => new}`. [#2468]
* Body type for `Responses` returned from `Response::{new, ok, etc...}` is now `BoxBody`. [#2468]
* The `Error` associated type on `MessageBody` type now requires `impl Error` (or similar). [#2468]
* Error types using in service builders now require `Into<Response<BoxBody>>`. [#2468]
* `From` implementations on error types now return a `Response<BoxBody>`. [#2468]
* `ResponseBuilder::body(B)` now returns `Response<EitherBody<B>>`. [#2468]
* `ResponseBuilder::finish()` now returns `Response<EitherBody<()>>`. [#2468]
* `on_connect_ext` methods now receive a `CloneableExtensions` object. [#2327]
### Removed
* `ResponseBuilder::streaming`. [#2468]
* `impl Future` for `ResponseBuilder`. [#2468]
* Remove unnecessary `MessageBody` bound on types passed to `body::AnyBody::new`. [#2468]
* Move `body::AnyBody` to `awc`. Replaced with `EitherBody` and `BoxBody`. [#2468]
* `impl Copy` for `ws::Codec`. [#1920]
* `header::qitem` helper. Replaced with `header::QualityItem::max`. [#2486]
* `impl TryFrom<u16>` for `header::Quality`. [#2486]
* `http` module. Most everything it contained is exported at the crate root. [#2488]
[#2327]: https://github.com/actix/actix-web/pull/2327
[#2483]: https://github.com/actix/actix-web/pull/2483
[#2468]: https://github.com/actix/actix-web/pull/2468
[#1920]: https://github.com/actix/actix-web/pull/1920
[#2486]: https://github.com/actix/actix-web/pull/2486
[#2488]: https://github.com/actix/actix-web/pull/2488
## 3.0.0-beta.14 - 2021-11-30
### Changed
* Guarantee ordering of `header::GetAll` iterator to be same as insertion order. [#2467]
* Expose `header::map` module. [#2467]
* Implement `ExactSizeIterator` and `FusedIterator` for all `HeaderMap` iterators. [#2470]
* Update `actix-tls` to `3.0.0-rc.1`. [#2474]
[#2467]: https://github.com/actix/actix-web/pull/2467
[#2470]: https://github.com/actix/actix-web/pull/2470
[#2474]: https://github.com/actix/actix-web/pull/2474
## 3.0.0-beta.13 - 2021-11-22
### Added
* `body::AnyBody::empty` for quickly creating an empty body. [#2446]
* `body::AnyBody::none` for quickly creating a "none" body. [#2456]
* `impl Clone` for `body::AnyBody<S> where S: Clone`. [#2448]
* `body::AnyBody::into_boxed` for quickly converting to a type-erased, boxed body type. [#2448]
### Changed
* Rename `body::AnyBody::{Message => Body}`. [#2446]
* Rename `body::AnyBody::{from_message => new_boxed}`. [#2448]
* Rename `body::AnyBody::{from_slice => copy_from_slice}`. [#2448]
* Rename `body::{BoxAnyBody => BoxBody}`. [#2448]
* Change representation of `AnyBody` to include a type parameter in `Body` variant. Defaults to `BoxBody`. [#2448]
* `Encoder::response` now returns `AnyBody<Encoder<B>>`. [#2448]
### Removed
* `body::AnyBody::Empty`; an empty body can now only be represented as a zero-length `Bytes` variant. [#2446]
* `body::BodySize::Empty`; an empty body can now only be represented as a `Sized(0)` variant. [#2446]
* `EncoderError::Boxed`; it is no longer required. [#2446]
* `body::ResponseBody`; is function is replaced by the new `body::AnyBody` enum. [#2446]
[#2446]: https://github.com/actix/actix-web/pull/2446
[#2448]: https://github.com/actix/actix-web/pull/2448
[#2456]: https://github.com/actix/actix-web/pull/2456
## 3.0.0-beta.12 - 2021-11-15
### Changed
* Update `actix-server` to `2.0.0-beta.9`. [#2442]
### Removed
* `client` module. [#2425]
* `trust-dns` feature. [#2425]
[#2425]: https://github.com/actix/actix-web/pull/2425
[#2442]: https://github.com/actix/actix-web/pull/2442
## 3.0.0-beta.11 - 2021-10-20
### Changed
* Updated rustls to v0.20. [#2414]
* Minimum supported Rust version (MSRV) is now 1.52.
[#2414]: https://github.com/actix/actix-web/pull/2414
## 3.0.0-beta.10 - 2021-09-09 ## 3.0.0-beta.10 - 2021-09-09

View File

@@ -1,14 +1,17 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.10" version = "3.0.0-beta.14"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
keywords = ["actix", "http", "framework", "async", "futures"] keywords = ["actix", "http", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web" repository = "https://github.com/actix/actix-web.git"
categories = ["network-programming", "asynchronous", categories = [
"network-programming",
"asynchronous",
"web-programming::http-server", "web-programming::http-server",
"web-programming::websocket"] "web-programming::websocket",
]
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
@@ -24,29 +27,25 @@ path = "src/lib.rs"
default = [] default = []
# openssl # openssl
openssl = ["actix-tls/openssl"] openssl = ["actix-tls/accept", "actix-tls/openssl"]
# rustls support # rustls support
rustls = ["actix-tls/rustls"] rustls = ["actix-tls/accept", "actix-tls/rustls"]
# enable compression support # enable compression support
compress-brotli = ["brotli2", "__compress"] compress-brotli = ["brotli2", "__compress"]
compress-gzip = ["flate2", "__compress"] compress-gzip = ["flate2", "__compress"]
compress-zstd = ["zstd", "__compress"] compress-zstd = ["zstd", "__compress"]
# trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"]
# Internal (PRIVATE!) features used to aid testing and cheking feature status. # Internal (PRIVATE!) features used to aid testing and cheking feature status.
# Don't rely on these whatsoever. They may disappear at anytime. # Don't rely on these whatsoever. They may disappear at anytime.
__compress = [] __compress = []
[dependencies] [dependencies]
actix-service = "2.0.0" actix-service = "2.0.0"
actix-codec = "0.4.0" actix-codec = "0.4.1"
actix-utils = "3.0.0" actix-utils = "3.0.0"
actix-rt = "2.2" actix-rt = "2.2"
actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
ahash = "0.7" ahash = "0.7"
base64 = "0.13" base64 = "0.13"
@@ -58,45 +57,45 @@ encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc", "sink"] }
h2 = "0.3.1" h2 = "0.3.1"
http = "0.2.2" http = "0.2.5"
httparse = "1.5.1" httparse = "1.5.1"
httpdate = "1.0.1"
itoa = "0.4" itoa = "0.4"
language-tags = "0.3" language-tags = "0.3"
local-channel = "0.1" local-channel = "0.1"
once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2" pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
regex = "1.3"
serde = "1.0"
sha-1 = "0.9" sha-1 = "0.9"
smallvec = "1.6.1" smallvec = "1.6.1"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tokio = { version = "1.2", features = ["sync"] } # tls
actix-tls = { version = "3.0.0-rc.1", default-features = false, optional = true }
# compression # compression
brotli2 = { version="0.3.2", optional = true } brotli2 = { version="0.3.2", optional = true }
flate2 = { version = "1.0.13", optional = true } flate2 = { version = "1.0.13", optional = true }
zstd = { version = "0.7", optional = true } zstd = { version = "0.9", optional = true }
trust-dns-resolver = { version = "0.20.0", optional = true }
[dev-dependencies] [dev-dependencies]
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-rc.1"
actix-http-test = { version = "3.0.0-beta.5", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.7", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] } actix-tls = { version = "3.0.0-rc.1", features = ["openssl"] }
async-stream = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.9"
rcgen = "0.8" rcgen = "0.8"
regex = "1.3"
rustls-pemfile = "0.2"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } static_assertions = "1"
tls-rustls = { version = "0.19", package = "rustls" } tls-openssl = { package = "openssl", version = "0.10.9" }
webpki = { version = "0.21.0" } tls-rustls = { package = "rustls", version = "0.20.0" }
tokio = { version = "1.2", features = ["net", "rt"] }
[[example]] [[example]]
name = "ws" name = "ws"
@@ -113,3 +112,7 @@ harness = false
[[bench]] [[bench]]
name = "uninit-headers" name = "uninit-headers"
harness = false harness = false
[[bench]]
name = "quality-value"
harness = false

View File

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

View File

@@ -0,0 +1,90 @@
use criterion::{criterion_group, criterion_main, BenchmarkId, Criterion};
const CODES: &[u16] = &[0, 1000, 201, 800, 550];
fn bench_quality_display_impls(c: &mut Criterion) {
let mut group = c.benchmark_group("quality value display impls");
for i in CODES.iter() {
group.bench_with_input(BenchmarkId::new("New (fast?)", i), i, |b, &i| {
b.iter(|| _new::Quality(i).to_string())
});
group.bench_with_input(BenchmarkId::new("Naive", i), i, |b, &i| {
b.iter(|| _naive::Quality(i).to_string())
});
}
group.finish();
}
criterion_group!(benches, bench_quality_display_impls);
criterion_main!(benches);
mod _new {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
// some number in the range 1999
x => {
f.write_str("0.")?;
// this implementation avoids string allocation otherwise required
// for `.trim_end_matches('0')`
if x < 10 {
f.write_str("00")?;
// 0 is handled so it's not possible to have a trailing 0, we can just return
itoa::fmt(f, x)
} else if x < 100 {
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
} else {
// x is in range 101999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
}
}
}
}
}
}
mod _naive {
use std::fmt;
pub struct Quality(pub(crate) u16);
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
1000 => f.write_str("1"),
x => {
write!(f, "{}", format!("{:03}", x).trim_end_matches('0'))
}
}
}
}
}

View File

@@ -1,6 +1,6 @@
use std::io; use std::io;
use actix_http::{http::StatusCode, Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response, StatusCode};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt as _; use futures_util::StreamExt as _;

View File

@@ -1,12 +1,14 @@
use std::io; use std::io;
use actix_http::{body::Body, http::HeaderValue, http::StatusCode}; use actix_http::{
use actix_http::{Error, HttpService, Request, Response}; body::MessageBody, header::HeaderValue, Error, HttpService, Request, Response,
StatusCode,
};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt as _; use futures_util::StreamExt as _;
async fn handle_request(mut req: Request) -> Result<Response<Body>, Error> { async fn handle_request(mut req: Request) -> Result<Response<impl MessageBody>, Error> {
let mut body = BytesMut::new(); let mut body = BytesMut::new();
while let Some(item) = req.payload().next().await { while let Some(item) = req.payload().next().await {
body.extend_from_slice(&item?) body.extend_from_slice(&item?)

View File

@@ -1,6 +1,6 @@
use std::{convert::Infallible, io}; use std::{convert::Infallible, io};
use actix_http::{http::StatusCode, HttpService, Response}; use actix_http::{HttpService, Response, StatusCode};
use actix_server::Server; use actix_server::Server;
use http::header::HeaderValue; use http::header::HeaderValue;

View File

@@ -85,22 +85,31 @@ impl Stream for Heartbeat {
fn tls_config() -> rustls::ServerConfig { fn tls_config() -> rustls::ServerConfig {
use std::io::BufReader; use std::io::BufReader;
use rustls::{ use rustls::{Certificate, PrivateKey};
internal::pemfile::{certs, pkcs8_private_keys}, use rustls_pemfile::{certs, pkcs8_private_keys};
NoClientAuth, ServerConfig,
};
let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap(); let cert = rcgen::generate_simple_self_signed(vec!["localhost".to_owned()]).unwrap();
let cert_file = cert.serialize_pem().unwrap(); let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_pem(); 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 cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).unwrap(); let cert_chain = certs(cert_file)
.unwrap()
.into_iter()
.map(Certificate)
.collect();
let mut keys = pkcs8_private_keys(key_file).unwrap(); let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
let mut config = rustls::ServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
.unwrap();
config.alpn_protocols.push(b"http/1.1".to_vec());
config.alpn_protocols.push(b"h2".to_vec());
config config
} }

View File

@@ -1,227 +0,0 @@
use std::{
borrow::Cow,
error::Error as StdError,
fmt, mem,
pin::Pin,
task::{Context, Poll},
};
use bytes::{Bytes, BytesMut};
use futures_core::Stream;
use crate::error::Error;
use super::{BodySize, BodyStream, MessageBody, MessageBodyMapErr, SizedStream};
pub type Body = AnyBody;
/// Represents various types of HTTP message body.
pub enum AnyBody {
/// Empty response. `Content-Length` header is not set.
None,
/// Zero sized response body. `Content-Length` header is set to `0`.
Empty,
/// Specific response body.
Bytes(Bytes),
/// Generic message body.
Message(BoxAnyBody),
}
impl AnyBody {
/// Create body from slice (copy)
pub fn from_slice(s: &[u8]) -> Self {
Self::Bytes(Bytes::copy_from_slice(s))
}
/// Create body from generic message body.
pub fn from_message<B>(body: B) -> Self
where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
Self::Message(BoxAnyBody::from_body(body))
}
}
impl MessageBody for AnyBody {
type Error = Error;
fn size(&self) -> BodySize {
match self {
AnyBody::None => BodySize::None,
AnyBody::Empty => BodySize::Empty,
AnyBody::Bytes(ref bin) => BodySize::Sized(bin.len() as u64),
AnyBody::Message(ref body) => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.get_mut() {
AnyBody::None => Poll::Ready(None),
AnyBody::Empty => Poll::Ready(None),
AnyBody::Bytes(ref mut bin) => {
let len = bin.len();
if len == 0 {
Poll::Ready(None)
} else {
Poll::Ready(Some(Ok(mem::take(bin))))
}
}
AnyBody::Message(body) => body
.as_pin_mut()
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err)),
}
}
}
impl PartialEq for AnyBody {
fn eq(&self, other: &Body) -> bool {
match *self {
AnyBody::None => matches!(*other, AnyBody::None),
AnyBody::Empty => matches!(*other, AnyBody::Empty),
AnyBody::Bytes(ref b) => match *other {
AnyBody::Bytes(ref b2) => b == b2,
_ => false,
},
AnyBody::Message(_) => false,
}
}
}
impl fmt::Debug for AnyBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
AnyBody::None => write!(f, "AnyBody::None"),
AnyBody::Empty => write!(f, "AnyBody::Empty"),
AnyBody::Bytes(ref b) => write!(f, "AnyBody::Bytes({:?})", b),
AnyBody::Message(_) => write!(f, "AnyBody::Message(_)"),
}
}
}
impl From<&'static str> for AnyBody {
fn from(s: &'static str) -> Body {
AnyBody::Bytes(Bytes::from_static(s.as_ref()))
}
}
impl From<&'static [u8]> for AnyBody {
fn from(s: &'static [u8]) -> Body {
AnyBody::Bytes(Bytes::from_static(s))
}
}
impl From<Vec<u8>> for AnyBody {
fn from(vec: Vec<u8>) -> Body {
AnyBody::Bytes(Bytes::from(vec))
}
}
impl From<String> for AnyBody {
fn from(s: String) -> Body {
s.into_bytes().into()
}
}
impl From<&'_ String> for AnyBody {
fn from(s: &String) -> Body {
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(&s)))
}
}
impl From<Cow<'_, str>> for AnyBody {
fn from(s: Cow<'_, str>) -> Body {
match s {
Cow::Owned(s) => AnyBody::from(s),
Cow::Borrowed(s) => {
AnyBody::Bytes(Bytes::copy_from_slice(AsRef::<[u8]>::as_ref(s)))
}
}
}
}
impl From<Bytes> for AnyBody {
fn from(s: Bytes) -> Body {
AnyBody::Bytes(s)
}
}
impl From<BytesMut> for AnyBody {
fn from(s: BytesMut) -> Body {
AnyBody::Bytes(s.freeze())
}
}
impl<S, E> From<SizedStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(s: SizedStream<S>) -> Body {
AnyBody::from_message(s)
}
}
impl<S, E> From<BodyStream<S>> for AnyBody
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
fn from(s: BodyStream<S>) -> Body {
AnyBody::from_message(s)
}
}
/// A boxed message body with boxed errors.
pub struct BoxAnyBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError + 'static>>>>);
impl BoxAnyBody {
/// Boxes a `MessageBody` and any errors it generates.
pub fn from_body<B>(body: B) -> Self
where
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError + 'static>>,
{
let body = MessageBodyMapErr::new(body, Into::into);
Self(Box::pin(body))
}
/// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut(
&mut self,
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError + 'static>>)> {
self.0.as_mut()
}
}
impl fmt::Debug for BoxAnyBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxAnyBody(dyn MessageBody)")
}
}
impl MessageBody for BoxAnyBody {
type Error = Error;
fn size(&self) -> BodySize {
self.0.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.0
.as_mut()
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err))
}
}

View File

@@ -20,6 +20,8 @@ pin_project! {
} }
} }
// TODO: from_infallible method
impl<S, E> BodyStream<S> impl<S, E> BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
@@ -75,10 +77,23 @@ mod tests {
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_core::ready; use futures_core::ready;
use futures_util::{stream, FutureExt as _}; use futures_util::{stream, FutureExt as _};
use pin_project_lite::pin_project;
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*; use super::*;
use crate::body::to_bytes; use crate::body::to_bytes;
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
assert_impl_all!(BodyStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
assert_not_impl_all!(BodyStream<stream::Empty<Bytes>>: MessageBody);
assert_not_impl_all!(BodyStream<stream::Repeat<Bytes>>: MessageBody);
// crate::Error is not Clone
assert_not_impl_all!(BodyStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
#[actix_rt::test] #[actix_rt::test]
async fn skips_empty_chunks() { async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter( let body = BodyStream::new(stream::iter(
@@ -124,19 +139,45 @@ mod tests {
assert!(matches!(to_bytes(body).await, Err(StreamErr))); assert!(matches!(to_bytes(body).await, Err(StreamErr)));
} }
#[actix_rt::test]
async fn stream_string_error() {
// `&'static str` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = BodyStream::new(stream::once(async { Err("stringy error") }));
assert!(matches!(to_bytes(body).await, Err("stringy error")));
}
#[actix_rt::test]
async fn stream_boxed_error() {
// `Box<dyn Error>` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = BodyStream::new(stream::once(async {
Err(Box::<dyn StdError>::from("stringy error"))
}));
assert_eq!(
to_bytes(body).await.unwrap_err().to_string(),
"stringy error"
);
}
#[actix_rt::test] #[actix_rt::test]
async fn stream_delayed_error() { async fn stream_delayed_error() {
let body = let body =
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)])); BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
assert!(matches!(to_bytes(body).await, Err(StreamErr))); assert!(matches!(to_bytes(body).await, Err(StreamErr)));
#[pin_project::pin_project(project = TimeDelayStreamProj)] pin_project! {
#[derive(Debug)] #[derive(Debug)]
#[project = TimeDelayStreamProj]
enum TimeDelayStream { enum TimeDelayStream {
Start, Start,
Sleep(Pin<Box<Sleep>>), Sleep { delay: Pin<Box<Sleep>> },
Done, Done,
} }
}
impl Stream for TimeDelayStream { impl Stream for TimeDelayStream {
type Item = Result<Bytes, StreamErr>; type Item = Result<Bytes, StreamErr>;
@@ -148,12 +189,14 @@ mod tests {
match self.as_mut().get_mut() { match self.as_mut().get_mut() {
TimeDelayStream::Start => { TimeDelayStream::Start => {
let sleep = sleep(Duration::from_millis(1)); let sleep = sleep(Duration::from_millis(1));
self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep))); self.as_mut().set(TimeDelayStream::Sleep {
delay: Box::pin(sleep),
});
cx.waker().wake_by_ref(); cx.waker().wake_by_ref();
Poll::Pending Poll::Pending
} }
TimeDelayStream::Sleep(ref mut delay) => { TimeDelayStream::Sleep { ref mut delay } => {
ready!(delay.poll_unpin(cx)); ready!(delay.poll_unpin(cx));
self.set(TimeDelayStream::Done); self.set(TimeDelayStream::Done);
cx.waker().wake_by_ref(); cx.waker().wake_by_ref();

View File

@@ -0,0 +1,80 @@
use std::{
error::Error as StdError,
fmt,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use super::{BodySize, MessageBody, MessageBodyMapErr};
use crate::Error;
/// A boxed message body with boxed errors.
pub struct BoxBody(Pin<Box<dyn MessageBody<Error = Box<dyn StdError>>>>);
impl BoxBody {
/// Boxes a `MessageBody` and any errors it generates.
pub fn new<B>(body: B) -> Self
where
B: MessageBody + 'static,
{
let body = MessageBodyMapErr::new(body, Into::into);
Self(Box::pin(body))
}
/// Returns a mutable pinned reference to the inner message body type.
pub fn as_pin_mut(
&mut self,
) -> Pin<&mut (dyn MessageBody<Error = Box<dyn StdError>>)> {
self.0.as_mut()
}
}
impl fmt::Debug for BoxBody {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("BoxBody(dyn MessageBody)")
}
}
impl MessageBody for BoxBody {
type Error = Error;
fn size(&self) -> BodySize {
self.0.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.0
.as_mut()
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err))
}
}
#[cfg(test)]
mod tests {
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*;
use crate::body::to_bytes;
assert_impl_all!(BoxBody: MessageBody, fmt::Debug, Unpin);
assert_not_impl_all!(BoxBody: Send, Sync, Unpin);
#[actix_rt::test]
async fn nested_boxed_body() {
let body = Bytes::from_static(&[1, 2, 3]);
let boxed_body = BoxBody::new(BoxBody::new(body));
assert_eq!(
to_bytes(boxed_body).await.unwrap(),
Bytes::from(vec![1, 2, 3]),
);
}
}

View File

@@ -0,0 +1,83 @@
use std::{
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use pin_project_lite::pin_project;
use super::{BodySize, BoxBody, MessageBody};
use crate::Error;
pin_project! {
#[project = EitherBodyProj]
#[derive(Debug, Clone)]
pub enum EitherBody<L, R = BoxBody> {
/// A body of type `L`.
Left { #[pin] body: L },
/// A body of type `R`.
Right { #[pin] body: R },
}
}
impl<L> EitherBody<L, BoxBody> {
/// Creates new `EitherBody` using left variant and boxed right variant.
pub fn new(body: L) -> Self {
Self::Left { body }
}
}
impl<L, R> EitherBody<L, R> {
/// Creates new `EitherBody` using left variant.
pub fn left(body: L) -> Self {
Self::Left { body }
}
/// Creates new `EitherBody` using right variant.
pub fn right(body: R) -> Self {
Self::Right { body }
}
}
impl<L, R> MessageBody for EitherBody<L, R>
where
L: MessageBody + 'static,
R: MessageBody + 'static,
{
type Error = Error;
fn size(&self) -> BodySize {
match self {
EitherBody::Left { body } => body.size(),
EitherBody::Right { body } => body.size(),
}
}
fn poll_next(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() {
EitherBodyProj::Left { body } => body
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err)),
EitherBodyProj::Right { body } => body
.poll_next(cx)
.map_err(|err| Error::new_body().with_cause(err)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn type_parameter_inference() {
let _body: EitherBody<(), _> = EitherBody::new(());
let _body: EitherBody<_, ()> = EitherBody::left(());
let _body: EitherBody<(), _> = EitherBody::right(());
}
}

View File

@@ -2,6 +2,7 @@
use std::{ use std::{
convert::Infallible, convert::Infallible,
error::Error as StdError,
mem, mem,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
@@ -13,9 +14,12 @@ use pin_project_lite::pin_project;
use super::BodySize; use super::BodySize;
/// An interface for response bodies. /// An interface types that can converted to bytes and used as response bodies.
// TODO: examples
pub trait MessageBody { pub trait MessageBody {
type Error; // TODO: consider this bound to only fmt::Display since the error type is not really used
// and there is an impl for Into<Box<StdError>> on String
type Error: Into<Box<dyn StdError>>;
/// Body size hint. /// Body size hint.
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
@@ -27,58 +31,84 @@ pub trait MessageBody {
) -> Poll<Option<Result<Bytes, Self::Error>>>; ) -> Poll<Option<Result<Bytes, Self::Error>>>;
} }
impl MessageBody for () { mod foreign_impls {
use super::*;
impl MessageBody for Infallible {
type Error = Infallible; type Error = Infallible;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Empty match *self {}
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
match *self {}
}
}
impl MessageBody for () {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::Sized(0)
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(None) Poll::Ready(None)
} }
} }
impl<B> MessageBody for Box<B> impl<B> MessageBody for Box<B>
where where
B: MessageBody + Unpin, B: MessageBody + Unpin,
{ {
type Error = B::Error; type Error = B::Error;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
#[inline]
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Pin::new(self.get_mut().as_mut()).poll_next(cx) Pin::new(self.get_mut().as_mut()).poll_next(cx)
} }
} }
impl<B> MessageBody for Pin<Box<B>> impl<B> MessageBody for Pin<Box<B>>
where where
B: MessageBody, B: MessageBody,
{ {
type Error = B::Error; type Error = B::Error;
#[inline]
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
#[inline]
fn poll_next( fn poll_next(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.as_mut().poll_next(cx) self.as_mut().poll_next(cx)
} }
} }
impl MessageBody for Bytes { impl MessageBody for &'static [u8] {
type Error = Infallible; type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@@ -87,17 +117,19 @@ impl MessageBody for Bytes {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(mem::take(self.get_mut())))) let bytes = mem::take(self.get_mut());
let bytes = Bytes::from_static(bytes);
Poll::Ready(Some(Ok(bytes)))
}
} }
} }
}
impl MessageBody for BytesMut { impl MessageBody for Bytes {
type Error = Infallible; type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@@ -106,17 +138,18 @@ impl MessageBody for BytesMut {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(mem::take(self.get_mut()).freeze()))) let bytes = mem::take(self.get_mut());
Poll::Ready(Some(Ok(bytes)))
}
} }
} }
}
impl MessageBody for &'static str { impl MessageBody for BytesMut {
type Error = Infallible; type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@@ -125,19 +158,18 @@ impl MessageBody for &'static str {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(Bytes::from_static( let bytes = mem::take(self.get_mut()).freeze();
mem::take(self.get_mut()).as_ref(), Poll::Ready(Some(Ok(bytes)))
)))) }
} }
} }
}
impl MessageBody for Vec<u8> { impl MessageBody for Vec<u8> {
type Error = Infallible; type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@@ -146,17 +178,18 @@ impl MessageBody for Vec<u8> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(Bytes::from(mem::take(self.get_mut()))))) let bytes = mem::take(self.get_mut());
Poll::Ready(Some(Ok(Bytes::from(bytes))))
}
} }
} }
}
impl MessageBody for String { impl MessageBody for &'static str {
type Error = Infallible; type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
@@ -165,14 +198,51 @@ impl MessageBody for String {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
Poll::Ready(Some(Ok(Bytes::from( let string = mem::take(self.get_mut());
mem::take(self.get_mut()).into_bytes(), let bytes = Bytes::from_static(string.as_bytes());
)))) Poll::Ready(Some(Ok(bytes)))
}
}
}
impl MessageBody for String {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() {
Poll::Ready(None)
} else {
let string = mem::take(self.get_mut());
Poll::Ready(Some(Ok(Bytes::from(string))))
}
}
}
impl MessageBody for bytestring::ByteString {
type Error = Infallible;
fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64)
}
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let string = mem::take(self.get_mut());
Poll::Ready(Some(Ok(string.into_bytes())))
} }
} }
} }
@@ -202,6 +272,7 @@ impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
where where
B: MessageBody, B: MessageBody,
F: FnOnce(B::Error) -> E, F: FnOnce(B::Error) -> E,
E: Into<Box<dyn StdError>>,
{ {
type Error = E; type Error = E;
@@ -226,3 +297,129 @@ where
} }
} }
} }
#[cfg(test)]
mod tests {
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use super::*;
macro_rules! assert_poll_next {
($pin:expr, $exp:expr) => {
assert_eq!(
poll_fn(|cx| $pin.as_mut().poll_next(cx))
.await
.unwrap() // unwrap option
.unwrap(), // unwrap result
$exp
);
};
}
macro_rules! assert_poll_next_none {
($pin:expr) => {
assert!(poll_fn(|cx| $pin.as_mut().poll_next(cx)).await.is_none());
};
}
#[actix_rt::test]
async fn boxing_equivalence() {
assert_eq!(().size(), BodySize::Sized(0));
assert_eq!(().size(), Box::new(()).size());
assert_eq!(().size(), Box::pin(()).size());
let pl = Box::new(());
pin!(pl);
assert_poll_next_none!(pl);
let mut pl = Box::pin(());
assert_poll_next_none!(pl);
}
#[actix_rt::test]
async fn test_unit() {
let pl = ();
assert_eq!(pl.size(), BodySize::Sized(0));
pin!(pl);
assert_poll_next_none!(pl);
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!("".size(), BodySize::Sized(0));
assert_eq!("test".size(), BodySize::Sized(4));
let pl = "test";
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(b"".as_ref().size(), BodySize::Sized(0));
assert_eq!(b"test".as_ref().size(), BodySize::Sized(4));
let pl = b"test".as_ref();
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(vec![0; 0].size(), BodySize::Sized(0));
assert_eq!(Vec::from("test").size(), BodySize::Sized(4));
let pl = Vec::from("test");
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_bytes() {
assert_eq!(Bytes::new().size(), BodySize::Sized(0));
assert_eq!(Bytes::from_static(b"test").size(), BodySize::Sized(4));
let pl = Bytes::from_static(b"test");
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_bytes_mut() {
assert_eq!(BytesMut::new().size(), BodySize::Sized(0));
assert_eq!(BytesMut::from(b"test".as_ref()).size(), BodySize::Sized(4));
let pl = BytesMut::from("test");
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
#[actix_rt::test]
async fn test_string() {
assert_eq!(String::new().size(), BodySize::Sized(0));
assert_eq!("test".to_owned().size(), BodySize::Sized(4));
let pl = "test".to_owned();
pin!(pl);
assert_poll_next!(pl, Bytes::from("test"));
}
// down-casting used to be done with a method on MessageBody trait
// test is kept to demonstrate equivalence of Any trait
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
let resp_body: &mut dyn std::any::Any = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
}

View File

@@ -1,263 +1,20 @@
//! Traits and structures to aid consuming and writing HTTP payloads. //! Traits and structures to aid consuming and writing HTTP payloads.
use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
#[allow(clippy::module_inception)]
mod body;
mod body_stream; mod body_stream;
mod boxed;
mod either;
mod message_body; mod message_body;
mod response_body; mod none;
mod size; mod size;
mod sized_stream; mod sized_stream;
mod utils;
pub use self::body::{AnyBody, Body, BoxAnyBody};
pub use self::body_stream::BodyStream; pub use self::body_stream::BodyStream;
pub use self::boxed::BoxBody;
pub use self::either::EitherBody;
pub use self::message_body::MessageBody; pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr; pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::response_body::ResponseBody; pub use self::none::None;
pub use self::size::BodySize; pub use self::size::BodySize;
pub use self::sized_stream::SizedStream; pub use self::sized_stream::SizedStream;
pub use self::utils::to_bytes;
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{Body, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = Body::Empty;
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Body::Bytes(Bytes::from_static(b"123"));
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)]
mod tests {
use std::pin::Pin;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use super::*;
impl Body {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
Body::Bytes(ref bin) => bin,
_ => panic!(),
}
}
}
#[actix_rt::test]
async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0));
assert_eq!(Body::from("test").size(), BodySize::Sized(4));
assert_eq!(Body::from("test").get_ref(), b"test");
assert_eq!("test".size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| Pin::new(&mut "test").poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_static_bytes() {
assert_eq!(Body::from(b"test".as_ref()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b"test".as_ref()).get_ref(), b"test");
assert_eq!(
Body::from_slice(b"test".as_ref()).size(),
BodySize::Sized(4)
);
assert_eq!(Body::from_slice(b"test".as_ref()).get_ref(), b"test");
let sb = Bytes::from(&b"test"[..]);
pin!(sb);
assert_eq!(sb.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| sb.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_vec() {
assert_eq!(Body::from(Vec::from("test")).size(), BodySize::Sized(4));
assert_eq!(Body::from(Vec::from("test")).get_ref(), b"test");
let test_vec = Vec::from("test");
pin!(test_vec);
assert_eq!(test_vec.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| test_vec.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes() {
let b = Bytes::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_bytes_mut() {
let b = BytesMut::from("test");
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_string() {
let b = "test".to_owned();
assert_eq!(Body::from(b.clone()).size(), BodySize::Sized(4));
assert_eq!(Body::from(b.clone()).get_ref(), b"test");
assert_eq!(Body::from(&b).size(), BodySize::Sized(4));
assert_eq!(Body::from(&b).get_ref(), b"test");
pin!(b);
assert_eq!(b.size(), BodySize::Sized(4));
assert_eq!(
poll_fn(|cx| b.as_mut().poll_next(cx)).await.unwrap().ok(),
Some(Bytes::from("test"))
);
}
#[actix_rt::test]
async fn test_unit() {
assert_eq!(().size(), BodySize::Empty);
assert!(poll_fn(|cx| Pin::new(&mut ()).poll_next(cx))
.await
.is_none());
}
#[actix_rt::test]
async fn test_box_and_pin() {
let val = Box::new(());
pin!(val);
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
let mut val = Box::pin(());
assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none());
}
#[actix_rt::test]
async fn test_body_eq() {
assert!(
Body::Bytes(Bytes::from_static(b"1"))
== Body::Bytes(Bytes::from_static(b"1"))
);
assert!(Body::Bytes(Bytes::from_static(b"1")) != Body::None);
}
#[actix_rt::test]
async fn test_body_debug() {
assert!(format!("{:?}", Body::None).contains("Body::None"));
assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains('1'));
}
#[actix_rt::test]
async fn test_serde_json() {
use serde_json::{json, Value};
assert_eq!(
Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap())
.size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap())
.size(),
BodySize::Sized(25)
);
}
// down-casting used to be done with a method on MessageBody trait
// test is kept to demonstrate equivalence of Any trait
#[actix_rt::test]
async fn test_body_casting() {
let mut body = String::from("hello cast");
// let mut resp_body: &mut dyn MessageBody<Error = Error> = &mut body;
let resp_body: &mut dyn std::any::Any = &mut body;
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap();
body.push('!');
let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast!");
let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none());
}
#[actix_rt::test]
async fn test_to_bytes() {
let body = Body::Empty;
let bytes = to_bytes(body).await.unwrap();
assert!(bytes.is_empty());
let body = Body::Bytes(Bytes::from_static(b"123"));
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
}
}

View File

@@ -0,0 +1,43 @@
use std::{
convert::Infallible,
pin::Pin,
task::{Context, Poll},
};
use bytes::Bytes;
use super::{BodySize, MessageBody};
/// Body type for responses that forbid payloads.
///
/// Distinct from an empty response which would contain a Content-Length header.
///
/// For an "empty" body, use `()` or `Bytes::new()`.
#[derive(Debug, Clone, Copy, Default)]
#[non_exhaustive]
pub struct None;
impl None {
/// Constructs new "none" body.
#[inline]
pub fn new() -> Self {
None
}
}
impl MessageBody for None {
type Error = Infallible;
#[inline]
fn size(&self) -> BodySize {
BodySize::None
}
#[inline]
fn poll_next(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(Option::None)
}
}

View File

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

View File

@@ -6,14 +6,9 @@ pub enum BodySize {
/// Will skip writing Content-Length header. /// Will skip writing Content-Length header.
None, None,
/// Zero size body.
///
/// Will write `Content-Length: 0` header.
Empty,
/// Known size body. /// Known size body.
/// ///
/// Will write `Content-Length: N` header. `Sized(0)` is treated the same as `Empty`. /// Will write `Content-Length: N` header.
Sized(u64), Sized(u64),
/// Unknown size body. /// Unknown size body.
@@ -23,18 +18,19 @@ pub enum BodySize {
} }
impl BodySize { impl BodySize {
/// Returns true if size hint indicates no or empty body. /// Returns true if size hint indicates omitted or empty body.
///
/// Streams will return false because it cannot be known without reading the stream.
/// ///
/// ``` /// ```
/// # use actix_http::body::BodySize; /// # use actix_http::body::BodySize;
/// assert!(BodySize::None.is_eof()); /// assert!(BodySize::None.is_eof());
/// assert!(BodySize::Empty.is_eof());
/// assert!(BodySize::Sized(0).is_eof()); /// assert!(BodySize::Sized(0).is_eof());
/// ///
/// assert!(!BodySize::Sized(64).is_eof()); /// assert!(!BodySize::Sized(64).is_eof());
/// assert!(!BodySize::Stream.is_eof()); /// assert!(!BodySize::Stream.is_eof());
/// ``` /// ```
pub fn is_eof(&self) -> bool { pub fn is_eof(&self) -> bool {
matches!(self, BodySize::None | BodySize::Empty | BodySize::Sized(0)) matches!(self, BodySize::None | BodySize::Sized(0))
} }
} }

View File

@@ -32,6 +32,8 @@ where
} }
} }
// TODO: from_infallible method
impl<S, E> MessageBody for SizedStream<S> impl<S, E> MessageBody for SizedStream<S>
where where
S: Stream<Item = Result<Bytes, E>>, S: Stream<Item = Result<Bytes, E>>,
@@ -72,10 +74,22 @@ mod tests {
use actix_rt::pin; use actix_rt::pin;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use futures_util::stream; use futures_util::stream;
use static_assertions::{assert_impl_all, assert_not_impl_all};
use super::*; use super::*;
use crate::body::to_bytes; use crate::body::to_bytes;
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, crate::Error>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, &'static str>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Empty<Result<Bytes, Infallible>>>: MessageBody);
assert_impl_all!(SizedStream<stream::Repeat<Result<Bytes, Infallible>>>: MessageBody);
assert_not_impl_all!(SizedStream<stream::Empty<Bytes>>: MessageBody);
assert_not_impl_all!(SizedStream<stream::Repeat<Bytes>>: MessageBody);
// crate::Error is not Clone
assert_not_impl_all!(SizedStream<stream::Repeat<Result<Bytes, crate::Error>>>: MessageBody);
#[actix_rt::test] #[actix_rt::test]
async fn skips_empty_chunks() { async fn skips_empty_chunks() {
let body = SizedStream::new( let body = SizedStream::new(
@@ -119,4 +133,37 @@ mod tests {
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12"))); assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
} }
#[actix_rt::test]
async fn stream_string_error() {
// `&'static str` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = SizedStream::new(0, stream::once(async { Err("stringy error") }));
assert_eq!(to_bytes(body).await, Ok(Bytes::new()));
let body = SizedStream::new(1, stream::once(async { Err("stringy error") }));
assert!(matches!(to_bytes(body).await, Err("stringy error")));
}
#[actix_rt::test]
async fn stream_boxed_error() {
// `Box<dyn Error>` does not impl `Error`
// but it does impl `Into<Box<dyn Error>>`
let body = SizedStream::new(
0,
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
);
assert_eq!(to_bytes(body).await.unwrap(), Bytes::new());
let body = SizedStream::new(
1,
stream::once(async { Err(Box::<dyn StdError>::from("stringy error")) }),
);
assert_eq!(
to_bytes(body).await.unwrap_err().to_string(),
"stringy error"
);
}
} }

View File

@@ -0,0 +1,78 @@
use std::task::Poll;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut};
use futures_core::ready;
use super::{BodySize, MessageBody};
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{self, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = body::None::new();
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Bytes::from_static(b"123");
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
// good enough first guess for chunk size
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)]
mod test {
use futures_util::{stream, StreamExt as _};
use super::*;
use crate::{body::BodyStream, Error};
#[actix_rt::test]
async fn test_to_bytes() {
let bytes = to_bytes(()).await.unwrap();
assert!(bytes.is_empty());
let body = Bytes::from_static(b"123");
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123"[..]);
let stream =
stream::iter(vec![Bytes::from_static(b"123"), Bytes::from_static(b"abc")])
.map(Ok::<_, Error>);
let body = BodyStream::new(stream);
let bytes = to_bytes(body).await.unwrap();
assert_eq!(bytes, b"123abc"[..]);
}
}

View File

@@ -1,15 +1,16 @@
use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc}; use std::{fmt, marker::PhantomData, net, rc::Rc};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::{ use crate::{
body::{AnyBody, MessageBody}, body::{BoxBody, MessageBody},
config::{KeepAlive, ServiceConfig}, config::{KeepAlive, ServiceConfig},
extensions::CloneableExtensions,
h1::{self, ExpectHandler, H1Service, UpgradeHandler}, h1::{self, ExpectHandler, H1Service, UpgradeHandler},
h2::H2Service, h2::H2Service,
service::HttpService, service::HttpService,
ConnectCallback, Extensions, Request, Response, ConnectCallback, Request, Response,
}; };
/// A HTTP service builder /// A HTTP service builder
@@ -31,7 +32,7 @@ pub struct HttpServiceBuilder<T, S, X = ExpectHandler, U = UpgradeHandler> {
impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler> impl<T, S> HttpServiceBuilder<T, S, ExpectHandler, UpgradeHandler>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
{ {
@@ -54,11 +55,11 @@ where
impl<T, S, X, U> HttpServiceBuilder<T, S, X, U> impl<T, S, X, U> HttpServiceBuilder<T, S, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -120,7 +121,7 @@ where
where where
F: IntoServiceFactory<X1, Request>, F: IntoServiceFactory<X1, Request>,
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Response<AnyBody>>, X1::Error: Into<Response<BoxBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
HttpServiceBuilder { HttpServiceBuilder {
@@ -167,7 +168,7 @@ where
/// and handlers. /// and handlers.
pub fn on_connect_ext<F>(mut self, f: F) -> Self pub fn on_connect_ext<F>(mut self, f: F) -> Self
where where
F: Fn(&T, &mut Extensions) + 'static, F: Fn(&T, &mut CloneableExtensions) + 'static,
{ {
self.on_connect_ext = Some(Rc::new(f)); self.on_connect_ext = Some(Rc::new(f));
self self
@@ -178,7 +179,7 @@ where
where where
B: MessageBody, B: MessageBody,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
@@ -200,12 +201,11 @@ where
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@@ -223,12 +223,11 @@ where
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U> pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where where
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,

View File

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

View File

@@ -23,7 +23,7 @@ use zstd::stream::write::Decoder as ZstdDecoder;
use crate::{ use crate::{
encoding::Writer, encoding::Writer,
error::{BlockingError, PayloadError}, error::{BlockingError, PayloadError},
http::header::{ContentEncoding, HeaderMap, CONTENT_ENCODING}, header::{ContentEncoding, HeaderMap, CONTENT_ENCODING},
}; };
const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049; const MAX_CHUNK_SIZE_DECODE_IN_PLACE: usize = 2049;

View File

@@ -12,7 +12,7 @@ use actix_rt::task::{spawn_blocking, JoinHandle};
use bytes::Bytes; use bytes::Bytes;
use derive_more::Display; use derive_more::Display;
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project_lite::pin_project;
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
use brotli2::write::BrotliEncoder; use brotli2::write::BrotliEncoder;
@@ -23,98 +23,100 @@ use flate2::write::{GzEncoder, ZlibEncoder};
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
use zstd::stream::write::Encoder as ZstdEncoder; use zstd::stream::write::Encoder as ZstdEncoder;
use crate::{
body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
http::{
header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode,
},
ResponseHead,
};
use super::Writer; use super::Writer;
use crate::error::BlockingError; use crate::{
body::{BodySize, MessageBody},
error::BlockingError,
header::{self, ContentEncoding, HeaderValue, CONTENT_ENCODING},
ResponseHead, StatusCode,
};
const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024; const MAX_CHUNK_SIZE_ENCODE_IN_PLACE: usize = 1024;
#[pin_project] pin_project! {
pub struct Encoder<B> { pub struct Encoder<B> {
eof: bool,
#[pin] #[pin]
body: EncoderBody<B>, body: EncoderBody<B>,
encoder: Option<ContentEncoder>, encoder: Option<ContentEncoder>,
fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>, fut: Option<JoinHandle<Result<ContentEncoder, io::Error>>>,
eof: bool,
}
} }
impl<B: MessageBody> Encoder<B> { impl<B: MessageBody> Encoder<B> {
fn none() -> Self {
Encoder {
body: EncoderBody::None,
encoder: None,
fut: None,
eof: true,
}
}
pub fn response( pub fn response(
encoding: ContentEncoding, encoding: ContentEncoding,
head: &mut ResponseHead, head: &mut ResponseHead,
body: ResponseBody<B>, body: B,
) -> ResponseBody<Encoder<B>> { ) -> Self {
let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING) let can_encode = !(head.headers().contains_key(&CONTENT_ENCODING)
|| head.status == StatusCode::SWITCHING_PROTOCOLS || head.status == StatusCode::SWITCHING_PROTOCOLS
|| head.status == StatusCode::NO_CONTENT || head.status == StatusCode::NO_CONTENT
|| encoding == ContentEncoding::Identity || encoding == ContentEncoding::Identity
|| encoding == ContentEncoding::Auto); || encoding == ContentEncoding::Auto);
let body = match body { match body.size() {
ResponseBody::Other(b) => match b { // no need to compress an empty body
Body::None => return ResponseBody::Other(Body::None), BodySize::None => return Self::none(),
Body::Empty => return ResponseBody::Other(Body::Empty),
Body::Bytes(buf) => { // we cannot assume that Sized is not a stream
if can_encode { BodySize::Sized(_) | BodySize::Stream => {}
EncoderBody::Bytes(buf)
} else {
return ResponseBody::Other(Body::Bytes(buf));
} }
}
Body::Message(stream) => EncoderBody::BoxedStream(stream), // TODO potentially some optimisation for single-chunk responses here by trying to read the
}, // payload eagerly, stopping after 2 polls if the first is a chunk and the second is None
ResponseBody::Body(stream) => EncoderBody::Stream(stream),
};
if can_encode { if can_encode {
// Modify response body only if encoder is not None // Modify response body only if encoder is set
if let Some(enc) = ContentEncoder::encoder(encoding) { if let Some(enc) = ContentEncoder::encoder(encoding) {
update_head(encoding, head); update_head(encoding, head);
head.no_chunking(false); head.no_chunking(false);
return ResponseBody::Body(Encoder {
body, return Encoder {
eof: false, body: EncoderBody::Stream { body },
fut: None,
encoder: Some(enc), encoder: Some(enc),
}); fut: None,
eof: false,
};
} }
} }
ResponseBody::Body(Encoder { Encoder {
body, body: EncoderBody::Stream { body },
eof: false,
fut: None,
encoder: None, encoder: None,
}) fut: None,
eof: false,
}
} }
} }
#[pin_project(project = EncoderBodyProj)] pin_project! {
enum EncoderBody<B> { #[project = EncoderBodyProj]
Bytes(Bytes), enum EncoderBody<B> {
Stream(#[pin] B), None,
BoxedStream(BoxAnyBody), Stream { #[pin] body: B },
}
} }
impl<B> MessageBody for EncoderBody<B> impl<B> MessageBody for EncoderBody<B>
where where
B: MessageBody, B: MessageBody,
{ {
type Error = EncoderError<B::Error>; type Error = EncoderError;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EncoderBody::Bytes(ref b) => b.size(), EncoderBody::None => BodySize::None,
EncoderBody::Stream(ref b) => b.size(), EncoderBody::Stream { body } => body.size(),
EncoderBody::BoxedStream(ref b) => b.size(),
} }
} }
@@ -123,17 +125,11 @@ where
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { match self.project() {
EncoderBodyProj::Bytes(b) => { EncoderBodyProj::None => Poll::Ready(None),
if b.is_empty() {
Poll::Ready(None) EncoderBodyProj::Stream { body } => body
} else { .poll_next(cx)
Poll::Ready(Some(Ok(std::mem::take(b)))) .map_err(|err| EncoderError::Body(err.into())),
}
}
EncoderBodyProj::Stream(b) => b.poll_next(cx).map_err(EncoderError::Body),
EncoderBodyProj::BoxedStream(ref mut b) => {
b.as_pin_mut().poll_next(cx).map_err(EncoderError::Boxed)
}
} }
} }
} }
@@ -142,7 +138,7 @@ impl<B> MessageBody for Encoder<B>
where where
B: MessageBody, B: MessageBody,
{ {
type Error = EncoderError<B::Error>; type Error = EncoderError;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_none() { if self.encoder.is_none() {
@@ -205,6 +201,7 @@ where
None => { None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish().map_err(EncoderError::Io)?; let chunk = encoder.finish().map_err(EncoderError::Io)?;
if chunk.is_empty() { if chunk.is_empty() {
return Poll::Ready(None); return Poll::Ready(None);
} else { } else {
@@ -222,7 +219,7 @@ where
fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) { fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
head.headers_mut().insert( head.headers_mut().insert(
CONTENT_ENCODING, header::CONTENT_ENCODING,
HeaderValue::from_static(encoding.as_str()), HeaderValue::from_static(encoding.as_str()),
); );
} }
@@ -230,12 +227,15 @@ fn update_head(encoding: ContentEncoding, head: &mut ResponseHead) {
enum ContentEncoder { enum ContentEncoder {
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Deflate(ZlibEncoder<Writer>), Deflate(ZlibEncoder<Writer>),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
Br(BrotliEncoder<Writer>), Br(BrotliEncoder<Writer>),
// We need explicit 'static lifetime here because ZstdEncoder need lifetime
// argument, and we use `spawn_blocking` in `Encoder::poll_next` that require `FnOnce() -> R + Send + 'static` // Wwe need explicit 'static lifetime here because ZstdEncoder needs a lifetime argument and we
// use `spawn_blocking` in `Encoder::poll_next` that requires `FnOnce() -> R + Send + 'static`.
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
Zstd(ZstdEncoder<'static, Writer>), Zstd(ZstdEncoder<'static, Writer>),
} }
@@ -248,20 +248,24 @@ impl ContentEncoder {
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new( ContentEncoding::Gzip => Some(ContentEncoder::Gzip(GzEncoder::new(
Writer::new(), Writer::new(),
flate2::Compression::fast(), flate2::Compression::fast(),
))), ))),
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoding::Br => { ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
} }
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
ContentEncoding::Zstd => { ContentEncoding::Zstd => {
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?; let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
Some(ContentEncoder::Zstd(encoder)) Some(ContentEncoder::Zstd(encoder))
} }
_ => None, _ => None,
} }
} }
@@ -271,10 +275,13 @@ impl ContentEncoder {
match *self { match *self {
#[cfg(feature = "compress-brotli")] #[cfg(feature = "compress-brotli")]
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
} }
@@ -287,16 +294,19 @@ impl ContentEncoder {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(encoder) => match encoder.finish() { ContentEncoder::Gzip(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(encoder) => match encoder.finish() { ContentEncoder::Deflate(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(encoder) => match encoder.finish() { ContentEncoder::Zstd(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
@@ -315,6 +325,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Gzip(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -323,6 +334,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-gzip")] #[cfg(feature = "compress-gzip")]
ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Deflate(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -331,6 +343,7 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
#[cfg(feature = "compress-zstd")] #[cfg(feature = "compress-zstd")]
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) { ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()), Ok(_) => Ok(()),
@@ -345,12 +358,9 @@ impl ContentEncoder {
#[derive(Debug, Display)] #[derive(Debug, Display)]
#[non_exhaustive] #[non_exhaustive]
pub enum EncoderError<E> { pub enum EncoderError {
#[display(fmt = "body")] #[display(fmt = "body")]
Body(E), Body(Box<dyn StdError>),
#[display(fmt = "boxed")]
Boxed(Box<dyn StdError>),
#[display(fmt = "blocking")] #[display(fmt = "blocking")]
Blocking(BlockingError), Blocking(BlockingError),
@@ -359,19 +369,18 @@ pub enum EncoderError<E> {
Io(io::Error), Io(io::Error),
} }
impl<E: StdError + 'static> StdError for EncoderError<E> { impl StdError for EncoderError {
fn source(&self) -> Option<&(dyn StdError + 'static)> { fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self { match self {
EncoderError::Body(err) => Some(err), EncoderError::Body(err) => Some(&**err),
EncoderError::Boxed(err) => Some(&**err),
EncoderError::Blocking(err) => Some(err), EncoderError::Blocking(err) => Some(err),
EncoderError::Io(err) => Some(err), EncoderError::Io(err) => Some(err),
} }
} }
} }
impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error { impl From<EncoderError> for crate::Error {
fn from(err: EncoderError<E>) -> Self { fn from(err: EncoderError) -> Self {
crate::Error::new_encoder().with_cause(err) crate::Error::new_encoder().with_cause(err)
} }
} }

View File

@@ -10,6 +10,9 @@ mod encoder;
pub use self::decoder::Decoder; pub use self::decoder::Decoder;
pub use self::encoder::Encoder; pub use self::encoder::Encoder;
/// Special-purpose writer for streaming (de-)compression.
///
/// Pre-allocates 8KiB of capacity.
pub(self) struct Writer { pub(self) struct Writer {
buf: BytesMut, buf: BytesMut,
} }

View File

@@ -5,10 +5,7 @@ use std::{error::Error as StdError, fmt, io, str::Utf8Error, string::FromUtf8Err
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{uri::InvalidUri, StatusCode}; use http::{uri::InvalidUri, StatusCode};
use crate::{ use crate::{body::BoxBody, ws, Response};
body::{AnyBody, Body},
ws, Response,
};
pub use http::Error as HttpError; pub use http::Error as HttpError;
@@ -29,6 +26,11 @@ impl Error {
} }
} }
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
self.inner.cause = Some(cause.into());
self
}
pub(crate) fn new_http() -> Self { pub(crate) fn new_http() -> Self {
Self::new(Kind::Http) Self::new(Kind::Http)
} }
@@ -49,14 +51,12 @@ impl Error {
Self::new(Kind::SendResponse) Self::new(Kind::SendResponse)
} }
// TODO: remove allow #[allow(unused)] // reserved for future use (TODO: remove allow when being used)
#[allow(dead_code)]
pub(crate) fn new_io() -> Self { pub(crate) fn new_io() -> Self {
Self::new(Kind::Io) Self::new(Kind::Io)
} }
// used in encoder behind feature flag so ignore unused warning #[allow(unused)] // used in encoder behind feature flag so ignore unused warning
#[allow(unused)]
pub(crate) fn new_encoder() -> Self { pub(crate) fn new_encoder() -> Self {
Self::new(Kind::Encoder) Self::new(Kind::Encoder)
} }
@@ -64,26 +64,22 @@ impl Error {
pub(crate) fn new_ws() -> Self { pub(crate) fn new_ws() -> Self {
Self::new(Kind::Ws) Self::new(Kind::Ws)
} }
pub(crate) fn with_cause(mut self, cause: impl Into<Box<dyn StdError>>) -> Self {
self.inner.cause = Some(cause.into());
self
}
} }
impl From<Error> for Response<AnyBody> { impl From<Error> for Response<BoxBody> {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
// TODO: more appropriate error status codes, usage assessment needed
let status_code = match err.inner.kind { let status_code = match err.inner.kind {
Kind::Parse => StatusCode::BAD_REQUEST, Kind::Parse => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR, _ => StatusCode::INTERNAL_SERVER_ERROR,
}; };
Response::new(status_code).set_body(Body::from(err.to_string())) Response::new(status_code).set_body(BoxBody::new(err.to_string()))
} }
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Display)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Display)]
pub enum Kind { pub(crate) enum Kind {
#[display(fmt = "error processing HTTP")] #[display(fmt = "error processing HTTP")]
Http, Http,
@@ -137,12 +133,6 @@ impl From<std::convert::Infallible> for Error {
} }
} }
impl From<ws::ProtocolError> for Error {
fn from(err: ws::ProtocolError) -> Self {
Self::new_ws().with_cause(err)
}
}
impl From<HttpError> for Error { impl From<HttpError> for Error {
fn from(err: HttpError) -> Self { fn from(err: HttpError) -> Self {
Self::new_http().with_cause(err) Self::new_http().with_cause(err)
@@ -155,6 +145,12 @@ impl From<ws::HandshakeError> for Error {
} }
} }
impl From<ws::ProtocolError> for Error {
fn from(err: ws::ProtocolError) -> Self {
Self::new_ws().with_cause(err)
}
}
/// A set of errors that can occur during parsing HTTP streams. /// A set of errors that can occur during parsing HTTP streams.
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[non_exhaustive] #[non_exhaustive]
@@ -245,7 +241,7 @@ impl From<ParseError> for Error {
} }
} }
impl From<ParseError> for Response<AnyBody> { impl From<ParseError> for Response<BoxBody> {
fn from(err: ParseError) -> Self { fn from(err: ParseError) -> Self {
Error::from(err).into() Error::from(err).into()
} }
@@ -342,7 +338,7 @@ pub enum DispatchError {
/// Service error /// Service error
// FIXME: display and error type // FIXME: display and error type
#[display(fmt = "Service Error")] #[display(fmt = "Service Error")]
Service(#[error(not(source))] Response<AnyBody>), Service(#[error(not(source))] Response<BoxBody>),
/// Body error /// Body error
// FIXME: display and error type // FIXME: display and error type
@@ -426,11 +422,11 @@ mod tests {
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response<AnyBody> = ParseError::Incomplete.into(); let resp: Response<BoxBody> = ParseError::Incomplete.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into(); let err: HttpError = StatusCode::from_u16(10000).err().unwrap().into();
let resp: Response<AnyBody> = Error::new_http().with_cause(err).into(); let resp: Response<BoxBody> = Error::new_http().with_cause(err).into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }
@@ -455,7 +451,7 @@ mod tests {
fn test_error_http_response() { fn test_error_http_response() {
let orig = io::Error::new(io::ErrorKind::Other, "other"); let orig = io::Error::new(io::ErrorKind::Other, "other");
let err = Error::new_io().with_cause(orig); let err = Error::new_io().with_cause(orig);
let resp: Response<AnyBody> = err.into(); let resp: Response<BoxBody> = err.into();
assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR); assert_eq!(resp.status(), StatusCode::INTERNAL_SERVER_ERROR);
} }

View File

@@ -1,6 +1,6 @@
use std::{ use std::{
any::{Any, TypeId}, any::{Any, TypeId},
fmt, mem, fmt,
}; };
use ahash::AHashMap; use ahash::AHashMap;
@@ -10,8 +10,7 @@ use ahash::AHashMap;
/// All entries into this map must be owned types (or static references). /// All entries into this map must be owned types (or static references).
#[derive(Default)] #[derive(Default)]
pub struct Extensions { pub struct Extensions {
/// Use FxHasher with a std HashMap with for faster /// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
/// lookups on the small `TypeId` (u64 equivalent) keys.
map: AHashMap<TypeId, Box<dyn Any>>, map: AHashMap<TypeId, Box<dyn Any>>,
} }
@@ -124,9 +123,11 @@ impl Extensions {
self.map.extend(other.map); self.map.extend(other.map);
} }
/// Sets (or overrides) items from `other` into this map. /// Sets (or overrides) items from cloneable extensions map into this map.
pub(crate) fn drain_from(&mut self, other: &mut Self) { pub(crate) fn clone_from(&mut self, other: &CloneableExtensions) {
self.map.extend(mem::take(&mut other.map)); for (k, val) in &other.map {
self.map.insert(*k, (**val).clone_to_any());
}
} }
} }
@@ -140,6 +141,104 @@ fn downcast_owned<T: 'static>(boxed: Box<dyn Any>) -> Option<T> {
boxed.downcast().ok().map(|boxed| *boxed) boxed.downcast().ok().map(|boxed| *boxed)
} }
#[doc(hidden)]
pub trait CloneToAny {
/// Cast `self` into an `Any` reference.
#[cfg(test)]
fn any_ref(&self) -> &dyn Any;
/// Clone `self` to a new `Box<Any>` object.
fn clone_to_any(&self) -> Box<dyn Any>;
/// Clone `self` to a new `Box<CloneAny>` object.
fn clone_to_clone_any(&self) -> Box<dyn CloneAny>;
}
impl<T: Clone + Any> CloneToAny for T {
#[cfg(test)]
fn any_ref(&self) -> &dyn Any {
&*self
}
#[inline]
fn clone_to_any(&self) -> Box<dyn Any> {
Box::new(self.clone())
}
#[inline]
fn clone_to_clone_any(&self) -> Box<dyn CloneAny> {
Box::new(self.clone())
}
}
/// An [`Any`] trait with an additional [`Clone`] requirement.
pub trait CloneAny: CloneToAny + Any {}
impl<T: Any + Clone> CloneAny for T {}
impl Clone for Box<dyn CloneAny> {
#[inline]
fn clone(&self) -> Self {
(**self).clone_to_clone_any()
}
}
trait UncheckedAnyExt {
/// # Safety
/// Caller must ensure type `T` is true type.
#[inline]
unsafe fn downcast_unchecked<T: 'static>(self: Box<Self>) -> Box<T> {
Box::from_raw(Box::into_raw(self) as *mut T)
}
}
impl UncheckedAnyExt for dyn CloneAny {}
/// A type map for `on_connect` extensions.
///
/// All entries into this map must be owned types and implement `Clone` trait.
///
/// Many requests can be processed for each connection but the `on_connect` will only be run once
/// when the connection is opened. Therefore, items added to this special map type need to be cloned
/// into the regular extensions map for each request. Most useful connection information types are
/// cloneable already but you can use reference counted wrappers if not.
#[derive(Default)]
pub struct CloneableExtensions {
/// Use AHasher with a std HashMap with for faster lookups on the small `TypeId` keys.
map: AHashMap<TypeId, Box<dyn CloneAny>>,
}
impl CloneableExtensions {
/// Insert an item into the map.
///
/// If an item of this type was already stored, it will be replaced and returned.
///
/// # Examples
/// ```
/// # 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: CloneAny>(&mut self, val: T) -> Option<T> {
self.map
.insert(TypeId::of::<T>(), Box::new(val))
.map(|boxed| {
// Safety:
// Box is owned and `T` is known to be true type from map.
*unsafe { UncheckedAnyExt::downcast_unchecked::<T>(boxed) }
})
}
#[cfg(test)]
fn get<T: CloneAny>(&self) -> Option<&T> {
self.map
.get(&TypeId::of::<T>())
.and_then(|boxed| boxed.as_ref().any_ref().downcast_ref())
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -179,6 +278,8 @@ mod tests {
#[test] #[test]
fn test_integers() { fn test_integers() {
static A: u32 = 8;
let mut map = Extensions::new(); let mut map = Extensions::new();
map.insert::<i8>(8); map.insert::<i8>(8);
@@ -191,6 +292,7 @@ mod tests {
map.insert::<u32>(32); map.insert::<u32>(32);
map.insert::<u64>(64); map.insert::<u64>(64);
map.insert::<u128>(128); map.insert::<u128>(128);
map.insert::<&'static u32>(&A);
assert!(map.get::<i8>().is_some()); assert!(map.get::<i8>().is_some());
assert!(map.get::<i16>().is_some()); assert!(map.get::<i16>().is_some());
assert!(map.get::<i32>().is_some()); assert!(map.get::<i32>().is_some());
@@ -201,6 +303,7 @@ mod tests {
assert!(map.get::<u32>().is_some()); assert!(map.get::<u32>().is_some());
assert!(map.get::<u64>().is_some()); assert!(map.get::<u64>().is_some());
assert!(map.get::<u128>().is_some()); assert!(map.get::<u128>().is_some());
assert!(map.get::<&'static u32>().is_some());
} }
#[test] #[test]
@@ -281,25 +384,41 @@ mod tests {
} }
#[test] #[test]
fn test_drain_from() { fn test_clone_from() {
#[derive(Clone)]
struct NonCopy {
num: u8,
}
let mut ext = Extensions::new(); let mut ext = Extensions::new();
ext.insert(2isize); ext.insert(2isize);
let mut more_ext = Extensions::new();
more_ext.insert(5isize);
more_ext.insert(5usize);
assert_eq!(ext.get::<isize>(), Some(&2isize)); assert_eq!(ext.get::<isize>(), Some(&2isize));
assert_eq!(ext.get::<usize>(), None);
assert_eq!(more_ext.get::<isize>(), Some(&5isize));
assert_eq!(more_ext.get::<usize>(), Some(&5usize));
ext.drain_from(&mut more_ext); let mut more_ext = CloneableExtensions::default();
more_ext.insert(3isize);
more_ext.insert(3usize);
more_ext.insert(NonCopy { num: 8 });
assert_eq!(ext.get::<isize>(), Some(&5isize)); ext.clone_from(&more_ext);
assert_eq!(ext.get::<usize>(), Some(&5usize));
assert_eq!(more_ext.get::<isize>(), None); assert_eq!(ext.get::<isize>(), Some(&3isize));
assert_eq!(more_ext.get::<usize>(), None); assert_eq!(ext.get::<usize>(), Some(&3usize));
assert_eq!(more_ext.get::<isize>(), Some(&3isize));
assert_eq!(more_ext.get::<usize>(), Some(&3usize));
assert!(ext.get::<NonCopy>().is_some());
assert!(more_ext.get::<NonCopy>().is_some());
}
#[test]
fn boxes_not_aliased() {
let a: Box<dyn CloneAny> = Box::new(42);
let b = a.clone_to_clone_any();
assert_ne!(Box::into_raw(a) as *const (), Box::into_raw(b) as *const ());
let a: Box<dyn CloneAny> = Box::new(42);
let b = a.clone_to_any();
assert_ne!(Box::into_raw(a) as *const (), Box::into_raw(b) as *const ());
} }
} }

View File

@@ -120,7 +120,7 @@ impl Decoder for ClientCodec {
debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set"); debug_assert!(!self.inner.payload.is_some(), "Payload decoder is set");
if let Some((req, payload)) = self.inner.decoder.decode(src)? { if let Some((req, payload)) = self.inner.decoder.decode(src)? {
if let Some(ctype) = req.ctype() { if let Some(ctype) = req.conn_type() {
// do not use peer's keep-alive // do not use peer's keep-alive
self.inner.ctype = if ctype == ConnectionType::KeepAlive { self.inner.ctype = if ctype == ConnectionType::KeepAlive {
self.inner.ctype self.inner.ctype

View File

@@ -29,7 +29,7 @@ pub struct Codec {
decoder: decoder::MessageDecoder<Request>, decoder: decoder::MessageDecoder<Request>,
payload: Option<PayloadDecoder>, payload: Option<PayloadDecoder>,
version: Version, version: Version,
ctype: ConnectionType, conn_type: ConnectionType,
// encoder part // encoder part
flags: Flags, flags: Flags,
@@ -65,7 +65,7 @@ impl Codec {
decoder: decoder::MessageDecoder::default(), decoder: decoder::MessageDecoder::default(),
payload: None, payload: None,
version: Version::HTTP_11, version: Version::HTTP_11,
ctype: ConnectionType::Close, conn_type: ConnectionType::Close,
encoder: encoder::MessageEncoder::default(), encoder: encoder::MessageEncoder::default(),
} }
} }
@@ -73,13 +73,13 @@ impl Codec {
/// Check if request is upgrade. /// Check if request is upgrade.
#[inline] #[inline]
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.ctype == ConnectionType::Upgrade self.conn_type == ConnectionType::Upgrade
} }
/// Check if last response is keep-alive. /// Check if last response is keep-alive.
#[inline] #[inline]
pub fn keepalive(&self) -> bool { pub fn keepalive(&self) -> bool {
self.ctype == ConnectionType::KeepAlive self.conn_type == ConnectionType::KeepAlive
} }
/// Check if keep-alive enabled on server level. /// Check if keep-alive enabled on server level.
@@ -124,11 +124,11 @@ impl Decoder for Codec {
let head = req.head(); let head = req.head();
self.flags.set(Flags::HEAD, head.method == Method::HEAD); self.flags.set(Flags::HEAD, head.method == Method::HEAD);
self.version = head.version; self.version = head.version;
self.ctype = head.connection_type(); self.conn_type = head.connection_type();
if self.ctype == ConnectionType::KeepAlive if self.conn_type == ConnectionType::KeepAlive
&& !self.flags.contains(Flags::KEEPALIVE_ENABLED) && !self.flags.contains(Flags::KEEPALIVE_ENABLED)
{ {
self.ctype = ConnectionType::Close self.conn_type = ConnectionType::Close
} }
match payload { match payload {
PayloadType::None => self.payload = None, PayloadType::None => self.payload = None,
@@ -159,14 +159,14 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
res.head_mut().version = self.version; res.head_mut().version = self.version;
// connection status // connection status
self.ctype = if let Some(ct) = res.head().ctype() { self.conn_type = if let Some(ct) = res.head().conn_type() {
if ct == ConnectionType::KeepAlive { if ct == ConnectionType::KeepAlive {
self.ctype self.conn_type
} else { } else {
ct ct
} }
} else { } else {
self.ctype self.conn_type
}; };
// encode message // encode message
@@ -177,10 +177,9 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
self.flags.contains(Flags::STREAM), self.flags.contains(Flags::STREAM),
self.version, self.version,
length, length,
self.ctype, self.conn_type,
&self.config, &self.config,
)?; )?;
// self.headers_size = (dst.len() - len) as u32;
} }
Message::Chunk(Some(bytes)) => { Message::Chunk(Some(bytes)) => {
self.encoder.encode_chunk(bytes.as_ref(), dst)?; self.encoder.encode_chunk(bytes.as_ref(), dst)?;
@@ -189,6 +188,7 @@ impl Encoder<Message<(Response<()>, BodySize)>> for Codec {
self.encoder.encode_eof(dst)?; self.encoder.encode_eof(dst)?;
} }
} }
Ok(()) Ok(())
} }
} }

View File

@@ -174,7 +174,7 @@ pub(crate) trait MessageType: Sized {
self.set_expect() self.set_expect()
} }
// https://tools.ietf.org/html/rfc7230#section-3.3.3 // https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.3
if chunked { if chunked {
// Chunked encoding // Chunked encoding
Ok(PayloadLength::Payload(PayloadType::Payload( Ok(PayloadLength::Payload(PayloadType::Payload(
@@ -511,7 +511,7 @@ mod tests {
use super::*; use super::*;
use crate::{ use crate::{
error::ParseError, error::ParseError,
http::header::{HeaderName, SET_COOKIE}, header::{HeaderName, SET_COOKIE},
HttpMessage as _, HttpMessage as _,
}; };

View File

@@ -1,6 +1,5 @@
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
io, mem, net, io, mem, net,
@@ -19,7 +18,7 @@ use log::{error, trace};
use pin_project::pin_project; use pin_project::pin_project;
use crate::{ use crate::{
body::{AnyBody, BodySize, MessageBody}, body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::{DispatchError, ParseError, PayloadError}, error::{DispatchError, ParseError, PayloadError},
service::HttpFlow, service::HttpFlow,
@@ -51,13 +50,12 @@ bitflags! {
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -73,13 +71,12 @@ where
enum DispatcherState<T, S, B, X, U> enum DispatcherState<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -92,13 +89,12 @@ where
struct InnerDispatcher<T, S, B, X, U> struct InnerDispatcher<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -137,13 +133,12 @@ where
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
None, None,
ExpectCall(#[pin] X::Future), ExpectCall(#[pin] X::Future),
ServiceCall(#[pin] S::Future), ServiceCall(#[pin] S::Future),
SendPayload(#[pin] B), SendPayload(#[pin] B),
SendErrorPayload(#[pin] AnyBody), SendErrorPayload(#[pin] BoxBody),
} }
impl<S, B, X> State<S, B, X> impl<S, B, X> State<S, B, X>
@@ -153,7 +148,6 @@ where
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
fn is_empty(&self) -> bool { fn is_empty(&self) -> bool {
matches!(self, State::None) matches!(self, State::None)
@@ -171,14 +165,13 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -232,14 +225,13 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -303,9 +295,9 @@ where
body: &impl MessageBody, body: &impl MessageBody,
) -> Result<BodySize, DispatchError> { ) -> Result<BodySize, DispatchError> {
let size = body.size(); let size = body.size();
let mut this = self.project(); let this = self.project();
this.codec this.codec
.encode(Message::Item((message, size)), &mut this.write_buf) .encode(Message::Item((message, size)), this.write_buf)
.map_err(|err| { .map_err(|err| {
if let Some(mut payload) = this.payload.take() { if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Incomplete(None)); payload.set_error(PayloadError::Incomplete(None));
@@ -325,7 +317,7 @@ where
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
BodySize::None | BodySize::Empty => State::None, BodySize::None | BodySize::Sized(0) => State::None,
_ => State::SendPayload(body), _ => State::SendPayload(body),
}; };
self.project().state.set(state); self.project().state.set(state);
@@ -335,11 +327,11 @@ where
fn send_error_response( fn send_error_response(
mut self: Pin<&mut Self>, mut self: Pin<&mut Self>,
message: Response<()>, message: Response<()>,
body: AnyBody, body: BoxBody,
) -> Result<(), DispatchError> { ) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?; let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size { let state = match size {
BodySize::None | BodySize::Empty => State::None, BodySize::None | BodySize::Sized(0) => State::None,
_ => State::SendErrorPayload(body), _ => State::SendErrorPayload(body),
}; };
self.project().state.set(state); self.project().state.set(state);
@@ -380,7 +372,7 @@ where
// send_response would update InnerDispatcher state to SendPayload or // send_response would update InnerDispatcher state to SendPayload or
// None(If response body is empty). // None(If response body is empty).
// continue loop to poll it. // continue loop to poll it.
self.as_mut().send_error_response(res, AnyBody::Empty)?; self.as_mut().send_error_response(res, BoxBody::new(()))?;
} }
// return with upgrade request and poll it exclusively. // return with upgrade request and poll it exclusively.
@@ -400,7 +392,7 @@ where
// send service call error as response // send service call error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<AnyBody> = err.into(); let res: Response<BoxBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_error_response(res, body)?; self.as_mut().send_error_response(res, body)?;
} }
@@ -425,13 +417,13 @@ where
Poll::Ready(Some(Ok(item))) => { Poll::Ready(Some(Ok(item))) => {
this.codec.encode( this.codec.encode(
Message::Chunk(Some(item)), Message::Chunk(Some(item)),
&mut this.write_buf, this.write_buf,
)?; )?;
} }
Poll::Ready(None) => { Poll::Ready(None) => {
this.codec this.codec
.encode(Message::Chunk(None), &mut this.write_buf)?; .encode(Message::Chunk(None), this.write_buf)?;
// payload stream finished. // payload stream finished.
// set state to None and handle next message // set state to None and handle next message
this.state.set(State::None); this.state.set(State::None);
@@ -460,13 +452,13 @@ where
Poll::Ready(Some(Ok(item))) => { Poll::Ready(Some(Ok(item))) => {
this.codec.encode( this.codec.encode(
Message::Chunk(Some(item)), Message::Chunk(Some(item)),
&mut this.write_buf, this.write_buf,
)?; )?;
} }
Poll::Ready(None) => { Poll::Ready(None) => {
this.codec this.codec
.encode(Message::Chunk(None), &mut this.write_buf)?; .encode(Message::Chunk(None), this.write_buf)?;
// payload stream finished. // payload stream finished.
// set state to None and handle next message // set state to None and handle next message
this.state.set(State::None); this.state.set(State::None);
@@ -497,7 +489,7 @@ where
// send expect error as response // send expect error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<AnyBody> = err.into(); let res: Response<BoxBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_error_response(res, body)?; self.as_mut().send_error_response(res, body)?;
} }
@@ -546,7 +538,7 @@ where
// to notify the dispatcher a new state is set and the outer loop // to notify the dispatcher a new state is set and the outer loop
// should be continue. // should be continue.
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<AnyBody> = err.into(); let res: Response<BoxBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
return self.send_error_response(res, body); return self.send_error_response(res, body);
} }
@@ -566,7 +558,7 @@ where
Poll::Pending => Ok(()), Poll::Pending => Ok(()),
// see the comment on ExpectCall state branch's Ready(Err(err)). // see the comment on ExpectCall state branch's Ready(Err(err)).
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response<AnyBody> = err.into(); let res: Response<BoxBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.send_error_response(res, body) self.send_error_response(res, body)
} }
@@ -592,7 +584,7 @@ where
let mut updated = false; let mut updated = false;
let mut this = self.as_mut().project(); let mut this = self.as_mut().project();
loop { loop {
match this.codec.decode(&mut this.read_buf) { match this.codec.decode(this.read_buf) {
Ok(Some(msg)) => { Ok(Some(msg)) => {
updated = true; updated = true;
this.flags.insert(Flags::STARTED); this.flags.insert(Flags::STARTED);
@@ -772,7 +764,7 @@ where
trace!("Slow request timeout"); trace!("Slow request timeout");
let _ = self.as_mut().send_error_response( let _ = self.as_mut().send_error_response(
Response::with_body(StatusCode::REQUEST_TIMEOUT, ()), Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
AnyBody::Empty, BoxBody::new(()),
); );
this = self.project(); this = self.project();
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
@@ -909,14 +901,13 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -1046,9 +1037,8 @@ mod tests {
use crate::{ use crate::{
error::Error, error::Error,
h1::{ExpectHandler, UpgradeHandler}, h1::{ExpectHandler, UpgradeHandler},
http::Method,
test::{TestBuffer, TestSeqBuffer}, test::{TestBuffer, TestSeqBuffer},
HttpMessage, KeepAlive, HttpMessage, KeepAlive, Method,
}; };
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> { fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
@@ -1067,17 +1057,19 @@ mod tests {
} }
} }
fn ok_service() -> impl Service<Request, Response = Response<AnyBody>, Error = Error> fn ok_service(
) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error>
{ {
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok()))) fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok())))
} }
fn echo_path_service( fn echo_path_service(
) -> impl Service<Request, Response = Response<AnyBody>, Error = Error> { ) -> impl Service<Request, Response = Response<impl MessageBody>, Error = Error>
{
fn_service(|req: Request| { fn_service(|req: Request| {
let path = req.path().as_bytes(); let path = req.path().as_bytes();
ready(Ok::<_, Error>( ready(Ok::<_, Error>(
Response::ok().set_body(AnyBody::from_slice(path)), Response::ok().set_body(Bytes::copy_from_slice(path)),
)) ))
}) })
} }

View File

@@ -20,6 +20,7 @@ const AVERAGE_HEADER_SIZE: usize = 30;
#[derive(Debug)] #[derive(Debug)]
pub(crate) struct MessageEncoder<T: MessageType> { pub(crate) struct MessageEncoder<T: MessageType> {
#[allow(dead_code)]
pub length: BodySize, pub length: BodySize,
pub te: TransferEncoding, pub te: TransferEncoding,
_phantom: PhantomData<T>, _phantom: PhantomData<T>,
@@ -55,7 +56,7 @@ pub(crate) trait MessageType: Sized {
dst: &mut BytesMut, dst: &mut BytesMut,
version: Version, version: Version,
mut length: BodySize, mut length: BodySize,
ctype: ConnectionType, conn_type: ConnectionType,
config: &ServiceConfig, config: &ServiceConfig,
) -> io::Result<()> { ) -> io::Result<()> {
let chunked = self.chunked(); let chunked = self.chunked();
@@ -70,14 +71,24 @@ pub(crate) trait MessageType: Sized {
| StatusCode::PROCESSING | StatusCode::PROCESSING
| StatusCode::NO_CONTENT => { | StatusCode::NO_CONTENT => {
// skip content-length and transfer-encoding headers // skip content-length and transfer-encoding headers
// See https://tools.ietf.org/html/rfc7230#section-3.3.1 // see https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.1
// and https://tools.ietf.org/html/rfc7230#section-3.3.2 // and https://datatracker.ietf.org/doc/html/rfc7230#section-3.3.2
skip_len = true; skip_len = true;
length = BodySize::None length = BodySize::None
} }
StatusCode::NOT_MODIFIED => {
// 304 responses should never have a body but should retain a manually set
// content-length header
// see https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
skip_len = false;
length = BodySize::None;
}
_ => {} _ => {}
} }
} }
match length { match length {
BodySize::Stream => { BodySize::Stream => {
if chunked { if chunked {
@@ -92,19 +103,16 @@ pub(crate) trait MessageType: Sized {
dst.put_slice(b"\r\n"); dst.put_slice(b"\r\n");
} }
} }
BodySize::Empty => { BodySize::Sized(0) if camel_case => {
if camel_case { dst.put_slice(b"\r\nContent-Length: 0\r\n")
dst.put_slice(b"\r\nContent-Length: 0\r\n");
} else {
dst.put_slice(b"\r\ncontent-length: 0\r\n");
}
} }
BodySize::Sized(0) => dst.put_slice(b"\r\ncontent-length: 0\r\n"),
BodySize::Sized(len) => helpers::write_content_length(len, dst), BodySize::Sized(len) => helpers::write_content_length(len, dst),
BodySize::None => dst.put_slice(b"\r\n"), BodySize::None => dst.put_slice(b"\r\n"),
} }
// Connection // Connection
match ctype { match conn_type {
ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"), ConnectionType::Upgrade => dst.put_slice(b"connection: upgrade\r\n"),
ConnectionType::KeepAlive if version < Version::HTTP_11 => { ConnectionType::KeepAlive if version < Version::HTTP_11 => {
if camel_case { if camel_case {
@@ -329,13 +337,13 @@ impl<T: MessageType> MessageEncoder<T> {
stream: bool, stream: bool,
version: Version, version: Version,
length: BodySize, length: BodySize,
ctype: ConnectionType, conn_type: ConnectionType,
config: &ServiceConfig, config: &ServiceConfig,
) -> io::Result<()> { ) -> io::Result<()> {
// transfer encoding // transfer encoding
if !head { if !head {
self.te = match length { self.te = match length {
BodySize::Empty => TransferEncoding::empty(), BodySize::Sized(0) => TransferEncoding::empty(),
BodySize::Sized(len) => TransferEncoding::length(len), BodySize::Sized(len) => TransferEncoding::length(len),
BodySize::Stream => { BodySize::Stream => {
if message.chunked() && !stream { if message.chunked() && !stream {
@@ -351,7 +359,7 @@ impl<T: MessageType> MessageEncoder<T> {
} }
message.encode_status(dst)?; message.encode_status(dst)?;
message.encode_headers(dst, version, length, ctype, config) message.encode_headers(dst, version, length, conn_type, config)
} }
} }
@@ -365,10 +373,12 @@ pub(crate) struct TransferEncoding {
enum TransferEncodingKind { enum TransferEncodingKind {
/// An Encoder for when Transfer-Encoding includes `chunked`. /// An Encoder for when Transfer-Encoding includes `chunked`.
Chunked(bool), Chunked(bool),
/// An Encoder for when Content-Length is set. /// An Encoder for when Content-Length is set.
/// ///
/// Enforces that the body is not longer than the Content-Length header. /// Enforces that the body is not longer than the Content-Length header.
Length(u64), Length(u64),
/// An Encoder for when Content-Length is not known. /// An Encoder for when Content-Length is not known.
/// ///
/// Application decides when to stop writing. /// Application decides when to stop writing.
@@ -521,8 +531,10 @@ mod tests {
use http::header::AUTHORIZATION; use http::header::AUTHORIZATION;
use super::*; use super::*;
use crate::http::header::{HeaderValue, CONTENT_TYPE}; use crate::{
use crate::RequestHead; header::{HeaderValue, CONTENT_TYPE},
RequestHead,
};
#[test] #[test]
fn test_chunked_te() { fn test_chunked_te() {
@@ -552,7 +564,7 @@ mod tests {
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
BodySize::Empty, BodySize::Sized(0),
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
@@ -623,7 +635,7 @@ mod tests {
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
BodySize::Empty, BodySize::Sized(0),
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );

View File

@@ -1,5 +1,4 @@
use std::{ use std::{
error::Error as StdError,
fmt, fmt,
marker::PhantomData, marker::PhantomData,
net, net,
@@ -16,7 +15,7 @@ use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
body::{AnyBody, MessageBody}, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::DispatchError, error::DispatchError,
service::HttpServiceHandler, service::HttpServiceHandler,
@@ -38,7 +37,7 @@ pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
impl<T, S, B> H1Service<T, S, B> impl<T, S, B> H1Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
@@ -63,21 +62,20 @@ impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<TcpStream, Codec>), Config = (), Response = ()>,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
@@ -102,9 +100,11 @@ where
mod openssl { mod openssl {
use super::*; use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::{ use actix_tls::accept::{
openssl::{Acceptor, SslAcceptor, SslError, TlsStream}, openssl::{
reexports::{Error as SslError, SslAcceptor},
Acceptor, TlsStream,
},
TlsError, TlsError,
}; };
@@ -112,16 +112,15 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -130,10 +129,10 @@ mod openssl {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create OpenSSL based service.
pub fn openssl( pub fn openssl(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@@ -145,11 +144,13 @@ mod openssl {
InitError = (), InitError = (),
> { > {
Acceptor::new(acceptor) Acceptor::new(acceptor)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(|io: TlsStream<TcpStream>| { })
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok(); let peer_addr = io.get_ref().peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}) })
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
@@ -158,30 +159,30 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*;
use std::io; use std::io;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{ use actix_tls::accept::{
rustls::{Acceptor, ServerConfig, TlsStream}, rustls::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError, TlsError,
}; };
use super::*;
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -190,10 +191,10 @@ mod rustls {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self, self,
config: ServerConfig, config: ServerConfig,
@@ -205,11 +206,13 @@ mod rustls {
InitError = (), InitError = (),
> { > {
Acceptor::new(config) Acceptor::new(config)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(|io: TlsStream<TcpStream>| { })
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok(); let peer_addr = io.get_ref().0.peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}) })
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
@@ -219,7 +222,7 @@ mod rustls {
impl<T, S, B, X, U> H1Service<T, S, B, X, U> impl<T, S, B, X, U> H1Service<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
@@ -227,7 +230,7 @@ where
pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> H1Service<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Response = Request>, X1: ServiceFactory<Request, Response = Request>,
X1::Error: Into<Response<AnyBody>>, X1::Error: Into<Response<BoxBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
H1Service { H1Service {
@@ -270,21 +273,20 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@@ -340,17 +342,16 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;

View File

@@ -1,22 +1,30 @@
use std::future::Future; use std::{
use std::pin::Pin; future::Future,
use std::task::{Context, Poll}; pin::Pin,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use pin_project_lite::pin_project;
use crate::body::{BodySize, MessageBody}; use crate::{
use crate::error::Error; body::{BodySize, MessageBody},
use crate::h1::{Codec, Message}; error::Error,
use crate::response::Response; h1::{Codec, Message},
response::Response,
};
/// Send HTTP/1 response pin_project! {
#[pin_project::pin_project] /// Send HTTP/1 response
pub struct SendResponse<T, B> { pub struct SendResponse<T, B> {
res: Option<Message<(Response<()>, BodySize)>>, res: Option<Message<(Response<()>, BodySize)>>,
#[pin] #[pin]
body: Option<B>, body: Option<B>,
#[pin] #[pin]
framed: Option<Framed<T, Codec>>, framed: Option<Framed<T, Codec>>,
}
} }
impl<T, B> SendResponse<T, B> impl<T, B> SendResponse<T, B>

View File

@@ -10,17 +10,21 @@ use std::{
}; };
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::{sleep, Sleep};
use actix_service::Service; use actix_service::Service;
use actix_utils::future::poll_fn; use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready; use futures_core::ready;
use h2::server::{Connection, SendResponse}; use h2::{
server::{Connection, SendResponse},
Ping, PingPong,
};
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace}; use log::{error, trace};
use pin_project_lite::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body::{AnyBody, BodySize, MessageBody}, body::{BodySize, BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
service::HttpFlow, service::HttpFlow,
OnConnectData, Payload, Request, Response, ResponseHead, OnConnectData, Payload, Request, Response, ResponseHead,
@@ -36,40 +40,63 @@ pin_project! {
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
_phantom: PhantomData<B>, ping_pong: Option<H2PingPong>,
_phantom: PhantomData<B>
} }
} }
impl<T, S, B, X, U> Dispatcher<T, S, B, X, U> { impl<T, S, B, X, U> Dispatcher<T, S, B, X, U>
where
T: AsyncRead + AsyncWrite + Unpin,
{
pub(crate) fn new( pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>, flow: Rc<HttpFlow<S, X, U>>,
connection: Connection<T, Bytes>, mut conn: Connection<T, Bytes>,
on_connect_data: OnConnectData, on_connect_data: OnConnectData,
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
timer: Option<Pin<Box<Sleep>>>,
) -> Self { ) -> Self {
let ping_pong = config.keep_alive().map(|dur| H2PingPong {
timer: timer
.map(|mut timer| {
// reset timer if it's received from new function.
timer.as_mut().reset(config.now() + dur);
timer
})
.unwrap_or_else(|| Box::pin(sleep(dur))),
on_flight: false,
ping_pong: conn.ping_pong().unwrap(),
});
Self { Self {
flow, flow,
config, config,
peer_addr, peer_addr,
connection, connection: conn,
on_connect_data, on_connect_data,
ping_pong,
_phantom: PhantomData, _phantom: PhantomData,
} }
} }
} }
struct H2PingPong {
timer: Pin<Box<Sleep>>,
on_flight: bool,
ping_pong: PingPong,
}
impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U> impl<T, S, B, X, U> Future for Dispatcher<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), crate::error::DispatchError>; type Output = Result<(), crate::error::DispatchError>;
@@ -77,9 +104,9 @@ where
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut(); let this = self.get_mut();
while let Some((req, tx)) = loop {
ready!(Pin::new(&mut this.connection).poll_accept(cx)?) match Pin::new(&mut this.connection).poll_accept(cx)? {
{ Poll::Ready(Some((req, tx))) => {
let (parts, body) = req.into_parts(); let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body); let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl); let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
@@ -104,7 +131,7 @@ where
let res = match fut.await { let res = match fut.await {
Ok(res) => handle_response(res.into(), tx, config).await, Ok(res) => handle_response(res.into(), tx, config).await,
Err(err) => { Err(err) => {
let res: Response<AnyBody> = err.into(); let res: Response<BoxBody> = err.into();
handle_response(res, tx, config).await handle_response(res, tx, config).await
} }
}; };
@@ -123,8 +150,46 @@ where
} }
}); });
} }
Poll::Ready(None) => return Poll::Ready(Ok(())),
Poll::Pending => match this.ping_pong.as_mut() {
Some(ping_pong) => loop {
if ping_pong.on_flight {
// When have on flight ping pong. poll pong and and keep alive timer.
// on success pong received update keep alive timer to determine the next timing of
// ping pong.
match ping_pong.ping_pong.poll_pong(cx)? {
Poll::Ready(_) => {
ping_pong.on_flight = false;
Poll::Ready(Ok(())) let dead_line =
this.config.keep_alive_expire().unwrap();
ping_pong.timer.as_mut().reset(dead_line);
}
Poll::Pending => {
return ping_pong
.timer
.as_mut()
.poll(cx)
.map(|_| Ok(()))
}
}
} else {
// When there is no on flight ping pong. keep alive timer is used to wait for next
// timing of ping pong. Therefore at this point it serves as an interval instead.
ready!(ping_pong.timer.as_mut().poll(cx));
ping_pong.ping_pong.send_ping(Ping::opaque())?;
let dead_line = this.config.keep_alive_expire().unwrap();
ping_pong.timer.as_mut().reset(dead_line);
ping_pong.on_flight = true;
}
},
None => return Poll::Pending,
},
}
}
} }
} }
@@ -141,7 +206,6 @@ async fn handle_response<B>(
) -> Result<(), DispatchError> ) -> Result<(), DispatchError>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
@@ -226,9 +290,11 @@ fn prepare_response(
let _ = match size { let _ = match size {
BodySize::None | BodySize::Stream => None, BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
BodySize::Sized(0) => res
.headers_mut() .headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")), .insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => { BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new(); let mut buf = itoa::Buffer::new();
@@ -243,7 +309,7 @@ fn prepare_response(
for (key, value) in head.headers.iter() { for (key, value) in head.headers.iter() {
match *key { match *key {
// TODO: consider skipping other headers according to: // TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2 // https://datatracker.ietf.org/doc/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers // omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue, CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue, CONTENT_LENGTH if skip_len => continue,

View File

@@ -1,20 +1,30 @@
//! HTTP/2 protocol. //! HTTP/2 protocol.
use std::{ use std::{
future::Future,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::time::Sleep;
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use h2::RecvStream; use h2::{
server::{handshake, Connection, Handshake},
RecvStream,
};
mod dispatcher; mod dispatcher;
mod service; mod service;
pub use self::dispatcher::Dispatcher; pub use self::dispatcher::Dispatcher;
pub use self::service::H2Service; pub use self::service::H2Service;
use crate::error::PayloadError;
use crate::{
config::ServiceConfig,
error::{DispatchError, PayloadError},
};
/// HTTP/2 peer stream. /// HTTP/2 peer stream.
pub struct Payload { pub struct Payload {
@@ -50,3 +60,44 @@ impl Stream for Payload {
} }
} }
} }
pub(crate) fn handshake_with_timeout<T>(
io: T,
config: &ServiceConfig,
) -> HandshakeWithTimeout<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
HandshakeWithTimeout {
handshake: handshake(io),
timer: config.client_timer().map(Box::pin),
}
}
pub(crate) struct HandshakeWithTimeout<T: AsyncRead + AsyncWrite + Unpin> {
handshake: Handshake<T>,
timer: Option<Pin<Box<Sleep>>>,
}
impl<T> Future for HandshakeWithTimeout<T>
where
T: AsyncRead + AsyncWrite + Unpin,
{
type Output = Result<(Connection<T, Bytes>, Option<Pin<Box<Sleep>>>), DispatchError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.get_mut();
match Pin::new(&mut this.handshake).poll(cx)? {
// return the timer on success handshake. It can be re-used for h2 ping-pong.
Poll::Ready(conn) => Poll::Ready(Ok((conn, this.timer.take()))),
Poll::Pending => match this.timer.as_mut() {
Some(timer) => {
ready!(timer.as_mut().poll(cx));
Poll::Ready(Err(DispatchError::SlowRequestTimeout))
}
None => Poll::Pending,
},
}
}
}

View File

@@ -1,5 +1,4 @@
use std::{ use std::{
error::Error as StdError,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
net, net,
@@ -15,20 +14,18 @@ use actix_service::{
ServiceFactoryExt as _, ServiceFactoryExt as _,
}; };
use actix_utils::future::ready; use actix_utils::future::ready;
use bytes::Bytes;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use log::error; use log::error;
use crate::{ use crate::{
body::{AnyBody, MessageBody}, body::{BoxBody, MessageBody},
config::ServiceConfig, config::ServiceConfig,
error::DispatchError, error::DispatchError,
service::HttpFlow, service::HttpFlow,
ConnectCallback, OnConnectData, Request, Response, ConnectCallback, OnConnectData, Request, Response,
}; };
use super::dispatcher::Dispatcher; use super::{dispatcher::Dispatcher, handshake_with_timeout, HandshakeWithTimeout};
/// `ServiceFactory` implementation for HTTP/2 transport /// `ServiceFactory` implementation for HTTP/2 transport
pub struct H2Service<T, S, B> { pub struct H2Service<T, S, B> {
@@ -41,12 +38,11 @@ pub struct H2Service<T, S, B> {
impl<T, S, B> H2Service<T, S, B> impl<T, S, B> H2Service<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create new `H2Service` instance with config. /// Create new `H2Service` instance with config.
pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>( pub(crate) fn with_config<F: IntoServiceFactory<S, Request>>(
@@ -72,12 +68,11 @@ impl<S, B> H2Service<TcpStream, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create plain TCP based service /// Create plain TCP based service
pub fn tcp( pub fn tcp(
@@ -101,9 +96,14 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::{fn_factory, fn_service, ServiceFactoryExt}; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{
reexports::{Error as SslError, SslAcceptor},
Acceptor, TlsStream,
},
TlsError,
};
use super::*; use super::*;
@@ -111,14 +111,13 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create OpenSSL based service /// Create OpenSSL based service.
pub fn openssl( pub fn openssl(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@@ -130,16 +129,14 @@ mod openssl {
InitError = S::InitError, InitError = S::InitError,
> { > {
Acceptor::new(acceptor) Acceptor::new(acceptor)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(fn_factory(|| { })
ready(Ok::<_, S::InitError>(fn_service( .map_err(TlsError::into_service_error)
|io: TlsStream<TcpStream>| { .map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().peer_addr().ok(); let peer_addr = io.get_ref().peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}, })
)))
}))
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
} }
@@ -147,24 +144,27 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*;
use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream};
use actix_tls::accept::TlsError;
use std::io; use std::io;
use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::{
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*;
impl<S, B> H2Service<TlsStream<TcpStream>, S, B> impl<S, B> H2Service<TlsStream<TcpStream>, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create Rustls based service /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@@ -177,19 +177,17 @@ mod rustls {
> { > {
let mut protos = vec![b"h2".to_vec()]; let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols); protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.alpn_protocols = protos;
Acceptor::new(config) Acceptor::new(config)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(fn_factory(|| { })
ready(Ok::<_, S::InitError>(fn_service( .map_err(TlsError::into_service_error)
|io: TlsStream<TcpStream>| { .map(|io: TlsStream<TcpStream>| {
let peer_addr = io.get_ref().0.peer_addr().ok(); let peer_addr = io.get_ref().0.peer_addr().ok();
ready(Ok((io, peer_addr))) (io, peer_addr)
}, })
)))
}))
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
} }
@@ -201,12 +199,11 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@@ -241,7 +238,7 @@ where
impl<T, S, B> H2ServiceHandler<T, S, B> impl<T, S, B> H2ServiceHandler<T, S, B>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
@@ -264,11 +261,10 @@ impl<T, S, B> Service<(T, Option<net::SocketAddr>)> for H2ServiceHandler<T, S, B
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@@ -292,7 +288,7 @@ where
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,
h2_handshake(io), handshake_with_timeout(io, &self.cfg),
), ),
} }
} }
@@ -309,7 +305,7 @@ where
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>, Option<net::SocketAddr>,
OnConnectData, OnConnectData,
H2Handshake<T, Bytes>, HandshakeWithTimeout<T>,
), ),
} }
@@ -317,7 +313,7 @@ pub struct H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
@@ -329,11 +325,10 @@ impl<T, S, B> Future for H2ServiceHandlerResponse<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), DispatchError>; type Output = Result<(), DispatchError>;
@@ -347,7 +342,7 @@ where
ref mut on_connect_data, ref mut on_connect_data,
ref mut handshake, ref mut handshake,
) => match ready!(Pin::new(handshake).poll(cx)) { ) => match ready!(Pin::new(handshake).poll(cx)) {
Ok(conn) => { Ok((conn, timer)) => {
let on_connect_data = std::mem::take(on_connect_data); let on_connect_data = std::mem::take(on_connect_data);
self.state = State::Incoming(Dispatcher::new( self.state = State::Incoming(Dispatcher::new(
srv.take().unwrap(), srv.take().unwrap(),
@@ -355,12 +350,13 @@ where
on_connect_data, on_connect_data,
config.take().unwrap(), config.take().unwrap(),
*peer_addr, *peer_addr,
timer,
)); ));
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {
trace!("H2 handshake error: {}", err); trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err.into())) Poll::Ready(Err(err))
} }
}, },
} }

View File

@@ -1,11 +1,12 @@
//! Helper trait for types that can be effectively borrowed as a [HeaderValue]. //! Sealed [`AsHeaderName`] trait and implementations.
//!
//! [HeaderValue]: crate::http::HeaderValue
use std::{borrow::Cow, str::FromStr}; use std::{borrow::Cow, str::FromStr as _};
use http::header::{HeaderName, InvalidHeaderName}; use http::header::{HeaderName, InvalidHeaderName};
/// Sealed trait implemented for types that can be effectively borrowed as a [`HeaderValue`].
///
/// [`HeaderValue`]: crate::http::HeaderValue
pub trait AsHeaderName: Sealed {} pub trait AsHeaderName: Sealed {}
pub struct Seal; pub struct Seal;

View File

@@ -1,4 +1,6 @@
use std::convert::TryFrom; //! [`IntoHeaderPair`] trait and implementations.
use std::convert::TryFrom as _;
use http::{ use http::{
header::{HeaderName, InvalidHeaderName, InvalidHeaderValue}, header::{HeaderName, InvalidHeaderName, InvalidHeaderValue},
@@ -7,7 +9,10 @@ use http::{
use super::{Header, IntoHeaderValue}; use super::{Header, IntoHeaderValue};
/// Transforms structures into header K/V pairs for inserting into `HeaderMap`s. /// An interface for types that can be converted into a [`HeaderName`]/[`HeaderValue`] pair for
/// insertion into a [`HeaderMap`].
///
/// [`HeaderMap`]: crate::http::HeaderMap
pub trait IntoHeaderPair: Sized { pub trait IntoHeaderPair: Sized {
type Error: Into<HttpError>; type Error: Into<HttpError>;

View File

@@ -1,10 +1,12 @@
use std::convert::TryFrom; //! [`IntoHeaderValue`] trait and implementations.
use std::convert::TryFrom as _;
use bytes::Bytes; use bytes::Bytes;
use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue}; use http::{header::InvalidHeaderValue, Error as HttpError, HeaderValue};
use mime::Mime; use mime::Mime;
/// A trait for any object that can be Converted to a `HeaderValue` /// An interface for types that can be converted into a [`HeaderValue`].
pub trait IntoHeaderValue: Sized { pub trait IntoHeaderValue: Sized {
/// The type returned in the event of a conversion error. /// The type returned in the event of a conversion error.
type Error: Into<HttpError>; type Error: Into<HttpError>;

View File

@@ -1,6 +1,6 @@
//! A multi-value [`HeaderMap`] and its iterators. //! A multi-value [`HeaderMap`] and its iterators.
use std::{borrow::Cow, collections::hash_map, ops}; use std::{borrow::Cow, collections::hash_map, iter, ops};
use ahash::AHashMap; use ahash::AHashMap;
use http::header::{HeaderName, HeaderValue}; use http::header::{HeaderName, HeaderValue};
@@ -14,7 +14,7 @@ use crate::header::AsHeaderName;
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::http::{header, HeaderMap, HeaderValue}; /// use actix_http::header::{self, HeaderMap, HeaderValue};
/// ///
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
@@ -75,7 +75,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::HeaderMap; /// # use actix_http::header::HeaderMap;
/// let map = HeaderMap::new(); /// let map = HeaderMap::new();
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
@@ -92,7 +92,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::HeaderMap; /// # use actix_http::header::HeaderMap;
/// let map = HeaderMap::with_capacity(16); /// let map = HeaderMap::with_capacity(16);
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
@@ -139,7 +139,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert_eq!(map.len(), 0); /// assert_eq!(map.len(), 0);
/// ///
@@ -162,7 +162,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert_eq!(map.len_keys(), 0); /// assert_eq!(map.len_keys(), 0);
/// ///
@@ -181,7 +181,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert!(map.is_empty()); /// assert!(map.is_empty());
/// ///
@@ -198,7 +198,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
@@ -231,7 +231,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
@@ -264,7 +264,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1")); /// map.insert(header::SET_COOKIE, HeaderValue::from_static("one=1"));
@@ -288,12 +288,12 @@ impl HeaderMap {
/// Returns an iterator over all values associated with a header name. /// Returns an iterator over all values associated with a header name.
/// ///
/// The returned iterator does not incur any allocations and will yield no items if there are no /// The returned iterator does not incur any allocations and will yield no items if there are no
/// values associated with the key. Iteration order is **not** guaranteed to be the same as /// values associated with the key. Iteration order is guaranteed to be the same as
/// insertion order. /// insertion order.
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut none_iter = map.get_all(header::ORIGIN); /// let mut none_iter = map.get_all(header::ORIGIN);
@@ -319,7 +319,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// assert!(!map.contains_key(header::ACCEPT)); /// assert!(!map.contains_key(header::ACCEPT));
/// ///
@@ -342,7 +342,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain")); /// map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
@@ -355,6 +355,19 @@ impl HeaderMap {
/// ///
/// assert_eq!(map.len(), 1); /// assert_eq!(map.len(), 1);
/// ``` /// ```
///
/// A convenience method is provided on the returned iterator to check if the insertion replaced
/// any values.
/// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new();
///
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/plain"));
/// assert!(removed.is_empty());
///
/// let removed = map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
/// assert!(!removed.is_empty());
/// ```
pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed { pub fn insert(&mut self, key: HeaderName, val: HeaderValue) -> Removed {
let value = self.inner.insert(key, Value::one(val)); let value = self.inner.insert(key, Value::one(val));
Removed::new(value) Removed::new(value)
@@ -368,7 +381,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.append(header::HOST, HeaderValue::from_static("example.com")); /// map.append(header::HOST, HeaderValue::from_static("example.com"));
@@ -393,9 +406,12 @@ impl HeaderMap {
/// Removes all headers for a particular header name from the map. /// Removes all headers for a particular header name from the map.
/// ///
/// Providing an invalid header names (as a string argument) will have no effect and return
/// without error.
///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1")); /// map.append(header::SET_COOKIE, HeaderValue::from_static("one=1"));
@@ -409,6 +425,21 @@ impl HeaderMap {
/// assert!(removed.next().is_none()); /// assert!(removed.next().is_none());
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
/// ```
///
/// A convenience method is provided on the returned iterator to check if the `remove` call
/// actually removed any values.
/// ```
/// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new();
///
/// let removed = map.remove("accept");
/// assert!(removed.is_empty());
///
/// map.insert(header::ACCEPT, HeaderValue::from_static("text/html"));
/// let removed = map.remove("accept");
/// assert!(!removed.is_empty());
/// ```
pub fn remove(&mut self, key: impl AsHeaderName) -> Removed { pub fn remove(&mut self, key: impl AsHeaderName) -> Removed {
let value = match key.try_as_name(super::as_name::Seal) { let value = match key.try_as_name(super::as_name::Seal) {
Ok(Cow::Borrowed(name)) => self.inner.remove(name), Ok(Cow::Borrowed(name)) => self.inner.remove(name),
@@ -428,7 +459,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::HeaderMap; /// # use actix_http::header::HeaderMap;
/// let map = HeaderMap::with_capacity(16); /// let map = HeaderMap::with_capacity(16);
/// ///
/// assert!(map.is_empty()); /// assert!(map.is_empty());
@@ -448,7 +479,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::HeaderMap; /// # use actix_http::header::HeaderMap;
/// let mut map = HeaderMap::with_capacity(2); /// let mut map = HeaderMap::with_capacity(2);
/// assert!(map.capacity() >= 2); /// assert!(map.capacity() >= 2);
/// ///
@@ -468,7 +499,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut iter = map.iter(); /// let mut iter = map.iter();
@@ -500,7 +531,7 @@ impl HeaderMap {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut iter = map.keys(); /// let mut iter = map.keys();
@@ -528,7 +559,7 @@ impl HeaderMap {
/// Keeps the allocated memory for reuse. /// Keeps the allocated memory for reuse.
/// # Examples /// # Examples
/// ``` /// ```
/// # use actix_http::http::{header, HeaderMap, HeaderValue}; /// # use actix_http::header::{self, HeaderMap, HeaderValue};
/// let mut map = HeaderMap::new(); /// let mut map = HeaderMap::new();
/// ///
/// let mut iter = map.drain(); /// let mut iter = map.drain();
@@ -550,7 +581,8 @@ impl HeaderMap {
} }
} }
/// Note that this implementation will clone a [HeaderName] for each value. /// Note that this implementation will clone a [HeaderName] for each value. Consider using
/// [`drain`](Self::drain) to control header name cloning.
impl IntoIterator for HeaderMap { impl IntoIterator for HeaderMap {
type Item = (HeaderName, HeaderValue); type Item = (HeaderName, HeaderValue);
type IntoIter = IntoIter; type IntoIter = IntoIter;
@@ -571,7 +603,7 @@ impl<'a> IntoIterator for &'a HeaderMap {
} }
} }
/// Iterator for all values with the same header name. /// Iterator over borrowed values with the same associated name.
/// ///
/// See [`HeaderMap::get_all`]. /// See [`HeaderMap::get_all`].
#[derive(Debug)] #[derive(Debug)]
@@ -613,18 +645,36 @@ impl<'a> Iterator for GetAll<'a> {
} }
} }
/// Iterator for owned [`HeaderValue`]s with the same associated [`HeaderName`] returned from methods impl ExactSizeIterator for GetAll<'_> {}
/// on [`HeaderMap`] that remove or replace items.
impl iter::FusedIterator for GetAll<'_> {}
/// Iterator over removed, owned values with the same associated name.
///
/// Returned from methods that remove or replace items. See [`HeaderMap::insert`]
/// and [`HeaderMap::remove`].
#[derive(Debug)] #[derive(Debug)]
pub struct Removed { pub struct Removed {
inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>, inner: Option<smallvec::IntoIter<[HeaderValue; 4]>>,
} }
impl<'a> Removed { impl Removed {
fn new(value: Option<Value>) -> Self { fn new(value: Option<Value>) -> Self {
let inner = value.map(|value| value.inner.into_iter()); let inner = value.map(|value| value.inner.into_iter());
Self { inner } Self { inner }
} }
/// Returns true if iterator contains no elements, without consuming it.
///
/// If called immediately after [`HeaderMap::insert`] or [`HeaderMap::remove`], it will indicate
/// wether any items were actually replaced or removed, respectively.
pub fn is_empty(&self) -> bool {
match self.inner {
// size hint lower bound of smallvec is the correct length
Some(ref iter) => iter.size_hint().0 == 0,
None => true,
}
}
} }
impl Iterator for Removed { impl Iterator for Removed {
@@ -644,7 +694,11 @@ impl Iterator for Removed {
} }
} }
/// Iterator over all [`HeaderName`]s in the map. impl ExactSizeIterator for Removed {}
impl iter::FusedIterator for Removed {}
/// Iterator over all names in the map.
#[derive(Debug)] #[derive(Debug)]
pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>); pub struct Keys<'a>(hash_map::Keys<'a, HeaderName, Value>);
@@ -662,6 +716,11 @@ impl<'a> Iterator for Keys<'a> {
} }
} }
impl ExactSizeIterator for Keys<'_> {}
impl iter::FusedIterator for Keys<'_> {}
/// Iterator over borrowed name-value pairs.
#[derive(Debug)] #[derive(Debug)]
pub struct Iter<'a> { pub struct Iter<'a> {
inner: hash_map::Iter<'a, HeaderName, Value>, inner: hash_map::Iter<'a, HeaderName, Value>,
@@ -713,6 +772,10 @@ impl<'a> Iterator for Iter<'a> {
} }
} }
impl ExactSizeIterator for Iter<'_> {}
impl iter::FusedIterator for Iter<'_> {}
/// Iterator over drained name-value pairs. /// Iterator over drained name-value pairs.
/// ///
/// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning. /// Iterator items are `(Option<HeaderName>, HeaderValue)` to avoid cloning.
@@ -764,6 +827,10 @@ impl<'a> Iterator for Drain<'a> {
} }
} }
impl ExactSizeIterator for Drain<'_> {}
impl iter::FusedIterator for Drain<'_> {}
/// Iterator over owned name-value pairs. /// Iterator over owned name-value pairs.
/// ///
/// Implementation necessarily clones header names for each value. /// Implementation necessarily clones header names for each value.
@@ -814,12 +881,27 @@ impl Iterator for IntoIter {
} }
} }
impl ExactSizeIterator for IntoIter {}
impl iter::FusedIterator for IntoIter {}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::iter::FusedIterator;
use http::header; use http::header;
use static_assertions::assert_impl_all;
use super::*; use super::*;
assert_impl_all!(HeaderMap: IntoIterator);
assert_impl_all!(Keys<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(GetAll<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Removed: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Iter<'_>: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(IntoIter: Iterator, ExactSizeIterator, FusedIterator);
assert_impl_all!(Drain<'_>: Iterator, ExactSizeIterator, FusedIterator);
#[test] #[test]
fn create() { fn create() {
let map = HeaderMap::new(); let map = HeaderMap::new();
@@ -945,6 +1027,56 @@ mod tests {
assert_eq!(vals.next(), removed.next().as_ref()); assert_eq!(vals.next(), removed.next().as_ref());
} }
#[test]
fn get_all_iteration_order_matches_insertion_order() {
let mut map = HeaderMap::new();
let mut vals = map.get_all(header::COOKIE);
assert!(vals.next().is_none());
map.append(header::COOKIE, HeaderValue::from_static("1"));
let mut vals = map.get_all(header::COOKIE);
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
assert!(vals.next().is_none());
map.append(header::COOKIE, HeaderValue::from_static("2"));
let mut vals = map.get_all(header::COOKIE);
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
assert_eq!(vals.next().unwrap().as_bytes(), b"2");
assert!(vals.next().is_none());
map.append(header::COOKIE, HeaderValue::from_static("3"));
map.append(header::COOKIE, HeaderValue::from_static("4"));
map.append(header::COOKIE, HeaderValue::from_static("5"));
let mut vals = map.get_all(header::COOKIE);
assert_eq!(vals.next().unwrap().as_bytes(), b"1");
assert_eq!(vals.next().unwrap().as_bytes(), b"2");
assert_eq!(vals.next().unwrap().as_bytes(), b"3");
assert_eq!(vals.next().unwrap().as_bytes(), b"4");
assert_eq!(vals.next().unwrap().as_bytes(), b"5");
assert!(vals.next().is_none());
let _ = map.insert(header::COOKIE, HeaderValue::from_static("6"));
let mut vals = map.get_all(header::COOKIE);
assert_eq!(vals.next().unwrap().as_bytes(), b"6");
assert!(vals.next().is_none());
let _ = map.insert(header::COOKIE, HeaderValue::from_static("7"));
let _ = map.insert(header::COOKIE, HeaderValue::from_static("8"));
let mut vals = map.get_all(header::COOKIE);
assert_eq!(vals.next().unwrap().as_bytes(), b"8");
assert!(vals.next().is_none());
map.append(header::COOKIE, HeaderValue::from_static("9"));
let mut vals = map.get_all(header::COOKIE);
assert_eq!(vals.next().unwrap().as_bytes(), b"8");
assert_eq!(vals.next().unwrap().as_bytes(), b"9");
assert!(vals.next().is_none());
// check for fused-ness
assert!(vals.next().is_none());
}
fn owned_pair<'a>( fn owned_pair<'a>(
(name, val): (&'a HeaderName, &'a HeaderValue), (name, val): (&'a HeaderName, &'a HeaderValue),
) -> (HeaderName, HeaderValue) { ) -> (HeaderName, HeaderValue) {

View File

@@ -29,35 +29,34 @@ pub use http::header::{
X_FRAME_OPTIONS, X_XSS_PROTECTION, X_FRAME_OPTIONS, X_XSS_PROTECTION,
}; };
use crate::error::ParseError; use crate::{error::ParseError, HttpMessage};
use crate::HttpMessage;
mod as_name; mod as_name;
mod into_pair; mod into_pair;
mod into_value; mod into_value;
mod utils; pub mod map;
pub(crate) mod map;
mod shared; mod shared;
mod utils;
#[doc(hidden)]
pub use self::shared::*;
pub use self::as_name::AsHeaderName; pub use self::as_name::AsHeaderName;
pub use self::into_pair::IntoHeaderPair; pub use self::into_pair::IntoHeaderPair;
pub use self::into_value::IntoHeaderValue; pub use self::into_value::IntoHeaderValue;
#[doc(hidden)]
pub use self::map::GetAll;
pub use self::map::HeaderMap; pub use self::map::HeaderMap;
pub use self::utils::*; pub use self::shared::{
parse_extended_value, q, Charset, ContentEncoding, ExtendedValue, HttpDate,
LanguageTag, Quality, QualityItem,
};
pub use self::utils::{
fmt_comma_delimited, from_comma_delimited, from_one_raw_str, http_percent_encode,
};
/// A trait for any object that already represents a valid header field and value. /// An interface for types that already represent a valid header.
pub trait Header: IntoHeaderValue { pub trait Header: IntoHeaderValue {
/// Returns the name of the header field /// Returns the name of the header field
fn name() -> HeaderName; fn name() -> HeaderName;
/// Parse a header /// Parse a header
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<M: HttpMessage>(msg: &M) -> Result<Self, ParseError>;
} }
/// Convert `http::HeaderMap` to our `HeaderMap`. /// Convert `http::HeaderMap` to our `HeaderMap`.
@@ -68,7 +67,7 @@ impl From<http::HeaderMap> for HeaderMap {
} }
/// This encode set is used for HTTP header values and is defined at /// This encode set is used for HTTP header values and is defined at
/// https://tools.ietf.org/html/rfc5987#section-3.2. /// <https://datatracker.ietf.org/doc/html/rfc5987#section-3.2>.
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
.add(b' ') .add(b' ')
.add(b'"') .add(b'"')

View File

@@ -1,14 +1,13 @@
use std::fmt::{self, Display}; use std::{fmt, str};
use std::str::FromStr;
use self::Charset::*; use self::Charset::*;
/// A Mime charset. /// A MIME character set.
/// ///
/// The string representation is normalized to upper case. /// The string representation is normalized to upper case.
/// ///
/// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>. /// See <http://www.iana.org/assignments/character-sets/character-sets.xhtml>.
#[derive(Clone, Debug, PartialEq)] #[derive(Debug, Clone, PartialEq, Eq)]
#[allow(non_camel_case_types)] #[allow(non_camel_case_types)]
pub enum Charset { pub enum Charset {
/// US ASCII /// US ASCII
@@ -88,20 +87,20 @@ impl Charset {
Iso_8859_8_E => "ISO-8859-8-E", Iso_8859_8_E => "ISO-8859-8-E",
Iso_8859_8_I => "ISO-8859-8-I", Iso_8859_8_I => "ISO-8859-8-I",
Gb2312 => "GB2312", Gb2312 => "GB2312",
Big5 => "big5", Big5 => "Big5",
Koi8_R => "KOI8-R", Koi8_R => "KOI8-R",
Ext(ref s) => s, Ext(ref s) => s,
} }
} }
} }
impl Display for Charset { impl fmt::Display for Charset {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.label()) f.write_str(self.label())
} }
} }
impl FromStr for Charset { impl str::FromStr for Charset {
type Err = crate::Error; type Err = crate::Error;
fn from_str(s: &str) -> Result<Charset, crate::Error> { fn from_str(s: &str) -> Result<Charset, crate::Error> {
@@ -128,7 +127,7 @@ impl FromStr for Charset {
"ISO-8859-8-E" => Iso_8859_8_E, "ISO-8859-8-E" => Iso_8859_8_E,
"ISO-8859-8-I" => Iso_8859_8_I, "ISO-8859-8-I" => Iso_8859_8_I,
"GB2312" => Gb2312, "GB2312" => Gb2312,
"big5" => Big5, "BIG5" => Big5,
"KOI8-R" => Koi8_R, "KOI8-R" => Koi8_R,
s => Ext(s.to_owned()), s => Ext(s.to_owned()),
}) })

View File

@@ -9,14 +9,17 @@ use crate::{
HttpMessage, HttpMessage,
}; };
/// Error return when a content encoding is unknown. /// Error returned when a content encoding is unknown.
///
/// Example: 'compress'
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "unsupported content encoding")] #[display(fmt = "unsupported content encoding")]
pub struct ContentEncodingParseError; pub struct ContentEncodingParseError;
/// Represents a supported content encoding. /// Represents a supported content encoding.
///
/// Includes a commonly-used subset of media types appropriate for use as HTTP content encodings.
/// See [IANA HTTP Content Coding Registry].
///
/// [IANA HTTP Content Coding Registry]: https://www.iana.org/assignments/http-parameters/http-parameters.xhtml
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive] #[non_exhaustive]
pub enum ContentEncoding { pub enum ContentEncoding {
@@ -32,7 +35,7 @@ pub enum ContentEncoding {
/// Gzip algorithm. /// Gzip algorithm.
Gzip, Gzip,
// Zstd algorithm. /// Zstd algorithm.
Zstd, Zstd,
/// Indicates the identity function (i.e. no compression, nor modification). /// Indicates the identity function (i.e. no compression, nor modification).

View File

@@ -1,17 +1,17 @@
//! Originally taken from `hyper::header::parsing`.
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use language_tags::LanguageTag; use language_tags::LanguageTag;
use crate::header::{Charset, HTTP_VALUE}; 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 value part of an extended parameter consisting of three parts:
/// - The REQUIRED character set name (`charset`). /// - The REQUIRED character set name (`charset`).
/// - The OPTIONAL language information (`language_tag`). /// - The OPTIONAL language information (`language_tag`).
/// - A character sequence representing the actual value (`value`), separated by single quotes. /// - A character sequence representing the actual value (`value`), separated by single quotes.
/// ///
/// It is defined in [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). /// It is defined in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
#[derive(Clone, Debug, PartialEq)] #[derive(Clone, Debug, PartialEq)]
pub struct ExtendedValue { pub struct ExtendedValue {
/// The character set that is used to encode the `value` to a string. /// The character set that is used to encode the `value` to a string.
@@ -24,17 +24,17 @@ pub struct ExtendedValue {
pub value: Vec<u8>, pub value: Vec<u8>,
} }
/// Parses extended header parameter values (`ext-value`), as defined in /// Parses extended header parameter values (`ext-value`), as defined
/// [RFC 5987](https://tools.ietf.org/html/rfc5987#section-3.2). /// in [RFC 5987 §3.2](https://datatracker.ietf.org/doc/html/rfc5987#section-3.2).
/// ///
/// Extended values are denoted by parameter names that end with `*`. /// Extended values are denoted by parameter names that end with `*`.
/// ///
/// ## ABNF /// ## ABNF
/// ///
/// ```text /// ```plain
/// ext-value = charset "'" [ language ] "'" value-chars /// ext-value = charset "'" [ language ] "'" value-chars
/// ; like RFC 2231's <extended-initial-value> /// ; like RFC 2231's <extended-initial-value>
/// ; (see [RFC2231], Section 7) /// ; (see [RFC 2231 §7])
/// ///
/// charset = "UTF-8" / "ISO-8859-1" / mime-charset /// charset = "UTF-8" / "ISO-8859-1" / mime-charset
/// ///
@@ -43,22 +43,26 @@ pub struct ExtendedValue {
/// / "!" / "#" / "$" / "%" / "&" /// / "!" / "#" / "$" / "%" / "&"
/// / "+" / "-" / "^" / "_" / "`" /// / "+" / "-" / "^" / "_" / "`"
/// / "{" / "}" / "~" /// / "{" / "}" / "~"
/// ; as <mime-charset> in Section 2.3 of [RFC2978] /// ; as <mime-charset> in [RFC 2978 §2.3]
/// ; except that the single quote is not included /// ; except that the single quote is not included
/// ; SHOULD be registered in the IANA charset registry /// ; SHOULD be registered in the IANA charset registry
/// ///
/// language = <Language-Tag, defined in [RFC5646], Section 2.1> /// language = <Language-Tag, defined in [RFC 5646 §2.1]>
/// ///
/// value-chars = *( pct-encoded / attr-char ) /// value-chars = *( pct-encoded / attr-char )
/// ///
/// pct-encoded = "%" HEXDIG HEXDIG /// pct-encoded = "%" HEXDIG HEXDIG
/// ; see [RFC3986], Section 2.1 /// ; see [RFC 3986 §2.1]
/// ///
/// attr-char = ALPHA / DIGIT /// attr-char = ALPHA / DIGIT
/// / "!" / "#" / "$" / "&" / "+" / "-" / "." /// / "!" / "#" / "$" / "&" / "+" / "-" / "."
/// / "^" / "_" / "`" / "|" / "~" /// / "^" / "_" / "`" / "|" / "~"
/// ; token except ( "*" / "'" / "%" ) /// ; token except ( "*" / "'" / "%" )
/// ``` /// ```
///
/// [RFC 2231 §7]: https://datatracker.ietf.org/doc/html/rfc2231#section-7
/// [RFC 2978 §2.3]: https://datatracker.ietf.org/doc/html/rfc2978#section-2.3
/// [RFC 3986 §2.1]: https://datatracker.ietf.org/doc/html/rfc5646#section-2.1
pub fn parse_extended_value( pub fn parse_extended_value(
val: &str, val: &str,
) -> Result<ExtendedValue, crate::error::ParseError> { ) -> Result<ExtendedValue, crate::error::ParseError> {

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,208 @@
use std::{
convert::{TryFrom, TryInto},
fmt,
};
use derive_more::{Display, Error};
const MAX_QUALITY_INT: u16 = 1000;
const MAX_QUALITY_FLOAT: f32 = 1.0;
/// Represents a quality used in q-factor values.
///
/// The default value is equivalent to `q=1.0` (the [max](Self::MAX) value).
///
/// # Implementation notes
/// The quality value is defined as a number between 0.0 and 1.0 with three decimal places.
/// This means there are 1001 possible values. Since floating point numbers are not exact and the
/// smallest floating point data type (`f32`) consumes four bytes, we use an `u16` value to store
/// the quality internally.
///
/// [RFC 7231 §5.3.1] gives more information on quality values in HTTP header fields.
///
/// # Examples
/// ```
/// use actix_http::header::{Quality, q};
/// assert_eq!(q(1.0), Quality::MAX);
///
/// assert_eq!(q(0.42).to_string(), "0.42");
/// assert_eq!(q(1.0).to_string(), "1");
/// assert_eq!(Quality::MIN.to_string(), "0");
/// ```
///
/// [RFC 7231 §5.3.1]: https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
pub struct Quality(pub(super) u16);
impl Quality {
/// The maximum quality value, equivalent to `q=1.0`.
pub const MAX: Quality = Quality(MAX_QUALITY_INT);
/// The minimum quality value, equivalent to `q=0.0`.
pub const MIN: Quality = Quality(0);
/// Converts a float in the range 0.01.0 to a `Quality`.
///
/// Intentionally private. External uses should rely on the `TryFrom` impl.
///
/// # Panics
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0.
fn from_f32(value: f32) -> Self {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
"q value must be between 0.0 and 1.0"
);
Quality((value * MAX_QUALITY_INT as f32) as u16)
}
}
/// The default value is [`Quality::MAX`].
impl Default for Quality {
fn default() -> Quality {
Quality::MAX
}
}
impl fmt::Display for Quality {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.0 {
0 => f.write_str("0"),
MAX_QUALITY_INT => f.write_str("1"),
// some number in the range 1999
x => {
f.write_str("0.")?;
// This implementation avoids string allocation for removing trailing zeroes.
// In benchmarks it is twice as fast as approach using something like
// `format!("{}").trim_end_matches('0')` for non-fast-path quality values.
if x < 10 {
// x in is range 19
f.write_str("00")?;
// 0 is already handled so it's not possible to have a trailing 0 in this range
// we can just write the integer
itoa::fmt(f, x)
} else if x < 100 {
// x in is range 1099
f.write_str("0")?;
if x % 10 == 0 {
// trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
} else {
// x is in range 100999
if x % 100 == 0 {
// two trailing 0s, divide by 100 and write
itoa::fmt(f, x / 100)
} else if x % 10 == 0 {
// one trailing 0, divide by 10 and write
itoa::fmt(f, x / 10)
} else {
itoa::fmt(f, x)
}
}
}
}
}
}
#[derive(Debug, Clone, Display, Error)]
#[display(fmt = "quality out of bounds")]
#[non_exhaustive]
pub struct QualityOutOfBounds;
impl TryFrom<f32> for Quality {
type Error = QualityOutOfBounds;
#[inline]
fn try_from(value: f32) -> Result<Self, Self::Error> {
if (0.0..=MAX_QUALITY_FLOAT).contains(&value) {
Ok(Quality::from_f32(value))
} else {
Err(QualityOutOfBounds)
}
}
}
/// Convenience function to create a [`Quality`] from an `f32` (0.01.0).
///
/// Not recommended for use with user input. Rely on the `TryFrom` impls where possible.
///
/// # Panics
/// Panics if value is out of range.
///
/// # Examples
/// ```
/// # use actix_http::header::{q, Quality};
/// let q1 = q(1.0);
/// assert_eq!(q1, Quality::MAX);
///
/// let q2 = q(0.0);
/// assert_eq!(q2, Quality::MIN);
///
/// let q3 = q(0.42);
/// ```
///
/// An out-of-range `f32` quality will panic.
/// ```should_panic
/// # use actix_http::header::q;
/// let _q2 = q(1.42);
/// ```
#[inline]
pub fn q<T>(quality: T) -> Quality
where
T: TryInto<Quality>,
T::Error: fmt::Debug,
{
quality.try_into().expect("quality value was out of bounds")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn q_helper() {
assert_eq!(q(0.5), Quality(500));
}
#[test]
fn display_output() {
assert_eq!(q(0.0).to_string(), "0");
assert_eq!(q(1.0).to_string(), "1");
assert_eq!(q(0.001).to_string(), "0.001");
assert_eq!(q(0.5).to_string(), "0.5");
assert_eq!(q(0.22).to_string(), "0.22");
assert_eq!(q(0.123).to_string(), "0.123");
assert_eq!(q(0.999).to_string(), "0.999");
for x in 0..=1000 {
// if trailing zeroes are handled correctly, we would not expect the serialized length
// to ever exceed "0." + 3 decimal places = 5 in length
assert!(q(x as f32 / 1000.0).to_string().len() <= 5);
}
}
#[test]
#[should_panic]
fn negative_quality() {
q(-1.0);
}
#[test]
#[should_panic]
fn quality_out_of_bounds() {
q(2.0);
}
}

View File

@@ -1,104 +1,65 @@
use std::{ use std::{cmp, convert::TryFrom as _, fmt, str};
cmp,
convert::{TryFrom, TryInto},
fmt,
str::{self, FromStr},
};
use derive_more::{Display, Error};
use crate::error::ParseError; use crate::error::ParseError;
const MAX_QUALITY: u16 = 1000; use super::Quality;
const MAX_FLOAT_QUALITY: f32 = 1.0;
/// Represents a quality used in quality values. /// Represents an item with a quality value as defined
/// in [RFC 7231 §5.3.1](https://datatracker.ietf.org/doc/html/rfc7231#section-5.3.1).
/// ///
/// Can be created with the [`q`] function. /// # Parsing and Formatting
/// This wrapper be used to parse header value items that have a q-factor annotation as well as
/// serialize items with a their q-factor.
/// ///
/// # Implementation notes /// # Ordering
/// Since this context of use for this type is header value items, ordering is defined for
/// `QualityItem`s but _only_ considers the item's quality. Order of appearance should be used as
/// the secondary sorting parameter; i.e., a stable sort over the quality values will produce a
/// correctly sorted sequence.
/// ///
/// The quality value is defined as a number between 0 and 1 with three decimal /// # Examples
/// places. This means there are 1001 possible values. Since floating point /// ```
/// numbers are not exact and the smallest floating point data type (`f32`) /// # use actix_http::header::{QualityItem, q};
/// consumes four bytes, hyper uses an `u16` value to store the /// let q_item: QualityItem<String> = "hello;q=0.3".parse().unwrap();
/// quality internally. For performance reasons you may set quality directly to /// assert_eq!(&q_item.item, "hello");
/// a value between 0 and 1000 e.g. `Quality(532)` matches the quality /// assert_eq!(q_item.quality, q(0.3));
/// `q=0.532`.
/// ///
/// [RFC7231 Section 5.3.1](https://tools.ietf.org/html/rfc7231#section-5.3.1) /// // note that format is normalized compared to parsed item
/// gives more information on quality values in HTTP header fields. /// assert_eq!(q_item.to_string(), "hello; q=0.3");
#[derive(Copy, Clone, Debug, PartialEq, Eq, PartialOrd, Ord)] ///
pub struct Quality(u16); /// // item with q=0.3 is greater than item with q=0.1
/// let q_item_fallback: QualityItem<String> = "abc;q=0.1".parse().unwrap();
impl Quality { /// assert!(q_item > q_item_fallback);
/// # Panics /// ```
/// Panics in debug mode when value is not in the range 0.0 <= n <= 1.0. #[derive(Debug, Clone, PartialEq, Eq)]
fn from_f32(value: f32) -> Self {
// Check that `value` is within range should be done before calling this method.
// Just in case, this debug_assert should catch if we were forgetful.
debug_assert!(
(0.0f32..=1.0f32).contains(&value),
"q value must be between 0.0 and 1.0"
);
Quality((value * MAX_QUALITY as f32) as u16)
}
}
impl Default for Quality {
fn default() -> Quality {
Quality(MAX_QUALITY)
}
}
#[derive(Debug, Clone, Display, Error)]
pub struct QualityOutOfBounds;
impl TryFrom<u16> for Quality {
type Error = QualityOutOfBounds;
fn try_from(value: u16) -> Result<Self, Self::Error> {
if (0..=MAX_QUALITY).contains(&value) {
Ok(Quality(value))
} else {
Err(QualityOutOfBounds)
}
}
}
impl TryFrom<f32> for Quality {
type Error = QualityOutOfBounds;
fn try_from(value: f32) -> Result<Self, Self::Error> {
if (0.0..=MAX_FLOAT_QUALITY).contains(&value) {
Ok(Quality::from_f32(value))
} else {
Err(QualityOutOfBounds)
}
}
}
/// Represents an item with a quality value as defined in
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-5.3.1).
#[derive(Clone, PartialEq, Debug)]
pub struct QualityItem<T> { pub struct QualityItem<T> {
/// The actual contents of the field. /// The wrapped contents of the field.
pub item: T, pub item: T,
/// The quality (client or server preference) for the value. /// The quality (client or server preference) for the value.
pub quality: Quality, pub quality: Quality,
} }
impl<T> QualityItem<T> { impl<T> QualityItem<T> {
/// Creates a new `QualityItem` from an item and a quality. /// Constructs a new `QualityItem` from an item and a quality value.
/// The item can be of any type. ///
/// The quality should be a value in the range [0, 1]. /// The item can be of any type. The quality should be a value in the range [0, 1].
pub fn new(item: T, quality: Quality) -> QualityItem<T> { pub fn new(item: T, quality: Quality) -> Self {
QualityItem { item, quality } QualityItem { item, quality }
} }
/// Constructs a new `QualityItem` from an item, using the maximum q-value.
pub fn max(item: T) -> Self {
Self::new(item, Quality::MAX)
}
/// Constructs a new `QualityItem` from an item, using the minimum q-value.
pub fn min(item: T) -> Self {
Self::new(item, Quality::MIN)
}
} }
impl<T: PartialEq> cmp::PartialOrd for QualityItem<T> { impl<T: PartialEq> PartialOrd for QualityItem<T> {
fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> { fn partial_cmp(&self, other: &QualityItem<T>) -> Option<cmp::Ordering> {
self.quality.partial_cmp(&other.quality) self.quality.partial_cmp(&other.quality)
} }
@@ -108,93 +69,77 @@ impl<T: fmt::Display> fmt::Display for QualityItem<T> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.item, f)?; fmt::Display::fmt(&self.item, f)?;
match self.quality.0 { match self.quality {
MAX_QUALITY => Ok(()), // q-factor value is implied for max value
0 => f.write_str("; q=0"), Quality::MAX => Ok(()),
x => write!(f, "; q=0.{}", format!("{:03}", x).trim_end_matches('0')),
Quality::MIN => f.write_str("; q=0"),
q => write!(f, "; q={}", q),
} }
} }
} }
impl<T: FromStr> FromStr for QualityItem<T> { impl<T: str::FromStr> str::FromStr for QualityItem<T> {
type Err = ParseError; type Err = ParseError;
fn from_str(qitem_str: &str) -> Result<Self, Self::Err> { fn from_str(q_item_str: &str) -> Result<Self, Self::Err> {
if !qitem_str.is_ascii() { if !q_item_str.is_ascii() {
return Err(ParseError::Header); return Err(ParseError::Header);
} }
// Set defaults used if parsing fails. // set defaults used if quality-item parsing fails, i.e., item has no q attribute
let mut raw_item = qitem_str; let mut raw_item = q_item_str;
let mut quality = 1f32; let mut quality = Quality::MAX;
let parts: Vec<_> = qitem_str.rsplitn(2, ';').map(str::trim).collect(); let parts = q_item_str
.rsplit_once(';')
.map(|(item, q_attr)| (item.trim(), q_attr.trim()));
if parts.len() == 2 { if let Some((val, q_attr)) = parts {
// example for item with q-factor: // example for item with q-factor:
// //
// gzip; q=0.65 // gzip;q=0.65
// ^^^^^^ parts[0] // ^^^^ val
// ^^ start // ^^^^^^ q_attr
// ^^ q
// ^^^^ q_val // ^^^^ q_val
// ^^^^ parts[1]
if parts[0].len() < 2 { if q_attr.len() < 2 {
// Can't possibly be an attribute since an attribute needs at least a name followed // Can't possibly be an attribute since an attribute needs at least a name followed
// by an equals sign. And bare identifiers are forbidden. // by an equals sign. And bare identifiers are forbidden.
return Err(ParseError::Header); return Err(ParseError::Header);
} }
let start = &parts[0][0..2]; let q = &q_attr[0..2];
if start == "q=" || start == "Q=" { if q == "q=" || q == "Q=" {
let q_val = &parts[0][2..]; let q_val = &q_attr[2..];
if q_val.len() > 5 { if q_val.len() > 5 {
// longer than 5 indicates an over-precise q-factor // longer than 5 indicates an over-precise q-factor
return Err(ParseError::Header); return Err(ParseError::Header);
} }
let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?; let q_value = q_val.parse::<f32>().map_err(|_| ParseError::Header)?;
let q_value =
Quality::try_from(q_value).map_err(|_| ParseError::Header)?;
if (0f32..=1f32).contains(&q_value) {
quality = q_value; quality = q_value;
raw_item = parts[1]; raw_item = val;
} else {
return Err(ParseError::Header);
}
} }
} }
let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?; let item = raw_item.parse::<T>().map_err(|_| ParseError::Header)?;
// we already checked above that the quality is within range Ok(QualityItem::new(item, quality))
Ok(QualityItem::new(item, Quality::from_f32(quality)))
} }
} }
/// Convenience function to wrap a value in a `QualityItem`
/// Sets `q` to the default 1.0
pub fn qitem<T>(item: T) -> QualityItem<T> {
QualityItem::new(item, Quality::default())
}
/// Convenience function to create a `Quality` from a float or integer.
///
/// Implemented for `u16` and `f32`. Panics if value is out of range.
pub fn q<T>(val: T) -> Quality
where
T: TryInto<Quality>,
T::Error: fmt::Debug,
{
// TODO: on next breaking change, handle unwrap differently
val.try_into().unwrap()
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
// copy of encoding from actix-web headers // copy of encoding from actix-web headers
#[allow(clippy::enum_variant_names)] // allow Encoding prefix on EncodingExt
#[derive(Clone, PartialEq, Debug)] #[derive(Clone, PartialEq, Debug)]
pub enum Encoding { pub enum Encoding {
Chunked, Chunked,
@@ -223,7 +168,7 @@ mod tests {
} }
} }
impl FromStr for Encoding { impl str::FromStr for Encoding {
type Err = crate::error::ParseError; type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> { fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
use Encoding::*; use Encoding::*;
@@ -243,7 +188,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_fmt_q_1() { fn test_quality_item_fmt_q_1() {
use Encoding::*; use Encoding::*;
let x = qitem(Chunked); let x = QualityItem::max(Chunked);
assert_eq!(format!("{}", x), "chunked"); assert_eq!(format!("{}", x), "chunked");
} }
#[test] #[test]
@@ -342,25 +287,8 @@ mod tests {
fn test_quality_item_ordering() { fn test_quality_item_ordering() {
let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap(); let x: QualityItem<Encoding> = "gzip; q=0.5".parse().ok().unwrap();
let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap(); let y: QualityItem<Encoding> = "gzip; q=0.273".parse().ok().unwrap();
let comparision_result: bool = x.gt(&y); let comparison_result: bool = x.gt(&y);
assert!(comparision_result) assert!(comparison_result)
}
#[test]
fn test_quality() {
assert_eq!(q(0.5), Quality(500));
}
#[test]
#[should_panic]
fn test_quality_invalid() {
q(-1.0);
}
#[test]
#[should_panic]
fn test_quality_invalid2() {
q(2.0);
} }
#[test] #[test]

View File

@@ -1,3 +1,5 @@
//! Header parsing utilities.
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use super::HeaderValue; use super::HeaderValue;
@@ -10,9 +12,12 @@ where
I: Iterator<Item = &'a HeaderValue> + 'a, I: Iterator<Item = &'a HeaderValue> + 'a,
T: FromStr, T: FromStr,
{ {
let mut result = Vec::new(); let size_guess = all.size_hint().1.unwrap_or(2);
let mut result = Vec::with_capacity(size_guess);
for h in all { for h in all {
let s = h.to_str().map_err(|_| ParseError::Header)?; let s = h.to_str().map_err(|_| ParseError::Header)?;
result.extend( result.extend(
s.split(',') s.split(',')
.filter_map(|x| match x.trim() { .filter_map(|x| match x.trim() {
@@ -22,6 +27,7 @@ where
.filter_map(|x| x.trim().parse().ok()), .filter_map(|x| x.trim().parse().ok()),
) )
} }
Ok(result) Ok(result)
} }
@@ -30,10 +36,12 @@ where
pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> { pub fn from_one_raw_str<T: FromStr>(val: Option<&HeaderValue>) -> Result<T, ParseError> {
if let Some(line) = val { if let Some(line) = val {
let line = line.to_str().map_err(|_| ParseError::Header)?; let line = line.to_str().map_err(|_| ParseError::Header)?;
if !line.is_empty() { if !line.is_empty() {
return T::from_str(line).or(Err(ParseError::Header)); return T::from_str(line).or(Err(ParseError::Header));
} }
} }
Err(ParseError::Header) Err(ParseError::Header)
} }
@@ -44,19 +52,53 @@ where
T: fmt::Display, T: fmt::Display,
{ {
let mut iter = parts.iter(); let mut iter = parts.iter();
if let Some(part) = iter.next() { if let Some(part) = iter.next() {
fmt::Display::fmt(part, f)?; fmt::Display::fmt(part, f)?;
} }
for part in iter { for part in iter {
f.write_str(", ")?; f.write_str(", ")?;
fmt::Display::fmt(part, f)?; fmt::Display::fmt(part, f)?;
} }
Ok(()) Ok(())
} }
/// Percent encode a sequence of bytes with a character set defined in /// Percent encode a sequence of bytes with a character set defined in [RFC 5987 §3.2].
/// <https://tools.ietf.org/html/rfc5987#section-3.2> ///
/// [RFC 5987 §3.2]: https://datatracker.ietf.org/doc/html/rfc5987#section-3.2
#[inline]
pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result { pub fn http_percent_encode(f: &mut fmt::Formatter<'_>, bytes: &[u8]) -> fmt::Result {
let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE); let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
fmt::Display::fmt(&encoded, f) fmt::Display::fmt(&encoded, f)
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn comma_delimited_parsing() {
let headers = vec![];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![0; 0]);
let headers = vec![
HeaderValue::from_static("1, 2"),
HeaderValue::from_static("3,4"),
];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![1, 2, 3, 4]);
let headers = vec![
HeaderValue::from_static(""),
HeaderValue::from_static(","),
HeaderValue::from_static(" "),
HeaderValue::from_static("1 ,"),
HeaderValue::from_static(""),
];
let res: Vec<usize> = from_comma_delimited(headers.iter()).unwrap();
assert_eq!(res, vec![1]);
}
}

View File

@@ -29,7 +29,6 @@ extern crate log;
pub mod body; pub mod body;
mod builder; mod builder;
pub mod client;
mod config; mod config;
#[cfg(feature = "__compress")] #[cfg(feature = "__compress")]
@@ -44,7 +43,6 @@ mod request;
mod response; mod response;
mod response_builder; mod response_builder;
mod service; mod service;
mod time_parser;
pub mod error; pub mod error;
pub mod h1; pub mod h1;
@@ -55,7 +53,7 @@ pub mod ws;
pub use self::builder::HttpServiceBuilder; pub use self::builder::HttpServiceBuilder;
pub use self::config::{KeepAlive, ServiceConfig}; pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::Error; pub use self::error::Error;
pub use self::extensions::Extensions; pub use self::extensions::{CloneableExtensions, Extensions};
pub use self::header::ContentEncoding; pub use self::header::ContentEncoding;
pub use self::http_message::HttpMessage; pub use self::http_message::HttpMessage;
pub use self::message::ConnectionType; pub use self::message::ConnectionType;
@@ -69,26 +67,6 @@ pub use self::service::HttpService;
pub use ::http::{uri, uri::Uri}; pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version}; pub use ::http::{Method, StatusCode, Version};
// TODO: deprecate this mish-mash of random items
pub mod http {
//! Various HTTP related types.
// re-exports
pub use http::header::{HeaderName, HeaderValue};
pub use http::uri::PathAndQuery;
pub use http::{uri, Error, Uri};
pub use http::{Method, StatusCode, Version};
pub use crate::header::HeaderMap;
/// A collection of HTTP headers and helpers.
pub mod header {
pub use crate::header::*;
}
pub use crate::header::ContentEncoding;
pub use crate::message::ConnectionType;
}
/// A major HTTP protocol version. /// A major HTTP protocol version.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
#[non_exhaustive] #[non_exhaustive]
@@ -98,19 +76,14 @@ pub enum Protocol {
Http3, Http3,
} }
type ConnectCallback<IO> = dyn Fn(&IO, &mut Extensions); type ConnectCallback<IO> = dyn Fn(&IO, &mut CloneableExtensions);
/// Container for data that extract with ConnectCallback. /// Container for data that extract with ConnectCallback.
/// ///
/// # Implementation Details /// # Implementation Details
/// Uses Option to reduce necessary allocations when merging with request extensions. /// Uses Option to reduce necessary allocations when merging with request extensions.
pub(crate) struct OnConnectData(Option<Extensions>); #[derive(Default)]
pub(crate) struct OnConnectData(Option<CloneableExtensions>);
impl Default for OnConnectData {
fn default() -> Self {
Self(None)
}
}
impl OnConnectData { impl OnConnectData {
/// Construct by calling the on-connect callback with the underlying transport I/O. /// Construct by calling the on-connect callback with the underlying transport I/O.
@@ -119,7 +92,7 @@ impl OnConnectData {
on_connect_ext: Option<&ConnectCallback<T>>, on_connect_ext: Option<&ConnectCallback<T>>,
) -> Self { ) -> Self {
let ext = on_connect_ext.map(|handler| { let ext = on_connect_ext.map(|handler| {
let mut extensions = Extensions::new(); let mut extensions = CloneableExtensions::default();
handler(io, &mut extensions); handler(io, &mut extensions);
extensions extensions
}); });
@@ -130,8 +103,8 @@ impl OnConnectData {
/// Merge self into given request's extensions. /// Merge self into given request's extensions.
#[inline] #[inline]
pub(crate) fn merge_into(&mut self, req: &mut Request) { pub(crate) fn merge_into(&mut self, req: &mut Request) {
if let Some(ref mut ext) = self.0 { if let Some(ref ext) = self.0 {
req.head.extensions.get_mut().drain_from(ext); req.head.extensions.get_mut().clone_from(ext);
} }
} }
} }

View File

@@ -46,8 +46,8 @@ pub trait Head: Default + 'static {
#[derive(Debug)] #[derive(Debug)]
pub struct RequestHead { pub struct RequestHead {
pub uri: Uri,
pub method: Method, pub method: Method,
pub uri: Uri,
pub version: Version, pub version: Version,
pub headers: HeaderMap, pub headers: HeaderMap,
pub extensions: RefCell<Extensions>, pub extensions: RefCell<Extensions>,
@@ -58,13 +58,13 @@ pub struct RequestHead {
impl Default for RequestHead { impl Default for RequestHead {
fn default() -> RequestHead { fn default() -> RequestHead {
RequestHead { RequestHead {
uri: Uri::default(),
method: Method::default(), method: Method::default(),
uri: Uri::default(),
version: Version::HTTP_11, version: Version::HTTP_11,
headers: HeaderMap::with_capacity(16), headers: HeaderMap::with_capacity(16),
flags: Flags::empty(),
peer_addr: None,
extensions: RefCell::new(Extensions::new()), extensions: RefCell::new(Extensions::new()),
peer_addr: None,
flags: Flags::empty(),
} }
} }
} }
@@ -192,6 +192,7 @@ impl RequestHead {
} }
#[derive(Debug)] #[derive(Debug)]
#[allow(clippy::large_enum_variant)]
pub enum RequestHeadType { pub enum RequestHeadType {
Owned(RequestHead), Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>), Rc(Rc<RequestHead>, Option<HeaderMap>),
@@ -317,7 +318,7 @@ impl ResponseHead {
} }
#[inline] #[inline]
pub(crate) fn ctype(&self) -> Option<ConnectionType> { pub(crate) fn conn_type(&self) -> Option<ConnectionType> {
if self.flags.contains(Flags::CLOSE) { if self.flags.contains(Flags::CLOSE) {
Some(ConnectionType::Close) Some(ConnectionType::Close)
} else if self.flags.contains(Flags::KEEP_ALIVE) { } else if self.flags.contains(Flags::KEEP_ALIVE) {

View File

@@ -6,14 +6,14 @@ use std::{
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use bytestring::ByteString;
use crate::{ use crate::{
body::{AnyBody, MessageBody}, body::{BoxBody, MessageBody},
error::Error,
extensions::Extensions, extensions::Extensions,
http::{HeaderMap, StatusCode}, header::{self, HeaderMap, IntoHeaderValue},
message::{BoxedResponseHead, ResponseHead}, message::{BoxedResponseHead, ResponseHead},
ResponseBuilder, Error, ResponseBuilder, StatusCode,
}; };
/// An HTTP response. /// An HTTP response.
@@ -22,13 +22,13 @@ pub struct Response<B> {
pub(crate) body: B, pub(crate) body: B,
} }
impl Response<AnyBody> { impl Response<BoxBody> {
/// Constructs a new response with default body. /// Constructs a new response with default body.
#[inline] #[inline]
pub fn new(status: StatusCode) -> Self { pub fn new(status: StatusCode) -> Self {
Response { Response {
head: BoxedResponseHead::new(status), head: BoxedResponseHead::new(status),
body: AnyBody::Empty, body: BoxBody::new(()),
} }
} }
@@ -189,6 +189,14 @@ impl<B> Response<B> {
} }
} }
#[inline]
pub fn map_into_boxed_body(self) -> Response<BoxBody>
where
B: MessageBody + 'static,
{
self.map_body(|_, body| BoxBody::new(body))
}
/// Returns body, consuming this response. /// Returns body, consuming this response.
pub fn into_body(self) -> B { pub fn into_body(self) -> B {
self.body self.body
@@ -223,81 +231,99 @@ impl<B: Default> Default for Response<B> {
} }
} }
impl<I: Into<Response<AnyBody>>, E: Into<Error>> From<Result<I, E>> impl<I: Into<Response<BoxBody>>, E: Into<Error>> From<Result<I, E>>
for Response<AnyBody> for Response<BoxBody>
{ {
fn from(res: Result<I, E>) -> Self { fn from(res: Result<I, E>) -> Self {
match res { match res {
Ok(val) => val.into(), Ok(val) => val.into(),
Err(err) => err.into().into(), Err(err) => Response::from(err.into()),
} }
} }
} }
impl From<ResponseBuilder> for Response<AnyBody> { impl From<ResponseBuilder> for Response<BoxBody> {
fn from(mut builder: ResponseBuilder) -> Self { fn from(mut builder: ResponseBuilder) -> Self {
builder.finish() builder.finish().map_into_boxed_body()
} }
} }
impl From<std::convert::Infallible> for Response<AnyBody> { impl From<std::convert::Infallible> for Response<BoxBody> {
fn from(val: std::convert::Infallible) -> Self { fn from(val: std::convert::Infallible) -> Self {
match val {} match val {}
} }
} }
impl From<&'static str> for Response<AnyBody> { impl From<&'static str> for Response<&'static str> {
fn from(val: &'static str) -> Self { fn from(val: &'static str) -> Self {
Response::build(StatusCode::OK) let mut res = Response::with_body(StatusCode::OK, val);
.content_type(mime::TEXT_PLAIN_UTF_8) let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
.body(val) res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
impl From<&'static [u8]> for Response<AnyBody> { impl From<&'static [u8]> for Response<&'static [u8]> {
fn from(val: &'static [u8]) -> Self { fn from(val: &'static [u8]) -> Self {
Response::build(StatusCode::OK) let mut res = Response::with_body(StatusCode::OK, val);
.content_type(mime::APPLICATION_OCTET_STREAM) let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
.body(val) res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
impl From<String> for Response<AnyBody> { impl From<String> for Response<String> {
fn from(val: String) -> Self { fn from(val: String) -> Self {
Response::build(StatusCode::OK) let mut res = Response::with_body(StatusCode::OK, val);
.content_type(mime::TEXT_PLAIN_UTF_8) let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
.body(val) res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
impl<'a> From<&'a String> for Response<AnyBody> { impl From<&String> for Response<String> {
fn from(val: &'a String) -> Self { fn from(val: &String) -> Self {
Response::build(StatusCode::OK) let mut res = Response::with_body(StatusCode::OK, val.clone());
.content_type(mime::TEXT_PLAIN_UTF_8) let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
.body(val) res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
impl From<Bytes> for Response<AnyBody> { impl From<Bytes> for Response<Bytes> {
fn from(val: Bytes) -> Self { fn from(val: Bytes) -> Self {
Response::build(StatusCode::OK) let mut res = Response::with_body(StatusCode::OK, val);
.content_type(mime::APPLICATION_OCTET_STREAM) let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
.body(val) res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
impl From<BytesMut> for Response<AnyBody> { impl From<BytesMut> for Response<BytesMut> {
fn from(val: BytesMut) -> Self { fn from(val: BytesMut) -> Self {
Response::build(StatusCode::OK) let mut res = Response::with_body(StatusCode::OK, val);
.content_type(mime::APPLICATION_OCTET_STREAM) let mime = mime::APPLICATION_OCTET_STREAM.try_into_value().unwrap();
.body(val) res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
}
}
impl From<ByteString> for Response<ByteString> {
fn from(val: ByteString) -> Self {
let mut res = Response::with_body(StatusCode::OK, val);
let mime = mime::TEXT_PLAIN_UTF_8.try_into_value().unwrap();
res.headers_mut().insert(header::CONTENT_TYPE, mime);
res
} }
} }
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::http::header::{HeaderValue, CONTENT_TYPE, COOKIE}; use crate::{
body::to_bytes,
header::{HeaderValue, CONTENT_TYPE, COOKIE},
};
#[test] #[test]
fn test_debug() { fn test_debug() {
@@ -309,73 +335,73 @@ mod tests {
assert!(dbg.contains("Response")); assert!(dbg.contains("Response"));
} }
#[test] #[actix_rt::test]
fn test_into_response() { async fn test_into_response() {
let resp: Response<AnyBody> = "test".into(); let res = Response::from("test");
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8") HeaderValue::from_static("text/plain; charset=utf-8")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
let resp: Response<AnyBody> = b"test".as_ref().into(); let res = Response::from(b"test".as_ref());
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
let resp: Response<AnyBody> = "test".to_owned().into(); let res = Response::from("test".to_owned());
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8") HeaderValue::from_static("text/plain; charset=utf-8")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
let resp: Response<AnyBody> = (&"test".to_owned()).into(); let res = Response::from("test".to_owned());
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("text/plain; charset=utf-8") HeaderValue::from_static("text/plain; charset=utf-8")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
let b = Bytes::from_static(b"test"); let b = Bytes::from_static(b"test");
let resp: Response<AnyBody> = b.into(); let res = Response::from(b);
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
let b = Bytes::from_static(b"test"); let b = Bytes::from_static(b"test");
let resp: Response<AnyBody> = b.into(); let res = Response::from(b);
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
let b = BytesMut::from("test"); let b = BytesMut::from("test");
let resp: Response<AnyBody> = b.into(); let res = Response::from(b);
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!( assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(), res.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/octet-stream") HeaderValue::from_static("application/octet-stream")
); );
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
assert_eq!(resp.body().get_ref(), b"test"); assert_eq!(to_bytes(res.into_body()).await.unwrap(), &b"test"[..]);
} }
} }

View File

@@ -2,19 +2,11 @@
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefMut},
error::Error as StdError, fmt, str,
fmt,
future::Future,
pin::Pin,
str,
task::{Context, Poll},
}; };
use bytes::Bytes;
use futures_core::Stream;
use crate::{ use crate::{
body::{AnyBody, BodyStream}, body::{EitherBody, MessageBody},
error::{Error, HttpError}, error::{Error, HttpError},
header::{self, IntoHeaderPair, IntoHeaderValue}, header::{self, IntoHeaderPair, IntoHeaderValue},
message::{BoxedResponseHead, ConnectionType, ResponseHead}, message::{BoxedResponseHead, ConnectionType, ResponseHead},
@@ -28,7 +20,7 @@ use crate::{
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::{Response, ResponseBuilder, body, http::StatusCode, http::header}; /// use actix_http::{Response, ResponseBuilder, StatusCode, body, header};
/// ///
/// # actix_rt::System::new().block_on(async { /// # actix_rt::System::new().block_on(async {
/// let mut res: Response<_> = Response::build(StatusCode::OK) /// let mut res: Response<_> = Response::build(StatusCode::OK)
@@ -55,9 +47,7 @@ impl ResponseBuilder {
/// Create response builder /// Create response builder
/// ///
/// # Examples /// # Examples
/// ``` // /// use actix_http::{Response, ResponseBuilder, StatusCode};, / ``
/// use actix_http::{Response, ResponseBuilder, http::StatusCode};
///
/// let res: Response<_> = ResponseBuilder::default().finish(); /// let res: Response<_> = ResponseBuilder::default().finish();
/// assert_eq!(res.status(), StatusCode::OK); /// assert_eq!(res.status(), StatusCode::OK);
/// ``` /// ```
@@ -72,9 +62,7 @@ impl ResponseBuilder {
/// Set HTTP status code of this response. /// Set HTTP status code of this response.
/// ///
/// # Examples /// # Examples
/// ``` // /// use actix_http::{ResponseBuilder, StatusCode};, / ``
/// use actix_http::{ResponseBuilder, http::StatusCode};
///
/// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish(); /// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish();
/// assert_eq!(res.status(), StatusCode::NOT_FOUND); /// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// ``` /// ```
@@ -90,7 +78,7 @@ impl ResponseBuilder {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::{ResponseBuilder, http::header}; /// use actix_http::{ResponseBuilder, header};
/// ///
/// let res = ResponseBuilder::default() /// let res = ResponseBuilder::default()
/// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) /// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
@@ -120,7 +108,7 @@ impl ResponseBuilder {
/// ///
/// # Examples /// # Examples
/// ``` /// ```
/// use actix_http::{ResponseBuilder, http::header}; /// use actix_http::{ResponseBuilder, header};
/// ///
/// let res = ResponseBuilder::default() /// let res = ResponseBuilder::default()
/// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON)) /// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
@@ -235,10 +223,14 @@ impl ResponseBuilder {
/// Generate response with a wrapped body. /// Generate response with a wrapped body.
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
#[inline] pub fn body<B>(&mut self, body: B) -> Response<EitherBody<B>>
pub fn body<B: Into<AnyBody>>(&mut self, body: B) -> Response<AnyBody> { where
self.message_body(body.into()) B: MessageBody + 'static,
.unwrap_or_else(Response::from) {
match self.message_body(body) {
Ok(res) => res.map_body(|_, body| EitherBody::left(body)),
Err(err) => Response::from(err).map_body(|_, body| EitherBody::right(body)),
}
} }
/// Generate response with a body. /// Generate response with a body.
@@ -253,24 +245,12 @@ impl ResponseBuilder {
Ok(Response { head, body }) Ok(Response { head, body })
} }
/// Generate response with a streaming body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> Response<AnyBody>
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
self.body(AnyBody::from_message(BodyStream::new(stream)))
}
/// Generate response with an empty body. /// Generate response with an empty body.
/// ///
/// This `ResponseBuilder` will be left in a useless state. /// This `ResponseBuilder` will be left in a useless state.
#[inline] #[inline]
pub fn finish(&mut self) -> Response<AnyBody> { pub fn finish(&mut self) -> Response<EitherBody<()>> {
self.body(AnyBody::Empty) self.body(())
} }
/// Create an owned `ResponseBuilder`, leaving the original in a useless state. /// Create an owned `ResponseBuilder`, leaving the original in a useless state.
@@ -327,14 +307,6 @@ impl<'a> From<&'a ResponseHead> for ResponseBuilder {
} }
} }
impl Future for ResponseBuilder {
type Output = Result<Response<AnyBody>, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Ok(self.finish()))
}
}
impl fmt::Debug for ResponseBuilder { impl fmt::Debug for ResponseBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = self.head.as_ref().unwrap(); let head = self.head.as_ref().unwrap();
@@ -356,9 +328,10 @@ impl fmt::Debug for ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes;
use super::*; use super::*;
use crate::body::Body; use crate::header::{HeaderName, HeaderValue, CONTENT_TYPE};
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
#[test] #[test]
fn test_basic_builder() { fn test_basic_builder() {
@@ -383,20 +356,28 @@ mod tests {
#[test] #[test]
fn test_force_close() { fn test_force_close() {
let resp = Response::build(StatusCode::OK).force_close().finish(); let resp = Response::build(StatusCode::OK).force_close().finish();
assert!(!resp.keep_alive()) assert!(!resp.keep_alive());
} }
#[test] #[test]
fn test_content_type() { fn test_content_type() {
let resp = Response::build(StatusCode::OK) let resp = Response::build(StatusCode::OK)
.content_type("text/plain") .content_type("text/plain")
.body(Body::Empty); .body(Bytes::new());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain") assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain");
let resp = Response::build(StatusCode::OK)
.content_type(mime::APPLICATION_JAVASCRIPT_UTF_8)
.body(Bytes::new());
assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(),
"application/javascript; charset=utf-8"
);
} }
#[test] #[test]
fn test_into_builder() { fn test_into_builder() {
let mut resp: Response<Body> = "test".into(); let mut resp: Response<_> = "test".into();
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
resp.headers_mut().insert( resp.headers_mut().insert(

View File

@@ -1,5 +1,4 @@
use std::{ use std::{
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
marker::PhantomData, marker::PhantomData,
@@ -9,18 +8,16 @@ use std::{
task::{Context, Poll}, task::{Context, Poll},
}; };
use ::h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{ use actix_service::{
fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _, fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
}; };
use bytes::Bytes;
use futures_core::{future::LocalBoxFuture, ready}; use futures_core::{future::LocalBoxFuture, ready};
use pin_project::pin_project; use pin_project_lite::pin_project;
use crate::{ use crate::{
body::{AnyBody, MessageBody}, body::{BoxBody, MessageBody},
builder::HttpServiceBuilder, builder::HttpServiceBuilder,
config::{KeepAlive, ServiceConfig}, config::{KeepAlive, ServiceConfig},
error::DispatchError, error::DispatchError,
@@ -40,7 +37,7 @@ pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
impl<T, S, B> HttpService<T, S, B> impl<T, S, B> HttpService<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@@ -55,12 +52,11 @@ where
impl<T, S, B> HttpService<T, S, B> impl<T, S, B> HttpService<T, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
/// Create new `HttpService` instance. /// Create new `HttpService` instance.
pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self { pub fn new<F: IntoServiceFactory<S, Request>>(service: F) -> Self {
@@ -95,7 +91,7 @@ where
impl<T, S, B, X, U> HttpService<T, S, B, X, U> impl<T, S, B, X, U> HttpService<T, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
@@ -109,7 +105,7 @@ where
pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U> pub fn expect<X1>(self, expect: X1) -> HttpService<T, S, B, X1, U>
where where
X1: ServiceFactory<Request, Config = (), Response = Request>, X1: ServiceFactory<Request, Config = (), Response = Request>,
X1::Error: Into<Response<AnyBody>>, X1::Error: Into<Response<BoxBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
HttpService { HttpService {
@@ -153,17 +149,16 @@ impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -172,7 +167,7 @@ where
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
@@ -195,9 +190,14 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{
reexports::{Error as SslError, SslAcceptor},
Acceptor, TlsStream,
},
TlsError,
};
use super::*; use super::*;
@@ -205,17 +205,16 @@ mod openssl {
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -224,10 +223,10 @@ mod openssl {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create OpenSSL based service.
pub fn openssl( pub fn openssl(
self, self,
acceptor: SslAcceptor, acceptor: SslAcceptor,
@@ -239,9 +238,11 @@ mod openssl {
InitError = (), InitError = (),
> { > {
Acceptor::new(acceptor) Acceptor::new(acceptor)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
.and_then(|io: TlsStream<TcpStream>| async { })
.map_err(TlsError::into_service_error)
.map(|io: TlsStream<TcpStream>| {
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
if protos.windows(2).any(|window| window == b"h2") { if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2 Protocol::Http2
@@ -251,8 +252,9 @@ mod openssl {
} else { } else {
Protocol::Http1 Protocol::Http1
}; };
let peer_addr = io.get_ref().peer_addr().ok(); let peer_addr = io.get_ref().peer_addr().ok();
Ok((io, proto, peer_addr)) (io, proto, peer_addr)
}) })
.and_then(self.map_err(TlsError::Service)) .and_then(self.map_err(TlsError::Service))
} }
@@ -263,27 +265,28 @@ mod openssl {
mod rustls { mod rustls {
use std::io; use std::io;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, Session, TlsStream}; use actix_service::ServiceFactoryExt as _;
use actix_tls::accept::TlsError; use actix_tls::accept::{
rustls::{reexports::ServerConfig, Acceptor, TlsStream},
TlsError,
};
use super::*; use super::*;
use actix_service::ServiceFactoryExt;
impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> HttpService<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
@@ -292,10 +295,10 @@ mod rustls {
Response = (), Response = (),
>, >,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create Rustls based service.
pub fn rustls( pub fn rustls(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@@ -308,14 +311,15 @@ mod rustls {
> { > {
let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()]; let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols); protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.alpn_protocols = protos;
Acceptor::new(config) Acceptor::new(config)
.map_err(TlsError::Tls) .map_init_err(|_| {
.map_init_err(|_| panic!()) unreachable!("TLS acceptor service factory does not error on init")
})
.map_err(TlsError::into_service_error)
.and_then(|io: TlsStream<TcpStream>| async { .and_then(|io: TlsStream<TcpStream>| async {
let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol() let proto = if let Some(protos) = io.get_ref().1.alpn_protocol() {
{
if protos.windows(2).any(|window| window == b"h2") { if protos.windows(2).any(|window| window == b"h2") {
Protocol::Http2 Protocol::Http2
} else { } else {
@@ -339,22 +343,21 @@ where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: ServiceFactory<Request, Config = (), Response = Request>, X: ServiceFactory<Request, Config = (), Response = Request>,
X::Future: 'static, X::Future: 'static,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U::Future: 'static, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@@ -417,11 +420,11 @@ where
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U> impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
X: Service<Request>, X: Service<Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, h1::Codec>)>, U: Service<(Request, Framed<T, h1::Codec>)>,
U::Error: Into<Response<AnyBody>>, U::Error: Into<Response<BoxBody>>,
{ {
pub(super) fn new( pub(super) fn new(
cfg: ServiceConfig, cfg: ServiceConfig,
@@ -441,7 +444,7 @@ where
pub(super) fn _poll_ready( pub(super) fn _poll_ready(
&self, &self,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Result<(), Response<AnyBody>>> { ) -> Poll<Result<(), Response<BoxBody>>> {
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?; ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
@@ -477,18 +480,17 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display + Into<Response<AnyBody>>, U::Error: fmt::Display + Into<Response<BoxBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
@@ -510,82 +512,93 @@ where
match proto { match proto {
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake(Some(( state: State::H2Handshake {
h2_handshake(io), handshake: Some((
self.cfg.clone(), h2::handshake_with_timeout(io, &self.cfg),
self.flow.clone(),
on_connect_data,
peer_addr,
))),
},
Protocol::Http1 => HttpServiceHandlerResponse {
state: State::H1(h1::Dispatcher::new(
io,
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
on_connect_data, on_connect_data,
peer_addr, peer_addr,
)), )),
}, },
},
Protocol::Http1 => HttpServiceHandlerResponse {
state: State::H1 {
dispatcher: h1::Dispatcher::new(
io,
self.cfg.clone(),
self.flow.clone(),
on_connect_data,
peer_addr,
),
},
},
proto => unimplemented!("Unsupported HTTP version: {:?}.", proto), proto => unimplemented!("Unsupported HTTP version: {:?}.", proto),
} }
} }
} }
#[pin_project(project = StateProj)] pin_project! {
enum State<T, S, B, X, U> #[project = StateProj]
where enum State<T, S, B, X, U>
T: AsyncRead + AsyncWrite + Unpin, where
T: AsyncRead,
T: AsyncWrite,
T: Unpin,
S: Service<Request>, S: Service<Request>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>>, S::Error: Into<Response<BoxBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
H1(#[pin] h1::Dispatcher<T, S, B, X, U>), H1 { #[pin] dispatcher: h1::Dispatcher<T, S, B, X, U> },
H2(#[pin] h2::Dispatcher<T, S, B, X, U>), H2 { #[pin] dispatcher: h2::Dispatcher<T, S, B, X, U> },
H2Handshake( H2Handshake {
Option<( handshake: Option<(
H2Handshake<T, Bytes>, h2::HandshakeWithTimeout<T>,
ServiceConfig, ServiceConfig,
Rc<HttpFlow<S, X, U>>, Rc<HttpFlow<S, X, U>>,
OnConnectData, OnConnectData,
Option<net::SocketAddr>, Option<net::SocketAddr>,
)>, )>,
), },
}
} }
#[pin_project] pin_project! {
pub struct HttpServiceHandlerResponse<T, S, B, X, U> pub struct HttpServiceHandlerResponse<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead,
T: AsyncWrite,
T: Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>>,
S::Error: 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>>,
S::Response: 'static,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
#[pin] #[pin]
state: State<T, S, B, X, U>, state: State<T, S, B, X, U>,
}
} }
impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U> impl<T, S, B, X, U> Future for HttpServiceHandlerResponse<T, S, B, X, U>
@@ -593,15 +606,14 @@ where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Response<AnyBody>> + 'static, S::Error: Into<Response<BoxBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static, B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Response<AnyBody>>, X::Error: Into<Response<BoxBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
@@ -610,27 +622,29 @@ where
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project().state.project() { match self.as_mut().project().state.project() {
StateProj::H1(disp) => disp.poll(cx), StateProj::H1 { dispatcher } => dispatcher.poll(cx),
StateProj::H2(disp) => disp.poll(cx), StateProj::H2 { dispatcher } => dispatcher.poll(cx),
StateProj::H2Handshake(data) => { StateProj::H2Handshake { handshake: data } => {
match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) { match ready!(Pin::new(&mut data.as_mut().unwrap().0).poll(cx)) {
Ok(conn) => { Ok((conn, timer)) => {
let (_, cfg, srv, on_connect_data, peer_addr) = let (_, config, flow, on_connect_data, peer_addr) =
data.take().unwrap(); data.take().unwrap();
self.as_mut().project().state.set(State::H2(
h2::Dispatcher::new( self.as_mut().project().state.set(State::H2 {
srv, dispatcher: h2::Dispatcher::new(
flow,
conn, conn,
on_connect_data, on_connect_data,
cfg, config,
peer_addr, peer_addr,
timer,
), ),
)); });
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {
trace!("H2 handshake error: {}", err); trace!("H2 handshake error: {}", err);
Poll::Ready(Err(err.into())) Poll::Ready(Err(err))
} }
} }
} }

View File

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

View File

@@ -63,8 +63,8 @@ pub enum Item {
Last(Bytes), Last(Bytes),
} }
#[derive(Debug, Copy, Clone)]
/// WebSocket protocol codec. /// WebSocket protocol codec.
#[derive(Debug, Clone)]
pub struct Codec { pub struct Codec {
flags: Flags, flags: Flags,
max_size: usize, max_size: usize,
@@ -89,7 +89,8 @@ impl Codec {
/// Set max frame size. /// Set max frame size.
/// ///
/// By default max size is set to 64kB. /// By default max size is set to 64KiB.
#[must_use = "This returns the a new Codec, without modifying the original."]
pub fn max_size(mut self, size: usize) -> Self { pub fn max_size(mut self, size: usize) -> Self {
self.max_size = size; self.max_size = size;
self self
@@ -98,12 +99,19 @@ impl Codec {
/// Set decoder to client mode. /// Set decoder to client mode.
/// ///
/// By default decoder works in server mode. /// By default decoder works in server mode.
#[must_use = "This returns the a new Codec, without modifying the original."]
pub fn client_mode(mut self) -> Self { pub fn client_mode(mut self) -> Self {
self.flags.remove(Flags::SERVER); self.flags.remove(Flags::SERVER);
self self
} }
} }
impl Default for Codec {
fn default() -> Self {
Self::new()
}
}
impl Encoder<Message> for Codec { impl Encoder<Message> for Codec {
type Error = ProtocolError; type Error = ProtocolError;

View File

@@ -4,17 +4,21 @@ use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_service::{IntoService, Service}; use actix_service::{IntoService, Service};
use pin_project_lite::pin_project;
use super::{Codec, Frame, Message}; use super::{Codec, Frame, Message};
#[pin_project::pin_project] pin_project! {
pub struct Dispatcher<S, T> pub struct Dispatcher<S, T>
where where
S: Service<Frame, Response = Message> + 'static, S: Service<Frame, Response = Message>,
T: AsyncRead + AsyncWrite, S: 'static,
{ T: AsyncRead,
T: AsyncWrite,
{
#[pin] #[pin]
inner: inner::Dispatcher<S, T, Codec, Message>, inner: inner::Dispatcher<S, T, Codec, Message>,
}
} }
impl<S, T> Dispatcher<S, T> impl<S, T> Dispatcher<S, T>
@@ -72,7 +76,7 @@ mod inner {
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
use crate::{body::AnyBody, Response}; use crate::{body::BoxBody, Response};
/// Framed transport errors /// Framed transport errors
pub enum DispatcherError<E, U, I> pub enum DispatcherError<E, U, I>
@@ -136,7 +140,7 @@ mod inner {
} }
} }
impl<E, U, I> From<DispatcherError<E, U, I>> for Response<AnyBody> impl<E, U, I> From<DispatcherError<E, U, I>> for Response<BoxBody>
where where
E: fmt::Debug + fmt::Display, E: fmt::Debug + fmt::Display,
U: Encoder<I> + Decoder, U: Encoder<I> + Decoder,
@@ -144,7 +148,7 @@ mod inner {
<U as Decoder>::Error: fmt::Debug, <U as Decoder>::Error: fmt::Debug,
{ {
fn from(err: DispatcherError<E, U, I>) -> Self { fn from(err: DispatcherError<E, U, I>) -> Self {
Response::internal_server_error().set_body(AnyBody::from(err.to_string())) Response::internal_server_error().set_body(BoxBody::new(err.to_string()))
} }
} }

View File

@@ -25,8 +25,8 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
// //
// un aligned prefix and suffix would be mask/unmask per byte. // un aligned prefix and suffix would be mask/unmask per byte.
// proper aligned middle slice goes into fast path and operates on 4-byte blocks. // proper aligned middle slice goes into fast path and operates on 4-byte blocks.
let (mut prefix, words, mut suffix) = unsafe { buf.align_to_mut::<u32>() }; let (prefix, words, suffix) = unsafe { buf.align_to_mut::<u32>() };
apply_mask_fallback(&mut prefix, mask); apply_mask_fallback(prefix, mask);
let head = prefix.len() & 3; let head = prefix.len() & 3;
let mask_u32 = if head > 0 { let mask_u32 = if head > 0 {
if cfg!(target_endian = "big") { if cfg!(target_endian = "big") {
@@ -40,7 +40,7 @@ pub fn apply_mask_fast32(buf: &mut [u8], mask: [u8; 4]) {
for word in words.iter_mut() { for word in words.iter_mut() {
*word ^= mask_u32; *word ^= mask_u32;
} }
apply_mask_fallback(&mut suffix, mask_u32.to_ne_bytes()); apply_mask_fallback(suffix, mask_u32.to_ne_bytes());
} }
#[cfg(test)] #[cfg(test)]

View File

@@ -8,9 +8,9 @@ use std::io;
use derive_more::{Display, Error, From}; use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::body::BoxBody;
use crate::{ use crate::{
body::AnyBody, header::HeaderValue, message::RequestHead, response::Response, header::HeaderValue, message::RequestHead, response::Response, ResponseBuilder,
ResponseBuilder,
}; };
mod codec; mod codec;
@@ -69,7 +69,7 @@ pub enum ProtocolError {
} }
/// WebSocket handshake errors /// WebSocket handshake errors
#[derive(Debug, PartialEq, Display, Error)] #[derive(Debug, Clone, Copy, PartialEq, Display, Error)]
pub enum HandshakeError { pub enum HandshakeError {
/// Only get method is allowed. /// Only get method is allowed.
#[display(fmt = "Method not allowed.")] #[display(fmt = "Method not allowed.")]
@@ -96,8 +96,8 @@ pub enum HandshakeError {
BadWebsocketKey, BadWebsocketKey,
} }
impl From<&HandshakeError> for Response<AnyBody> { impl From<HandshakeError> for Response<BoxBody> {
fn from(err: &HandshakeError) -> Self { fn from(err: HandshakeError) -> Self {
match err { match err {
HandshakeError::GetMethodRequired => { HandshakeError::GetMethodRequired => {
let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED); let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
@@ -139,9 +139,9 @@ impl From<&HandshakeError> for Response<AnyBody> {
} }
} }
impl From<HandshakeError> for Response<AnyBody> { impl From<&HandshakeError> for Response<BoxBody> {
fn from(err: HandshakeError) -> Self { fn from(err: &HandshakeError) -> Self {
(&err).into() (*err).into()
} }
} }
@@ -210,7 +210,6 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
Response::build(StatusCode::SWITCHING_PROTOCOLS) Response::build(StatusCode::SWITCHING_PROTOCOLS)
.upgrade("websocket") .upgrade("websocket")
.insert_header((header::TRANSFER_ENCODING, "chunked"))
.insert_header(( .insert_header((
header::SEC_WEBSOCKET_ACCEPT, header::SEC_WEBSOCKET_ACCEPT,
// key is known to be header value safe ascii // key is known to be header value safe ascii
@@ -221,9 +220,10 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use crate::{header, Method};
use super::*; use super::*;
use crate::{body::AnyBody, test::TestRequest}; use crate::test::TestRequest;
use http::{header, Method};
#[test] #[test]
fn test_handshake() { fn test_handshake() {
@@ -337,17 +337,17 @@ mod tests {
#[test] #[test]
fn test_ws_error_http_response() { fn test_ws_error_http_response() {
let resp: Response<AnyBody> = HandshakeError::GetMethodRequired.into(); let resp: Response<BoxBody> = HandshakeError::GetMethodRequired.into();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let resp: Response<AnyBody> = HandshakeError::NoWebsocketUpgrade.into(); let resp: Response<BoxBody> = HandshakeError::NoWebsocketUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<AnyBody> = HandshakeError::NoConnectionUpgrade.into(); let resp: Response<BoxBody> = HandshakeError::NoConnectionUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<AnyBody> = HandshakeError::NoVersionHeader.into(); let resp: Response<BoxBody> = HandshakeError::NoVersionHeader.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<AnyBody> = HandshakeError::UnsupportedVersion.into(); let resp: Response<BoxBody> = HandshakeError::UnsupportedVersion.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response<AnyBody> = HandshakeError::BadWebsocketKey.into(); let resp: Response<BoxBody> = HandshakeError::BadWebsocketKey.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@@ -3,7 +3,9 @@ use std::{
fmt, fmt,
}; };
/// Operation codes as part of RFC6455. /// Operation codes defined in [RFC 6455 §11.8].
///
/// [RFC 6455]: https://datatracker.ietf.org/doc/html/rfc6455#section-11.8
#[derive(Debug, Eq, PartialEq, Clone, Copy)] #[derive(Debug, Eq, PartialEq, Clone, Copy)]
pub enum OpCode { pub enum OpCode {
/// Indicates a continuation frame of a fragmented message. /// Indicates a continuation frame of a fragmented message.
@@ -105,7 +107,7 @@ pub enum CloseCode {
Abnormal, Abnormal,
/// Indicates that an endpoint is terminating the connection because it has received data within /// Indicates that an endpoint is terminating the connection because it has received data within
/// a message that was not consistent with the type of the message (e.g., non-UTF-8 \[RFC3629\] /// a message that was not consistent with the type of the message (e.g., non-UTF-8 \[RFC 3629\]
/// data within a text message). /// data within a text message).
Invalid, Invalid,
@@ -220,7 +222,8 @@ impl<T: Into<String>> From<(CloseCode, T)> for CloseReason {
} }
} }
/// The WebSocket GUID as stated in the spec. See https://tools.ietf.org/html/rfc6455#section-1.3. /// The WebSocket GUID as stated in the spec.
/// See <https://datatracker.ietf.org/doc/html/rfc6455#section-1.3>.
static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11"; static WS_GUID: &[u8] = b"258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
/// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec. /// Hashes the `Sec-WebSocket-Key` header according to the WebSocket spec.

View File

@@ -1,7 +1,7 @@
use std::convert::Infallible; use std::convert::Infallible;
use actix_http::{ use actix_http::{
body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response, body::BoxBody, HttpMessage, HttpService, Request, Response, StatusCode,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
@@ -99,7 +99,7 @@ async fn test_with_query_parameter() {
#[display(fmt = "expect failed")] #[display(fmt = "expect failed")]
struct ExpectFailed; struct ExpectFailed;
impl From<ExpectFailed> for Response<AnyBody> { impl From<ExpectFailed> for Response<BoxBody> {
fn from(_: ExpectFailed) -> Self { fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED) Response::new(StatusCode::EXPECTATION_FAILED)
} }

View File

@@ -0,0 +1,153 @@
use std::io;
use actix_http::{error::Error, HttpService, Response};
use actix_server::Server;
use tokio::io::AsyncWriteExt;
#[actix_rt::test]
async fn h2_ping_pong() -> io::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let lst = std::net::TcpListener::bind("127.0.0.1:0")?;
let addr = lst.local_addr().unwrap();
let join = std::thread::spawn(move || {
actix_rt::System::new().block_on(async move {
let srv = Server::build()
.disable_signals()
.workers(1)
.listen("h2_ping_pong", lst, || {
HttpService::build()
.keep_alive(3)
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
.tcp()
})?
.run();
tx.send(srv.handle()).unwrap();
srv.await
})
});
let handle = rx.recv().unwrap();
let (sync_tx, rx) = std::sync::mpsc::sync_channel(1);
// use a separate thread for h2 client so it can be blocked.
std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
let stream = tokio::net::TcpStream::connect(addr).await.unwrap();
let (mut tx, conn) = h2::client::handshake(stream).await.unwrap();
tokio::spawn(async move { conn.await.unwrap() });
let (res, _) = tx.send_request(::http::Request::new(()), true).unwrap();
let res = res.await.unwrap();
assert_eq!(res.status().as_u16(), 200);
sync_tx.send(()).unwrap();
// intentionally block the client thread so it can not answer ping pong.
std::thread::sleep(std::time::Duration::from_secs(1000));
})
});
rx.recv().unwrap();
let now = std::time::Instant::now();
// stop server gracefully. this step would take up to 30 seconds.
handle.stop(true).await;
// join server thread. only when connection are all gone this step would finish.
join.join().unwrap()?;
// check the time used for join server thread so it's known that the server shutdown
// is from keep alive and not server graceful shutdown timeout.
assert!(now.elapsed() < std::time::Duration::from_secs(30));
Ok(())
}
#[actix_rt::test]
async fn h2_handshake_timeout() -> io::Result<()> {
let (tx, rx) = std::sync::mpsc::sync_channel(1);
let lst = std::net::TcpListener::bind("127.0.0.1:0")?;
let addr = lst.local_addr().unwrap();
let join = std::thread::spawn(move || {
actix_rt::System::new().block_on(async move {
let srv = Server::build()
.disable_signals()
.workers(1)
.listen("h2_ping_pong", lst, || {
HttpService::build()
.keep_alive(30)
// set first request timeout to 5 seconds.
// this is the timeout used for http2 handshake.
.client_timeout(5000)
.h2(|_| async { Ok::<_, Error>(Response::ok()) })
.tcp()
})?
.run();
tx.send(srv.handle()).unwrap();
srv.await
})
});
let handle = rx.recv().unwrap();
let (sync_tx, rx) = std::sync::mpsc::sync_channel(1);
// use a separate thread for tcp client so it can be blocked.
std::thread::spawn(move || {
tokio::runtime::Builder::new_current_thread()
.enable_all()
.build()
.unwrap()
.block_on(async move {
let mut stream = tokio::net::TcpStream::connect(addr).await.unwrap();
// do not send the last new line intentionally.
// This should hang the server handshake
let malicious_buf = b"PRI * HTTP/2.0\r\n\r\nSM\r\n";
stream.write_all(malicious_buf).await.unwrap();
stream.flush().await.unwrap();
sync_tx.send(()).unwrap();
// intentionally block the client thread so it sit idle and not do handshake.
std::thread::sleep(std::time::Duration::from_secs(1000));
drop(stream)
})
});
rx.recv().unwrap();
let now = std::time::Instant::now();
// stop server gracefully. this step would take up to 30 seconds.
handle.stop(true).await;
// join server thread. only when connection are all gone this step would finish.
join.join().unwrap()?;
// check the time used for join server thread so it's known that the server shutdown
// is from handshake timeout and not server graceful shutdown timeout.
assert!(now.elapsed() < std::time::Duration::from_secs(30));
Ok(())
}

View File

@@ -5,13 +5,10 @@ extern crate tls_openssl as openssl;
use std::{convert::Infallible, io}; use std::{convert::Infallible, io};
use actix_http::{ use actix_http::{
body::{AnyBody, Body, SizedStream}, body::{BodyStream, BoxBody, SizedStream},
error::PayloadError, error::PayloadError,
http::{ header::{self, HeaderValue},
header::{self, HeaderName, HeaderValue}, Error, HttpMessage, HttpService, Method, Request, Response, StatusCode, Version,
Method, StatusCode, Version,
},
Error, HttpMessage, HttpService, Request, Response,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_service, ServiceFactoryExt}; use actix_service::{fn_service, ServiceFactoryExt};
@@ -143,38 +140,25 @@ async fn test_h2_content_length() {
}) })
.await; .await;
let header = HeaderName::from_static("content-length"); static VALUE: HeaderValue = HeaderValue::from_static("0");
let value = HeaderValue::from_static("0");
{ {
for &i in &[0] { let req = srv.request(Method::HEAD, srv.surl("/0")).send();
let req = srv req.await.expect_err("should timeout on recv 1xx frame");
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
.send();
let _response = req.await.expect_err("should timeout on recv 1xx frame");
// assert_eq!(response.headers().get(&header), None);
let req = srv let req = srv.request(Method::GET, srv.surl("/0")).send();
.request(Method::GET, srv.surl(&format!("/{}", i))) req.await.expect_err("should timeout on recv 1xx frame");
.send();
let _response = req.await.expect_err("should timeout on recv 1xx frame");
// assert_eq!(response.headers().get(&header), None);
}
for &i in &[1] { let req = srv.request(Method::GET, srv.surl("/1")).send();
let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = req.await.unwrap(); let response = req.await.unwrap();
assert_eq!(response.headers().get(&header), None); assert!(response.headers().get("content-length").is_none());
}
for &i in &[2, 3] { for &i in &[2, 3] {
let req = srv let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i))) .request(Method::GET, srv.surl(&format!("/{}", i)))
.send(); .send();
let response = req.await.unwrap(); let response = req.await.unwrap();
assert_eq!(response.headers().get(&header), Some(&value)); assert_eq!(response.headers().get("content-length"), Some(&VALUE));
} }
} }
} }
@@ -361,7 +345,7 @@ async fn test_h2_body_chunked_explicit() {
ok::<_, Infallible>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .body(BodyStream::new(body)),
) )
}) })
.openssl(tls_config()) .openssl(tls_config())
@@ -412,9 +396,11 @@ async fn test_h2_response_http_error_handling() {
#[display(fmt = "error")] #[display(fmt = "error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<AnyBody> { impl From<BadRequest> for Response<BoxBody> {
fn from(err: BadRequest) -> Self { fn from(err: BadRequest) -> Self {
Response::build(StatusCode::BAD_REQUEST).body(err.to_string()) Response::build(StatusCode::BAD_REQUEST)
.body(err.to_string())
.map_into_boxed_body()
} }
} }
@@ -422,7 +408,7 @@ impl From<BadRequest> for Response<AnyBody> {
async fn test_h2_service_error() { async fn test_h2_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| err::<Response<Body>, _>(BadRequest)) .h2(|_| err::<Response<BoxBody>, _>(BadRequest))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })

View File

@@ -3,33 +3,28 @@
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use std::{ use std::{
convert::Infallible, convert::{Infallible, TryFrom},
io::{self, BufReader, Write}, io::{self, BufReader, Write},
net::{SocketAddr, TcpStream as StdTcpStream}, net::{SocketAddr, TcpStream as StdTcpStream},
sync::Arc, sync::Arc,
}; };
use actix_http::{ use actix_http::{
body::{AnyBody, Body, SizedStream}, body::{BodyStream, BoxBody, SizedStream},
error::PayloadError, error::PayloadError,
http::{
header::{self, HeaderName, HeaderValue}, header::{self, HeaderName, HeaderValue},
Method, StatusCode, Version, Error, HttpService, Method, Request, Response, StatusCode, Version,
},
Error, HttpService, Request, Response,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_factory_with_config, fn_service}; use actix_service::{fn_factory_with_config, fn_service};
use actix_tls::connect::rustls::webpki_roots_cert_store;
use actix_utils::future::{err, ok}; use actix_utils::future::{err, ok};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error}; use derive_more::{Display, Error};
use futures_core::Stream; use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _}; use futures_util::stream::{once, StreamExt as _};
use rustls::{ use rustls::{Certificate, PrivateKey, ServerConfig as RustlsServerConfig, ServerName};
internal::pemfile::{certs, pkcs8_private_keys}, use rustls_pemfile::{certs, pkcs8_private_keys};
NoClientAuth, ServerConfig as RustlsServerConfig, Session,
};
use webpki::DNSNameRef;
async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError> async fn load_body<S>(mut stream: S) -> Result<BytesMut, PayloadError>
where where
@@ -47,13 +42,24 @@ fn tls_config() -> RustlsServerConfig {
let cert_file = cert.serialize_pem().unwrap(); let cert_file = cert.serialize_pem().unwrap();
let key_file = cert.serialize_private_key_pem(); let key_file = cert.serialize_private_key_pem();
let mut config = RustlsServerConfig::new(NoClientAuth::new());
let cert_file = &mut BufReader::new(cert_file.as_bytes()); let cert_file = &mut BufReader::new(cert_file.as_bytes());
let key_file = &mut BufReader::new(key_file.as_bytes()); let key_file = &mut BufReader::new(key_file.as_bytes());
let cert_chain = certs(cert_file).unwrap(); let cert_chain = certs(cert_file)
.unwrap()
.into_iter()
.map(Certificate)
.collect();
let mut keys = pkcs8_private_keys(key_file).unwrap(); let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
let mut config = RustlsServerConfig::builder()
.with_safe_defaults()
.with_no_client_auth()
.with_single_cert(cert_chain, PrivateKey(keys.remove(0)))
.unwrap();
config.alpn_protocols.push(HTTP1_1_ALPN_PROTOCOL.to_vec());
config.alpn_protocols.push(H2_ALPN_PROTOCOL.to_vec());
config config
} }
@@ -62,19 +68,28 @@ pub fn get_negotiated_alpn_protocol(
addr: SocketAddr, addr: SocketAddr,
client_alpn_protocol: &[u8], client_alpn_protocol: &[u8],
) -> Option<Vec<u8>> { ) -> Option<Vec<u8>> {
let mut config = rustls::ClientConfig::new(); let mut config = rustls::ClientConfig::builder()
.with_safe_defaults()
.with_root_certificates(webpki_roots_cert_store())
.with_no_client_auth();
config.alpn_protocols.push(client_alpn_protocol.to_vec()); config.alpn_protocols.push(client_alpn_protocol.to_vec());
let mut sess = rustls::ClientSession::new(
&Arc::new(config), let mut sess = rustls::ClientConnection::new(
DNSNameRef::try_from_ascii_str("localhost").unwrap(), Arc::new(config),
); ServerName::try_from("localhost").unwrap(),
)
.unwrap();
let mut sock = StdTcpStream::connect(addr).unwrap(); let mut sock = StdTcpStream::connect(addr).unwrap();
let mut stream = rustls::Stream::new(&mut sess, &mut sock); let mut stream = rustls::Stream::new(&mut sess, &mut sock);
// The handshake will fails because the client will not be able to verify the server // The handshake will fails because the client will not be able to verify the server
// certificate, but it doesn't matter here as we are just interested in the negotiated ALPN // certificate, but it doesn't matter here as we are just interested in the negotiated ALPN
// protocol // protocol
let _ = stream.flush(); let _ = stream.flush();
sess.get_alpn_protocol().map(|proto| proto.to_vec())
sess.alpn_protocol().map(|proto| proto.to_vec())
} }
#[actix_rt::test] #[actix_rt::test]
@@ -398,7 +413,7 @@ async fn test_h2_body_chunked_explicit() {
ok::<_, Infallible>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .body(BodyStream::new(body)),
) )
}) })
.rustls(tls_config()) .rustls(tls_config())
@@ -449,9 +464,9 @@ async fn test_h2_response_http_error_handling() {
#[display(fmt = "error")] #[display(fmt = "error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<AnyBody> { impl From<BadRequest> for Response<BoxBody> {
fn from(_: BadRequest) -> Self { fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(AnyBody::from("error")) Response::bad_request().set_body(BoxBody::new("error"))
} }
} }
@@ -459,7 +474,7 @@ impl From<BadRequest> for Response<AnyBody> {
async fn test_h2_service_error() { async fn test_h2_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| err::<Response<Body>, _>(BadRequest)) .h2(|_| err::<Response<BoxBody>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@@ -476,7 +491,7 @@ async fn test_h2_service_error() {
async fn test_h1_service_error() { async fn test_h1_service_error() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| err::<Response<Body>, _>(BadRequest)) .h1(|_| err::<Response<BoxBody>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;

View File

@@ -6,9 +6,8 @@ use std::{
}; };
use actix_http::{ use actix_http::{
body::{AnyBody, Body, SizedStream}, body::{self, BodyStream, BoxBody, SizedStream},
header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response, header, Error, HttpMessage, HttpService, KeepAlive, Request, Response, StatusCode,
StatusCode,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::time::sleep; use actix_rt::time::sleep;
@@ -24,7 +23,7 @@ use regex::Regex;
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() { async fn test_h1() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.client_timeout(1000) .client_timeout(1000)
@@ -39,11 +38,13 @@ async fn test_h1() {
let response = srv.get("/").send().await.unwrap(); let response = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_2() { async fn test_h1_2() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.client_timeout(1000) .client_timeout(1000)
@@ -59,13 +60,15 @@ async fn test_h1_2() {
let response = srv.get("/").send().await.unwrap(); let response = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
srv.stop().await;
} }
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "expect failed")] #[display(fmt = "expect failed")]
struct ExpectFailed; struct ExpectFailed;
impl From<ExpectFailed> for Response<AnyBody> { impl From<ExpectFailed> for Response<BoxBody> {
fn from(_: ExpectFailed) -> Self { fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED) Response::new(StatusCode::EXPECTATION_FAILED)
} }
@@ -73,7 +76,7 @@ impl From<ExpectFailed> for Response<AnyBody> {
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue() { async fn test_expect_continue() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.expect(fn_service(|req: Request| { .expect(fn_service(|req: Request| {
if req.head().uri.query() == Some("yes=") { if req.head().uri.query() == Some("yes=") {
@@ -98,11 +101,13 @@ async fn test_expect_continue() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue_h1() { async fn test_expect_continue_h1() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.expect(fn_service(|req: Request| { .expect(fn_service(|req: Request| {
sleep(Duration::from_millis(20)).then(move |_| { sleep(Duration::from_millis(20)).then(move |_| {
@@ -129,6 +134,8 @@ async fn test_expect_continue_h1() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n")); assert!(data.starts_with("HTTP/1.1 100 Continue\r\n\r\nHTTP/1.1 200 OK\r\n"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -136,7 +143,7 @@ async fn test_chunked_payload() {
let chunk_sizes = vec![32768, 32, 32768]; let chunk_sizes = vec![32768, 32, 32768];
let total_size: usize = chunk_sizes.iter().sum(); let total_size: usize = chunk_sizes.iter().sum();
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(fn_service(|mut request: Request| { .h1(fn_service(|mut request: Request| {
request request
@@ -183,15 +190,18 @@ async fn test_chunked_payload() {
Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(), Some(caps) => caps.get(1).unwrap().as_str().parse().unwrap(),
None => panic!("Failed to find size in HTTP Response: {}", data), None => panic!("Failed to find size in HTTP Response: {}", data),
}; };
size size
}; };
assert_eq!(returned_size, total_size); assert_eq!(returned_size, total_size);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_slow_request() { async fn test_slow_request() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.client_timeout(100) .client_timeout(100)
.finish(|_| ok::<_, Infallible>(Response::ok())) .finish(|_| ok::<_, Infallible>(Response::ok()))
@@ -204,11 +214,13 @@ async fn test_slow_request() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 408 Request Timeout")); assert!(data.starts_with("HTTP/1.1 408 Request Timeout"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_malformed_request() { async fn test_http1_malformed_request() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@@ -220,11 +232,13 @@ async fn test_http1_malformed_request() {
let mut data = String::new(); let mut data = String::new();
let _ = stream.read_to_string(&mut data); let _ = stream.read_to_string(&mut data);
assert!(data.starts_with("HTTP/1.1 400 Bad Request")); assert!(data.starts_with("HTTP/1.1 400 Bad Request"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive() { async fn test_http1_keepalive() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@@ -241,11 +255,13 @@ async fn test_http1_keepalive() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let _ = stream.read(&mut data); let _ = stream.read(&mut data);
assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n"); assert_eq!(&data[..17], b"HTTP/1.1 200 OK\r\n");
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_timeout() { async fn test_http1_keepalive_timeout() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(1) .keep_alive(1)
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@@ -263,11 +279,13 @@ async fn test_http1_keepalive_timeout() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_close() { async fn test_http1_keepalive_close() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@@ -284,11 +302,13 @@ async fn test_http1_keepalive_close() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http10_keepalive_default_close() { async fn test_http10_keepalive_default_close() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@@ -304,11 +324,13 @@ async fn test_http10_keepalive_default_close() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http10_keepalive() { async fn test_http10_keepalive() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
@@ -331,11 +353,13 @@ async fn test_http10_keepalive() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_http1_keepalive_disabled() { async fn test_http1_keepalive_disabled() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.h1(|_| ok::<_, Infallible>(Response::ok())) .h1(|_| ok::<_, Infallible>(Response::ok()))
@@ -352,16 +376,18 @@ async fn test_http1_keepalive_disabled() {
let mut data = vec![0; 1024]; let mut data = vec![0; 1024];
let res = stream.read(&mut data).unwrap(); let res = stream.read(&mut data).unwrap();
assert_eq!(res, 0); assert_eq!(res, 0);
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_content_length() { async fn test_content_length() {
use actix_http::http::{ use actix_http::{
header::{HeaderName, HeaderValue}, header::{HeaderName, HeaderValue},
StatusCode, StatusCode,
}; };
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|req: Request| { .h1(|req: Request| {
let indx: usize = req.uri().path()[1..].parse().unwrap(); let indx: usize = req.uri().path()[1..].parse().unwrap();
@@ -399,6 +425,8 @@ async fn test_content_length() {
assert_eq!(response.headers().get(&header), Some(&value)); assert_eq!(response.headers().get(&header), Some(&value));
} }
} }
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -438,6 +466,8 @@ async fn test_h1_headers() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from(data2)); assert_eq!(bytes, Bytes::from(data2));
srv.stop().await;
} }
const STR: &str = "Hello World Hello World Hello World Hello World Hello World \ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
@@ -477,6 +507,8 @@ async fn test_h1_body() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -502,6 +534,8 @@ async fn test_h1_head_empty() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -527,11 +561,13 @@ async fn test_h1_head_binary() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert!(bytes.is_empty()); assert!(bytes.is_empty());
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_head_binary2() { async fn test_h1_head_binary2() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
@@ -548,6 +584,8 @@ async fn test_h1_head_binary2() {
.unwrap(); .unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap()); assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
} }
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -570,6 +608,8 @@ async fn test_h1_body_length() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -581,7 +621,7 @@ async fn test_h1_body_chunked_explicit() {
ok::<_, Infallible>( ok::<_, Infallible>(
Response::build(StatusCode::OK) Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .body(BodyStream::new(body)),
) )
}) })
.tcp() .tcp()
@@ -605,6 +645,8 @@ async fn test_h1_body_chunked_explicit() {
// decode // decode
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -613,7 +655,9 @@ async fn test_h1_body_chunked_implicit() {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body)) ok::<_, Infallible>(
Response::build(StatusCode::OK).body(BodyStream::new(body)),
)
}) })
.tcp() .tcp()
}) })
@@ -634,6 +678,8 @@ async fn test_h1_body_chunked_implicit() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
@@ -661,15 +707,17 @@ async fn test_h1_response_http_error_handling() {
bytes, bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value") Bytes::from_static(b"error processing HTTP: failed to parse header value")
); );
srv.stop().await;
} }
#[derive(Debug, Display, Error)] #[derive(Debug, Display, Error)]
#[display(fmt = "error")] #[display(fmt = "error")]
struct BadRequest; struct BadRequest;
impl From<BadRequest> for Response<AnyBody> { impl From<BadRequest> for Response<BoxBody> {
fn from(_: BadRequest) -> Self { fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(AnyBody::from("error")) Response::bad_request().set_body(BoxBody::new("error"))
} }
} }
@@ -677,7 +725,7 @@ impl From<BadRequest> for Response<AnyBody> {
async fn test_h1_service_error() { async fn test_h1_service_error() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| err::<Response<Body>, _>(BadRequest)) .h1(|_| err::<Response<()>, _>(BadRequest))
.tcp() .tcp()
}) })
.await; .await;
@@ -688,11 +736,13 @@ async fn test_h1_service_error() {
// read response // read response
let bytes = srv.load_body(response).await.unwrap(); let bytes = srv.load_body(response).await.unwrap();
assert_eq!(bytes, Bytes::from_static(b"error")); assert_eq!(bytes, Bytes::from_static(b"error"));
srv.stop().await;
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_on_connect() { async fn test_h1_on_connect() {
let srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.on_connect_ext(|_, data| { .on_connect_ext(|_, data| {
data.insert(20isize); data.insert(20isize);
@@ -707,4 +757,92 @@ async fn test_h1_on_connect() {
let response = srv.get("/").send().await.unwrap(); let response = srv.get("/").send().await.unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());
srv.stop().await;
}
/// Tests compliance with 304 Not Modified spec in RFC 7232 §4.1.
/// https://datatracker.ietf.org/doc/html/rfc7232#section-4.1
#[actix_rt::test]
async fn test_not_modified_spec_h1() {
// TODO: this test needing a few seconds to complete reveals some weirdness with either the
// dispatcher or the client, though similar hangs occur on other tests in this file, only
// succeeding, it seems, because of the keepalive timer
static CL: header::HeaderName = header::CONTENT_LENGTH;
let mut srv = test_server(|| {
HttpService::build()
.h1(|req: Request| {
let res: Response<BoxBody> = match req.path() {
// with no content-length
"/none" => {
Response::with_body(StatusCode::NOT_MODIFIED, body::None::new())
.map_into_boxed_body()
}
// with no content-length
"/body" => Response::with_body(StatusCode::NOT_MODIFIED, "1234")
.map_into_boxed_body(),
// with manual content-length header and specific None body
"/cl-none" => {
let mut res = Response::with_body(
StatusCode::NOT_MODIFIED,
body::None::new(),
);
res.headers_mut()
.insert(CL.clone(), header::HeaderValue::from_static("24"));
res.map_into_boxed_body()
}
// with manual content-length header and ignore-able body
"/cl-body" => {
let mut res =
Response::with_body(StatusCode::NOT_MODIFIED, "1234");
res.headers_mut()
.insert(CL.clone(), header::HeaderValue::from_static("4"));
res.map_into_boxed_body()
}
_ => panic!("unknown route"),
};
ok::<_, Infallible>(res)
})
.tcp()
})
.await;
let res = srv.get("/none").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(res.headers().get(&CL), None);
assert!(srv.load_body(res).await.unwrap().is_empty());
let res = srv.get("/body").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(res.headers().get(&CL), None);
assert!(srv.load_body(res).await.unwrap().is_empty());
let res = srv.get("/cl-none").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(
res.headers().get(&CL),
Some(&header::HeaderValue::from_static("24")),
);
assert!(srv.load_body(res).await.unwrap().is_empty());
let res = srv.get("/cl-body").send().await.unwrap();
assert_eq!(res.status(), http::StatusCode::NOT_MODIFIED);
assert_eq!(
res.headers().get(&CL),
Some(&header::HeaderValue::from_static("4")),
);
// server does not prevent payload from being sent but clients may choose not to read it
// TODO: this is probably a bug, especially since CL header can differ in length from the body
assert!(!srv.load_body(res).await.unwrap().is_empty());
// TODO: add stream response tests
srv.stop().await;
} }

View File

@@ -6,7 +6,7 @@ use std::{
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::{ use actix_http::{
body::{AnyBody, BodySize}, body::{BodySize, BoxBody},
h1, h1,
ws::{self, CloseCode, Frame, Item, Message}, ws::{self, CloseCode, Frame, Item, Message},
Error, HttpService, Request, Response, Error, HttpService, Request, Response,
@@ -50,14 +50,14 @@ enum WsServiceError {
Dispatcher, Dispatcher,
} }
impl From<WsServiceError> for Response<AnyBody> { impl From<WsServiceError> for Response<BoxBody> {
fn from(err: WsServiceError) -> Self { fn from(err: WsServiceError) -> Self {
match err { match err {
WsServiceError::Http(err) => err.into(), WsServiceError::Http(err) => err.into(),
WsServiceError::Ws(err) => err.into(), WsServiceError::Ws(err) => err.into(),
WsServiceError::Io(_err) => unreachable!(), WsServiceError::Io(_err) => unreachable!(),
WsServiceError::Dispatcher => Response::internal_server_error() WsServiceError::Dispatcher => Response::internal_server_error()
.set_body(AnyBody::from(format!("{}", err))), .set_body(BoxBody::new(format!("{}", err))),
} }
} }
} }

View File

@@ -3,6 +3,27 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.9 - 2021-12-01
* Polling `Field` after dropping `Multipart` now fails immediately instead of hanging forever. [#2463]
[#2463]: https://github.com/actix/actix-web/pull/2463
## 0.4.0-beta.8 - 2021-11-22
* Ensure a correct Content-Disposition header is included in every part of a multipart message. [#2451]
* Added `MultipartError::NoContentDisposition` variant. [#2451]
* Since Content-Disposition is now ensured, `Field::content_disposition` is now infallible. [#2451]
* Added `Field::name` method for getting the field name. [#2451]
* `MultipartError` now marks variants with inner errors as the source. [#2451]
* `MultipartError` is now marked as non-exhaustive. [#2451]
[#2451]: https://github.com/actix/actix-web/pull/2451
## 0.4.0-beta.7 - 2021-10-20
* Minimum supported Rust version (MSRV) is now 1.52.
## 0.4.0-beta.6 - 2021-09-09 ## 0.4.0-beta.6 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51. * Minimum supported Rust version (MSRV) is now 1.51.

View File

@@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.6" version = "0.4.0-beta.9"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart form support for Actix Web" description = "Multipart form support for Actix Web"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
@@ -14,13 +14,12 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.9", default-features = false } actix-web = { version = "4.0.0-beta.11", default-features = false }
actix-utils = "3.0.0" actix-utils = "3.0.0"
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
httparse = "1.3" httparse = "1.3"
local-waker = "0.1" local-waker = "0.1"
log = "0.4" log = "0.4"
@@ -29,6 +28,7 @@ twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.2" actix-rt = "2.2"
actix-http = "3.0.0-beta.10" actix-http = "3.0.0-beta.14"
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

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

View File

@@ -2,39 +2,52 @@
use actix_web::error::{ParseError, PayloadError}; use actix_web::error::{ParseError, PayloadError};
use actix_web::http::StatusCode; use actix_web::http::StatusCode;
use actix_web::ResponseError; use actix_web::ResponseError;
use derive_more::{Display, From}; use derive_more::{Display, Error, From};
/// A set of errors that can occur during parsing multipart streams /// A set of errors that can occur during parsing multipart streams
#[derive(Debug, Display, From)] #[non_exhaustive]
#[derive(Debug, Display, From, Error)]
pub enum MultipartError { pub enum MultipartError {
/// Content-Disposition header is not found or is not equal to "form-data".
///
/// According to [RFC 7578 §4.2](https://datatracker.ietf.org/doc/html/rfc7578#section-4.2) a
/// Content-Disposition header must always be present and equal to "form-data".
#[display(fmt = "No Content-Disposition `form-data` header")]
NoContentDisposition,
/// Content-Type header is not found /// Content-Type header is not found
#[display(fmt = "No Content-type header found")] #[display(fmt = "No Content-Type header found")]
NoContentType, NoContentType,
/// Can not parse Content-Type header /// Can not parse Content-Type header
#[display(fmt = "Can not parse Content-Type header")] #[display(fmt = "Can not parse Content-Type header")]
ParseContentType, ParseContentType,
/// Multipart boundary is not found /// Multipart boundary is not found
#[display(fmt = "Multipart boundary is not found")] #[display(fmt = "Multipart boundary is not found")]
Boundary, Boundary,
/// Nested multipart is not supported /// Nested multipart is not supported
#[display(fmt = "Nested multipart is not supported")] #[display(fmt = "Nested multipart is not supported")]
Nested, Nested,
/// Multipart stream is incomplete /// Multipart stream is incomplete
#[display(fmt = "Multipart stream is incomplete")] #[display(fmt = "Multipart stream is incomplete")]
Incomplete, Incomplete,
/// Error during field parsing /// Error during field parsing
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Parse(ParseError), Parse(ParseError),
/// Payload error /// Payload error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Payload(PayloadError), Payload(PayloadError),
/// Not consumed /// Not consumed
#[display(fmt = "Multipart stream is not consumed")] #[display(fmt = "Multipart stream is not consumed")]
NotConsumed, NotConsumed,
} }
impl std::error::Error for MultipartError {}
/// Return `BadRequest` for `MultipartError` /// Return `BadRequest` for `MultipartError`
impl ResponseError for MultipartError { impl ResponseError for MultipartError {
fn status_code(&self) -> StatusCode { fn status_code(&self) -> StatusCode {

View File

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

View File

@@ -1,18 +1,22 @@
//! Multipart response payload support. //! Multipart response payload support.
use std::cell::{Cell, RefCell, RefMut}; use std::{
use std::convert::TryFrom; cell::{Cell, RefCell, RefMut},
use std::marker::PhantomData; cmp,
use std::pin::Pin; convert::TryFrom,
use std::rc::Rc; fmt,
use std::task::{Context, Poll}; marker::PhantomData,
use std::{cmp, fmt}; pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_web::error::{ParseError, PayloadError}; use actix_web::{
use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; error::{ParseError, PayloadError},
http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue},
};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::stream::{LocalBoxStream, Stream}; use futures_core::stream::{LocalBoxStream, Stream};
use futures_util::stream::StreamExt as _;
use local_waker::LocalWaker; use local_waker::LocalWaker;
use crate::error::MultipartError; use crate::error::MultipartError;
@@ -28,7 +32,7 @@ const MAX_HEADERS: usize = 32;
pub struct Multipart { pub struct Multipart {
safety: Safety, safety: Safety,
error: Option<MultipartError>, error: Option<MultipartError>,
inner: Option<Rc<RefCell<InnerMultipart>>>, inner: Option<InnerMultipart>,
} }
enum InnerMultipartItem { enum InnerMultipartItem {
@@ -40,10 +44,13 @@ enum InnerMultipartItem {
enum InnerState { enum InnerState {
/// Stream eof /// Stream eof
Eof, Eof,
/// Skip data until first boundary /// Skip data until first boundary
FirstBoundary, FirstBoundary,
/// Reading boundary /// Reading boundary
Boundary, Boundary,
/// Reading Headers, /// Reading Headers,
Headers, Headers,
} }
@@ -59,7 +66,7 @@ impl Multipart {
/// Create multipart instance for boundary. /// Create multipart instance for boundary.
pub fn new<S>(headers: &HeaderMap, stream: S) -> Multipart pub fn new<S>(headers: &HeaderMap, stream: S) -> Multipart
where where
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static, S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{ {
match Self::boundary(headers) { match Self::boundary(headers) {
Ok(boundary) => Multipart::from_boundary(boundary, stream), Ok(boundary) => Multipart::from_boundary(boundary, stream),
@@ -69,39 +76,32 @@ impl Multipart {
/// Extract boundary info from headers. /// Extract boundary info from headers.
pub(crate) fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> { pub(crate) fn boundary(headers: &HeaderMap) -> Result<String, MultipartError> {
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) { headers
if let Ok(content_type) = content_type.to_str() { .get(&header::CONTENT_TYPE)
if let Ok(ct) = content_type.parse::<mime::Mime>() { .ok_or(MultipartError::NoContentType)?
if let Some(boundary) = ct.get_param(mime::BOUNDARY) { .to_str()
Ok(boundary.as_str().to_owned()) .ok()
} else { .and_then(|content_type| content_type.parse::<mime::Mime>().ok())
Err(MultipartError::Boundary) .ok_or(MultipartError::ParseContentType)?
} .get_param(mime::BOUNDARY)
} else { .map(|boundary| boundary.as_str().to_owned())
Err(MultipartError::ParseContentType) .ok_or(MultipartError::Boundary)
}
} else {
Err(MultipartError::ParseContentType)
}
} else {
Err(MultipartError::NoContentType)
}
} }
/// Create multipart instance for given boundary and stream /// Create multipart instance for given boundary and stream
pub(crate) fn from_boundary<S>(boundary: String, stream: S) -> Multipart pub(crate) fn from_boundary<S>(boundary: String, stream: S) -> Multipart
where where
S: Stream<Item = Result<Bytes, PayloadError>> + Unpin + 'static, S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
{ {
Multipart { Multipart {
error: None, error: None,
safety: Safety::new(), safety: Safety::new(),
inner: Some(Rc::new(RefCell::new(InnerMultipart { inner: Some(InnerMultipart {
boundary, boundary,
payload: PayloadRef::new(PayloadBuffer::new(Box::new(stream))), payload: PayloadRef::new(PayloadBuffer::new(stream)),
state: InnerState::FirstBoundary, state: InnerState::FirstBoundary,
item: InnerMultipartItem::None, item: InnerMultipartItem::None,
}))), }),
} }
} }
@@ -118,20 +118,27 @@ impl Multipart {
impl Stream for Multipart { impl Stream for Multipart {
type Item = Result<Field, MultipartError>; type Item = Result<Field, MultipartError>;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if let Some(err) = self.error.take() {
Poll::Ready(Some(Err(err)))
} else if self.safety.current() {
let this = self.get_mut(); let this = self.get_mut();
let mut inner = this.inner.as_mut().unwrap().borrow_mut();
if let Some(mut payload) = inner.payload.get_mut(&this.safety) { match this.inner.as_mut() {
payload.poll_stream(cx)?; Some(inner) => {
} if let Some(mut buffer) = inner.payload.get_mut(&this.safety) {
inner.poll(&this.safety, cx) // check safety and poll read payload to buffer.
} else if !self.safety.is_clean() { buffer.poll_stream(cx)?;
Poll::Ready(Some(Err(MultipartError::NotConsumed))) } else if !this.safety.is_clean() {
// safety violation
return Poll::Ready(Some(Err(MultipartError::NotConsumed)));
} else { } else {
Poll::Pending return Poll::Pending;
}
inner.poll(&this.safety, cx)
}
None => Poll::Ready(Some(Err(this
.error
.take()
.expect("Multipart polled after finish")))),
} }
} }
} }
@@ -152,17 +159,15 @@ impl InnerMultipart {
Ok(httparse::Status::Complete((_, hdrs))) => { Ok(httparse::Status::Complete((_, hdrs))) => {
// convert headers // convert headers
let mut headers = HeaderMap::with_capacity(hdrs.len()); let mut headers = HeaderMap::with_capacity(hdrs.len());
for h in hdrs { for h in hdrs {
if let Ok(name) = HeaderName::try_from(h.name) { let name =
if let Ok(value) = HeaderValue::try_from(h.value) { HeaderName::try_from(h.name).map_err(|_| ParseError::Header)?;
let value = HeaderValue::try_from(h.value)
.map_err(|_| ParseError::Header)?;
headers.append(name, value); headers.append(name, value);
} else {
return Err(ParseError::Header.into());
}
} else {
return Err(ParseError::Header.into());
}
} }
Ok(Some(headers)) Ok(Some(headers))
} }
Ok(httparse::Status::Partial) => Err(ParseError::Header.into()), Ok(httparse::Status::Partial) => Err(ParseError::Header.into()),
@@ -332,31 +337,55 @@ impl InnerMultipart {
return Poll::Pending; return Poll::Pending;
}; };
// content type // According to RFC 7578 §4.2, a Content-Disposition header must always be present and
let mut mt = mime::APPLICATION_OCTET_STREAM; // set to "form-data".
if let Some(content_type) = headers.get(&header::CONTENT_TYPE) {
if let Ok(content_type) = content_type.to_str() { let content_disposition = headers
if let Ok(ct) = content_type.parse::<mime::Mime>() { .get(&header::CONTENT_DISPOSITION)
mt = ct; .and_then(|cd| ContentDisposition::from_raw(cd).ok())
} .filter(|content_disposition| {
} let is_form_data =
} content_disposition.disposition == header::DispositionType::FormData;
let has_field_name = content_disposition
.parameters
.iter()
.any(|param| matches!(param, header::DispositionParam::Name(_)));
is_form_data && has_field_name
});
let cd = if let Some(content_disposition) = content_disposition {
content_disposition
} else {
return Poll::Ready(Some(Err(MultipartError::NoContentDisposition)));
};
let ct: mime::Mime = headers
.get(&header::CONTENT_TYPE)
.and_then(|ct| ct.to_str().ok())
.and_then(|ct| ct.parse().ok())
.unwrap_or(mime::APPLICATION_OCTET_STREAM);
self.state = InnerState::Boundary; self.state = InnerState::Boundary;
// nested multipart stream // nested multipart stream is not supported
if mt.type_() == mime::MULTIPART { if ct.type_() == mime::MULTIPART {
Poll::Ready(Some(Err(MultipartError::Nested))) return Poll::Ready(Some(Err(MultipartError::Nested)));
} else { }
let field = Rc::new(RefCell::new(InnerField::new(
self.payload.clone(), let field =
self.boundary.clone(), InnerField::new_in_rc(self.payload.clone(), self.boundary.clone(), &headers)?;
&headers,
)?));
self.item = InnerMultipartItem::Field(Rc::clone(&field)); self.item = InnerMultipartItem::Field(Rc::clone(&field));
Poll::Ready(Some(Ok(Field::new(safety.clone(cx), headers, mt, field)))) Poll::Ready(Some(Ok(Field::new(
} safety.clone(cx),
headers,
ct,
cd,
field,
))))
} }
} }
} }
@@ -371,6 +400,7 @@ impl Drop for InnerMultipart {
/// A single field in a multipart stream /// A single field in a multipart stream
pub struct Field { pub struct Field {
ct: mime::Mime, ct: mime::Mime,
cd: ContentDisposition,
headers: HeaderMap, headers: HeaderMap,
inner: Rc<RefCell<InnerField>>, inner: Rc<RefCell<InnerField>>,
safety: Safety, safety: Safety,
@@ -381,35 +411,52 @@ impl Field {
safety: Safety, safety: Safety,
headers: HeaderMap, headers: HeaderMap,
ct: mime::Mime, ct: mime::Mime,
cd: ContentDisposition,
inner: Rc<RefCell<InnerField>>, inner: Rc<RefCell<InnerField>>,
) -> Self { ) -> Self {
Field { Field {
ct, ct,
cd,
headers, headers,
inner, inner,
safety, safety,
} }
} }
/// Get a map of headers /// Returns a reference to the field's header map.
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
&self.headers &self.headers
} }
/// Get the content type of the field /// Returns a reference to the field's content (mime) type.
pub fn content_type(&self) -> &mime::Mime { pub fn content_type(&self) -> &mime::Mime {
&self.ct &self.ct
} }
/// Get the content disposition of the field, if it exists /// Returns the field's Content-Disposition.
pub fn content_disposition(&self) -> Option<ContentDisposition> { ///
// RFC 7578: 'Each part MUST contain a Content-Disposition header field /// Per [RFC 7578 §4.2]: "Each part MUST contain a Content-Disposition header field where the
// where the disposition type is "form-data".' /// disposition type is `form-data`. The Content-Disposition header field MUST also contain an
if let Some(content_disposition) = self.headers.get(&header::CONTENT_DISPOSITION) { /// additional parameter of `name`; the value of the `name` parameter is the original field name
ContentDisposition::from_raw(content_disposition).ok() /// from the form."
} else { ///
None /// This crate validates that it exists before returning a `Field`. As such, it is safe to
/// unwrap `.content_disposition().get_name()`. The [name](Self::name) method is provided as
/// a convenience.
///
/// [RFC 7578 §4.2]: https://datatracker.ietf.org/doc/html/rfc7578#section-4.2
pub fn content_disposition(&self) -> &ContentDisposition {
&self.cd
} }
/// Returns the field's name.
///
/// See [content_disposition](Self::content_disposition) regarding guarantees about existence of
/// the name field.
pub fn name(&self) -> &str {
self.content_disposition()
.get_name()
.expect("field name should be guaranteed to exist in multipart form-data")
} }
} }
@@ -417,17 +464,19 @@ impl Stream for Field {
type Item = Result<Bytes, MultipartError>; type Item = Result<Bytes, MultipartError>;
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> { fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
if self.safety.current() { let this = self.get_mut();
let mut inner = self.inner.borrow_mut(); let mut inner = this.inner.borrow_mut();
if let Some(mut payload) = inner.payload.as_ref().unwrap().get_mut(&self.safety) { if let Some(mut buffer) = inner.payload.as_ref().unwrap().get_mut(&this.safety) {
payload.poll_stream(cx)?; // check safety and poll read payload to buffer.
} buffer.poll_stream(cx)?;
inner.poll(&self.safety) } else if !this.safety.is_clean() {
} else if !self.safety.is_clean() { // safety violation
Poll::Ready(Some(Err(MultipartError::NotConsumed))) return Poll::Ready(Some(Err(MultipartError::NotConsumed)));
} else { } else {
Poll::Pending return Poll::Pending;
} }
inner.poll(&this.safety)
} }
} }
@@ -451,20 +500,23 @@ struct InnerField {
} }
impl InnerField { impl InnerField {
fn new_in_rc(
payload: PayloadRef,
boundary: String,
headers: &HeaderMap,
) -> Result<Rc<RefCell<InnerField>>, PayloadError> {
Self::new(payload, boundary, headers).map(|this| Rc::new(RefCell::new(this)))
}
fn new( fn new(
payload: PayloadRef, payload: PayloadRef,
boundary: String, boundary: String,
headers: &HeaderMap, headers: &HeaderMap,
) -> Result<InnerField, PayloadError> { ) -> Result<InnerField, PayloadError> {
let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) { let len = if let Some(len) = headers.get(&header::CONTENT_LENGTH) {
if let Ok(s) = len.to_str() { match len.to_str().ok().and_then(|len| len.parse::<u64>().ok()) {
if let Ok(len) = s.parse::<u64>() { Some(len) => Some(len),
Some(len) None => return Err(PayloadError::Incomplete(None)),
} else {
return Err(PayloadError::Incomplete(None));
}
} else {
return Err(PayloadError::Incomplete(None));
} }
} else { } else {
None None
@@ -638,10 +690,7 @@ impl PayloadRef {
} }
} }
fn get_mut<'a, 'b>(&'a self, s: &'b Safety) -> Option<RefMut<'a, PayloadBuffer>> fn get_mut(&self, s: &Safety) -> Option<RefMut<'_, PayloadBuffer>> {
where
'a: 'b,
{
if s.current() { if s.current() {
Some(self.payload.borrow_mut()) Some(self.payload.borrow_mut())
} else { } else {
@@ -658,9 +707,11 @@ impl Clone for PayloadRef {
} }
} }
/// Counter. It tracks of number of clones of payloads and give access to /// Counter. It tracks of number of clones of payloads and give access to payload only to top most.
/// payload only to top most task panics if Safety get destroyed and it not top /// * When dropped, parent task is awakened. This is to support the case where Field is
/// most task. /// dropped in a separate task than Multipart.
/// * Assumes that parent owners don't move to different tasks; only the top-most is allowed to.
/// * If dropped and is not top most owner, is_clean flag is set to false.
#[derive(Debug)] #[derive(Debug)]
struct Safety { struct Safety {
task: LocalWaker, task: LocalWaker,
@@ -703,15 +754,16 @@ impl Safety {
impl Drop for Safety { impl Drop for Safety {
fn drop(&mut self) { fn drop(&mut self) {
// parent task is dead
if Rc::strong_count(&self.payload) != self.level { if Rc::strong_count(&self.payload) != self.level {
self.clean.set(true); // Multipart dropped leaving a Field
self.clean.set(false);
} }
self.task.wake(); self.task.wake();
} }
} }
/// Payload buffer /// Payload buffer.
struct PayloadBuffer { struct PayloadBuffer {
eof: bool, eof: bool,
buf: BytesMut, buf: BytesMut,
@@ -719,7 +771,7 @@ struct PayloadBuffer {
} }
impl PayloadBuffer { impl PayloadBuffer {
/// Create new `PayloadBuffer` instance /// Constructs new `PayloadBuffer` instance.
fn new<S>(stream: S) -> Self fn new<S>(stream: S) -> Self
where where
S: Stream<Item = Result<Bytes, PayloadError>> + 'static, S: Stream<Item = Result<Bytes, PayloadError>> + 'static,
@@ -727,7 +779,7 @@ impl PayloadBuffer {
PayloadBuffer { PayloadBuffer {
eof: false, eof: false,
buf: BytesMut::new(), buf: BytesMut::new(),
stream: stream.boxed_local(), stream: Box::pin(stream),
} }
} }
@@ -767,7 +819,7 @@ impl PayloadBuffer {
} }
/// Read until specified ending /// Read until specified ending
pub fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> { fn read_until(&mut self, line: &[u8]) -> Result<Option<Bytes>, MultipartError> {
let res = twoway::find_bytes(&self.buf, line) let res = twoway::find_bytes(&self.buf, line)
.map(|idx| self.buf.split_to(idx + line.len()).freeze()); .map(|idx| self.buf.split_to(idx + line.len()).freeze());
@@ -779,12 +831,12 @@ impl PayloadBuffer {
} }
/// Read bytes until new line delimiter /// Read bytes until new line delimiter
pub fn readline(&mut self) -> Result<Option<Bytes>, MultipartError> { fn readline(&mut self) -> Result<Option<Bytes>, MultipartError> {
self.read_until(b"\n") self.read_until(b"\n")
} }
/// Read bytes until new line delimiter or eof /// Read bytes until new line delimiter or eof
pub fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> { fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> {
match self.readline() { match self.readline() {
Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())), Err(MultipartError::Incomplete) if self.eof => Ok(Some(self.buf.split().freeze())),
line => line, line => line,
@@ -792,7 +844,7 @@ impl PayloadBuffer {
} }
/// Put unprocessed data back to the buffer /// Put unprocessed data back to the buffer
pub fn unprocessed(&mut self, data: Bytes) { fn unprocessed(&mut self, data: Bytes) {
let buf = BytesMut::from(data.as_ref()); let buf = BytesMut::from(data.as_ref());
let buf = std::mem::replace(&mut self.buf, buf); let buf = std::mem::replace(&mut self.buf, buf);
self.buf.extend_from_slice(&buf); self.buf.extend_from_slice(&buf);
@@ -805,10 +857,12 @@ mod tests {
use actix_http::h1::Payload; use actix_http::h1::Payload;
use actix_web::http::header::{DispositionParam, DispositionType}; use actix_web::http::header::{DispositionParam, DispositionType};
use actix_web::rt;
use actix_web::test::TestRequest; use actix_web::test::TestRequest;
use actix_web::FromRequest; use actix_web::FromRequest;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::lazy; use futures_util::{future::lazy, StreamExt};
use std::time::Duration;
use tokio::sync::mpsc; use tokio::sync::mpsc;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -914,6 +968,7 @@ mod tests {
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\ test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\ --abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\ Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\ data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n", --abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
@@ -965,7 +1020,7 @@ mod tests {
let mut multipart = Multipart::new(&headers, payload); let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await { match multipart.next().await {
Some(Ok(mut field)) => { Some(Ok(mut field)) => {
let cd = field.content_disposition().unwrap(); let cd = field.content_disposition();
assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.disposition, DispositionType::FormData);
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
@@ -1027,7 +1082,7 @@ mod tests {
let mut multipart = Multipart::new(&headers, payload); let mut multipart = Multipart::new(&headers, payload);
match multipart.next().await.unwrap() { match multipart.next().await.unwrap() {
Ok(mut field) => { Ok(mut field) => {
let cd = field.content_disposition().unwrap(); let cd = field.content_disposition();
assert_eq!(cd.disposition, DispositionType::FormData); assert_eq!(cd.disposition, DispositionType::FormData);
assert_eq!(cd.parameters[0], DispositionParam::Name("file".into())); assert_eq!(cd.parameters[0], DispositionParam::Name("file".into()));
@@ -1182,4 +1237,99 @@ mod tests {
_ => unreachable!(), _ => unreachable!(),
} }
} }
#[actix_rt::test]
async fn no_content_disposition() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = SlowStream::new(bytes);
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
MultipartError::NoContentDisposition,
));
}
#[actix_rt::test]
async fn no_name_in_content_disposition() {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let payload = SlowStream::new(bytes);
let mut multipart = Multipart::new(&headers, payload);
let res = multipart.next().await.unwrap();
assert!(res.is_err());
assert!(matches!(
res.unwrap_err(),
MultipartError::NoContentDisposition,
));
}
#[actix_rt::test]
async fn test_drop_multipart_dont_hang() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
let mut field = multipart.next().await.unwrap().unwrap();
drop(multipart);
// should fail immediately
match field.next().await {
Some(Err(MultipartError::NotConsumed)) => {}
_ => panic!(),
};
}
#[actix_rt::test]
async fn test_drop_field_awaken_multipart() {
let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
sender.send(Ok(bytes)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
let mut field = multipart.next().await.unwrap().unwrap();
let task = rt::spawn(async move {
rt::time::sleep(Duration::from_secs(1)).await;
assert_eq!(field.next().await.unwrap().unwrap(), "test");
drop(field);
});
// dropping field should awaken current task
let _ = multipart.next().await.unwrap().unwrap();
task.await.unwrap();
}
} }

View File

@@ -1,6 +1,7 @@
# Changes # Changes
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
* Minimum supported Rust version (MSRV) is now 1.52.
## 0.5.0-beta.2 - 2021-09-09 ## 0.5.0-beta.2 - 2021-09-09

View File

@@ -30,7 +30,7 @@ serde = "1"
[dev-dependencies] [dev-dependencies]
criterion = { version = "0.3", features = ["html_reports"] } criterion = { version = "0.3", features = ["html_reports"] }
firestorm = { version = "0.4", features = ["enable_system_time"] } firestorm = { version = "0.4", features = ["enable_system_time"] }
http = "0.2.3" http = "0.2.5"
serde = { version = "1", features = ["derive"] } serde = { version = "1", features = ["derive"] }
[[bench]] [[bench]]

View File

@@ -394,9 +394,7 @@ impl ResourceDef {
pub fn set_name(&mut self, name: impl Into<String>) { pub fn set_name(&mut self, name: impl Into<String>) {
let name = name.into(); let name = name.into();
if name.is_empty() { assert!(!name.is_empty(), "resource name should not be empty");
panic!("resource name should not be empty");
}
self.name = Some(name) self.name = Some(name)
} }
@@ -978,9 +976,7 @@ impl ResourceDef {
let (name, pattern) = match param.find(':') { let (name, pattern) = match param.find(':') {
Some(idx) => { Some(idx) => {
if tail { assert!(!tail, "custom regex is not supported for tail match");
panic!("custom regex is not supported for tail match");
}
let (name, pattern) = param.split_at(idx); let (name, pattern) = param.split_at(idx);
(name, &pattern[1..]) (name, &pattern[1..])
@@ -1087,12 +1083,12 @@ impl ResourceDef {
re.push_str(&escape(unprocessed)); re.push_str(&escape(unprocessed));
} }
if dyn_segment_count > MAX_DYNAMIC_SEGMENTS { assert!(
panic!( dyn_segment_count <= MAX_DYNAMIC_SEGMENTS,
"Only {} dynamic segments are allowed, provided: {}", "Only {} dynamic segments are allowed, provided: {}",
MAX_DYNAMIC_SEGMENTS, dyn_segment_count MAX_DYNAMIC_SEGMENTS,
dyn_segment_count
); );
}
// Store the pattern in capture group #1 to have context info outside it // Store the pattern in capture group #1 to have context info outside it
let mut re = format!("({})", re); let mut re = format!("({})", re);
@@ -1774,6 +1770,12 @@ mod tests {
match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1"); match_methods_agree!(["/v{v}", "/ver/{v}"] => "", "s/v", "/v1", "/v1/xx", "/ver/i3/5", "/ver/1");
} }
#[test]
#[should_panic]
fn duplicate_segment_name() {
ResourceDef::new("/user/{id}/post/{id}");
}
#[test] #[test]
#[should_panic] #[should_panic]
fn invalid_dynamic_segment_delimiter() { fn invalid_dynamic_segment_delimiter() {

View File

@@ -6,8 +6,9 @@ use crate::{IntoPatterns, Resource, ResourceDef, ResourcePath};
pub struct ResourceId(pub u16); pub struct ResourceId(pub u16);
/// Information about current resource /// Information about current resource
#[derive(Clone, Debug)] #[derive(Debug, Clone)]
pub struct ResourceInfo { pub struct ResourceInfo {
#[allow(dead_code)]
resource: ResourceId, resource: ResourceId,
} }

View File

@@ -3,6 +3,23 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.1.0-beta.7 - 2021-11-22
* Fix compatibility with experimental `io-uring` feature of `actix-rt`. [#2408]
[#2408]: https://github.com/actix/actix-web/pull/2408
## 0.1.0-beta.6 - 2021-11-15
* No significant changes from `0.1.0-beta.5`.
## 0.1.0-beta.5 - 2021-10-20
* Updated rustls to v0.20. [#2414]
* Minimum supported Rust version (MSRV) is now 1.52.
[#2414]: https://github.com/actix/actix-web/pull/2414
## 0.1.0-beta.4 - 2021-09-09 ## 0.1.0-beta.4 - 2021-09-09
* Minimum supported Rust version (MSRV) is now 1.51. * Minimum supported Rust version (MSRV) is now 1.51.

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