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

Compare commits

...

118 Commits

Author SHA1 Message Date
c260fb1c48 beta.7 releases (#2266) 2021-06-19 11:51:20 +01:00
532f7b9923 refined error model (#2253) 2021-06-17 17:57:58 +01:00
bb0331ae28 fix cargo cache on msrv 2021-06-17 16:49:31 +01:00
8d124713fc files: inline disposition for common web app file types (#2257) 2021-06-16 20:33:22 +01:00
fb2b362b60 Adjust JSON limit to 2MB and report on sizes (#2162)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-06-16 15:52:49 +01:00
75f65fea4f Extends Rustls ALPN protocols instead of replacing them when creating Rustls based services (#2226) 2021-06-10 16:25:21 +01:00
812269d656 clarify docs for BodyEncoding::encoding() (#2258) 2021-06-10 15:38:35 +01:00
e46cda5228 Deduplicate rt::main macro logic (#2255) 2021-06-08 22:44:56 +01:00
2e1d761854 add Seal argument to sealed AsHeaderName methods (#2252) 2021-06-08 12:57:19 +01:00
b1e841f168 Don't normalize URIs with no valid path (#2246) 2021-06-05 17:19:45 +01:00
0bb035cfa7 Add information about Actix discord server (#2247) 2021-06-04 02:54:40 +01:00
3479293416 Add zstd ContentEncoding support (#2244)
Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-06-03 21:32:52 +01:00
136dac1352 Additional test coverage and tidyup (middleware::normalize) (#2243) 2021-06-03 03:28:09 +01:00
e5b713b04a files: Fix redirect_to_slash_directory() when used with show_files_listing() (#2225) 2021-05-26 10:42:29 +01:00
3847429d00 Response::from_error take impl Into<Error> (#2214) 2021-05-26 13:41:48 +09:00
bb7d33c9d4 refactor h2 dispatcher to async/await.reduce duplicate code (#2211) 2021-05-25 03:21:20 +01:00
4598a7c0cc Only run UI tests on MSRV (#2232) 2021-05-25 00:09:38 +09:00
b1de196509 Fix clippy warnings (#2217) 2021-05-15 01:13:33 +01:00
2a8c650f2c move internalerror to actix web (#2215) 2021-05-14 16:40:00 +01:00
f277b128b6 cleanup ws test (#2213) 2021-05-13 12:24:32 +01:00
4903950b22 update changelog 2021-05-09 20:15:49 +01:00
f55e8d7a11 remove error field from response 2021-05-09 20:15:48 +01:00
900c9e270e remove responsebody indirection from response (#2201) 2021-05-09 20:12:48 +01:00
a9dc1586a0 remove rogue eprintln 2021-05-07 10:14:25 +01:00
947caa3599 examples use info log level by default 2021-05-06 20:24:18 +01:00
7d1d5c8acd Expose SererBuilder::worker_max_blocking_threads (#2200) 2021-05-06 18:35:04 +01:00
ddaf8c3e43 add associated error type to MessageBody (#2183) 2021-05-05 18:36:02 +01:00
dd1a3e7675 Fix loophole in soundness of __private_get_type_id__ (#2199) 2021-05-05 11:16:12 +01:00
c17662fe39 Reduce the level of the emitted log line from error to debug. (#2196)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-05-03 00:58:14 +01:00
3a0fb3f89e Static either extract future (#2184) 2021-05-01 03:02:56 +01:00
1fcf92e11f Update dependency "language-tags" (#2188) 2021-04-28 01:23:12 +01:00
6a29a50f25 files doc wording 2021-04-22 18:37:45 +01:00
75867bd073 clean up files service docs and rename method
follow on from #2046
2021-04-22 18:31:21 +01:00
f44a0bc159 add support of filtering guards in Files of actix-files (#2046)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-22 18:13:13 +01:00
07036b5640 static form extract future (#2181) 2021-04-22 13:54:29 +01:00
a7cd4e85cf use stable codec 0.4.0 2021-04-21 11:14:22 +01:00
6a9c4f1026 update awc docs link, formatting (#2180) 2021-04-20 19:57:27 +01:00
427fe6bd82 improve responseerror trait docs 2021-04-19 23:16:04 +01:00
2aa674c1fd Fix perf drop in HttpResponseBuilder (#2174) 2021-04-19 23:15:57 +01:00
52bb2b5daf hide downcast macros 2021-04-19 03:42:53 +01:00
db97974dc1 make some http re-exports more accessible (#2171) 2021-04-19 03:29:38 +01:00
b9dbc58e20 content disposition methods take impl AsRef<str> 2021-04-19 02:31:11 +01:00
35f8188410 restore cookie methods on ServiceRequest 2021-04-19 02:24:20 +01:00
8ffb1f2011 update files changelog 2021-04-19 02:11:07 +01:00
26e9c80626 Named file service (#2135) 2021-04-18 23:34:51 +01:00
f462aaa7b6 prepare actix-test release 0.1.0-beta.2 2021-04-17 15:53:54 +01:00
5a162932f3 prepare awc release 3.0.0-beta.5 2021-04-17 15:30:31 +01:00
b2d6b6a70c prepare web release 4.0.0-beta.6 2021-04-17 15:28:13 +01:00
f743e885a3 prepare http release 3.0.0-beta.6 2021-04-17 15:24:18 +01:00
5747f84736 bump utils to stable v3 2021-04-17 02:07:33 +01:00
879a4cbcd8 re-export ready boilerplate macros in dev 2021-04-16 23:21:02 +01:00
2449f2555c missed one pipeline_factory 2021-04-16 20:48:37 +01:00
d8f56eee3e bump service to stable v2 2021-04-16 20:28:21 +01:00
8d88a0a9af rename header generator macros 2021-04-16 19:15:10 +01:00
845c02cb86 Add responder impl for Cow<str> (#2164) 2021-04-16 00:54:51 +01:00
64bed506c2 chore: update benchmaks to round 20 (#2163) 2021-04-15 19:11:30 +01:00
ff65f1d006 non exhaustive http errors (#2161) 2021-04-14 06:07:59 +01:00
a9f26286f9 reduce branches in h1 dispatcher poll_keepalive (#2089) 2021-04-14 05:20:45 +01:00
037ac80a32 document messagebody trait items 2021-04-14 03:23:15 +01:00
1bfdfd1f41 implement parts as assoc method 2021-04-14 02:57:28 +01:00
5202bf03c1 add some doc examples to response builder 2021-04-14 02:45:58 +01:00
387c229f28 move response builder code to own file 2021-04-14 02:12:47 +01:00
23e0c9b6e0 remove http-codes builders from actix-http (#2159) 2021-04-14 02:00:14 +01:00
02ced426fd add body to_bytes helper (#2158) 2021-04-13 13:34:22 +01:00
4442535a45 clippy 2021-04-13 12:44:38 +01:00
edd9f14752 remove unpin from body types (#2152) 2021-04-13 11:16:12 +01:00
ce50cc9523 files: Don't use canonical path when serving file (#2156) 2021-04-13 05:28:30 +01:00
981c54432c remove json and url encoded form support from -http (#2148) 2021-04-12 10:30:28 +01:00
44c55dd036 remove cookie support from -http (#2065) 2021-04-09 18:07:10 +01:00
c72d77065d derive debug where possible (#2142) 2021-04-09 03:22:51 +01:00
44a2d2214c update year in MIT license (#2143) 2021-04-09 01:28:35 +01:00
3f5a73793a make module/crate re-exports doc inline (#2141) 2021-04-08 20:51:16 +01:00
e0b2246c68 prepare test release 0.1.0-beta.1 2021-04-02 10:03:01 +01:00
e0ae8e59bf prepare actors release 4.0.0-beta.4 2021-04-02 09:55:35 +01:00
a9641e475a prepare http-test release 3.0.0-beta.4 2021-04-02 09:54:35 +01:00
05c7505563 prepare multipart release 0.4.0-beta.4 2021-04-02 09:45:31 +01:00
8561263545 prepare files release 0.6.0-beta.4 2021-04-02 09:43:51 +01:00
a32151525c prepare awc release 3.0.0-beta.4 2021-04-02 09:40:36 +01:00
546e7c5da4 prepare web release 4.0.0-beta.5 2021-04-02 09:37:51 +01:00
6fb06a720a prepare http release 3.0.0-beta.5 2021-04-02 09:27:11 +01:00
c54a0713de migrate integration testing to new crate (#2112) 2021-04-02 08:26:59 +01:00
50dc13f280 move typed headers and implement FromRequest (#2094)
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-01 16:42:18 +01:00
c8ed8dd1a4 migrate to -utils beta 4 (#2127) 2021-04-01 15:26:13 +01:00
a807d33600 added TestServer::client_headers (#2097)
Co-authored-by: fakeshadow <24548779@qq.com>
Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-04-01 06:40:10 +01:00
1f1be6fd3d add Client::headers (#2114) 2021-03-31 11:43:56 +01:00
c49fe79207 Simplify lifetime annotation in HttpServiceBuilder. Simplify PlStream (#2129) 2021-03-30 15:46:09 +01:00
f66774e30b remove From<OffsetDateTime> impl from HttpDate
fully removes time crate from public api of -http
2021-03-30 03:32:22 +01:00
1281a748d0 merge H1ServiceHandler requests into HttpServiceHandler (#2126) 2021-03-30 03:06:16 +01:00
222acfd070 Fix build for next actix-tls-beta release (#2122) 2021-03-29 13:45:48 +01:00
980ecc5f07 fix openssl windows ci 2021-03-29 13:01:37 +01:00
e8ce73b496 update dep docs 2021-03-29 11:52:59 +01:00
f954a30c34 Fix typo in CHANGES.md (#2124) 2021-03-29 10:18:05 +01:00
60f9cfbb2a Refactor actix_http::h2::service module. Reduce loc. (#2118) 2021-03-26 18:24:51 +00:00
6822bf2f58 Refactor actix_http::h1::service (#2117) 2021-03-26 16:15:04 +00:00
2f7f1fa97a fix broken pipe for h2 when client is instantly dropped (#2113) 2021-03-26 00:05:31 +00:00
8c2ce2dedb fix awc compress feature (#2116) 2021-03-25 22:47:37 +00:00
3188ef5731 don't use rust annotation on code doc blocks 2021-03-25 08:45:52 +00:00
9704beddf8 Relax MessageBody limit to 2048kb (#2110)
* relax MessageBody limit to 2048kb

* fix clippy

* Update awc/src/response.rs

Co-authored-by: Rob Ede <robjtede@icloud.com>

* fix default body limit

Co-authored-by: Rob Ede <robjtede@icloud.com>
2021-03-24 04:44:03 -07:00
1be54efbeb Simplify service factory macro (#2108) 2021-03-23 13:42:46 +00:00
746d983849 handle header error with CustomResponder (#2093) 2021-03-20 05:18:06 +00:00
8d9de76826 Simplify handler factory macro (#2086) 2021-03-19 16:30:53 +00:00
9488757c29 Update to socket2 v0.4 (#2092) 2021-03-19 12:17:06 +00:00
351286486c fix clippy warning on nightly (#2088)
* fix clippy warning on nightly
2021-03-19 19:25:35 +08:00
78fcd0237a Format extract macro (#2087) 2021-03-19 04:08:23 +00:00
81942d31d6 fix new dyn trait lint 2021-03-19 02:03:09 +00:00
b75b5114c3 refactor actix_http connection types and connector services (#2081) 2021-03-18 17:53:22 +00:00
abcb444dd9 fix routes in Path documentation (#2084) 2021-03-18 13:21:44 +00:00
983b6904a7 unvendor openssl 2021-03-17 00:38:54 +00:00
3dc2d145ef import some traits as _ 2021-03-17 00:38:54 +00:00
c8f6d37290 rename client io trait. reduce duplicate code (#2079) 2021-03-16 16:31:14 +00:00
69dd1a9bd6 Remove ConnectionLifetime trait. Simplify Acquired handling (#2072) 2021-03-16 02:56:23 +00:00
d93314a683 fix awc readme example (#2076) 2021-03-15 10:59:42 +00:00
a55e87faaa refactor actix_http::helpers to generic over bufmut trait (#2069) 2021-03-15 02:33:51 +00:00
515d0e3fb4 change behavior of default upgrade handler (#2071) 2021-03-13 22:20:18 +00:00
22dcc31193 Fix logger middleware properly escape %% (#2067) 2021-03-11 14:12:42 +00:00
909ef0344b document client mod removal
closes #2064
2021-03-11 00:43:03 +00:00
a2b0e86632 simplify connector generic type (#2063) 2021-03-10 23:57:32 +00:00
d0c1f1a84c remove actix_http::client::pool::Protocol (#2061) 2021-03-10 01:31:50 +00:00
216 changed files with 10009 additions and 6725 deletions

8
.cargo/config.toml Normal file
View File

@ -0,0 +1,8 @@
[alias]
chk = "check --workspace --all-features --tests --examples --bins"
lint = "clippy --workspace --tests --examples"
ci-min = "hack check --workspace --no-default-features"
ci-min-test = "hack check --workspace --no-default-features --tests --examples"
ci-default = "hack check --workspace"
ci-full = "check --workspace --bins --examples --tests"
ci-test = "test --workspace --all-features --no-fail-fast"

View File

@ -23,9 +23,27 @@ jobs:
name: ${{ matrix.target.name }} / ${{ matrix.version }} name: ${{ matrix.target.name }} / ${{ matrix.version }}
runs-on: ${{ matrix.target.os }} runs-on: ${{ matrix.target.os }}
env:
VCPKGRS_DYNAMIC: 1
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
# install OpenSSL on Windows
- name: Set vcpkg root
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: echo "VCPKG_ROOT=$env:VCPKG_INSTALLATION_ROOT" | Out-File -FilePath $env:GITHUB_ENV -Append
- name: Install OpenSSL
if: matrix.target.triple == 'x86_64-pc-windows-msvc'
run: vcpkg install openssl:x64-windows
- name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1
with:
toolchain: ${{ matrix.version }}-${{ matrix.target.triple }}
profile: minimal
override: true
- name: Install ${{ matrix.version }} - name: Install ${{ matrix.version }}
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
@ -50,7 +68,13 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: hack command: hack
args: --clean-per-run check --workspace --no-default-features --tests args: check --workspace --no-default-features
- name: check minimal + tests
uses: actions-rs/cargo@v1
with:
command: hack
args: check --workspace --no-default-features --tests --examples
- name: check full - name: check full
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@ -62,7 +86,7 @@ jobs:
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
args: -v --workspace --all-features --no-fail-fast -- --nocapture args: --workspace --all-features --no-fail-fast -- --nocapture
--skip=test_h2_content_length --skip=test_h2_content_length
--skip=test_reading_deflate_encoding_large_random_rustls --skip=test_reading_deflate_encoding_large_random_rustls
@ -99,5 +123,5 @@ jobs:
- name: Clear the cargo caches - name: Clear the cargo caches
run: | run: |
cargo install cargo-cache --no-default-features --features ci-autoclean cargo install cargo-cache --version 0.6.2 --no-default-features --features ci-autoclean
cargo-cache cargo-cache

View File

@ -3,6 +3,70 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 4.0.0-beta.7 - 2021-06-17
### Added
* `HttpServer::worker_max_blocking_threads` for setting block thread pool. [#2200]
### Changed
* Adjusted default JSON payload limit to 2MB (from 32kb) and included size and limits in the `JsonPayloadError::Overflow` error variant. [#2162]
[#2162]: (https://github.com/actix/actix-web/pull/2162)
* `ServiceResponse::error_response` now uses body type of `Body`. [#2201]
* `ServiceResponse::checked_expr` now returns a `Result`. [#2201]
* Update `language-tags` to `0.3`.
* `ServiceResponse::take_body`. [#2201]
* `ServiceResponse::map_body` closure receives and returns `B` instead of `ResponseBody<B>` types. [#2201]
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
* `middleware::normalize` now will not try to normalize URIs with no valid path [#2246]
### Removed
* `HttpResponse::take_body` and old `HttpResponse::into_body` method that casted body type. [#2201]
[#2200]: https://github.com/actix/actix-web/pull/2200
[#2201]: https://github.com/actix/actix-web/pull/2201
[#2253]: https://github.com/actix/actix-web/pull/2253
[#2246]: https://github.com/actix/actix-web/pull/2246
## 4.0.0-beta.6 - 2021-04-17
### Added
* `HttpResponse` and `HttpResponseBuilder` structs. [#2065]
### Changed
* Most error types are now marked `#[non_exhaustive]`. [#2148]
* Methods on `ContentDisposition` that took `T: AsRef<str>` now take `impl AsRef<str>`.
[#2065]: https://github.com/actix/actix-web/pull/2065
[#2148]: https://github.com/actix/actix-web/pull/2148
## 4.0.0-beta.5 - 2021-04-02
### Added
* `Header` extractor for extracting common HTTP headers in handlers. [#2094]
* Added `TestServer::client_headers` method. [#2097]
### Fixed
* Double ampersand in Logger format is escaped correctly. [#2067]
### Changed
* `CustomResponder` would return error as `HttpResponse` when `CustomResponder::with_header` failed
instead of skipping. (Only the first error is kept when multiple error occur) [#2093]
### Removed
* The `client` mod was removed. Clients should now use `awc` directly.
[871ca5e4](https://github.com/actix/actix-web/commit/871ca5e4ae2bdc22d1ea02701c2992fa8d04aed7)
* Integration testing was moved to new `actix-test` crate. Namely these items from the `test`
module: `TestServer`, `TestServerConfig`, `start`, `start_with`, and `unused_addr`. [#2112]
[#2067]: https://github.com/actix/actix-web/pull/2067
[#2093]: https://github.com/actix/actix-web/pull/2093
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2097]: https://github.com/actix/actix-web/pull/2097
[#2112]: https://github.com/actix/actix-web/pull/2112
## 4.0.0-beta.4 - 2021-03-09 ## 4.0.0-beta.4 - 2021-03-09
### Changed ### Changed
* Feature `cookies` is now optional and enabled by default. [#1981] * Feature `cookies` is now optional and enabled by default. [#1981]

View File

@ -1,26 +1,23 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "4.0.0-beta.4" version = "4.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust" description = "Actix Web is a powerful, pragmatic, and extremely fast web framework for Rust"
readme = "README.md"
keywords = ["actix", "http", "web", "framework", "async"] keywords = ["actix", "http", "web", "framework", "async"]
categories = [
"network-programming",
"asynchronous",
"web-programming::http-server",
"web-programming::websocket"
]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web"
documentation = "https://docs.rs/actix-web/"
categories = ["network-programming", "asynchronous",
"web-programming::http-server",
"web-programming::websocket"]
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", "secure-cookies"] features = ["openssl", "rustls", "compress", "cookies", "secure-cookies"]
[badges]
travis-ci = { repository = "actix/actix-web", branch = "master" }
codecov = { repository = "actix/actix-web", branch = "master", service = "github" }
[lib] [lib]
name = "actix_web" name = "actix_web"
@ -28,33 +25,108 @@ path = "src/lib.rs"
[workspace] [workspace]
members = [ members = [
".", ".",
"awc", "awc",
"actix-http", "actix-http",
"actix-files", "actix-files",
"actix-multipart", "actix-multipart",
"actix-web-actors", "actix-web-actors",
"actix-web-codegen", "actix-web-codegen",
"actix-http-test", "actix-http-test",
"actix-test",
] ]
# enable when MSRV is 1.51+
# resolver = "2"
[features] [features]
default = ["compress", "cookies"] default = ["compress", "cookies"]
# content-encoding support # content-encoding support
compress = ["actix-http/compress", "awc/compress"] compress = ["actix-http/compress"]
# support for cookies # support for cookies
cookies = ["actix-http/cookies", "awc/cookies"] cookies = ["cookie"]
# secure cookies feature # secure cookies feature
secure-cookies = ["actix-http/secure-cookies"] secure-cookies = ["cookie/secure"]
# openssl # openssl
openssl = ["tls-openssl", "actix-tls/accept", "actix-tls/openssl", "awc/openssl"] openssl = ["actix-http/openssl", "actix-tls/accept", "actix-tls/openssl"]
# rustls # rustls
rustls = ["tls-rustls", "actix-tls/accept", "actix-tls/rustls", "awc/rustls"] rustls = ["actix-http/rustls", "actix-tls/accept", "actix-tls/rustls"]
[dependencies]
actix-codec = "0.4.0"
actix-macros = "0.2.1"
actix-router = "0.2.7"
actix-rt = "2.2"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-tls = { version = "3.0.0-beta.5", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.7"
ahash = "0.7"
bytes = "1"
cookie = { version = "0.15", features = ["percent-encode"], optional = true }
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
itoa = "0.4"
language-tags = "0.3"
once_cell = "1.5"
log = "0.4"
mime = "0.3"
paste = "1"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6"
socket2 = "0.4.0"
time = { version = "0.2.23", default-features = false, features = ["std"] }
url = "2.1"
[dev-dependencies]
actix-test = { version = "0.1.0-beta.2", features = ["openssl", "rustls"] }
awc = { version = "3.0.0-beta.6", features = ["openssl"] }
brotli2 = "0.3.2"
criterion = "0.3"
env_logger = "0.8"
flate2 = "1.0.13"
zstd = "0.7"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
tls-openssl = { package = "openssl", version = "0.10.9" }
tls-rustls = { package = "rustls", version = "0.19.0" }
[profile.release]
lto = true
opt-level = 3
codegen-units = 1
[patch.crates-io]
actix-files = { path = "actix-files" }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-multipart = { path = "actix-multipart" }
actix-test = { path = "actix-test" }
actix-web = { path = "." }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
awc = { path = "awc" }
[[test]]
name = "test_server"
required-features = ["compress", "cookies"]
[[example]] [[example]]
name = "basic" name = "basic"
@ -64,83 +136,10 @@ required-features = ["compress"]
name = "uds" name = "uds"
required-features = ["compress"] required-features = ["compress"]
[[test]]
name = "test_server"
required-features = ["compress", "cookies"]
[[example]] [[example]]
name = "on_connect" name = "on_connect"
required-features = [] required-features = []
[[example]]
name = "client"
required-features = ["rustls"]
[dependencies]
actix-codec = "0.4.0-beta.1"
actix-macros = "0.2.0"
actix-router = "0.2.7"
actix-rt = "2.1"
actix-server = "2.0.0-beta.3"
actix-service = "2.0.0-beta.4"
actix-utils = "3.0.0-beta.2"
actix-tls = { version = "3.0.0-beta.4", default-features = false, optional = true }
actix-web-codegen = "0.5.0-beta.2"
actix-http = "3.0.0-beta.4"
awc = { version = "3.0.0-beta.3", default-features = false }
ahash = "0.7"
bytes = "1"
derive_more = "0.99.5"
either = "1.5.3"
encoding_rs = "0.8"
futures-core = { version = "0.3.7", default-features = false }
futures-util = { version = "0.3.7", default-features = false }
log = "0.4"
mime = "0.3"
pin-project = "1.0.0"
regex = "1.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
serde_urlencoded = "0.7"
smallvec = "1.6"
socket2 = "0.3.16"
time = { version = "0.2.23", default-features = false, features = ["std"] }
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }
url = "2.1"
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies]
brotli2 = "0.3.2"
criterion = "0.3"
env_logger = "0.8"
flate2 = "1.0.13"
rand = "0.8"
rcgen = "0.8"
serde_derive = "1.0"
[profile.release]
lto = true
opt-level = 3
codegen-units = 1
[patch.crates-io]
actix-web = { path = "." }
actix-http = { path = "actix-http" }
actix-http-test = { path = "actix-http-test" }
actix-web-actors = { path = "actix-web-actors" }
actix-web-codegen = { path = "actix-web-codegen" }
actix-multipart = { path = "actix-multipart" }
actix-files = { path = "actix-files" }
awc = { path = "awc" }
[[bench]] [[bench]]
name = "server" name = "server"
harness = false harness = false

View File

@ -1,4 +1,4 @@
Copyright (c) 2017 Actix Team Copyright (c) 2017-NOW Actix Team
Permission is hereby granted, free of charge, to any Permission is hereby granted, free of charge, to any
person obtaining a copy of this software and associated person obtaining a copy of this software and associated

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.4)](https://docs.rs/actix-web/4.0.0-beta.4) [![Documentation](https://docs.rs/actix-web/badge.svg?version=4.0.0-beta.7)](https://docs.rs/actix-web/4.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-web.svg) ![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.4/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.4) [![Dependency Status](https://deps.rs/crate/actix-web/4.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-web/4.0.0-beta.7)
<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)
@ -31,7 +31,7 @@
* Static assets * Static assets
* SSL support using OpenSSL or Rustls * SSL support using OpenSSL or Rustls
* Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/)) * Middlewares ([Logger, Session, CORS, etc](https://actix.rs/docs/middleware/))
* Includes an async [HTTP client](https://docs.rs/actix-web/latest/actix_web/client/index.html) * Includes an async [HTTP client](https://docs.rs/awc/)
* Runs on stable Rust 1.46+ * Runs on stable Rust 1.46+
## Documentation ## Documentation
@ -90,7 +90,7 @@ You may consider checking out
## Benchmarks ## Benchmarks
One of the fastest web frameworks available according to the One of the fastest web frameworks available according to the
[TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r19). [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r20&test=composite).
## License ## License

View File

@ -3,6 +3,25 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.6.0-beta.5 - 2021-06-17
* `NamedFile` now implements `ServiceFactory` and `HttpServiceFactory` making it much more useful in routing. For example, it can be used directly as a default service. [#2135]
* For symbolic links, `Content-Disposition` header no longer shows the filename of the original file. [#2156]
* `Files::redirect_to_slash_directory()` now works as expected when used with `Files::show_files_listing()`. [#2225]
* `application/{javascript, json, wasm}` mime type now have `inline` disposition by default. [#2257]
[#2135]: https://github.com/actix/actix-web/pull/2135
[#2156]: https://github.com/actix/actix-web/pull/2156
[#2225]: https://github.com/actix/actix-web/pull/2225
[#2257]: https://github.com/actix/actix-web/pull/2257
## 0.6.0-beta.4 - 2021-04-02
* No notable changes.
* Add support for `.guard` in `Files` to selectively filter `Files` services. [#2046]
[#2046]: https://github.com/actix/actix-web/pull/2046
## 0.6.0-beta.3 - 2021-03-09 ## 0.6.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

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

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.3)](https://docs.rs/actix-files/0.6.0-beta.3) [![Documentation](https://docs.rs/actix-files/badge.svg?version=0.6.0-beta.5)](https://docs.rs/actix-files/0.6.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![License](https://img.shields.io/crates/l/actix-files.svg) ![License](https://img.shields.io/crates/l/actix-files.svg)
<br /> <br />
[![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.3/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.3) [![dependency status](https://deps.rs/crate/actix-files/0.6.0-beta.5/status.svg)](https://deps.rs/crate/actix-files/0.6.0-beta.5)
[![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files) [![Download](https://img.shields.io/crates/d/actix-files.svg)](https://crates.io/crates/actix-files)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -1,4 +1,4 @@
use actix_web::{http::StatusCode, HttpResponse, ResponseError}; use actix_web::{http::StatusCode, ResponseError};
use derive_more::Display; use derive_more::Display;
/// Errors which can occur when serving static files. /// Errors which can occur when serving static files.
@ -16,8 +16,8 @@ pub enum FilesError {
/// Return `NotFound` for `FilesError` /// Return `NotFound` for `FilesError`
impl ResponseError for FilesError { impl ResponseError for FilesError {
fn error_response(&self) -> HttpResponse { fn status_code(&self) -> StatusCode {
HttpResponse::new(StatusCode::NOT_FOUND) StatusCode::NOT_FOUND
} }
} }

View File

@ -1,6 +1,7 @@
use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc}; use std::{cell::RefCell, fmt, io, path::PathBuf, rc::Rc};
use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt}; use actix_service::{boxed, IntoServiceFactory, ServiceFactory, ServiceFactoryExt};
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse}, dev::{AppService, HttpServiceFactory, ResourceDef, ServiceRequest, ServiceResponse},
error::Error, error::Error,
@ -8,7 +9,7 @@ use actix_web::{
http::header::DispositionType, http::header::DispositionType,
HttpRequest, HttpRequest,
}; };
use futures_util::future::{ok, FutureExt, LocalBoxFuture}; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService, directory_listing, named, Directory, DirectoryRenderer, FilesService, HttpNewService,
@ -19,7 +20,7 @@ use crate::{
/// ///
/// `Files` service must be registered with `App::service()` method. /// `Files` service must be registered with `App::service()` method.
/// ///
/// ```rust /// ```
/// use actix_web::App; /// use actix_web::App;
/// use actix_files::Files; /// use actix_files::Files;
/// ///
@ -36,7 +37,8 @@ pub struct Files {
renderer: Rc<DirectoryRenderer>, renderer: Rc<DirectoryRenderer>,
mime_override: Option<Rc<MimeOverride>>, mime_override: Option<Rc<MimeOverride>>,
file_flags: named::Flags, file_flags: named::Flags,
guards: Option<Rc<dyn Guard>>, use_guards: Option<Rc<dyn Guard>>,
guards: Vec<Rc<dyn Guard>>,
hidden_files: bool, hidden_files: bool,
} }
@ -58,6 +60,7 @@ impl Clone for Files {
file_flags: self.file_flags, file_flags: self.file_flags,
path: self.path.clone(), path: self.path.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
use_guards: self.use_guards.clone(),
guards: self.guards.clone(), guards: self.guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
} }
@ -79,10 +82,9 @@ impl Files {
/// If the mount path is set as the root path `/`, services registered after this one will /// If the mount path is set as the root path `/`, services registered after this one will
/// be inaccessible. Register more specific handlers and services first. /// be inaccessible. Register more specific handlers and services first.
/// ///
/// `Files` uses a threadpool for blocking filesystem operations. By default, the pool uses a /// `Files` utilizes the existing Tokio thread-pool for blocking filesystem operations.
/// max number of threads equal to `512 * HttpServer::worker`. Real time thread count are /// The number of running threads is adjusted over time as needed, up to a maximum of 512 times
/// adjusted with work load. More threads would spawn when need and threads goes idle for a /// the number of server [workers](actix_web::HttpServer::workers), by default.
/// period of time would be de-spawned.
pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files { pub fn new<T: Into<PathBuf>>(mount_path: &str, serve_from: T) -> Files {
let orig_dir = serve_from.into(); let orig_dir = serve_from.into();
let dir = match orig_dir.canonicalize() { let dir = match orig_dir.canonicalize() {
@ -103,7 +105,8 @@ impl Files {
renderer: Rc::new(directory_listing), renderer: Rc::new(directory_listing),
mime_override: None, mime_override: None,
file_flags: named::Flags::default(), file_flags: named::Flags::default(),
guards: None, use_guards: None,
guards: Vec::new(),
hidden_files: false, hidden_files: false,
} }
} }
@ -155,7 +158,6 @@ impl Files {
/// Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_etag(mut self, value: bool) -> Self { pub fn use_etag(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::ETAG, value); self.file_flags.set(named::Flags::ETAG, value);
self self
@ -164,7 +166,6 @@ impl Files {
/// Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
/// Default is true. /// Default is true.
#[inline]
pub fn use_last_modified(mut self, value: bool) -> Self { pub fn use_last_modified(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::LAST_MD, value); self.file_flags.set(named::Flags::LAST_MD, value);
self self
@ -173,31 +174,74 @@ impl Files {
/// Specifies whether text responses should signal a UTF-8 encoding. /// Specifies whether text responses should signal a UTF-8 encoding.
/// ///
/// Default is false (but will default to true in a future version). /// Default is false (but will default to true in a future version).
#[inline]
pub fn prefer_utf8(mut self, value: bool) -> Self { pub fn prefer_utf8(mut self, value: bool) -> Self {
self.file_flags.set(named::Flags::PREFER_UTF8, value); self.file_flags.set(named::Flags::PREFER_UTF8, value);
self self
} }
/// Specifies custom guards to use for directory listings and files. /// Adds a routing guard.
/// ///
/// Default behaviour allows GET and HEAD. /// Use this to allow multiple chained file services that respond to strictly different
#[inline] /// properties of a request. Due to the way routing works, if a guard check returns true and the
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self { /// request starts being handled by the file service, it will not be able to back-out and try
self.guards = Some(Rc::new(guards)); /// the next service, you will simply get a 404 (or 405) error response.
///
/// To allow `POST` requests to retrieve files, see [`Files::use_guards`].
///
/// # Examples
/// ```
/// use actix_web::{guard::Header, App};
/// use actix_files::Files;
///
/// App::new().service(
/// Files::new("/","/my/site/files")
/// .guard(Header("Host", "example.com"))
/// );
/// ```
pub fn guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.guards.push(Rc::new(guard));
self self
} }
/// Specifies guard to check before fetching directory listings or files.
///
/// Note that this guard has no effect on routing; it's main use is to guard on the request's
/// method just before serving the file, only allowing `GET` and `HEAD` requests by default.
/// See [`Files::guard`] for routing guards.
pub fn method_guard<G: Guard + 'static>(mut self, guard: G) -> Self {
self.use_guards = Some(Rc::new(guard));
self
}
#[doc(hidden)]
#[deprecated(since = "0.6.0", note = "Renamed to `method_guard`.")]
/// See [`Files::method_guard`].
pub fn use_guards<G: Guard + 'static>(self, guard: G) -> Self {
self.method_guard(guard)
}
/// Disable `Content-Disposition` header. /// Disable `Content-Disposition` header.
/// ///
/// By default Content-Disposition` header is enabled. /// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self { pub fn disable_content_disposition(mut self) -> Self {
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION); self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
self self
} }
/// Sets default handler which is used when no matched file could be found. /// Sets default handler which is used when no matched file could be found.
///
/// # Examples
/// Setting a fallback static file handler:
/// ```
/// use actix_files::{Files, NamedFile};
///
/// # fn run() -> Result<(), actix_web::Error> {
/// let files = Files::new("/", "./static")
/// .index_file("index.html")
/// .default_handler(NamedFile::open("./static/404.html")?);
/// # Ok(())
/// # }
/// ```
pub fn default_handler<F, U>(mut self, f: F) -> Self pub fn default_handler<F, U>(mut self, f: F) -> Self
where where
F: IntoServiceFactory<U, ServiceRequest>, F: IntoServiceFactory<U, ServiceRequest>,
@ -217,7 +261,6 @@ impl Files {
} }
/// Enables serving hidden files and directories, allowing a leading dots in url fragments. /// Enables serving hidden files and directories, allowing a leading dots in url fragments.
#[inline]
pub fn use_hidden_files(mut self) -> Self { pub fn use_hidden_files(mut self) -> Self {
self.hidden_files = true; self.hidden_files = true;
self self
@ -225,7 +268,19 @@ impl Files {
} }
impl HttpServiceFactory for Files { impl HttpServiceFactory for Files {
fn register(self, config: &mut AppService) { fn register(mut self, config: &mut AppService) {
let guards = if self.guards.is_empty() {
None
} else {
let guards = std::mem::take(&mut self.guards);
Some(
guards
.into_iter()
.map(|guard| -> Box<dyn Guard> { Box::new(guard) })
.collect::<Vec<_>>(),
)
};
if self.default.borrow().is_none() { if self.default.borrow().is_none() {
*self.default.borrow_mut() = Some(config.default_service()); *self.default.borrow_mut() = Some(config.default_service());
} }
@ -236,7 +291,7 @@ impl HttpServiceFactory for Files {
ResourceDef::prefix(&self.path) ResourceDef::prefix(&self.path)
}; };
config.register_service(rdef, None, self, None) config.register_service(rdef, guards, self, None)
} }
} }
@ -258,23 +313,23 @@ impl ServiceFactory<ServiceRequest> for Files {
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
mime_override: self.mime_override.clone(), mime_override: self.mime_override.clone(),
file_flags: self.file_flags, file_flags: self.file_flags,
guards: self.guards.clone(), guards: self.use_guards.clone(),
hidden_files: self.hidden_files, hidden_files: self.hidden_files,
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {
default let fut = default.new_service(());
.new_service(()) Box::pin(async {
.map(move |result| match result { match fut.await {
Ok(default) => { Ok(default) => {
srv.default = Some(default); srv.default = Some(default);
Ok(srv) Ok(srv)
} }
Err(_) => Err(()), Err(_) => Err(()),
}) }
.boxed_local() })
} else { } else {
ok(srv).boxed_local() Box::pin(ok(srv))
} }
} }
} }

View File

@ -3,7 +3,7 @@
//! Provides a non-blocking service for serving static files from disk. //! Provides a non-blocking service for serving static files from disk.
//! //!
//! # Example //! # Example
//! ```rust //! ```
//! use actix_web::App; //! use actix_web::App;
//! use actix_files::Files; //! use actix_files::Files;
//! //!
@ -65,6 +65,7 @@ mod tests {
}; };
use actix_service::ServiceFactory; use actix_service::ServiceFactory;
use actix_utils::future::ok;
use actix_web::{ use actix_web::{
guard, guard,
http::{ http::{
@ -76,7 +77,6 @@ mod tests {
web::{self, Bytes}, web::{self, Bytes},
App, HttpResponse, Responder, App, HttpResponse, Responder,
}; };
use futures_util::future::ok;
use super::*; use super::*;
@ -279,6 +279,22 @@ mod tests {
); );
} }
#[actix_rt::test]
async fn test_named_file_javascript() {
let file = NamedFile::open("tests/test.js").unwrap();
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).await.unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"application/javascript"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"test.js\""
);
}
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_image_attachment() { async fn test_named_file_image_attachment() {
let cd = ContentDisposition { let cd = ContentDisposition {
@ -413,7 +429,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_range_headers() { async fn test_named_file_content_range_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
@ -438,7 +454,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_named_file_content_length_headers() { async fn test_named_file_content_length_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
// Valid range header // Valid range header
let response = srv let response = srv
@ -477,7 +493,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_head_content_length_headers() { async fn test_head_content_length_headers() {
let srv = test::start(|| App::new().service(Files::new("/", "."))); let srv = actix_test::start(|| App::new().service(Files::new("/", ".")));
let response = srv.head("/tests/test.binary").send().await.unwrap(); let response = srv.head("/tests/test.binary").send().await.unwrap();
@ -532,7 +548,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_files_guards() { async fn test_files_guards() {
let srv = test::init_service( let srv = test::init_service(
App::new().service(Files::new("/", ".").use_guards(guard::Post())), App::new().service(Files::new("/", ".").method_guard(guard::Post())),
) )
.await; .await;
@ -632,7 +648,7 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_redirect_to_slash_directory() { async fn test_redirect_to_slash_directory() {
// should not redirect if no index // should not redirect if no index and files listing is disabled
let srv = test::init_service( let srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()), App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
) )
@ -654,6 +670,19 @@ mod tests {
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND); assert_eq!(resp.status(), StatusCode::FOUND);
// should redirect if files listing is enabled
let srv = test::init_service(
App::new().service(
Files::new("/", ".")
.show_files_listing()
.redirect_to_slash_directory(),
),
)
.await;
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&srv, req).await;
assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong // should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request(); let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&srv, req).await; let resp = test::call_service(&srv, req).await;
@ -754,4 +783,93 @@ mod tests {
let res = test::call_service(&srv, req).await; let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK); assert_eq!(res.status(), StatusCode::OK);
} }
#[actix_rt::test]
async fn test_serve_named_file() {
let srv =
test::init_service(App::new().service(NamedFile::open("Cargo.toml").unwrap()))
.await;
let req = TestRequest::get().uri("/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
let req = TestRequest::get().uri("/test/unknown").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_serve_named_file_prefix() {
let srv = test::init_service(
App::new()
.service(web::scope("/test").service(NamedFile::open("Cargo.toml").unwrap())),
)
.await;
let req = TestRequest::get().uri("/test/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
let req = TestRequest::get().uri("/Cargo.toml").to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::NOT_FOUND);
}
#[actix_rt::test]
async fn test_named_file_default_service() {
let srv = test::init_service(
App::new().default_service(NamedFile::open("Cargo.toml").unwrap()),
)
.await;
for route in ["/foobar", "/baz", "/"].iter() {
let req = TestRequest::get().uri(route).to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
let bytes = test::read_body(res).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
}
}
#[actix_rt::test]
async fn test_default_handler_named_file() {
let st = Files::new("/", ".")
.default_handler(NamedFile::open("Cargo.toml").unwrap())
.new_service(())
.await
.unwrap();
let req = TestRequest::with_uri("/missing").to_srv_request();
let resp = test::call_service(&st, req).await;
assert_eq!(resp.status(), StatusCode::OK);
let bytes = test::read_body(resp).await;
let data = Bytes::from(fs::read("Cargo.toml").unwrap());
assert_eq!(bytes, data);
}
#[actix_rt::test]
async fn test_symlinks() {
let srv = test::init_service(App::new().service(Files::new("test", "."))).await;
let req = TestRequest::get()
.uri("/test/tests/symlink-test.png")
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(
res.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"symlink-test.png\""
);
}
} }

View File

@ -1,3 +1,6 @@
use actix_service::{Service, ServiceFactory};
use actix_utils::future::{ok, ready, Ready};
use actix_web::dev::{AppService, HttpServiceFactory, ResourceDef};
use std::fs::{File, Metadata}; use std::fs::{File, Metadata};
use std::io; use std::io;
use std::ops::{Deref, DerefMut}; use std::ops::{Deref, DerefMut};
@ -8,14 +11,14 @@ use std::time::{SystemTime, UNIX_EPOCH};
use std::os::unix::fs::MetadataExt; use std::os::unix::fs::MetadataExt;
use actix_web::{ use actix_web::{
dev::{BodyEncoding, SizedStream}, dev::{BodyEncoding, ServiceRequest, ServiceResponse, SizedStream},
http::{ http::{
header::{ header::{
self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
}, },
ContentEncoding, StatusCode, ContentEncoding, StatusCode,
}, },
HttpMessage, HttpRequest, HttpResponse, Responder, Error, HttpMessage, HttpRequest, HttpResponse, Responder,
}; };
use bitflags::bitflags; use bitflags::bitflags;
use mime_guess::from_path; use mime_guess::from_path;
@ -39,6 +42,29 @@ impl Default for Flags {
} }
/// A file with an associated name. /// A file with an associated name.
///
/// `NamedFile` can be registered as services:
/// ```
/// use actix_web::App;
/// use actix_files::NamedFile;
///
/// # fn run() -> Result<(), Box<dyn std::error::Error>> {
/// let app = App::new()
/// .service(NamedFile::open("./static/index.html")?);
/// # Ok(())
/// # }
/// ```
///
/// They can also be returned from handlers:
/// ```
/// use actix_web::{Responder, get};
/// use actix_files::NamedFile;
///
/// #[get("/")]
/// async fn index() -> impl Responder {
/// NamedFile::open("./static/index.html")
/// }
/// ```
#[derive(Debug)] #[derive(Debug)]
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
@ -60,7 +86,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// use std::io::{self, Write}; /// use std::io::{self, Write};
/// use std::env; /// use std::env;
@ -94,6 +120,11 @@ impl NamedFile {
let disposition = match ct.type_() { let disposition = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
mime::APPLICATION => match ct.subtype() {
mime::JAVASCRIPT | mime::JSON => DispositionType::Inline,
name if name == "wasm" => DispositionType::Inline,
_ => DispositionType::Attachment,
},
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
@ -137,7 +168,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
/// let file = NamedFile::open("foo.txt"); /// let file = NamedFile::open("foo.txt");
@ -156,7 +187,7 @@ impl NamedFile {
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// # use std::io; /// # use std::io;
/// use actix_files::NamedFile; /// use actix_files::NamedFile;
/// ///
@ -187,9 +218,11 @@ impl NamedFile {
/// Set the Content-Disposition for serving this file. This allows /// Set the Content-Disposition for serving this file. This allows
/// changing the inline/attachment disposition as well as the filename /// changing the inline/attachment disposition as well as the filename
/// sent to the peer. By default the disposition is `inline` for text, /// sent to the peer.
/// image, and video content types, and `attachment` otherwise, and ///
/// the filename is taken from the path provided in the `open` method /// By default the disposition is `inline` for `text/*`, `image/*`, `video/*` and
/// `application/{javascript, json, wasm}` mime types, and `attachment` otherwise,
/// and the filename is taken from the path provided in the `open` method
/// after converting it to UTF-8 using. /// after converting it to UTF-8 using.
/// [`std::ffi::OsStr::to_string_lossy`] /// [`std::ffi::OsStr::to_string_lossy`]
#[inline] #[inline]
@ -209,6 +242,8 @@ impl NamedFile {
} }
/// Set content encoding for serving this file /// Set content encoding for serving this file
///
/// Must be used with [`actix_web::middleware::Compress`] to take effect.
#[inline] #[inline]
pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self { pub fn set_content_encoding(mut self, enc: ContentEncoding) -> Self {
self.encoding = Some(enc); self.encoding = Some(enc);
@ -480,3 +515,53 @@ impl Responder for NamedFile {
self.into_response(req) self.into_response(req)
} }
} }
impl ServiceFactory<ServiceRequest> for NamedFile {
type Response = ServiceResponse;
type Error = Error;
type Config = ();
type InitError = ();
type Service = NamedFileService;
type Future = Ready<Result<Self::Service, ()>>;
fn new_service(&self, _: ()) -> Self::Future {
ok(NamedFileService {
path: self.path.clone(),
})
}
}
#[doc(hidden)]
#[derive(Debug)]
pub struct NamedFileService {
path: PathBuf,
}
impl Service<ServiceRequest> for NamedFileService {
type Response = ServiceResponse;
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn call(&self, req: ServiceRequest) -> Self::Future {
let (req, _) = req.into_parts();
ready(
NamedFile::open(&self.path)
.map_err(|e| e.into())
.map(|f| f.into_response(&req))
.map(|res| ServiceResponse::new(req, res)),
)
}
}
impl HttpServiceFactory for NamedFile {
fn register(self, config: &mut AppService) {
config.register_service(
ResourceDef::root_prefix(self.path.to_string_lossy().as_ref()),
None,
self,
None,
)
}
}

View File

@ -3,8 +3,8 @@ use std::{
str::FromStr, str::FromStr,
}; };
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, FromRequest, HttpRequest}; use actix_web::{dev::Payload, FromRequest, HttpRequest};
use futures_util::future::{ready, Ready};
use crate::error::UriSegmentError; use crate::error::UriSegmentError;

View File

@ -1,6 +1,7 @@
use std::{fmt, io, path::PathBuf, rc::Rc, task::Poll}; use std::{fmt, io, 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,
@ -8,7 +9,7 @@ use actix_web::{
http::{header, Method}, http::{header, Method},
HttpResponse, HttpResponse,
}; };
use futures_util::future::{ok, Either, LocalBoxFuture, Ready}; use futures_core::future::LocalBoxFuture;
use crate::{ use crate::{
named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile, named, Directory, DirectoryRenderer, FilesError, HttpService, MimeOverride, NamedFile,
@ -29,19 +30,18 @@ pub struct FilesService {
pub(crate) hidden_files: bool, pub(crate) hidden_files: bool,
} }
type FilesServiceFuture = Either<
Ready<Result<ServiceResponse, Error>>,
LocalBoxFuture<'static, Result<ServiceResponse, Error>>,
>;
impl FilesService { impl FilesService {
fn handle_err(&self, e: io::Error, req: ServiceRequest) -> FilesServiceFuture { fn handle_err(
log::debug!("Failed to handle {}: {}", req.path(), e); &self,
err: io::Error,
req: ServiceRequest,
) -> LocalBoxFuture<'static, Result<ServiceResponse, Error>> {
log::debug!("error handling {}: {}", req.path(), err);
if let Some(ref default) = self.default { if let Some(ref default) = self.default {
Either::Right(default.call(req)) Box::pin(default.call(req))
} else { } else {
Either::Left(ok(req.error_response(e))) Box::pin(ok(req.error_response(err)))
} }
} }
} }
@ -55,7 +55,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 = FilesServiceFuture; type Future = LocalBoxFuture<'static, Result<ServiceResponse, Error>>;
actix_service::always_ready!(); actix_service::always_ready!();
@ -69,7 +69,7 @@ impl Service<ServiceRequest> for FilesService {
}; };
if !is_method_valid { if !is_method_valid {
return Either::Left(ok(req.into_response( return Box::pin(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."),
@ -79,28 +79,30 @@ impl Service<ServiceRequest> for FilesService {
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(), self.hidden_files) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Either::Left(ok(req.error_response(e))), Err(e) => return Box::pin(ok(req.error_response(e))),
}; };
// full file path // full file path
let path = match self.directory.join(&real_path).canonicalize() { let path = self.directory.join(&real_path);
Ok(path) => path, if let Err(err) = path.canonicalize() {
Err(e) => return self.handle_err(e, req), return Box::pin(self.handle_err(err, req));
}; }
if path.is_dir() { if path.is_dir() {
if self.redirect_to_slash
&& !req.path().ends_with('/')
&& (self.index.is_some() || self.show_index)
{
let redirect_to = format!("{}/", req.path());
return Box::pin(ok(req.into_response(
HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to))
.finish(),
)));
}
if let Some(ref redir_index) = self.index { if let Some(ref redir_index) = self.index {
if self.redirect_to_slash && !req.path().ends_with('/') {
let redirect_to = format!("{}/", req.path());
return Either::Left(ok(req.into_response(
HttpResponse::Found()
.insert_header((header::LOCATION, redirect_to))
.body("")
.into_body(),
)));
}
let path = path.join(redir_index); let path = path.join(redir_index);
match NamedFile::open(path) { match NamedFile::open(path) {
@ -114,9 +116,9 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res))) Box::pin(ok(ServiceResponse::new(req, res)))
} }
Err(e) => self.handle_err(e, req), Err(err) => self.handle_err(err, req),
} }
} else if self.show_index { } else if self.show_index {
let dir = Directory::new(self.directory.clone(), path); let dir = Directory::new(self.directory.clone(), path);
@ -124,12 +126,12 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let x = (self.renderer)(&dir, &req); let x = (self.renderer)(&dir, &req);
match x { Box::pin(match x {
Ok(resp) => Either::Left(ok(resp)), Ok(resp) => ok(resp),
Err(e) => Either::Left(ok(ServiceResponse::from_err(e, req))), Err(err) => ok(ServiceResponse::from_err(err, req)),
} })
} else { } else {
Either::Left(ok(ServiceResponse::from_err( Box::pin(ok(ServiceResponse::from_err(
FilesError::IsDirectory, FilesError::IsDirectory,
req.into_parts().0, req.into_parts().0,
))) )))
@ -145,9 +147,9 @@ impl Service<ServiceRequest> for FilesService {
let (req, _) = req.into_parts(); let (req, _) = req.into_parts();
let res = named_file.into_response(&req); let res = named_file.into_response(&req);
Either::Left(ok(ServiceResponse::new(req, res))) Box::pin(ok(ServiceResponse::new(req, res)))
} }
Err(e) => self.handle_err(e, req), Err(err) => self.handle_err(err, req),
} }
} }
} }

View File

@ -0,0 +1 @@
first

View File

@ -0,0 +1 @@
second

View File

@ -0,0 +1,36 @@
use actix_files::Files;
use actix_web::{
guard::Host,
http::StatusCode,
test::{self, TestRequest},
App,
};
use bytes::Bytes;
#[actix_rt::test]
async fn test_guard_filter() {
let srv = test::init_service(
App::new()
.service(Files::new("/", "./tests/fixtures/guards/first").guard(Host("first.com")))
.service(
Files::new("/", "./tests/fixtures/guards/second").guard(Host("second.com")),
),
)
.await;
let req = TestRequest::with_uri("/index.txt")
.append_header(("Host", "first.com"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(test::read_body(res).await, Bytes::from("first"));
let req = TestRequest::with_uri("/index.txt")
.append_header(("Host", "second.com"))
.to_request();
let res = test::call_service(&srv, req).await;
assert_eq!(res.status(), StatusCode::OK);
assert_eq!(test::read_body(res).await, Bytes::from("second"));
}

View File

@ -0,0 +1 @@
test.png

View File

@ -0,0 +1 @@
// this file is empty.

View File

@ -3,6 +3,12 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.4 - 2021-04-02
* Added `TestServer::client_headers` method. [#2097]
[#2097]: https://github.com/actix/actix-web/pull/2097
## 3.0.0-beta.3 - 2021-03-09 ## 3.0.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "3.0.0-beta.3" version = "3.0.0-beta.4"
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"
readme = "README.md" readme = "README.md"
@ -29,20 +29,20 @@ default = []
openssl = ["tls-openssl", "awc/openssl"] openssl = ["tls-openssl", "awc/openssl"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.4" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-tls = "3.0.0-beta.4" actix-tls = "3.0.0-beta.5"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0"
actix-rt = "2.1" actix-rt = "2.2"
actix-server = "2.0.0-beta.3" actix-server = "2.0.0-beta.3"
awc = { version = "3.0.0-beta.3", default-features = false } awc = { version = "3.0.0-beta.6", 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.2"
log = "0.4" log = "0.4"
socket2 = "0.3" socket2 = "0.4"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
slab = "0.4" slab = "0.4"
@ -50,12 +50,6 @@ serde_urlencoded = "0.7"
time = { version = "0.2.23", default-features = false, features = ["std"] } 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 }
[target.'cfg(windows)'.dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
optional = true
[dev-dependencies] [dev-dependencies]
actix-web = { version = "4.0.0-beta.4", default-features = false, features = ["cookies"] } actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] }
actix-http = "3.0.0-beta.4" actix-http = "3.0.0-beta.7"

View File

@ -3,9 +3,9 @@
> 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.3)](https://docs.rs/actix-http-test/3.0.0-beta.3) [![Documentation](https://docs.rs/actix-http-test/badge.svg?version=3.0.0-beta.4)](https://docs.rs/actix-http-test/3.0.0-beta.4)
![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)
[![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.3/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.3) [![Dependency Status](https://deps.rs/crate/actix-http-test/3.0.0-beta.4/status.svg)](https://deps.rs/crate/actix-http-test/3.0.0-beta.4)
[![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix-web](https://badges.gitter.im/actix/actix-web.svg)](https://gitter.im/actix/actix-web?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & Resources ## Documentation & Resources

View File

@ -13,7 +13,9 @@ 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::{error::PayloadError, ws, Client, ClientRequest, ClientResponse, Connector}; use awc::{
error::PayloadError, http::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;
@ -26,7 +28,7 @@ use socket2::{Domain, Protocol, Socket, Type};
/// ///
/// # Examples /// # Examples
/// ///
/// ```rust /// ```
/// use actix_http::HttpService; /// use actix_http::HttpService;
/// use actix_http_test::TestServer; /// use actix_http_test::TestServer;
/// use actix_web::{web, App, HttpResponse, Error}; /// use actix_web::{web, App, HttpResponse, Error};
@ -115,16 +117,6 @@ pub async fn test_server_with_addr<F: ServiceFactory<TcpStream>>(
} }
} }
/// Get first available unused address
pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket = Socket::new(Domain::ipv4(), Type::stream(), Some(Protocol::tcp())).unwrap();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = socket.into_tcp_listener();
tcp.local_addr().unwrap()
}
/// Test server controller /// Test server controller
pub struct TestServer { pub struct TestServer {
addr: net::SocketAddr, addr: net::SocketAddr,
@ -258,6 +250,14 @@ impl TestServer {
self.ws_at("/").await self.ws_at("/").await
} }
/// Get default HeaderMap of Client.
///
/// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time).
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
self.client.headers()
}
/// Stop HTTP server /// Stop HTTP server
fn stop(&mut self) { fn stop(&mut self) {
self.system.stop(); self.system.stop();
@ -269,3 +269,13 @@ impl Drop for TestServer {
self.stop() self.stop()
} }
} }
/// Get a localhost socket address with random, unused port.
pub fn unused_addr() -> net::SocketAddr {
let addr: net::SocketAddr = "127.0.0.1:0".parse().unwrap();
let socket = Socket::new(Domain::IPV4, Type::STREAM, Some(Protocol::TCP)).unwrap();
socket.bind(&addr.into()).unwrap();
socket.set_reuse_address(true).unwrap();
let tcp = net::TcpListener::from(socket);
tcp.local_addr().unwrap()
}

View File

@ -3,6 +3,102 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 3.0.0-beta.7 - 2021-06-17
### Added
* Alias `body::Body` as `body::AnyBody`. [#2215]
* `BoxAnyBody`: a boxed message body with boxed errors. [#2183]
* Re-export `http` crate's `Error` type as `error::HttpError`. [#2171]
* Re-export `StatusCode`, `Method`, `Version` and `Uri` at the crate root. [#2171]
* Re-export `ContentEncoding` and `ConnectionType` at the crate root. [#2171]
* `Response::into_body` that consumes response and returns body type. [#2201]
* `impl Default` for `Response`. [#2201]
* Add zstd support for `ContentEncoding`. [#2244]
### Changed
* The `MessageBody` trait now has an associated `Error` type. [#2183]
* All error trait bounds in server service builders have changed from `Into<Error>` to `Into<Response<AnyBody>>`. [#2253]
* All error trait bounds in message body and stream impls changed from `Into<Error>` to `Into<Box<dyn std::error::Error>>`. [#2253]
* Places in `Response` where `ResponseBody<B>` was received or returned now simply use `B`. [#2201]
* `header` mod is now public. [#2171]
* `uri` mod is now public. [#2171]
* Update `language-tags` to `0.3`.
* Reduce the level from `error` to `debug` for the log line that is emitted when a `500 Internal Server Error` is built using `HttpResponse::from_error`. [#2201]
* `ResponseBuilder::message_body` now returns a `Result`. [#2201]
* Remove `Unpin` bound on `ResponseBuilder::streaming`. [#2253]
* `HttpServer::{listen_rustls(), bind_rustls()}` now honor the ALPN protocols in the configuation parameter. [#2226]
### Removed
* Stop re-exporting `http` crate's `HeaderMap` types in addition to ours. [#2171]
* Down-casting for `MessageBody` types. [#2183]
* `error::Result` alias. [#2201]
* Error field from `Response` and `Response::error`. [#2205]
* `impl Future` for `Response`. [#2201]
* `Response::take_body` and old `Response::into_body` method that casted body type. [#2201]
* `InternalError` and all the error types it constructed. [#2215]
* Conversion (`impl Into`) of `Response<Body>` and `ResponseBuilder` to `Error`. [#2215]
[#2171]: https://github.com/actix/actix-web/pull/2171
[#2183]: https://github.com/actix/actix-web/pull/2183
[#2196]: https://github.com/actix/actix-web/pull/2196
[#2201]: https://github.com/actix/actix-web/pull/2201
[#2205]: https://github.com/actix/actix-web/pull/2205
[#2215]: https://github.com/actix/actix-web/pull/2215
[#2253]: https://github.com/actix/actix-web/pull/2253
[#2244]: https://github.com/actix/actix-web/pull/2244
## 3.0.0-beta.6 - 2021-04-17
### Added
* `impl<T: MessageBody> MessageBody for Pin<Box<T>>`. [#2152]
* `Response::{ok, bad_request, not_found, internal_server_error}`. [#2159]
* Helper `body::to_bytes` for async collecting message body into Bytes. [#2158]
### Changes
* The type parameter of `Response` no longer has a default. [#2152]
* The `Message` variant of `body::Body` is now `Pin<Box<dyn MessageBody>>`. [#2152]
* `BodyStream` and `SizedStream` are no longer restricted to Unpin types. [#2152]
* Error enum types are marked `#[non_exhaustive]`. [#2161]
### Removed
* `cookies` feature flag. [#2065]
* Top-level `cookies` mod (re-export). [#2065]
* `HttpMessage` trait loses the `cookies` and `cookie` methods. [#2065]
* `impl ResponseError for CookieParseError`. [#2065]
* Deprecated methods on `ResponseBuilder`: `if_true`, `if_some`. [#2148]
* `ResponseBuilder::json`. [#2148]
* `ResponseBuilder::{set_header, header}`. [#2148]
* `impl From<serde_json::Value> for Body`. [#2148]
* `Response::build_from`. [#2159]
* Most of the status code builders on `Response`. [#2159]
[#2065]: https://github.com/actix/actix-web/pull/2065
[#2148]: https://github.com/actix/actix-web/pull/2148
[#2152]: https://github.com/actix/actix-web/pull/2152
[#2159]: https://github.com/actix/actix-web/pull/2159
[#2158]: https://github.com/actix/actix-web/pull/2158
[#2161]: https://github.com/actix/actix-web/pull/2161
## 3.0.0-beta.5 - 2021-04-02
### Added
* `client::Connector::handshake_timeout` method for customizing TLS connection handshake timeout. [#2081]
* `client::ConnectorService` as `client::Connector::finish` method's return type [#2081]
* `client::ConnectionIo` trait alias [#2081]
### Changed
* `client::Connector` type now only have one generic type for `actix_service::Service`. [#2063]
### Removed
* Common typed HTTP headers were moved to actix-web. [2094]
* `ResponseError` impl for `actix_utils::timeout::TimeoutError`. [#2127]
[#2063]: https://github.com/actix/actix-web/pull/2063
[#2081]: https://github.com/actix/actix-web/pull/2081
[#2094]: https://github.com/actix/actix-web/pull/2094
[#2127]: https://github.com/actix/actix-web/pull/2127
## 3.0.0-beta.4 - 2021-03-08 ## 3.0.0-beta.4 - 2021-03-08
### Changed ### Changed
* Feature `cookies` is now optional and disabled by default. [#1981] * Feature `cookies` is now optional and disabled by default. [#1981]

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "3.0.0-beta.4" version = "3.0.0-beta.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "HTTP primitives for the Actix ecosystem" description = "HTTP primitives for the Actix ecosystem"
readme = "README.md" readme = "README.md"
@ -16,7 +16,7 @@ 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", "cookies", "secure-cookies"] features = ["openssl", "rustls", "compress"]
[lib] [lib]
name = "actix_http" name = "actix_http"
@ -32,31 +32,23 @@ openssl = ["actix-tls/openssl"]
rustls = ["actix-tls/rustls"] rustls = ["actix-tls/rustls"]
# enable compression support # enable compression support
compress = ["flate2", "brotli2"] compress = ["flate2", "brotli2", "zstd"]
# support for cookies
cookies = ["cookie"]
# support for secure cookies
secure-cookies = ["cookies", "cookie/secure"]
# trust-dns as client dns resolver # trust-dns as client dns resolver
trust-dns = ["trust-dns-resolver"] trust-dns = ["trust-dns-resolver"]
[dependencies] [dependencies]
actix-service = "2.0.0-beta.4" actix-service = "2.0.0"
actix-codec = "0.4.0-beta.1" actix-codec = "0.4.0"
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0"
actix-rt = "2.1" actix-rt = "2.2"
actix-tls = "3.0.0-beta.4" actix-tls = { version = "3.0.0-beta.5", features = ["accept", "connect"] }
ahash = "0.7" ahash = "0.7"
base64 = "0.13" base64 = "0.13"
bitflags = "1.2" bitflags = "1.2"
bytes = "1" bytes = "1"
bytestring = "1" bytestring = "1"
cfg-if = "1"
cookie = { version = "0.14.1", features = ["percent-encode"], optional = true }
derive_more = "0.99.5" derive_more = "0.99.5"
encoding_rs = "0.8" 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"] }
@ -65,17 +57,17 @@ h2 = "0.3.1"
http = "0.2.2" http = "0.2.2"
httparse = "1.3" httparse = "1.3"
itoa = "0.4" itoa = "0.4"
language-tags = "0.2" language-tags = "0.3"
local-channel = "0.1"
once_cell = "1.5" once_cell = "1.5"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "2.1" percent-encoding = "2.1"
pin-project = "1.0.0" pin-project = "1.0.0"
pin-project-lite = "0.2"
rand = "0.8" rand = "0.8"
regex = "1.3" regex = "1.3"
serde = "1.0" serde = "1.0"
serde_json = "1.0"
serde_urlencoded = "0.7"
sha-1 = "0.9" sha-1 = "0.9"
smallvec = "1.6" smallvec = "1.6"
time = { version = "0.2.23", default-features = false, features = ["std"] } time = { version = "0.2.23", default-features = false, features = ["std"] }
@ -84,24 +76,23 @@ tokio = { version = "1.2", features = ["sync"] }
# 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 }
trust-dns-resolver = { version = "0.20.0", 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-beta.3"
actix-http-test = { version = "3.0.0-beta.3", features = ["openssl"] } actix-http-test = { version = "3.0.0-beta.4", features = ["openssl"] }
actix-tls = { version = "3.0.0-beta.4", features = ["openssl"] } actix-tls = { version = "3.0.0-beta.5", features = ["openssl"] }
criterion = "0.3" async-stream = "0.3"
criterion = { version = "0.3", features = ["html_reports"] }
env_logger = "0.8" env_logger = "0.8"
rcgen = "0.8" rcgen = "0.8"
serde_derive = "1.0" serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tls-openssl = { version = "0.10", package = "openssl" } tls-openssl = { version = "0.10", package = "openssl" }
tls-rustls = { version = "0.19", package = "rustls" } tls-rustls = { version = "0.19", package = "rustls" }
webpki = { version = "0.21.0" }
[target.'cfg(windows)'.dev-dependencies.tls-openssl]
version = "0.10.9"
package = "openssl"
features = ["vendored"]
[[example]] [[example]]
name = "ws" name = "ws"

View File

@ -3,11 +3,11 @@
> 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.4)](https://docs.rs/actix-http/3.0.0-beta.4) [![Documentation](https://docs.rs/actix-http/badge.svg?version=3.0.0-beta.7)](https://docs.rs/actix-http/3.0.0-beta.7)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-http.svg) ![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.4/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.4) [![dependency status](https://deps.rs/crate/actix-http/3.0.0-beta.7/status.svg)](https://deps.rs/crate/actix-http/3.0.0-beta.7)
[![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)
[![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)

View File

@ -1,19 +1,17 @@
use std::{env, io}; use std::io;
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{http::StatusCode, Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt; use futures_util::StreamExt as _;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "echo=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
@ -23,9 +21,10 @@ async fn main() -> io::Result<()> {
body.extend_from_slice(&item?); body.extend_from_slice(&item?);
} }
info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok::<_, Error>( Ok::<_, Error>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header(( .insert_header((
"x-head", "x-head",
HeaderValue::from_static("dummy value!"), HeaderValue::from_static("dummy value!"),

View File

@ -1,31 +1,30 @@
use std::{env, io}; use std::io;
use actix_http::http::HeaderValue; use actix_http::{body::Body, http::HeaderValue, http::StatusCode};
use actix_http::{Error, HttpService, Request, Response}; use actix_http::{Error, HttpService, Request, Response};
use actix_server::Server; use actix_server::Server;
use bytes::BytesMut; use bytes::BytesMut;
use futures_util::StreamExt; use futures_util::StreamExt as _;
use log::info;
async fn handle_request(mut req: Request) -> Result<Response, Error> { async fn handle_request(mut req: Request) -> Result<Response<Body>, 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?)
} }
info!("request body: {:?}", body); log::info!("request body: {:?}", body);
Ok(Response::Ok()
Ok(Response::build(StatusCode::OK)
.insert_header(("x-head", HeaderValue::from_static("dummy value!"))) .insert_header(("x-head", HeaderValue::from_static("dummy value!")))
.body(body)) .body(body))
} }
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "echo=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("echo", "127.0.0.1:8080", || { .bind("echo", ("127.0.0.1", 8080), || {
HttpService::build().finish(handle_request).tcp() HttpService::build().finish(handle_request).tcp()
})? })?
.run() .run()

View File

@ -1,29 +1,28 @@
use std::{env, io}; use std::{convert::Infallible, io};
use actix_http::{HttpService, Response}; use actix_http::{http::StatusCode, HttpService, Response};
use actix_server::Server; use actix_server::Server;
use futures_util::future;
use http::header::HeaderValue; use http::header::HeaderValue;
use log::info;
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "hello_world=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("hello-world", "127.0.0.1:8080", || { .bind("hello-world", ("127.0.0.1", 8080), || {
HttpService::build() HttpService::build()
.client_timeout(1000) .client_timeout(1000)
.client_disconnect(1000) .client_disconnect(1000)
.finish(|_req| { .finish(|req| async move {
info!("{:?}", _req); log::info!("{:?}", req);
let mut res = Response::Ok();
let mut res = Response::build(StatusCode::OK);
res.insert_header(( res.insert_header((
"x-head", "x-head",
HeaderValue::from_static("dummy value!"), HeaderValue::from_static("dummy value!"),
)); ));
future::ok::<_, ()>(res.body("Hello world!"))
Ok::<_, Infallible>(res.body("Hello world!"))
}) })
.tcp() .tcp()
})? })?

View File

@ -0,0 +1,40 @@
//! Example showing response body (chunked) stream erroring.
//!
//! Test using `nc` or `curl`.
//! ```sh
//! $ curl -vN 127.0.0.1:8080
//! $ echo 'GET / HTTP/1.1\n\n' | nc 127.0.0.1 8080
//! ```
use std::{convert::Infallible, io, time::Duration};
use actix_http::{body::BodyStream, HttpService, Response};
use actix_server::Server;
use async_stream::stream;
use bytes::Bytes;
#[actix_rt::main]
async fn main() -> io::Result<()> {
env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
Server::build()
.bind("streaming-error", ("127.0.0.1", 8080), || {
HttpService::build()
.finish(|req| async move {
log::info!("{:?}", req);
let res = Response::ok();
Ok::<_, Infallible>(res.set_body(BodyStream::new(stream! {
yield Ok(Bytes::from("123"));
yield Ok(Bytes::from("456"));
actix_rt::time::sleep(Duration::from_millis(1000)).await;
yield Err(io::Error::new(io::ErrorKind::Other, ""));
})))
})
.tcp()
})?
.run()
.await
}

View File

@ -4,14 +4,14 @@
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use std::{ use std::{
env, io, io,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::Duration,
}; };
use actix_codec::Encoder; use actix_codec::Encoder;
use actix_http::{error::Error, ws, HttpService, Request, Response}; use actix_http::{body::BodyStream, error::Error, ws, HttpService, Request, Response};
use actix_rt::time::{interval, Interval}; use actix_rt::time::{interval, Interval};
use actix_server::Server; use actix_server::Server;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@ -20,8 +20,7 @@ use futures_core::{ready, Stream};
#[actix_rt::main] #[actix_rt::main]
async fn main() -> io::Result<()> { async fn main() -> io::Result<()> {
env::set_var("RUST_LOG", "actix=info,h2_ws=info"); env_logger::init_from_env(env_logger::Env::new().default_filter_or("info"));
env_logger::init();
Server::build() Server::build()
.bind("tcp", ("127.0.0.1", 8080), || { .bind("tcp", ("127.0.0.1", 8080), || {
@ -34,14 +33,14 @@ async fn main() -> io::Result<()> {
.await .await
} }
async fn handler(req: Request) -> Result<Response, Error> { async fn handler(req: Request) -> Result<Response<BodyStream<Heartbeat>>, Error> {
log::info!("handshaking"); log::info!("handshaking");
let mut res = ws::handshake(req.head())?; let mut res = ws::handshake(req.head())?;
// handshake will always fail under HTTP/2 // handshake will always fail under HTTP/2
log::info!("responding"); log::info!("responding");
Ok(res.streaming(Heartbeat::new(ws::Codec::new()))) Ok(res.message_body(BodyStream::new(Heartbeat::new(ws::Codec::new())))?)
} }
struct Heartbeat { struct Heartbeat {

View File

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

View File

@ -1,26 +1,29 @@
use std::{ use std::{
error::Error as StdError,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use crate::error::Error;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
/// Streaming response wrapper. pin_project! {
/// /// Streaming response wrapper.
/// Response does not contain `Content-Length` header and appropriate transfer encoding is used. ///
pub struct BodyStream<S: Unpin> { /// Response does not contain `Content-Length` header and appropriate transfer encoding is used.
stream: S, pub struct BodyStream<S> {
#[pin]
stream: S,
}
} }
impl<S, E> BodyStream<S> impl<S, E> BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>> + Unpin, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Box<dyn StdError>> + 'static,
{ {
pub fn new(stream: S) -> Self { pub fn new(stream: S) -> Self {
BodyStream { stream } BodyStream { stream }
@ -29,9 +32,11 @@ where
impl<S, E> MessageBody for BodyStream<S> impl<S, E> MessageBody for BodyStream<S>
where where
S: Stream<Item = Result<Bytes, E>> + Unpin, S: Stream<Item = Result<Bytes, E>>,
E: Into<Error>, E: Into<Box<dyn StdError>> + 'static,
{ {
type Error = E;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Stream BodySize::Stream
} }
@ -44,16 +49,123 @@ where
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, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
loop { loop {
let stream = &mut self.as_mut().stream; let stream = self.as_mut().project().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) { let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue, Some(Ok(ref bytes)) if bytes.is_empty() => continue,
opt => opt.map(|res| res.map_err(Into::into)), opt => opt,
}; };
return Poll::Ready(chunk); return Poll::Ready(chunk);
} }
} }
} }
#[cfg(test)]
mod tests {
use std::{convert::Infallible, time::Duration};
use actix_rt::{
pin,
time::{sleep, Sleep},
};
use actix_utils::future::poll_fn;
use derive_more::{Display, Error};
use futures_core::ready;
use futures_util::{stream, FutureExt as _};
use super::*;
use crate::body::to_bytes;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
));
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
#[actix_rt::test]
async fn read_to_bytes() {
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
));
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
#[derive(Debug, Display, Error)]
#[display(fmt = "stream error")]
struct StreamErr;
#[actix_rt::test]
async fn stream_immediate_error() {
let body = BodyStream::new(stream::once(async { Err(StreamErr) }));
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
}
#[actix_rt::test]
async fn stream_delayed_error() {
let body =
BodyStream::new(stream::iter(vec![Ok(Bytes::from("1")), Err(StreamErr)]));
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
#[pin_project::pin_project(project = TimeDelayStreamProj)]
#[derive(Debug)]
enum TimeDelayStream {
Start,
Sleep(Pin<Box<Sleep>>),
Done,
}
impl Stream for TimeDelayStream {
type Item = Result<Bytes, StreamErr>;
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> {
match self.as_mut().get_mut() {
TimeDelayStream::Start => {
let sleep = sleep(Duration::from_millis(1));
self.as_mut().set(TimeDelayStream::Sleep(Box::pin(sleep)));
cx.waker().wake_by_ref();
Poll::Pending
}
TimeDelayStream::Sleep(ref mut delay) => {
ready!(delay.poll_unpin(cx));
self.set(TimeDelayStream::Done);
cx.waker().wake_by_ref();
Poll::Pending
}
TimeDelayStream::Done => Poll::Ready(Some(Err(StreamErr))),
}
}
}
let body = BodyStream::new(TimeDelayStream::Start);
assert!(matches!(to_bytes(body).await, Err(StreamErr)));
}
}

View File

@ -1,32 +1,37 @@
//! [`MessageBody`] trait and foreign implementations. //! [`MessageBody`] trait and foreign implementations.
use std::{ use std::{
convert::Infallible,
mem, mem,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::ready;
use pin_project_lite::pin_project;
use crate::error::Error; use crate::error::Error;
use super::BodySize; use super::BodySize;
/// Type that implement this trait can be streamed to a peer. /// An interface for response bodies.
pub trait MessageBody { pub trait MessageBody {
type Error;
/// Body size hint.
fn size(&self) -> BodySize; fn size(&self) -> BodySize;
/// Attempt to pull out the next chunk of body bytes.
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>>; ) -> Poll<Option<Result<Bytes, Self::Error>>>;
downcast_get_type_id!();
} }
downcast!(MessageBody);
impl MessageBody for () { impl MessageBody for () {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Empty BodySize::Empty
} }
@ -34,12 +39,18 @@ impl MessageBody for () {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
Poll::Ready(None) Poll::Ready(None)
} }
} }
impl<T: MessageBody + Unpin> MessageBody for Box<T> { impl<B> MessageBody for Box<B>
where
B: MessageBody + Unpin,
B::Error: Into<Error>,
{
type Error = B::Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
self.as_ref().size() self.as_ref().size()
} }
@ -47,12 +58,33 @@ impl<T: MessageBody + Unpin> MessageBody for Box<T> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, 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>>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = B::Error;
fn size(&self) -> BodySize {
self.as_ref().size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
self.as_mut().poll_next(cx)
}
}
impl MessageBody for Bytes { impl MessageBody for Bytes {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -60,7 +92,7 @@ impl MessageBody for Bytes {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -70,6 +102,8 @@ impl MessageBody for Bytes {
} }
impl MessageBody for BytesMut { impl MessageBody for BytesMut {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -77,7 +111,7 @@ impl MessageBody for BytesMut {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -87,6 +121,8 @@ impl MessageBody for BytesMut {
} }
impl MessageBody for &'static str { impl MessageBody for &'static str {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -94,7 +130,7 @@ impl MessageBody for &'static str {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -106,6 +142,8 @@ impl MessageBody for &'static str {
} }
impl MessageBody for Vec<u8> { impl MessageBody for Vec<u8> {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -113,7 +151,7 @@ impl MessageBody for Vec<u8> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -123,6 +161,8 @@ impl MessageBody for Vec<u8> {
} }
impl MessageBody for String { impl MessageBody for String {
type Error = Infallible;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.len() as u64) BodySize::Sized(self.len() as u64)
} }
@ -130,7 +170,7 @@ impl MessageBody for String {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
_: &mut Context<'_>, _: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
if self.is_empty() { if self.is_empty() {
Poll::Ready(None) Poll::Ready(None)
} else { } else {
@ -140,3 +180,53 @@ impl MessageBody for String {
} }
} }
} }
pin_project! {
pub(crate) struct MessageBodyMapErr<B, F> {
#[pin]
body: B,
mapper: Option<F>,
}
}
impl<B, F, E> MessageBodyMapErr<B, F>
where
B: MessageBody,
F: FnOnce(B::Error) -> E,
{
pub(crate) fn new(body: B, mapper: F) -> Self {
Self {
body,
mapper: Some(mapper),
}
}
}
impl<B, F, E> MessageBody for MessageBodyMapErr<B, F>
where
B: MessageBody,
F: FnOnce(B::Error) -> E,
{
type Error = E;
fn size(&self) -> BodySize {
self.body.size()
}
fn poll_next(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Self::Error>>> {
let this = self.as_mut().project();
match ready!(this.body.poll_next(cx)) {
Some(Err(err)) => {
let f = self.as_mut().project().mapper.take().unwrap();
let mapped_err = (f)(err);
Poll::Ready(Some(Err(mapped_err)))
}
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
}
}

View File

@ -1,5 +1,12 @@
//! 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)] #[allow(clippy::module_inception)]
mod body; mod body;
mod body_stream; mod body_stream;
@ -8,20 +15,65 @@ mod response_body;
mod size; mod size;
mod sized_stream; mod sized_stream;
pub use self::body::Body; pub use self::body::{AnyBody, Body, BoxAnyBody};
pub use self::body_stream::BodyStream; pub use self::body_stream::BodyStream;
pub use self::message_body::MessageBody; pub use self::message_body::MessageBody;
pub(crate) use self::message_body::MessageBodyMapErr;
pub use self::response_body::ResponseBody; pub use self::response_body::ResponseBody;
pub use self::size::BodySize; pub use self::size::BodySize;
pub use self::sized_stream::SizedStream; pub use self::sized_stream::SizedStream;
/// Collects the body produced by a `MessageBody` implementation into `Bytes`.
///
/// Any errors produced by the body stream are returned immediately.
///
/// # Examples
/// ```
/// use actix_http::body::{Body, to_bytes};
/// use bytes::Bytes;
///
/// # async fn test_to_bytes() {
/// let body = Body::Empty;
/// let bytes = to_bytes(body).await.unwrap();
/// assert!(bytes.is_empty());
///
/// let body = Body::Bytes(Bytes::from_static(b"123"));
/// let bytes = to_bytes(body).await.unwrap();
/// assert_eq!(bytes, b"123"[..]);
/// # }
/// ```
pub async fn to_bytes<B: MessageBody>(body: B) -> Result<Bytes, B::Error> {
let cap = match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => return Ok(Bytes::new()),
BodySize::Sized(size) => size as usize,
BodySize::Stream => 32_768,
};
let mut buf = BytesMut::with_capacity(cap);
pin!(body);
poll_fn(|cx| loop {
let body = body.as_mut();
match ready!(body.poll_next(cx)) {
Some(Ok(bytes)) => buf.extend_from_slice(&*bytes),
None => return Poll::Ready(Ok(())),
Some(Err(err)) => return Poll::Ready(Err(err)),
}
})
.await?;
Ok(buf.freeze())
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use std::pin::Pin; use std::pin::Pin;
use actix_rt::pin; use actix_rt::pin;
use actix_utils::future::poll_fn;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_util::{future::poll_fn, stream};
use super::*; use super::*;
@ -34,15 +86,6 @@ mod tests {
} }
} }
impl ResponseBody<Body> {
pub(crate) fn get_ref(&self) -> &[u8] {
match *self {
ResponseBody::Body(ref b) => b.get_ref(),
ResponseBody::Other(ref b) => b.get_ref(),
}
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_static_str() { async fn test_static_str() {
assert_eq!(Body::from("").size(), BodySize::Sized(0)); assert_eq!(Body::from("").size(), BodySize::Sized(0));
@ -148,11 +191,15 @@ mod tests {
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_box() { async fn test_box_and_pin() {
let val = Box::new(()); let val = Box::new(());
pin!(val); pin!(val);
assert_eq!(val.size(), BodySize::Empty); assert_eq!(val.size(), BodySize::Empty);
assert!(poll_fn(|cx| val.as_mut().poll_next(cx)).await.is_none()); 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] #[actix_rt::test]
@ -173,73 +220,26 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_serde_json() { async fn test_serde_json() {
use serde_json::json; use serde_json::{json, Value};
assert_eq!( assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(), Body::from(serde_json::to_vec(&Value::String("test".to_owned())).unwrap())
.size(),
BodySize::Sized(6) BodySize::Sized(6)
); );
assert_eq!( assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(), Body::from(serde_json::to_vec(&json!({"test-key":"test-value"})).unwrap())
.size(),
BodySize::Sized(25) BodySize::Sized(25)
); );
} }
#[actix_rt::test] // down-casting used to be done with a method on MessageBody trait
async fn body_stream_skips_empty_chunks() { // test is kept to demonstrate equivalence of Any trait
let body = BodyStream::new(stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok(Bytes::from(v)) as Result<Bytes, ()>),
));
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
mod sized_stream {
use super::*;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(["1", "", "2"].iter().map(|&v| Ok(Bytes::from(v)))),
);
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_body_casting() { async fn test_body_casting() {
let mut body = String::from("hello cast"); let mut body = String::from("hello cast");
let resp_body: &mut dyn MessageBody = &mut body; // 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(); let body = resp_body.downcast_ref::<String>().unwrap();
assert_eq!(body, "hello cast"); assert_eq!(body, "hello cast");
let body = &mut resp_body.downcast_mut::<String>().unwrap(); let body = &mut resp_body.downcast_mut::<String>().unwrap();
@ -249,4 +249,15 @@ mod tests {
let not_body = resp_body.downcast_ref::<()>(); let not_body = resp_body.downcast_ref::<()>();
assert!(not_body.is_none()); 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

@ -5,7 +5,7 @@ use std::{
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::Stream; use futures_core::{ready, Stream};
use pin_project::pin_project; use pin_project::pin_project;
use crate::error::Error; use crate::error::Error;
@ -43,7 +43,13 @@ impl<B: MessageBody> ResponseBody<B> {
} }
} }
impl<B: MessageBody> MessageBody for ResponseBody<B> { impl<B> MessageBody for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = Error;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
ResponseBody::Body(ref body) => body.size(), ResponseBody::Body(ref body) => body.size(),
@ -54,15 +60,16 @@ impl<B: MessageBody> MessageBody for ResponseBody<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { Stream::poll_next(self, cx)
ResponseBodyProj::Body(body) => body.poll_next(cx),
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
}
} }
} }
impl<B: MessageBody> Stream for ResponseBody<B> { impl<B> Stream for ResponseBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Item = Result<Bytes, Error>; type Item = Result<Bytes, Error>;
fn poll_next( fn poll_next(
@ -70,7 +77,12 @@ impl<B: MessageBody> Stream for ResponseBody<B> {
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
match self.project() { match self.project() {
ResponseBodyProj::Body(body) => body.poll_next(cx), // TODO: MSRV 1.51: poll_map_err
ResponseBodyProj::Body(body) => match ready!(body.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(err.into()))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx), ResponseBodyProj::Other(body) => Pin::new(body).poll_next(cx),
} }
} }

View File

@ -1,37 +1,44 @@
use std::{ use std::{
error::Error as StdError,
pin::Pin, pin::Pin,
task::{Context, Poll}, task::{Context, Poll},
}; };
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use pin_project_lite::pin_project;
use crate::error::Error;
use super::{BodySize, MessageBody}; use super::{BodySize, MessageBody};
/// Known sized streaming response wrapper. pin_project! {
/// /// Known sized streaming response wrapper.
/// This body implementation should be used if total size of stream is known. Data get sent as is ///
/// without using transfer encoding. /// This body implementation should be used if total size of stream is known. Data is sent as-is
pub struct SizedStream<S: Unpin> { /// without using chunked transfer encoding.
size: u64, pub struct SizedStream<S> {
stream: S, size: u64,
#[pin]
stream: S,
}
} }
impl<S> SizedStream<S> impl<S, E> SizedStream<S>
where where
S: Stream<Item = Result<Bytes, Error>> + Unpin, S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{ {
pub fn new(size: u64, stream: S) -> Self { pub fn new(size: u64, stream: S) -> Self {
SizedStream { size, stream } SizedStream { size, stream }
} }
} }
impl<S> MessageBody for SizedStream<S> impl<S, E> MessageBody for SizedStream<S>
where where
S: Stream<Item = Result<Bytes, Error>> + Unpin, S: Stream<Item = Result<Bytes, E>>,
E: Into<Box<dyn StdError>> + 'static,
{ {
type Error = E;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
BodySize::Sized(self.size as u64) BodySize::Sized(self.size as u64)
} }
@ -44,11 +51,11 @@ where
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, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
loop { loop {
let stream = &mut self.as_mut().stream; let stream = self.as_mut().project().stream;
let chunk = match ready!(Pin::new(stream).poll_next(cx)) { let chunk = match ready!(stream.poll_next(cx)) {
Some(Ok(ref bytes)) if bytes.is_empty() => continue, Some(Ok(ref bytes)) if bytes.is_empty() => continue,
val => val, val => val,
}; };
@ -57,3 +64,59 @@ where
} }
} }
} }
#[cfg(test)]
mod tests {
use std::convert::Infallible;
use actix_rt::pin;
use actix_utils::future::poll_fn;
use futures_util::stream;
use super::*;
use crate::body::to_bytes;
#[actix_rt::test]
async fn skips_empty_chunks() {
let body = SizedStream::new(
2,
stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
),
);
pin!(body);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("1")),
);
assert_eq!(
poll_fn(|cx| body.as_mut().poll_next(cx))
.await
.unwrap()
.ok(),
Some(Bytes::from("2")),
);
}
#[actix_rt::test]
async fn read_to_bytes() {
let body = SizedStream::new(
2,
stream::iter(
["1", "", "2"]
.iter()
.map(|&v| Ok::<_, Infallible>(Bytes::from(v))),
),
);
assert_eq!(to_bytes(body).await.ok(), Some(Bytes::from("12")));
}
}

View File

@ -1,19 +1,16 @@
use std::marker::PhantomData; use std::{error::Error as StdError, fmt, marker::PhantomData, net, rc::Rc};
use std::rc::Rc;
use std::{fmt, net};
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{IntoServiceFactory, Service, ServiceFactory}; use actix_service::{IntoServiceFactory, Service, ServiceFactory};
use crate::body::MessageBody; use crate::{
use crate::config::{KeepAlive, ServiceConfig}; body::{AnyBody, MessageBody},
use crate::error::Error; config::{KeepAlive, ServiceConfig},
use crate::h1::{Codec, ExpectHandler, H1Service, UpgradeHandler}; h1::{self, ExpectHandler, H1Service, UpgradeHandler},
use crate::h2::H2Service; h2::H2Service,
use crate::request::Request; service::HttpService,
use crate::response::Response; ConnectCallback, Extensions, Request, Response,
use crate::service::HttpService; };
use crate::{ConnectCallback, Extensions};
/// A HTTP service builder /// A HTTP service builder
/// ///
@ -34,7 +31,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<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
<S::Service as Service<Request>>::Future: 'static, <S::Service as Service<Request>>::Future: 'static,
{ {
@ -57,17 +54,15 @@ 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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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<Error>, X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{ {
/// Set server keep-alive setting. /// Set server keep-alive setting.
/// ///
@ -125,9 +120,8 @@ 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<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -148,11 +142,10 @@ where
/// and this service get called with original request and framed object. /// and this service get called with original request and framed object.
pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1> pub fn upgrade<F, U1>(self, upgrade: F) -> HttpServiceBuilder<T, S, X, U1>
where where
F: IntoServiceFactory<U1, (Request, Framed<T, Codec>)>, F: IntoServiceFactory<U1, (Request, Framed<T, h1::Codec>)>,
U1: ServiceFactory<(Request, Framed<T, Codec>), Config = (), Response = ()>, U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, Codec>)>>::Future: 'static,
{ {
HttpServiceBuilder { HttpServiceBuilder {
keep_alive: self.keep_alive, keep_alive: self.keep_alive,
@ -185,7 +178,7 @@ where
where where
B: MessageBody, B: MessageBody,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
{ {
@ -206,12 +199,13 @@ where
/// Finish service configuration and create a HTTP service for HTTP/2 protocol. /// Finish service configuration and create a HTTP service for HTTP/2 protocol.
pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B> pub fn h2<F, B>(self, service: F) -> H2Service<T, S, B>
where where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{ {
let cfg = ServiceConfig::new( let cfg = ServiceConfig::new(
self.keep_alive, self.keep_alive,
@ -228,12 +222,13 @@ where
/// Finish service configuration and create `HttpService` instance. /// Finish service configuration and create `HttpService` instance.
pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U> pub fn finish<F, B>(self, service: F) -> HttpService<T, S, B, X, U>
where where
B: MessageBody + 'static,
F: IntoServiceFactory<S, Request>, F: IntoServiceFactory<S, Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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,
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

@ -8,6 +8,7 @@ const DEFAULT_H2_STREAM_WINDOW: u32 = 1024 * 1024; // 1MB
#[derive(Clone)] #[derive(Clone)]
pub(crate) struct ConnectorConfig { pub(crate) struct ConnectorConfig {
pub(crate) timeout: Duration, pub(crate) timeout: Duration,
pub(crate) handshake_timeout: Duration,
pub(crate) conn_lifetime: Duration, pub(crate) conn_lifetime: Duration,
pub(crate) conn_keep_alive: Duration, pub(crate) conn_keep_alive: Duration,
pub(crate) disconnect_timeout: Option<Duration>, pub(crate) disconnect_timeout: Option<Duration>,
@ -21,6 +22,7 @@ impl Default for ConnectorConfig {
fn default() -> Self { fn default() -> Self {
Self { Self {
timeout: Duration::from_secs(5), timeout: Duration::from_secs(5),
handshake_timeout: Duration::from_secs(5),
conn_lifetime: Duration::from_secs(75), conn_lifetime: Duration::from_secs(75),
conn_keep_alive: Duration::from_secs(15), conn_keep_alive: Duration::from_secs(15),
disconnect_timeout: Some(Duration::from_millis(3000)), disconnect_timeout: Some(Duration::from_millis(3000)),

View File

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

View File

@ -1,51 +1,53 @@
use std::{ use std::{
fmt, fmt,
future::Future, future::Future,
marker::PhantomData,
net::IpAddr, net::IpAddr,
pin::Pin, pin::Pin,
rc::Rc,
task::{Context, Poll}, task::{Context, Poll},
time::Duration, time::Duration,
}; };
use actix_codec::{AsyncRead, AsyncWrite}; use actix_rt::{
use actix_rt::net::TcpStream; net::{ActixStream, TcpStream},
use actix_service::{apply_fn, Service, ServiceExt}; time::{sleep, Sleep},
use actix_tls::connect::{
new_connector, Connect as TcpConnect, Connection as TcpConnection, Resolver,
}; };
use actix_utils::timeout::{TimeoutError, TimeoutService}; use actix_service::Service;
use actix_tls::connect::{
new_connector, Connect as TcpConnect, ConnectError as TcpConnectError,
Connection as TcpConnection, Resolver,
};
use futures_core::{future::LocalBoxFuture, ready};
use http::Uri; use http::Uri;
use pin_project::pin_project;
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{Connection, EitherIoConnection}; use super::connection::{Connection, ConnectionIo};
use super::error::ConnectError; use super::error::ConnectError;
use super::pool::{ConnectionPool, Protocol}; use super::pool::ConnectionPool;
use super::Connect; use super::Connect;
use super::Protocol;
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector; use actix_tls::connect::ssl::openssl::SslConnector as OpensslConnector;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::ClientConfig; use actix_tls::connect::ssl::rustls::ClientConfig;
#[cfg(feature = "rustls")]
use std::sync::Arc;
#[cfg(any(feature = "openssl", feature = "rustls"))]
enum SslConnector { enum SslConnector {
#[allow(dead_code)]
None,
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
Openssl(OpensslConnector), Openssl(OpensslConnector),
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
Rustls(Arc<ClientConfig>), Rustls(std::sync::Arc<ClientConfig>),
} }
#[cfg(not(any(feature = "openssl", feature = "rustls")))]
type SslConnector = ();
/// Manages HTTP client network connectivity. /// Manages HTTP client network connectivity.
/// ///
/// The `Connector` type uses a builder-like combinator pattern for service /// The `Connector` type uses a builder-like combinator pattern for service
/// construction that finishes by calling the `.finish()` method. /// construction that finishes by calling the `.finish()` method.
/// ///
/// ```rust,ignore /// ```ignore
/// use std::time::Duration; /// use std::time::Duration;
/// use actix_http::client::Connector; /// use actix_http::client::Connector;
/// ///
@ -53,18 +55,14 @@ type SslConnector = ();
/// .timeout(Duration::from_secs(5)) /// .timeout(Duration::from_secs(5))
/// .finish(); /// .finish();
/// ``` /// ```
pub struct Connector<T, U> { pub struct Connector<T> {
connector: T, connector: T,
config: ConnectorConfig, config: ConnectorConfig,
#[allow(dead_code)] #[allow(dead_code)]
ssl: SslConnector, ssl: SslConnector,
_phantom: PhantomData<U>,
} }
pub trait Io: AsyncRead + AsyncWrite + Unpin {} impl Connector<()> {
impl<T: AsyncRead + AsyncWrite + Unpin> Io for T {}
impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self, clippy::let_unit_value)] #[allow(clippy::new_ret_no_self, clippy::let_unit_value)]
pub fn new() -> Connector< pub fn new() -> Connector<
impl Service< impl Service<
@ -72,13 +70,11 @@ impl Connector<(), ()> {
Response = TcpConnection<Uri, TcpStream>, Response = TcpConnection<Uri, TcpStream>,
Error = actix_tls::connect::ConnectError, Error = actix_tls::connect::ConnectError,
> + Clone, > + Clone,
TcpStream,
> { > {
Connector { Connector {
ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]), ssl: Self::build_ssl(vec![b"h2".to_vec(), b"http/1.1".to_vec()]),
connector: new_connector(resolver::resolver()), connector: new_connector(resolver::resolver()),
config: ConnectorConfig::default(), config: ConnectorConfig::default(),
_phantom: PhantomData,
} }
} }
@ -109,51 +105,66 @@ impl Connector<(), ()> {
config.root_store.add_server_trust_anchors( config.root_store.add_server_trust_anchors(
&actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS, &actix_tls::connect::ssl::rustls::TLS_SERVER_ROOTS,
); );
SslConnector::Rustls(Arc::new(config)) SslConnector::Rustls(std::sync::Arc::new(config))
} }
// ssl turned off, provides empty ssl connector // ssl turned off, provides empty ssl connector
#[cfg(not(any(feature = "openssl", feature = "rustls")))] #[cfg(not(any(feature = "openssl", feature = "rustls")))]
fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {} fn build_ssl(_: Vec<Vec<u8>>) -> SslConnector {
SslConnector::None
}
} }
impl<T, U> Connector<T, U> { impl<S> Connector<S> {
/// Use custom connector. /// Use custom connector.
pub fn connector<T1, U1>(self, connector: T1) -> Connector<T1, U1> pub fn connector<S1, Io1>(self, connector: S1) -> Connector<S1>
where where
U1: AsyncRead + AsyncWrite + Unpin + fmt::Debug, Io1: ActixStream + fmt::Debug + 'static,
T1: Service< S1: Service<
TcpConnect<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, U1>, Response = TcpConnection<Uri, Io1>,
Error = actix_tls::connect::ConnectError, Error = TcpConnectError,
> + Clone, > + Clone,
{ {
Connector { Connector {
connector, connector,
config: self.config, config: self.config,
ssl: self.ssl, ssl: self.ssl,
_phantom: PhantomData,
} }
} }
} }
impl<T, U> Connector<T, U> impl<S, Io> Connector<S>
where where
U: AsyncRead + AsyncWrite + Unpin + fmt::Debug + 'static, // Note:
T: Service< // Input Io type is bound to ActixStream trait but internally in client module they
// are bound to ConnectionIo trait alias. And latter is the trait exposed to public
// in the form of Box<dyn ConnectionIo> type.
//
// This remap is to hide ActixStream's trait methods. They are not meant to be called
// from user code.
Io: ActixStream + fmt::Debug + 'static,
S: Service<
TcpConnect<Uri>, TcpConnect<Uri>,
Response = TcpConnection<Uri, U>, Response = TcpConnection<Uri, Io>,
Error = actix_tls::connect::ConnectError, Error = TcpConnectError,
> + Clone > + Clone
+ 'static, + 'static,
{ {
/// Connection timeout, i.e. max time to connect to remote host including dns name resolution. /// Tcp connection timeout, i.e. max time to connect to remote host including dns name
/// Set to 1 second by default. /// resolution. Set to 5 second by default.
pub fn timeout(mut self, timeout: Duration) -> Self { pub fn timeout(mut self, timeout: Duration) -> Self {
self.config.timeout = timeout; self.config.timeout = timeout;
self self
} }
/// Tls handshake timeout, i.e. max time to do tls handshake with remote host after tcp
/// connection established. Set to 5 second by default.
pub fn handshake_timeout(mut self, timeout: Duration) -> Self {
self.config.handshake_timeout = timeout;
self
}
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl(mut self, connector: OpensslConnector) -> Self { pub fn ssl(mut self, connector: OpensslConnector) -> Self {
@ -162,7 +173,8 @@ where
} }
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
pub fn rustls(mut self, connector: Arc<ClientConfig>) -> Self { /// Use custom `SslConnector` instance.
pub fn rustls(mut self, connector: std::sync::Arc<ClientConfig>) -> Self {
self.ssl = SslConnector::Rustls(connector); self.ssl = SslConnector::Rustls(connector);
self self
} }
@ -252,214 +264,422 @@ where
/// Finish configuration process and create connector service. /// Finish configuration process and create connector service.
/// The Connector builder always concludes by calling `finish()` last in /// The Connector builder always concludes by calling `finish()` last in
/// its combinator chain. /// its combinator chain.
pub fn finish( pub fn finish(self) -> ConnectorService<S, Io> {
self,
) -> impl Service<Connect, Response = impl Connection, Error = ConnectError> + Clone
{
let local_address = self.config.local_address; let local_address = self.config.local_address;
let timeout = self.config.timeout; let timeout = self.config.timeout;
let tcp_service = TimeoutService::new( let tcp_service_inner =
timeout, TcpConnectorInnerService::new(self.connector, timeout, local_address);
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
if let Some(local_addr) = local_address { #[allow(clippy::redundant_clone)]
req = req.set_local_addr(local_addr); let tcp_service = TcpConnectorService {
service: tcp_service_inner.clone(),
};
let tls_service = match self.ssl {
SslConnector::None => None,
#[cfg(feature = "openssl")]
SslConnector::Openssl(tls) => {
const H2: &[u8] = b"h2";
use actix_tls::connect::ssl::openssl::{OpensslConnector, SslStream};
impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, SslStream<Io>> {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
.ssl()
.selected_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock), Protocol::Http2)
} else {
(Box::new(sock), Protocol::Http1)
}
}
} }
srv.call(req) let handshake_timeout = self.config.handshake_timeout;
})
.map_err(ConnectError::from)
.map(|stream| (stream.into_parts().0, Protocol::Http1)),
)
.map_err(|e| match e {
TimeoutError::Service(e) => e,
TimeoutError::Timeout => ConnectError::Timeout,
});
#[cfg(not(any(feature = "openssl", feature = "rustls")))] let tls_service = TlsConnectorService {
{ tcp_service: tcp_service_inner,
// A dummy service for annotate tls pool's type signature. tls_service: OpensslConnector::service(tls),
pub type DummyService = Box< timeout: handshake_timeout,
dyn Service< };
Connect,
Response = (Box<dyn Io>, Protocol),
Error = ConnectError,
Future = futures_core::future::LocalBoxFuture<
'static,
Result<(Box<dyn Io>, Protocol), ConnectError>,
>,
>,
>;
InnerConnector::<_, DummyService, _, Box<dyn Io>> { Some(actix_service::boxed::rc_service(tls_service))
tcp_pool: ConnectionPool::new(
tcp_service,
self.config.no_disconnect_timeout(),
),
tls_pool: None,
} }
}
#[cfg(any(feature = "openssl", feature = "rustls"))]
{
const H2: &[u8] = b"h2";
use actix_service::{boxed::service, pipeline};
#[cfg(feature = "openssl")]
use actix_tls::connect::ssl::openssl::OpensslConnector;
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
use actix_tls::connect::ssl::rustls::{RustlsConnector, Session}; SslConnector::Rustls(tls) => {
const H2: &[u8] = b"h2";
let ssl_service = TimeoutService::new( use actix_tls::connect::ssl::rustls::{
timeout, RustlsConnector, Session, TlsStream,
pipeline( };
apply_fn(self.connector.clone(), move |msg: Connect, srv| {
let mut req = TcpConnect::new(msg.uri).set_addr(msg.addr);
if let Some(local_addr) = local_address { impl<Io: ConnectionIo> IntoConnectionIo for TcpConnection<Uri, TlsStream<Io>> {
req = req.set_local_addr(local_addr); fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol) {
let sock = self.into_parts().0;
let h2 = sock
.get_ref()
.1
.get_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock), Protocol::Http2)
} else {
(Box::new(sock), Protocol::Http1)
} }
}
}
srv.call(req) let handshake_timeout = self.config.handshake_timeout;
})
.map_err(ConnectError::from),
)
.and_then(match self.ssl {
#[cfg(feature = "openssl")]
SslConnector::Openssl(ssl) => service(
OpensslConnector::service(ssl)
.map(|stream| {
let sock = stream.into_parts().0;
let h2 = sock
.ssl()
.selected_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else {
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
}
})
.map_err(ConnectError::from),
),
#[cfg(feature = "rustls")]
SslConnector::Rustls(ssl) => service(
RustlsConnector::service(ssl)
.map_err(ConnectError::from)
.map(|stream| {
let sock = stream.into_parts().0;
let h2 = sock
.get_ref()
.1
.get_alpn_protocol()
.map(|protos| protos.windows(2).any(|w| w == H2))
.unwrap_or(false);
if h2 {
(Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} else {
(Box::new(sock) as Box<dyn Io>, Protocol::Http1)
}
}),
),
}),
)
.map_err(|e| match e {
TimeoutError::Service(e) => e,
TimeoutError::Timeout => ConnectError::Timeout,
});
InnerConnector { let tls_service = TlsConnectorService {
tcp_pool: ConnectionPool::new( tcp_service: tcp_service_inner,
tcp_service, tls_service: RustlsConnector::service(tls),
self.config.no_disconnect_timeout(), timeout: handshake_timeout,
), };
tls_pool: Some(ConnectionPool::new(ssl_service, self.config)),
Some(actix_service::boxed::rc_service(tls_service))
} }
};
let tcp_config = self.config.no_disconnect_timeout();
let tcp_pool = ConnectionPool::new(tcp_service, tcp_config);
let tls_config = self.config;
let tls_pool = tls_service
.map(move |tls_service| ConnectionPool::new(tls_service, tls_config));
ConnectorServicePriv { tcp_pool, tls_pool }
}
}
/// tcp service for map `TcpConnection<Uri, Io>` type to `(Io, Protocol)`
#[derive(Clone)]
pub struct TcpConnectorService<S: Clone> {
service: S,
}
impl<S, Io> Service<Connect> for TcpConnectorService<S>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone
+ 'static,
{
type Response = (Io, Protocol);
type Error = ConnectError;
type Future = TcpConnectorFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
TcpConnectorFuture {
fut: self.service.call(req),
} }
} }
} }
struct InnerConnector<S1, S2, Io1, Io2> #[pin_project]
pub struct TcpConnectorFuture<Fut> {
#[pin]
fut: Fut,
}
impl<Fut, Io> Future for TcpConnectorFuture<Fut>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, Fut: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, {
Io1: AsyncRead + AsyncWrite + Unpin + 'static, type Output = Result<(Io, Protocol), ConnectError>;
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project()
.fut
.poll(cx)
.map_ok(|res| (res.into_parts().0, Protocol::Http1))
}
}
/// service for establish tcp connection and do client tls handshake.
/// operation is canceled when timeout limit reached.
struct TlsConnectorService<S, St> {
/// tcp connection is canceled on `TcpConnectorInnerService`'s timeout setting.
tcp_service: S,
/// tls connection is canceled on `TlsConnectorService`'s timeout setting.
tls_service: St,
timeout: Duration,
}
impl<S, St, Io> Service<Connect> for TlsConnectorService<S, St>
where
S: Service<Connect, Response = TcpConnection<Uri, Io>, Error = ConnectError>
+ Clone
+ 'static,
St: Service<TcpConnection<Uri, Io>, Error = std::io::Error> + Clone + 'static,
Io: ConnectionIo,
St::Response: IntoConnectionIo,
{
type Response = (Box<dyn ConnectionIo>, Protocol);
type Error = ConnectError;
type Future = TlsConnectorFuture<St, S::Future, St::Future>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
ready!(self.tcp_service.poll_ready(cx))?;
ready!(self.tls_service.poll_ready(cx))?;
Poll::Ready(Ok(()))
}
fn call(&self, req: Connect) -> Self::Future {
let fut = self.tcp_service.call(req);
let tls_service = self.tls_service.clone();
let timeout = self.timeout;
TlsConnectorFuture::TcpConnect {
fut,
tls_service: Some(tls_service),
timeout,
}
}
}
#[pin_project(project = TlsConnectorProj)]
#[allow(clippy::large_enum_variant)]
enum TlsConnectorFuture<S, Fut1, Fut2> {
TcpConnect {
#[pin]
fut: Fut1,
tls_service: Option<S>,
timeout: Duration,
},
TlsConnect {
#[pin]
fut: Fut2,
#[pin]
timeout: Sleep,
},
}
/// helper trait for generic over different TlsStream types between tls crates.
trait IntoConnectionIo {
fn into_connection_io(self) -> (Box<dyn ConnectionIo>, Protocol);
}
impl<S, Io, Fut1, Fut2, Res> Future for TlsConnectorFuture<S, Fut1, Fut2>
where
S: Service<
TcpConnection<Uri, Io>,
Response = Res,
Error = std::io::Error,
Future = Fut2,
>,
S::Response: IntoConnectionIo,
Fut1: Future<Output = Result<TcpConnection<Uri, Io>, ConnectError>>,
Fut2: Future<Output = Result<S::Response, S::Error>>,
Io: ConnectionIo,
{
type Output = Result<(Box<dyn ConnectionIo>, Protocol), ConnectError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.as_mut().project() {
TlsConnectorProj::TcpConnect {
fut,
tls_service,
timeout,
} => {
let res = ready!(fut.poll(cx))?;
let fut = tls_service
.take()
.expect("TlsConnectorFuture polled after complete")
.call(res);
let timeout = sleep(*timeout);
self.set(TlsConnectorFuture::TlsConnect { fut, timeout });
self.poll(cx)
}
TlsConnectorProj::TlsConnect { fut, timeout } => match fut.poll(cx)? {
Poll::Ready(res) => Poll::Ready(Ok(res.into_connection_io())),
Poll::Pending => timeout.poll(cx).map(|_| Err(ConnectError::Timeout)),
},
}
}
}
/// service for establish tcp connection.
/// operation is canceled when timeout limit reached.
#[derive(Clone)]
pub struct TcpConnectorInnerService<S: Clone> {
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
}
impl<S: Clone> TcpConnectorInnerService<S> {
fn new(
service: S,
timeout: Duration,
local_address: Option<std::net::IpAddr>,
) -> Self {
Self {
service,
timeout,
local_address,
}
}
}
impl<S, Io> Service<Connect> for TcpConnectorInnerService<S>
where
S: Service<
TcpConnect<Uri>,
Response = TcpConnection<Uri, Io>,
Error = TcpConnectError,
> + Clone
+ 'static,
{
type Response = S::Response;
type Error = ConnectError;
type Future = TcpConnectorInnerFuture<S::Future>;
actix_service::forward_ready!(service);
fn call(&self, req: Connect) -> Self::Future {
let mut req = TcpConnect::new(req.uri).set_addr(req.addr);
if let Some(local_addr) = self.local_address {
req = req.set_local_addr(local_addr);
}
TcpConnectorInnerFuture {
fut: self.service.call(req),
timeout: sleep(self.timeout),
}
}
}
#[pin_project]
pub struct TcpConnectorInnerFuture<Fut> {
#[pin]
fut: Fut,
#[pin]
timeout: Sleep,
}
impl<Fut, Io> Future for TcpConnectorInnerFuture<Fut>
where
Fut: Future<Output = Result<TcpConnection<Uri, Io>, TcpConnectError>>,
{
type Output = Result<TcpConnection<Uri, Io>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.project();
match this.fut.poll(cx) {
Poll::Ready(res) => Poll::Ready(res.map_err(ConnectError::from)),
Poll::Pending => this.timeout.poll(cx).map(|_| Err(ConnectError::Timeout)),
}
}
}
/// Connector service for pooled Plain/Tls Tcp connections.
pub type ConnectorService<S, Io> = ConnectorServicePriv<
TcpConnectorService<TcpConnectorInnerService<S>>,
Rc<
dyn Service<
Connect,
Response = (Box<dyn ConnectionIo>, Protocol),
Error = ConnectError,
Future = LocalBoxFuture<
'static,
Result<(Box<dyn ConnectionIo>, Protocol), ConnectError>,
>,
>,
>,
Io,
Box<dyn ConnectionIo>,
>;
pub struct ConnectorServicePriv<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
tcp_pool: ConnectionPool<S1, Io1>, tcp_pool: ConnectionPool<S1, Io1>,
tls_pool: Option<ConnectionPool<S2, Io2>>, tls_pool: Option<ConnectionPool<S2, Io2>>,
} }
impl<S1, S2, Io1, Io2> Clone for InnerConnector<S1, S2, Io1, Io2> impl<S1, S2, Io1, Io2> Service<Connect> for ConnectorServicePriv<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, + Clone
Io1: AsyncRead + AsyncWrite + Unpin + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
fn clone(&self) -> Self { type Response = Connection<Io1, Io2>;
InnerConnector {
tcp_pool: self.tcp_pool.clone(),
tls_pool: self.tls_pool.as_ref().cloned(),
}
}
}
impl<S1, S2, Io1, Io2> Service<Connect> for InnerConnector<S1, S2, Io1, Io2>
where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static,
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static,
Io1: AsyncRead + AsyncWrite + Unpin + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static,
{
type Response = EitherIoConnection<Io1, Io2>;
type Error = ConnectError; type Error = ConnectError;
type Future = InnerConnectorResponse<S1, S2, Io1, Io2>; type Future = ConnectorServiceFuture<S1, S2, Io1, Io2>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.tcp_pool.poll_ready(cx) ready!(self.tcp_pool.poll_ready(cx))?;
if let Some(ref tls_pool) = self.tls_pool {
ready!(tls_pool.poll_ready(cx))?;
}
Poll::Ready(Ok(()))
} }
fn call(&self, req: Connect) -> Self::Future { fn call(&self, req: Connect) -> Self::Future {
match req.uri.scheme_str() { match req.uri.scheme_str() {
Some("https") | Some("wss") => match self.tls_pool { Some("https") | Some("wss") => match self.tls_pool {
None => InnerConnectorResponse::SslIsNotSupported, None => ConnectorServiceFuture::SslIsNotSupported,
Some(ref pool) => InnerConnectorResponse::Io2(pool.call(req)), Some(ref pool) => ConnectorServiceFuture::Tls(pool.call(req)),
}, },
_ => InnerConnectorResponse::Io1(self.tcp_pool.call(req)), _ => ConnectorServiceFuture::Tcp(self.tcp_pool.call(req)),
} }
} }
} }
#[pin_project::pin_project(project = InnerConnectorProj)] #[pin_project(project = ConnectorServiceProj)]
enum InnerConnectorResponse<S1, S2, Io1, Io2> pub enum ConnectorServiceFuture<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, + Clone
Io1: AsyncRead + AsyncWrite + Unpin + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
Io1(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future), Tcp(#[pin] <ConnectionPool<S1, Io1> as Service<Connect>>::Future),
Io2(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future), Tls(#[pin] <ConnectionPool<S2, Io2> as Service<Connect>>::Future),
SslIsNotSupported, SslIsNotSupported,
} }
impl<S1, S2, Io1, Io2> Future for InnerConnectorResponse<S1, S2, Io1, Io2> impl<S1, S2, Io1, Io2> Future for ConnectorServiceFuture<S1, S2, Io1, Io2>
where where
S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError> + 'static, S1: Service<Connect, Response = (Io1, Protocol), Error = ConnectError>
S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError> + 'static, + Clone
Io1: AsyncRead + AsyncWrite + Unpin + 'static, + 'static,
Io2: AsyncRead + AsyncWrite + Unpin + 'static, S2: Service<Connect, Response = (Io2, Protocol), Error = ConnectError>
+ Clone
+ 'static,
Io1: ConnectionIo,
Io2: ConnectionIo,
{ {
type Output = Result<EitherIoConnection<Io1, Io2>, ConnectError>; type Output = Result<Connection<Io1, Io2>, ConnectError>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
match self.project() { match self.project() {
InnerConnectorProj::Io1(fut) => fut.poll(cx).map_ok(EitherIoConnection::A), ConnectorServiceProj::Tcp(fut) => fut.poll(cx).map_ok(Connection::Tcp),
InnerConnectorProj::Io2(fut) => fut.poll(cx).map_ok(EitherIoConnection::B), ConnectorServiceProj::Tls(fut) => fut.poll(cx).map_ok(Connection::Tls),
InnerConnectorProj::SslIsNotSupported => { ConnectorServiceProj::SslIsNotSupported => {
Poll::Ready(Err(ConnectError::SslIsNotSupported)) Poll::Ready(Err(ConnectError::SslIsNotSupported))
} }
} }

View File

@ -1,15 +1,16 @@
use std::io; use std::{error::Error as StdError, fmt, io};
use derive_more::{Display, From}; use derive_more::{Display, From};
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
use actix_tls::accept::openssl::SslError; use actix_tls::accept::openssl::SslError;
use crate::error::{Error, ParseError, ResponseError}; use crate::error::{Error, ParseError};
use crate::http::{Error as HttpError, StatusCode}; use crate::http::Error as HttpError;
/// A set of errors that can occur while connecting to an HTTP host /// A set of errors that can occur while connecting to an HTTP host
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum ConnectError { pub enum ConnectError {
/// SSL feature is not enabled /// SSL feature is not enabled
#[display(fmt = "SSL is not supported")] #[display(fmt = "SSL is not supported")]
@ -64,6 +65,7 @@ impl From<actix_tls::connect::ConnectError> for ConnectError {
} }
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum InvalidUrl { pub enum InvalidUrl {
#[display(fmt = "Missing URL scheme")] #[display(fmt = "Missing URL scheme")]
MissingScheme, MissingScheme,
@ -82,6 +84,7 @@ impl std::error::Error for InvalidUrl {}
/// A set of errors that can occur during request sending and response reading /// A set of errors that can occur during request sending and response reading
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum SendRequestError { pub enum SendRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
@ -115,25 +118,17 @@ pub enum SendRequestError {
/// Error sending request body /// Error sending request body
Body(Error), Body(Error),
/// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)]
Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for SendRequestError {} impl std::error::Error for SendRequestError {}
/// Convert `SendRequestError` to a server `Response`
impl ResponseError for SendRequestError {
fn status_code(&self) -> StatusCode {
match *self {
SendRequestError::Connect(ConnectError::Timeout) => {
StatusCode::GATEWAY_TIMEOUT
}
SendRequestError::Connect(_) => StatusCode::BAD_REQUEST,
_ => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
/// A set of errors that can occur during freezing a request /// A set of errors that can occur during freezing a request
#[derive(Debug, Display, From)] #[derive(Debug, Display, From)]
#[non_exhaustive]
pub enum FreezeRequestError { pub enum FreezeRequestError {
/// Invalid URL /// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)] #[display(fmt = "Invalid URL: {}", _0)]
@ -142,15 +137,20 @@ pub enum FreezeRequestError {
/// HTTP error /// HTTP error
#[display(fmt = "{}", _0)] #[display(fmt = "{}", _0)]
Http(HttpError), Http(HttpError),
/// Other errors that can occur after submitting a request.
#[display(fmt = "{:?}: {}", _1, _0)]
Custom(Box<dyn StdError>, Box<dyn fmt::Debug>),
} }
impl std::error::Error for FreezeRequestError {} impl std::error::Error for FreezeRequestError {}
impl From<FreezeRequestError> for SendRequestError { impl From<FreezeRequestError> for SendRequestError {
fn from(e: FreezeRequestError) -> Self { fn from(err: FreezeRequestError) -> Self {
match e { match err {
FreezeRequestError::Url(e) => e.into(), FreezeRequestError::Url(err) => err.into(),
FreezeRequestError::Http(e) => e.into(), FreezeRequestError::Http(err) => err.into(),
FreezeRequestError::Custom(err, msg) => SendRequestError::Custom(err, msg),
} }
} }
} }

View File

@ -1,39 +1,38 @@
use std::io::Write; use std::{
use std::pin::Pin; io::Write,
use std::task::{Context, Poll}; pin::Pin,
use std::{io, time}; task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed, ReadBuf}; use actix_codec::Framed;
use actix_utils::future::poll_fn;
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_core::Stream; use futures_core::{ready, Stream};
use futures_util::{future::poll_fn, SinkExt, StreamExt}; use futures_util::SinkExt as _;
use crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::header::HeaderMap;
use crate::http::{ use crate::http::{
header::{IntoHeaderValue, EXPECT, HOST}, header::{HeaderMap, IntoHeaderValue, EXPECT, HOST},
StatusCode, StatusCode,
}; };
use crate::message::{RequestHeadType, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::{Payload, PayloadStream}; use crate::payload::Payload;
use crate::{error::PayloadError, Error};
use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::connection::{ConnectionIo, H1Connection};
use super::error::{ConnectError, SendRequestError}; use super::error::{ConnectError, SendRequestError};
use super::pool::Acquired;
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
pub(crate) async fn send_request<T, B>( pub(crate) async fn send_request<Io, B>(
io: T, io: H1Connection<Io>,
mut head: RequestHeadType, mut head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
// set request host header // set request host header
if !head.as_ref().headers.contains_key(HOST) if !head.as_ref().headers.contains_key(HOST)
@ -42,9 +41,9 @@ where
if let Some(host) = head.as_ref().uri.host() { if let Some(host) = head.as_ref().uri.host() {
let mut wrt = BytesMut::with_capacity(host.len() + 5).writer(); let mut wrt = BytesMut::with_capacity(host.len() + 5).writer();
let _ = match head.as_ref().uri.port_u16() { match head.as_ref().uri.port_u16() {
None | Some(80) | Some(443) => write!(wrt, "{}", host), None | Some(80) | Some(443) => write!(wrt, "{}", host)?,
Some(port) => write!(wrt, "{}:{}", host, port), Some(port) => write!(wrt, "{}:{}", host, port)?,
}; };
match wrt.get_mut().split().freeze().try_into_value() { match wrt.get_mut().split().freeze().try_into_value() {
@ -62,12 +61,6 @@ where
} }
} }
let io = H1Connection {
created,
pool,
io: Some(io),
};
// create Framed and prepare sending request // create Framed and prepare sending request
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
@ -77,10 +70,8 @@ where
let is_expect = if head.as_ref().headers.contains_key(EXPECT) { let is_expect = if head.as_ref().headers.contains_key(EXPECT) {
match body.size() { match body.size() {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => { BodySize::None | BodySize::Empty | BodySize::Sized(0) => {
let pin_framed = Pin::new(&mut framed); let keep_alive = framed.codec_ref().keepalive();
framed.io_mut().on_release(keep_alive);
let force_close = !pin_framed.codec_ref().keepalive();
release_connection(pin_framed, force_close);
// TODO: use a new variant or a new type better describing error violate // TODO: use a new variant or a new type better describing error violate
// `Requirements for clients` session of above RFC // `Requirements for clients` session of above RFC
@ -128,45 +119,43 @@ where
match pin_framed.codec_ref().message_type() { match pin_framed.codec_ref().message_type() {
h1::MessageType::None => { h1::MessageType::None => {
let force_close = !pin_framed.codec_ref().keepalive(); let keep_alive = pin_framed.codec_ref().keepalive();
release_connection(pin_framed, force_close); pin_framed.io_mut().on_release(keep_alive);
Ok((head, Payload::None)) Ok((head, Payload::None))
} }
_ => { _ => Ok((head, Payload::Stream(Box::pin(PlStream::new(framed))))),
let pl: PayloadStream = Box::pin(PlStream::new(framed));
Ok((head, pl.into()))
}
} }
} }
pub(crate) async fn open_tunnel<T>( pub(crate) async fn open_tunnel<Io>(
io: T, io: Io,
head: RequestHeadType, head: RequestHeadType,
) -> Result<(ResponseHead, Framed<T, h1::ClientCodec>), SendRequestError> ) -> Result<(ResponseHead, Framed<Io, h1::ClientCodec>), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
{ {
// create Framed and send request // create Framed and send request.
let mut framed = Framed::new(io, h1::ClientCodec::default()); let mut framed = Framed::new(io, h1::ClientCodec::default());
framed.send((head, BodySize::None).into()).await?; framed.send((head, BodySize::None).into()).await?;
// read response // read response head.
if let (Some(result), framed) = framed.into_future().await { let head = poll_fn(|cx| Pin::new(&mut framed).poll_next(cx))
let head = result.map_err(SendRequestError::from)?; .await
Ok((head, framed)) .ok_or(ConnectError::Disconnected)??;
} else {
Err(SendRequestError::from(ConnectError::Disconnected)) Ok((head, framed))
}
} }
/// send request body to the peer /// send request body to the peer
pub(crate) async fn send_body<T, B>( pub(crate) async fn send_body<Io, B>(
body: B, body: B,
mut framed: Pin<&mut Framed<T, h1::ClientCodec>>, mut framed: Pin<&mut Framed<Io, h1::ClientCodec>>,
) -> Result<(), SendRequestError> ) -> Result<(), SendRequestError>
where where
T: ConnectionLifetime + Unpin, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
actix_rt::pin!(body); actix_rt::pin!(body);
@ -174,9 +163,10 @@ where
while !eof { while !eof {
while !eof && !framed.as_ref().is_write_buf_full() { while !eof && !framed.as_ref().is_write_buf_full() {
match poll_fn(|cx| body.as_mut().poll_next(cx)).await { match poll_fn(|cx| body.as_mut().poll_next(cx)).await {
Some(result) => { Some(Ok(chunk)) => {
framed.as_mut().write(h1::Message::Chunk(Some(result?)))?; framed.as_mut().write(h1::Message::Chunk(Some(chunk)))?;
} }
Some(Err(err)) => return Err(err.into().into()),
None => { None => {
eof = true; eof = true;
framed.as_mut().write(h1::Message::Chunk(None))?; framed.as_mut().write(h1::Message::Chunk(None))?;
@ -200,104 +190,25 @@ where
} }
} }
SinkExt::flush(Pin::into_inner(framed)).await?; framed.get_mut().flush().await?;
Ok(()) Ok(())
} }
#[doc(hidden)]
/// HTTP client connection
pub struct H1Connection<T>
where
T: AsyncWrite + Unpin + 'static,
{
/// T should be `Unpin`
io: Option<T>,
created: time::Instant,
pool: Option<Acquired<T>>,
}
impl<T> ConnectionLifetime for H1Connection<T>
where
T: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close connection
fn close(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.close(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
/// Release this connection to the connection pool
fn release(mut self: Pin<&mut Self>) {
if let Some(mut pool) = self.pool.take() {
if let Some(io) = self.io.take() {
pool.release(IoConnection::new(
ConnectionType::H1(io),
self.created,
None,
));
}
}
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncRead for H1Connection<T> {
fn poll_read(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &mut ReadBuf<'_>,
) -> Poll<io::Result<()>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_read(cx, buf)
}
}
impl<T: AsyncRead + AsyncWrite + Unpin + 'static> AsyncWrite for H1Connection<T> {
fn poll_write(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
Pin::new(&mut self.io.as_mut().unwrap()).poll_write(cx, buf)
}
fn poll_flush(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<io::Result<()>> {
Pin::new(self.io.as_mut().unwrap()).poll_flush(cx)
}
fn poll_shutdown(
mut self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), io::Error>> {
Pin::new(self.io.as_mut().unwrap()).poll_shutdown(cx)
}
}
#[pin_project::pin_project] #[pin_project::pin_project]
pub(crate) struct PlStream<Io> { pub(crate) struct PlStream<Io: ConnectionIo> {
#[pin] #[pin]
framed: Option<Framed<Io, h1::ClientPayloadCodec>>, framed: Framed<H1Connection<Io>, h1::ClientPayloadCodec>,
} }
impl<Io: ConnectionLifetime> PlStream<Io> { impl<Io: ConnectionIo> PlStream<Io> {
fn new(framed: Framed<Io, h1::ClientCodec>) -> Self { fn new(framed: Framed<H1Connection<Io>, h1::ClientCodec>) -> Self {
let framed = framed.into_map_codec(|codec| codec.into_payload_codec()); let framed = framed.into_map_codec(|codec| codec.into_payload_codec());
PlStream { PlStream { framed }
framed: Some(framed),
}
} }
} }
impl<Io: ConnectionLifetime> Stream for PlStream<Io> { impl<Io: ConnectionIo> Stream for PlStream<Io> {
type Item = Result<Bytes, PayloadError>; type Item = Result<Bytes, PayloadError>;
fn poll_next( fn poll_next(
@ -306,30 +217,14 @@ impl<Io: ConnectionLifetime> Stream for PlStream<Io> {
) -> Poll<Option<Self::Item>> { ) -> Poll<Option<Self::Item>> {
let mut this = self.project(); let mut this = self.project();
match this.framed.as_mut().as_pin_mut().unwrap().next_item(cx)? { match ready!(this.framed.as_mut().next_item(cx)?) {
Poll::Pending => Poll::Pending, Some(Some(chunk)) => Poll::Ready(Some(Ok(chunk))),
Poll::Ready(Some(chunk)) => { Some(None) => {
if let Some(chunk) = chunk { let keep_alive = this.framed.codec_ref().keepalive();
Poll::Ready(Some(Ok(chunk))) this.framed.io_mut().on_release(keep_alive);
} else { Poll::Ready(None)
let framed = this.framed.as_mut().as_pin_mut().unwrap();
let force_close = !framed.codec_ref().keepalive();
release_connection(framed, force_close);
Poll::Ready(None)
}
} }
Poll::Ready(None) => Poll::Ready(None), None => Poll::Ready(None),
} }
} }
} }
fn release_connection<T, U>(framed: Pin<&mut Framed<T, U>>, force_close: bool)
where
T: ConnectionLifetime,
{
if !force_close && framed.is_read_buf_empty() && framed.is_write_buf_empty() {
framed.io_pin().release()
} else {
framed.io_pin().close()
}
}

View File

@ -1,9 +1,7 @@
use std::future::Future; use std::future::Future;
use std::time;
use actix_codec::{AsyncRead, AsyncWrite}; use actix_utils::future::poll_fn;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::poll_fn;
use h2::{ use h2::{
client::{Builder, Connection, SendRequest}, client::{Builder, Connection, SendRequest},
SendStream, SendStream,
@ -11,27 +9,29 @@ use h2::{
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING};
use http::{request::Request, Method, Version}; use http::{request::Request, Method, Version};
use crate::body::{BodySize, MessageBody}; use crate::{
use crate::header::HeaderMap; body::{BodySize, MessageBody},
use crate::message::{RequestHeadType, ResponseHead}; header::HeaderMap,
use crate::payload::Payload; message::{RequestHeadType, ResponseHead},
payload::Payload,
Error,
};
use super::config::ConnectorConfig; use super::{
use super::connection::{ConnectionType, IoConnection}; config::ConnectorConfig,
use super::error::SendRequestError; connection::{ConnectionIo, H2Connection},
use super::pool::Acquired; error::SendRequestError,
use crate::client::connection::H2Connection; };
pub(crate) async fn send_request<T, B>( pub(crate) async fn send_request<Io, B>(
mut io: H2Connection, mut io: H2Connection<Io>,
head: RequestHeadType, head: RequestHeadType,
body: B, body: B,
created: time::Instant,
pool: Option<Acquired<T>>,
) -> Result<(ResponseHead, Payload), SendRequestError> ) -> Result<(ResponseHead, Payload), SendRequestError>
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, Io: ConnectionIo,
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
@ -103,13 +103,13 @@ where
let res = poll_fn(|cx| io.poll_ready(cx)).await; let res = poll_fn(|cx| io.poll_ready(cx)).await;
if let Err(e) = res { if let Err(e) = res {
release(io, pool, created, e.is_io()); io.on_release(e.is_io());
return Err(SendRequestError::from(e)); return Err(SendRequestError::from(e));
} }
let resp = match io.send_request(req, eof) { let resp = match io.send_request(req, eof) {
Ok((fut, send)) => { Ok((fut, send)) => {
release(io, pool, created, false); io.on_release(false);
if !eof { if !eof {
send_body(body, send).await?; send_body(body, send).await?;
@ -117,7 +117,7 @@ where
fut.await.map_err(SendRequestError::from)? fut.await.map_err(SendRequestError::from)?
} }
Err(e) => { Err(e) => {
release(io, pool, created, e.is_io()); io.on_release(e.is_io());
return Err(e.into()); return Err(e.into());
} }
}; };
@ -131,10 +131,14 @@ where
Ok((head, payload)) Ok((head, payload))
} }
async fn send_body<B: MessageBody>( async fn send_body<B>(
body: B, body: B,
mut send: SendStream<Bytes>, mut send: SendStream<Bytes>,
) -> Result<(), SendRequestError> { ) -> Result<(), SendRequestError>
where
B: MessageBody,
B::Error: Into<Error>,
{
let mut buf = None; let mut buf = None;
actix_rt::pin!(body); actix_rt::pin!(body);
loop { loop {
@ -144,7 +148,7 @@ async fn send_body<B: MessageBody>(
send.reserve_capacity(b.len()); send.reserve_capacity(b.len());
buf = Some(b); buf = Some(b);
} }
Some(Err(e)) => return Err(e.into()), Some(Err(e)) => return Err(e.into().into()),
None => { None => {
if let Err(e) = send.send_data(Bytes::new(), true) { if let Err(e) = send.send_data(Bytes::new(), true) {
return Err(e.into()); return Err(e.into());
@ -178,28 +182,10 @@ async fn send_body<B: MessageBody>(
} }
} }
/// release SendRequest object pub(crate) fn handshake<Io: ConnectionIo>(
fn release<T: AsyncRead + AsyncWrite + Unpin + 'static>(
io: H2Connection,
pool: Option<Acquired<T>>,
created: time::Instant,
close: bool,
) {
if let Some(mut pool) = pool {
if close {
pool.close(IoConnection::new(ConnectionType::H2(io), created, None));
} else {
pool.release(IoConnection::new(ConnectionType::H2(io), created, None));
}
}
}
pub(crate) fn handshake<Io>(
io: Io, io: Io,
config: &ConnectorConfig, config: &ConnectorConfig,
) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>> ) -> impl Future<Output = Result<(SendRequest<Bytes>, Connection<Io, Bytes>), h2::Error>>
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
let mut builder = Builder::new(); let mut builder = Builder::new();
builder builder

View File

@ -14,10 +14,10 @@ pub use actix_tls::connect::{
Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection, Connect as TcpConnect, ConnectError as TcpConnectError, Connection as TcpConnection,
}; };
pub use self::connection::Connection; pub use self::connection::{Connection, ConnectionIo};
pub use self::connector::Connector; pub use self::connector::{Connector, ConnectorService};
pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use self::pool::Protocol; pub use crate::Protocol;
#[derive(Clone)] #[derive(Clone)]
pub struct Connect { pub struct Connect {

View File

@ -1,40 +1,38 @@
//! Client connection pooling keyed on the authority part of the connection URI. //! Client connection pooling keyed on the authority part of the connection URI.
use std::collections::VecDeque; use std::{
use std::future::Future; cell::RefCell,
use std::ops::Deref; collections::VecDeque,
use std::pin::Pin; future::Future,
use std::rc::Rc; io,
use std::sync::Arc; ops::Deref,
use std::task::{Context, Poll}; pin::Pin,
use std::time::{Duration, Instant}; rc::Rc,
use std::{cell::RefCell, io}; sync::Arc,
task::{Context, Poll},
time::{Duration, Instant},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use actix_rt::time::{sleep, Sleep}; use actix_rt::time::{sleep, Sleep};
use actix_service::Service; use actix_service::Service;
use ahash::AHashMap; use ahash::AHashMap;
use futures_core::future::LocalBoxFuture; use futures_core::future::LocalBoxFuture;
use http::uri::Authority; use http::uri::Authority;
use pin_project::pin_project; use pin_project::pin_project;
use tokio::io::ReadBuf;
use tokio::sync::{OwnedSemaphorePermit, Semaphore}; use tokio::sync::{OwnedSemaphorePermit, Semaphore};
use super::config::ConnectorConfig; use super::config::ConnectorConfig;
use super::connection::{ConnectionType, H2Connection, IoConnection}; use super::connection::{
ConnectionInnerType, ConnectionIo, ConnectionType, H2ConnectionInner,
};
use super::error::ConnectError; use super::error::ConnectError;
use super::h2proto::handshake; use super::h2proto::handshake;
use super::Connect; use super::Connect;
use super::Protocol;
#[derive(Clone, Copy, PartialEq)]
/// Protocol version
pub enum Protocol {
Http1,
Http2,
}
#[derive(Hash, Eq, PartialEq, Clone, Debug)] #[derive(Hash, Eq, PartialEq, Clone, Debug)]
pub(crate) struct Key { pub struct Key {
authority: Authority, authority: Authority,
} }
@ -44,17 +42,18 @@ impl From<Authority> for Key {
} }
} }
#[doc(hidden)]
/// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key. /// Connections pool for reuse Io type for certain [`http::uri::Authority`] as key.
pub(crate) struct ConnectionPool<S, Io> pub struct ConnectionPool<S, Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
connector: Rc<S>, connector: S,
inner: ConnectionPoolInner<Io>, inner: ConnectionPoolInner<Io>,
} }
/// wrapper type for check the ref count of Rc. /// wrapper type for check the ref count of Rc.
struct ConnectionPoolInner<Io>(Rc<ConnectionPoolInnerPriv<Io>>) pub struct ConnectionPoolInner<Io>(Rc<ConnectionPoolInnerPriv<Io>>)
where where
Io: AsyncWrite + Unpin + 'static; Io: AsyncWrite + Unpin + 'static;
@ -62,10 +61,21 @@ impl<Io> ConnectionPoolInner<Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
fn new(config: ConnectorConfig) -> Self {
let permits = Arc::new(Semaphore::new(config.limit));
let available = RefCell::new(AHashMap::default());
Self(Rc::new(ConnectionPoolInnerPriv {
config,
available,
permits,
}))
}
/// spawn a async for graceful shutdown h1 Io type with a timeout. /// spawn a async for graceful shutdown h1 Io type with a timeout.
fn close(&self, conn: ConnectionType<Io>) { fn close(&self, conn: ConnectionInnerType<Io>) {
if let Some(timeout) = self.config.disconnect_timeout { if let Some(timeout) = self.config.disconnect_timeout {
if let ConnectionType::H1(io) = conn { if let ConnectionInnerType::H1(io) = conn {
actix_rt::spawn(CloseConnection::new(io, timeout)); actix_rt::spawn(CloseConnection::new(io, timeout));
} }
} }
@ -110,7 +120,7 @@ where
} }
} }
struct ConnectionPoolInnerPriv<Io> pub struct ConnectionPoolInnerPriv<Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
@ -134,40 +144,22 @@ where
/// Any requests beyond limit would be wait in fifo order and get notified in async manner /// Any requests beyond limit would be wait in fifo order and get notified in async manner
/// by [`tokio::sync::Semaphore`] /// by [`tokio::sync::Semaphore`]
pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self { pub(crate) fn new(connector: S, config: ConnectorConfig) -> Self {
let permits = Arc::new(Semaphore::new(config.limit)); let inner = ConnectionPoolInner::new(config);
let available = RefCell::new(AHashMap::default());
let connector = Rc::new(connector);
let inner = ConnectionPoolInner(Rc::new(ConnectionPoolInnerPriv {
config,
available,
permits,
}));
Self { connector, inner } Self { connector, inner }
} }
} }
impl<S, Io> Clone for ConnectionPool<S, Io>
where
Io: AsyncWrite + Unpin + 'static,
{
fn clone(&self) -> Self {
Self {
connector: self.connector.clone(),
inner: self.inner.clone(),
}
}
}
impl<S, Io> Service<Connect> for ConnectionPool<S, Io> impl<S, Io> Service<Connect> for ConnectionPool<S, Io>
where where
S: Service<Connect, Response = (Io, Protocol), Error = ConnectError> + 'static, S: Service<Connect, Response = (Io, Protocol), Error = ConnectError>
Io: AsyncRead + AsyncWrite + Unpin + 'static, + Clone
+ 'static,
Io: ConnectionIo,
{ {
type Response = IoConnection<Io>; type Response = ConnectionType<Io>;
type Error = ConnectError; type Error = ConnectError;
type Future = LocalBoxFuture<'static, Result<IoConnection<Io>, ConnectError>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
actix_service::forward_ready!(connector); actix_service::forward_ready!(connector);
@ -211,7 +203,7 @@ where
inner.close(c.conn); inner.close(c.conn);
} else { } else {
// check if the connection is still usable // check if the connection is still usable
if let ConnectionType::H1(ref mut io) = c.conn { if let ConnectionInnerType::H1(ref mut io) = c.conn {
let check = ConnectionCheckFuture { io }; let check = ConnectionCheckFuture { io };
match check.await { match check.await {
ConnectionState::Tainted => { ConnectionState::Tainted => {
@ -235,28 +227,26 @@ where
// construct acquired. It's used to put Io type back to pool/ close the Io type. // construct acquired. It's used to put Io type back to pool/ close the Io type.
// permit is carried with the whole lifecycle of Acquired. // permit is carried with the whole lifecycle of Acquired.
let acquired = Some(Acquired { key, inner, permit }); let acquired = Acquired { key, inner, permit };
// match the connection and spawn new one if did not get anything. // match the connection and spawn new one if did not get anything.
match conn { match conn {
Some(conn) => Ok(IoConnection::new(conn.conn, conn.created, acquired)), Some(conn) => {
Ok(ConnectionType::from_pool(conn.conn, conn.created, acquired))
}
None => { None => {
let (io, proto) = connector.call(req).await?; let (io, proto) = connector.call(req).await?;
// TODO: remove when http3 is added in support.
assert!(proto != Protocol::Http3);
if proto == Protocol::Http1 { if proto == Protocol::Http1 {
Ok(IoConnection::new( Ok(ConnectionType::from_h1(io, Instant::now(), acquired))
ConnectionType::H1(io),
Instant::now(),
acquired,
))
} else { } else {
let config = &acquired.as_ref().unwrap().inner.config; let config = &acquired.inner.config;
let (sender, connection) = handshake(io, config).await?; let (sender, connection) = handshake(io, config).await?;
Ok(IoConnection::new( let inner = H2ConnectionInner::new(sender, connection);
ConnectionType::H2(H2Connection::new(sender, connection)), Ok(ConnectionType::from_h2(inner, Instant::now(), acquired))
Instant::now(),
acquired,
))
} }
} }
} }
@ -307,7 +297,7 @@ where
} }
struct PooledConnection<Io> { struct PooledConnection<Io> {
conn: ConnectionType<Io>, conn: ConnectionInnerType<Io>,
used: Instant, used: Instant,
created: Instant, created: Instant,
} }
@ -347,28 +337,26 @@ where
} }
} }
pub(crate) struct Acquired<Io> pub struct Acquired<Io>
where where
Io: AsyncWrite + Unpin + 'static, Io: AsyncWrite + Unpin + 'static,
{ {
/// authority key for identify connection.
key: Key, key: Key,
/// handle to connection pool.
inner: ConnectionPoolInner<Io>, inner: ConnectionPoolInner<Io>,
/// permit for limit concurrent in-flight connection for a Client object.
permit: OwnedSemaphorePermit, permit: OwnedSemaphorePermit,
} }
impl<Io> Acquired<Io> impl<Io: ConnectionIo> Acquired<Io> {
where
Io: AsyncRead + AsyncWrite + Unpin + 'static,
{
/// Close the IO. /// Close the IO.
pub(crate) fn close(&mut self, conn: IoConnection<Io>) { pub(super) fn close(&self, conn: ConnectionInnerType<Io>) {
let (conn, _) = conn.into_inner();
self.inner.close(conn); self.inner.close(conn);
} }
/// Release IO back into pool. /// Release IO back into pool.
pub(crate) fn release(&mut self, conn: IoConnection<Io>) { pub(super) fn release(&self, conn: ConnectionInnerType<Io>, created: Instant) {
let (io, created) = conn.into_inner();
let Acquired { key, inner, .. } = self; let Acquired { key, inner, .. } = self;
inner inner
@ -377,12 +365,12 @@ where
.entry(key.clone()) .entry(key.clone())
.or_insert_with(VecDeque::new) .or_insert_with(VecDeque::new)
.push_back(PooledConnection { .push_back(PooledConnection {
conn: io, conn,
created, created,
used: Instant::now(), used: Instant::now(),
}); });
let _ = &mut self.permit; let _ = &self.permit;
} }
} }
@ -393,7 +381,7 @@ mod test {
use http::Uri; use http::Uri;
use super::*; use super::*;
use crate::client::connection::IoConnection; use crate::client::connection::ConnectionType;
/// A stream type that always returns pending on async read. /// A stream type that always returns pending on async read.
/// ///
@ -440,6 +428,7 @@ mod test {
} }
} }
#[derive(Clone)]
struct TestPoolConnector { struct TestPoolConnector {
generated: Rc<Cell<usize>>, generated: Rc<Cell<usize>>,
} }
@ -458,12 +447,14 @@ mod test {
} }
} }
fn release<T>(conn: IoConnection<T>) fn release<T>(conn: ConnectionType<T>)
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
let (conn, created, mut acquired) = conn.into_parts(); match conn {
acquired.release(IoConnection::new(conn, created, None)); ConnectionType::H1(mut conn) => conn.on_release(true),
ConnectionType::H2(mut conn) => conn.on_release(false),
}
} }
#[actix_rt::test] #[actix_rt::test]

View File

@ -126,9 +126,7 @@ impl ServiceConfig {
pub fn client_timer(&self) -> Option<Sleep> { pub fn client_timer(&self) -> Option<Sleep> {
let delay_time = self.0.client_timeout; let delay_time = self.0.client_timeout;
if delay_time != 0 { if delay_time != 0 {
Some(sleep_until( Some(sleep_until(self.now() + Duration::from_millis(delay_time)))
self.0.date_service.now() + Duration::from_millis(delay_time),
))
} else { } else {
None None
} }
@ -138,7 +136,7 @@ impl ServiceConfig {
pub fn client_timer_expire(&self) -> Option<Instant> { pub fn client_timer_expire(&self) -> Option<Instant> {
let delay = self.0.client_timeout; let delay = self.0.client_timeout;
if delay != 0 { if delay != 0 {
Some(self.0.date_service.now() + Duration::from_millis(delay)) Some(self.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -148,7 +146,7 @@ impl ServiceConfig {
pub fn client_disconnect_timer(&self) -> Option<Instant> { pub fn client_disconnect_timer(&self) -> Option<Instant> {
let delay = self.0.client_disconnect; let delay = self.0.client_disconnect;
if delay != 0 { if delay != 0 {
Some(self.0.date_service.now() + Duration::from_millis(delay)) Some(self.now() + Duration::from_millis(delay))
} else { } else {
None None
} }
@ -157,20 +155,12 @@ impl ServiceConfig {
#[inline] #[inline]
/// Return keep-alive timer delay is configured. /// Return keep-alive timer delay is configured.
pub fn keep_alive_timer(&self) -> Option<Sleep> { pub fn keep_alive_timer(&self) -> Option<Sleep> {
if let Some(ka) = self.0.keep_alive { self.keep_alive().map(|ka| sleep_until(self.now() + ka))
Some(sleep_until(self.0.date_service.now() + ka))
} else {
None
}
} }
/// Keep-alive expire time /// Keep-alive expire time
pub fn keep_alive_expire(&self) -> Option<Instant> { pub fn keep_alive_expire(&self) -> Option<Instant> {
if let Some(ka) = self.0.keep_alive { self.keep_alive().map(|ka| self.now() + ka)
Some(self.0.date_service.now() + ka)
} else {
None
}
} }
#[inline] #[inline]

View File

@ -12,6 +12,7 @@ use brotli2::write::BrotliDecoder;
use bytes::Bytes; use bytes::Bytes;
use flate2::write::{GzDecoder, ZlibDecoder}; use flate2::write::{GzDecoder, ZlibDecoder};
use futures_core::{ready, Stream}; use futures_core::{ready, Stream};
use zstd::stream::write::Decoder as ZstdDecoder;
use crate::{ use crate::{
encoding::Writer, encoding::Writer,
@ -45,6 +46,12 @@ where
ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new( ContentEncoding::Gzip => Some(ContentDecoder::Gzip(Box::new(
GzDecoder::new(Writer::new()), GzDecoder::new(Writer::new()),
))), ))),
ContentEncoding::Zstd => Some(ContentDecoder::Zstd(Box::new(
ZstdDecoder::new(Writer::new()).expect(
"Failed to create zstd decoder. This is a bug. \
Please report it to the actix-web repository.",
),
))),
_ => None, _ => None,
}; };
@ -144,6 +151,9 @@ enum ContentDecoder {
Deflate(Box<ZlibDecoder<Writer>>), Deflate(Box<ZlibDecoder<Writer>>),
Gzip(Box<GzDecoder<Writer>>), Gzip(Box<GzDecoder<Writer>>),
Br(Box<BrotliDecoder<Writer>>), Br(Box<BrotliDecoder<Writer>>),
// We need explicit 'static lifetime here because ZstdDecoder need lifetime
// argument, and we use `spawn_blocking` in `Decoder::poll_next` that require `FnOnce() -> R + Send + 'static`
Zstd(Box<ZstdDecoder<'static, Writer>>),
} }
impl ContentDecoder { impl ContentDecoder {
@ -186,6 +196,18 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Zstd(ref mut decoder) => match decoder.flush() {
Ok(_) => {
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
Ok(None)
}
}
Err(e) => Err(e),
},
} }
} }
@ -232,6 +254,20 @@ impl ContentDecoder {
} }
Err(e) => Err(e), Err(e) => Err(e),
}, },
ContentDecoder::Zstd(ref mut decoder) => match decoder.write_all(&data) {
Ok(_) => {
decoder.flush()?;
let b = decoder.get_mut().take();
if !b.is_empty() {
Ok(Some(b))
} else {
Ok(None)
}
}
Err(e) => Err(e),
},
} }
} }
} }

View File

@ -1,6 +1,7 @@
//! Stream encoders. //! Stream encoders.
use std::{ use std::{
error::Error as StdError,
future::Future, future::Future,
io::{self, Write as _}, io::{self, Write as _},
pin::Pin, pin::Pin,
@ -10,12 +11,14 @@ use std::{
use actix_rt::task::{spawn_blocking, JoinHandle}; use actix_rt::task::{spawn_blocking, JoinHandle};
use brotli2::write::BrotliEncoder; use brotli2::write::BrotliEncoder;
use bytes::Bytes; use bytes::Bytes;
use derive_more::Display;
use flate2::write::{GzEncoder, ZlibEncoder}; use flate2::write::{GzEncoder, ZlibEncoder};
use futures_core::ready; use futures_core::ready;
use pin_project::pin_project; use pin_project::pin_project;
use zstd::stream::write::Encoder as ZstdEncoder;
use crate::{ use crate::{
body::{Body, BodySize, MessageBody, ResponseBody}, body::{Body, BodySize, BoxAnyBody, MessageBody, ResponseBody},
http::{ http::{
header::{ContentEncoding, CONTENT_ENCODING}, header::{ContentEncoding, CONTENT_ENCODING},
HeaderValue, StatusCode, HeaderValue, StatusCode,
@ -92,10 +95,16 @@ impl<B: MessageBody> Encoder<B> {
enum EncoderBody<B> { enum EncoderBody<B> {
Bytes(Bytes), Bytes(Bytes),
Stream(#[pin] B), Stream(#[pin] B),
BoxedStream(Box<dyn MessageBody + Unpin>), BoxedStream(BoxAnyBody),
} }
impl<B: MessageBody> MessageBody for EncoderBody<B> { impl<B> MessageBody for EncoderBody<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
match self { match self {
EncoderBody::Bytes(ref b) => b.size(), EncoderBody::Bytes(ref b) => b.size(),
@ -107,7 +116,7 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
match self.project() { match self.project() {
EncoderBodyProj::Bytes(b) => { EncoderBodyProj::Bytes(b) => {
if b.is_empty() { if b.is_empty() {
@ -116,15 +125,30 @@ impl<B: MessageBody> MessageBody for EncoderBody<B> {
Poll::Ready(Some(Ok(std::mem::take(b)))) Poll::Ready(Some(Ok(std::mem::take(b))))
} }
} }
EncoderBodyProj::Stream(b) => b.poll_next(cx), // TODO: MSRV 1.51: poll_map_err
EncoderBodyProj::Stream(b) => match ready!(b.poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Body(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
},
EncoderBodyProj::BoxedStream(ref mut b) => { EncoderBodyProj::BoxedStream(ref mut b) => {
Pin::new(b.as_mut()).poll_next(cx) match ready!(b.as_pin_mut().poll_next(cx)) {
Some(Err(err)) => Poll::Ready(Some(Err(EncoderError::Boxed(err)))),
Some(Ok(val)) => Poll::Ready(Some(Ok(val))),
None => Poll::Ready(None),
}
} }
} }
} }
} }
impl<B: MessageBody> MessageBody for Encoder<B> { impl<B> MessageBody for Encoder<B>
where
B: MessageBody,
B::Error: Into<Error>,
{
type Error = EncoderError<B::Error>;
fn size(&self) -> BodySize { fn size(&self) -> BodySize {
if self.encoder.is_none() { if self.encoder.is_none() {
self.body.size() self.body.size()
@ -136,7 +160,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
fn poll_next( fn poll_next(
self: Pin<&mut Self>, self: Pin<&mut Self>,
cx: &mut Context<'_>, cx: &mut Context<'_>,
) -> Poll<Option<Result<Bytes, Error>>> { ) -> Poll<Option<Result<Bytes, Self::Error>>> {
let mut this = self.project(); let mut this = self.project();
loop { loop {
if *this.eof { if *this.eof {
@ -144,8 +168,9 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
} }
if let Some(ref mut fut) = this.fut { if let Some(ref mut fut) = this.fut {
let mut encoder = let mut encoder = ready!(Pin::new(fut).poll(cx))
ready!(Pin::new(fut).poll(cx)).map_err(|_| BlockingError)??; .map_err(|_| EncoderError::Blocking(BlockingError))?
.map_err(EncoderError::Io)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
@ -164,7 +189,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
Some(Ok(chunk)) => { Some(Ok(chunk)) => {
if let Some(mut encoder) = this.encoder.take() { if let Some(mut encoder) = this.encoder.take() {
if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE { if chunk.len() < MAX_CHUNK_SIZE_ENCODE_IN_PLACE {
encoder.write(&chunk)?; encoder.write(&chunk).map_err(EncoderError::Io)?;
let chunk = encoder.take(); let chunk = encoder.take();
*this.encoder = Some(encoder); *this.encoder = Some(encoder);
@ -184,7 +209,7 @@ impl<B: MessageBody> MessageBody for Encoder<B> {
None => { None => {
if let Some(encoder) = this.encoder.take() { if let Some(encoder) = this.encoder.take() {
let chunk = encoder.finish()?; 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 {
@ -211,6 +236,9 @@ enum ContentEncoder {
Deflate(ZlibEncoder<Writer>), Deflate(ZlibEncoder<Writer>),
Gzip(GzEncoder<Writer>), Gzip(GzEncoder<Writer>),
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`
Zstd(ZstdEncoder<'static, Writer>),
} }
impl ContentEncoder { impl ContentEncoder {
@ -227,6 +255,10 @@ impl ContentEncoder {
ContentEncoding::Br => { ContentEncoding::Br => {
Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3))) Some(ContentEncoder::Br(BrotliEncoder::new(Writer::new(), 3)))
} }
ContentEncoding::Zstd => {
let encoder = ZstdEncoder::new(Writer::new(), 3).ok()?;
Some(ContentEncoder::Zstd(encoder))
}
_ => None, _ => None,
} }
} }
@ -237,6 +269,7 @@ impl ContentEncoder {
ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Br(ref mut encoder) => encoder.get_mut().take(),
ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Deflate(ref mut encoder) => encoder.get_mut().take(),
ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(), ContentEncoder::Gzip(ref mut encoder) => encoder.get_mut().take(),
ContentEncoder::Zstd(ref mut encoder) => encoder.get_mut().take(),
} }
} }
@ -254,6 +287,10 @@ impl ContentEncoder {
Ok(writer) => Ok(writer.buf.freeze()), Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err), Err(err) => Err(err),
}, },
ContentEncoder::Zstd(encoder) => match encoder.finish() {
Ok(writer) => Ok(writer.buf.freeze()),
Err(err) => Err(err),
},
} }
} }
@ -280,6 +317,46 @@ impl ContentEncoder {
Err(err) Err(err)
} }
}, },
ContentEncoder::Zstd(ref mut encoder) => match encoder.write_all(data) {
Ok(_) => Ok(()),
Err(err) => {
trace!("Error decoding ztsd encoding: {}", err);
Err(err)
}
},
} }
} }
} }
#[derive(Debug, Display)]
#[non_exhaustive]
pub enum EncoderError<E> {
#[display(fmt = "body")]
Body(E),
#[display(fmt = "boxed")]
Boxed(Box<dyn StdError>),
#[display(fmt = "blocking")]
Blocking(BlockingError),
#[display(fmt = "io")]
Io(io::Error),
}
impl<E: StdError + 'static> StdError for EncoderError<E> {
fn source(&self) -> Option<&(dyn StdError + 'static)> {
match self {
EncoderError::Body(err) => Some(err),
EncoderError::Boxed(err) => Some(&**err),
EncoderError::Blocking(err) => Some(err),
EncoderError::Io(err) => Some(err),
}
}
}
impl<E: StdError + 'static> From<EncoderError<E>> for crate::Error {
fn from(err: EncoderError<E>) -> Self {
crate::Error::new_encoder().with_cause(err)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1213,8 +1213,9 @@ mod tests {
#[test] #[test]
fn test_parse_chunked_payload_chunk_extension() { fn test_parse_chunked_payload_chunk_extension() {
let mut buf = BytesMut::from( let mut buf = BytesMut::from(
&"GET /test HTTP/1.1\r\n\ "GET /test HTTP/1.1\r\n\
transfer-encoding: chunked\r\n\r\n"[..], transfer-encoding: chunked\r\n\
\r\n",
); );
let mut reader = MessageDecoder::<Request>::default(); let mut reader = MessageDecoder::<Request>::default();
@ -1233,7 +1234,7 @@ mod tests {
#[test] #[test]
fn test_response_http10_read_until_eof() { fn test_response_http10_read_until_eof() {
let mut buf = BytesMut::from(&"HTTP/1.0 200 Ok\r\n\r\ntest data"[..]); let mut buf = BytesMut::from("HTTP/1.0 200 Ok\r\n\r\ntest data");
let mut reader = MessageDecoder::<ResponseHead>::default(); let mut reader = MessageDecoder::<ResponseHead>::default();
let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap(); let (_msg, pl) = reader.decode(&mut buf).unwrap().unwrap();

View File

@ -1,5 +1,6 @@
use std::{ use std::{
collections::VecDeque, collections::VecDeque,
error::Error as StdError,
fmt, fmt,
future::Future, future::Future,
io, mem, net, io, mem, net,
@ -17,18 +18,19 @@ use futures_core::ready;
use log::{error, trace}; use log::{error, trace};
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::{Body, BodySize, MessageBody, ResponseBody}; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, BodySize, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::error::{ParseError, PayloadError}; error::{DispatchError, ParseError, PayloadError},
use crate::request::Request; service::HttpFlow,
use crate::response::Response; OnConnectData, Request, Response, StatusCode,
use crate::service::HttpFlow; };
use crate::OnConnectData;
use super::codec::Codec; use super::{
use super::payload::{Payload, PayloadSender, PayloadStatus}; codec::Codec,
use super::{Message, MessageType}; payload::{Payload, PayloadSender, PayloadStatus},
Message, MessageType,
};
const LW_BUFFER_SIZE: usize = 1024; const LW_BUFFER_SIZE: usize = 1024;
const HW_BUFFER_SIZE: usize = 1024 * 8; const HW_BUFFER_SIZE: usize = 1024 * 8;
@ -49,10 +51,14 @@ 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<Error>, S::Error: Into<Response<AnyBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -67,10 +73,14 @@ 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<Error>, S::Error: Into<Response<AnyBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -82,10 +92,14 @@ 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<Error>, S::Error: Into<Response<AnyBody>>,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -121,19 +135,25 @@ enum State<S, B, X>
where where
S: Service<Request>, S: Service<Request>,
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] ResponseBody<B>), SendPayload(#[pin] B),
SendErrorPayload(#[pin] AnyBody),
} }
impl<S, B, X> State<S, B, X> impl<S, B, X> State<S, B, X>
where where
S: Service<Request>, S: Service<Request>,
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)
@ -149,12 +169,17 @@ enum PollResponse {
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 where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
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<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -205,12 +230,17 @@ where
impl<T, S, B, X, U> InnerDispatcher<T, S, B, X, U> impl<T, S, B, X, U> InnerDispatcher<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
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<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -267,11 +297,11 @@ where
io.poll_flush(cx) io.poll_flush(cx)
} }
fn send_response( fn send_response_inner(
self: Pin<&mut Self>, self: Pin<&mut Self>,
message: Response<()>, message: Response<()>,
body: ResponseBody<B>, body: &impl MessageBody,
) -> Result<(), DispatchError> { ) -> Result<BodySize, DispatchError> {
let size = body.size(); let size = body.size();
let mut this = self.project(); let mut this = self.project();
this.codec this.codec
@ -284,10 +314,35 @@ where
})?; })?;
this.flags.set(Flags::KEEPALIVE, this.codec.keepalive()); this.flags.set(Flags::KEEPALIVE, this.codec.keepalive());
match size {
BodySize::None | BodySize::Empty => this.state.set(State::None), Ok(size)
_ => this.state.set(State::SendPayload(body)), }
fn send_response(
mut self: Pin<&mut Self>,
message: Response<()>,
body: B,
) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size {
BodySize::None | BodySize::Empty => State::None,
_ => State::SendPayload(body),
}; };
self.project().state.set(state);
Ok(())
}
fn send_error_response(
mut self: Pin<&mut Self>,
message: Response<()>,
body: AnyBody,
) -> Result<(), DispatchError> {
let size = self.as_mut().send_response_inner(message, &body)?;
let state = match size {
BodySize::None | BodySize::Empty => State::None,
_ => State::SendErrorPayload(body),
};
self.project().state.set(state);
Ok(()) Ok(())
} }
@ -325,8 +380,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() self.as_mut().send_error_response(res, AnyBody::Empty)?;
.send_response(res, ResponseBody::Other(Body::Empty))?;
} }
// return with upgrade request and poll it exclusively. // return with upgrade request and poll it exclusively.
@ -346,9 +400,9 @@ where
// send service call error as response // send service call error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response = err.into().into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_error_response(res, body)?;
} }
// service call pending and could be waiting for more chunk messages. // service call pending and could be waiting for more chunk messages.
@ -385,7 +439,42 @@ where
} }
Poll::Ready(Some(Err(err))) => { Poll::Ready(Some(Err(err))) => {
return Err(DispatchError::Service(err)) return Err(DispatchError::Body(err.into()))
}
Poll::Pending => return Ok(PollResponse::DoNothing),
}
}
// buffer is beyond max size.
// return and try to write the whole buffer to io stream.
return Ok(PollResponse::DrainWriteBuf);
}
StateProj::SendErrorPayload(mut stream) => {
// TODO: de-dupe impl with SendPayload
// keep populate writer buffer until buffer size limit hit,
// get blocked or finished.
while this.write_buf.len() < super::payload::MAX_BUFFER_SIZE {
match stream.as_mut().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => {
this.codec.encode(
Message::Chunk(Some(item)),
&mut this.write_buf,
)?;
}
Poll::Ready(None) => {
this.codec
.encode(Message::Chunk(None), &mut this.write_buf)?;
// payload stream finished.
// set state to None and handle next message
this.state.set(State::None);
continue 'res;
}
Poll::Ready(Some(Err(err))) => {
return Err(DispatchError::Service(err.into()))
} }
Poll::Pending => return Ok(PollResponse::DoNothing), Poll::Pending => return Ok(PollResponse::DoNothing),
@ -405,12 +494,14 @@ where
let fut = this.flow.service.call(req); let fut = this.flow.service.call(req);
this.state.set(State::ServiceCall(fut)); this.state.set(State::ServiceCall(fut));
} }
// send expect error as response // send expect error as response
Poll::Ready(Err(err)) => { Poll::Ready(Err(err)) => {
let res: Response = err.into().into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.as_mut().send_response(res, body.into_body())?; self.as_mut().send_error_response(res, body)?;
} }
// expect must be solved before progress can be made. // expect must be solved before progress can be made.
Poll::Pending => return Ok(PollResponse::DoNothing), Poll::Pending => return Ok(PollResponse::DoNothing),
}, },
@ -456,10 +547,9 @@ 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 err = err.into(); let res: Response<AnyBody> = err.into();
let res: Response = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
return self.send_response(res, body.into_body()); return self.send_error_response(res, body);
} }
} }
} }
@ -477,9 +567,9 @@ 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 = err.into().into(); let res: Response<AnyBody> = err.into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
self.send_response(res, body.into_body()) self.send_error_response(res, body)
} }
}; };
} }
@ -563,7 +653,7 @@ where
); );
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::InternalServerError().finish().drop_body(), Response::internal_server_error().drop_body(),
)); ));
*this.error = Some(DispatchError::InternalError); *this.error = Some(DispatchError::InternalError);
break; break;
@ -576,7 +666,7 @@ where
error!("Internal server error: unexpected eof"); error!("Internal server error: unexpected eof");
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::InternalServerError().finish().drop_body(), Response::internal_server_error().drop_body(),
)); ));
*this.error = Some(DispatchError::InternalError); *this.error = Some(DispatchError::InternalError);
break; break;
@ -599,7 +689,10 @@ where
} }
// Requests overflow buffer size should be responded with 431 // Requests overflow buffer size should be responded with 431
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::RequestHeaderFieldsTooLarge().finish().drop_body(), Response::with_body(
StatusCode::REQUEST_HEADER_FIELDS_TOO_LARGE,
(),
),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(ParseError::TooLarge.into()); *this.error = Some(ParseError::TooLarge.into());
@ -612,7 +705,7 @@ where
// Malformed requests should be responded with 400 // Malformed requests should be responded with 400
this.messages.push_back(DispatcherMessage::Error( this.messages.push_back(DispatcherMessage::Error(
Response::BadRequest().finish().drop_body(), Response::bad_request().drop_body(),
)); ));
this.flags.insert(Flags::READ_DISCONNECT); this.flags.insert(Flags::READ_DISCONNECT);
*this.error = Some(err.into()); *this.error = Some(err.into());
@ -648,11 +741,6 @@ where
// go into Some<Pin<&mut Sleep>> branch // go into Some<Pin<&mut Sleep>> branch
this.ka_timer.set(Some(sleep_until(deadline))); this.ka_timer.set(Some(sleep_until(deadline)));
return self.poll_keepalive(cx); return self.poll_keepalive(cx);
} else {
this.flags.insert(Flags::READ_DISCONNECT);
if let Some(mut payload) = this.payload.take() {
payload.set_error(PayloadError::Incomplete(None));
}
} }
} }
} }
@ -662,7 +750,7 @@ where
// got timeout during shutdown, drop connection // got timeout during shutdown, drop connection
if this.flags.contains(Flags::SHUTDOWN) { if this.flags.contains(Flags::SHUTDOWN) {
return Err(DispatchError::DisconnectTimeout); return Err(DispatchError::DisconnectTimeout);
// exceed deadline. check for any outstanding tasks // exceed deadline. check for any outstanding tasks
} else if timer.deadline() >= *this.ka_expire { } else if timer.deadline() >= *this.ka_expire {
// have no task at hand. // have no task at hand.
if this.state.is_empty() && this.write_buf.is_empty() { if this.state.is_empty() && this.write_buf.is_empty() {
@ -682,28 +770,23 @@ where
} }
} else { } else {
// timeout on first request (slow request) return 408 // timeout on first request (slow request) return 408
if !this.flags.contains(Flags::STARTED) { trace!("Slow request timeout");
trace!("Slow request timeout"); let _ = self.as_mut().send_error_response(
let _ = self.as_mut().send_response( Response::with_body(StatusCode::REQUEST_TIMEOUT, ()),
Response::RequestTimeout().finish().drop_body(), AnyBody::Empty,
ResponseBody::Other(Body::Empty), );
); this = self.project();
this = self.project();
} else {
trace!("Keep-alive connection timeout");
}
this.flags.insert(Flags::STARTED | Flags::SHUTDOWN); this.flags.insert(Flags::STARTED | Flags::SHUTDOWN);
this.state.set(State::None);
} }
// still have unfinished task. try to reset and register keep-alive. // still have unfinished task. try to reset and register keep-alive.
} else if let Some(deadline) = } else if let Some(deadline) =
this.codec.config().keep_alive_expire() this.codec.config().keep_alive_expire()
{ {
timer.as_mut().reset(deadline); timer.as_mut().reset(deadline);
let _ = timer.poll(cx); let _ = timer.poll(cx);
} }
// timer resolved but still have not met the keep-alive expire deadline. // timer resolved but still have not met the keep-alive expire deadline.
// reset and register for later wakeup. // reset and register for later wakeup.
} else { } else {
timer.as_mut().reset(*this.ka_expire); timer.as_mut().reset(*this.ka_expire);
let _ = timer.poll(cx); let _ = timer.poll(cx);
@ -825,12 +908,17 @@ where
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<Error>, S::Error: Into<Response<AnyBody>>,
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<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -951,14 +1039,17 @@ mod tests {
use std::str; use std::str;
use actix_service::fn_service; use actix_service::fn_service;
use futures_util::future::{lazy, ready}; use actix_utils::future::{ready, Ready};
use bytes::Bytes;
use futures_util::future::lazy;
use super::*; use super::*;
use crate::test::TestBuffer;
use crate::{error::Error, KeepAlive};
use crate::{ use crate::{
error::Error,
h1::{ExpectHandler, UpgradeHandler}, h1::{ExpectHandler, UpgradeHandler},
test::TestSeqBuffer, http::Method,
test::{TestBuffer, TestSeqBuffer},
HttpMessage, KeepAlive,
}; };
fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> { fn find_slice(haystack: &[u8], needle: &[u8], from: usize) -> Option<usize> {
@ -977,19 +1068,23 @@ mod tests {
} }
} }
fn ok_service() -> impl Service<Request, Response = Response, Error = Error> { fn ok_service() -> impl Service<Request, Response = Response<AnyBody>, Error = Error>
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::Ok().finish()))) {
fn_service(|_req: Request| ready(Ok::<_, Error>(Response::ok())))
} }
fn echo_path_service() -> impl Service<Request, Response = Response, Error = Error> { fn echo_path_service(
) -> impl Service<Request, Response = Response<AnyBody>, 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>(Response::Ok().body(Body::from_slice(path)))) ready(Ok::<_, Error>(
Response::ok().set_body(AnyBody::from_slice(path)),
))
}) })
} }
fn echo_payload_service() -> impl Service<Request, Response = Response, Error = Error> fn echo_payload_service(
{ ) -> impl Service<Request, Response = Response<Bytes>, Error = Error> {
fn_service(|mut req: Request| { fn_service(|mut req: Request| {
Box::pin(async move { Box::pin(async move {
use futures_util::stream::StreamExt as _; use futures_util::stream::StreamExt as _;
@ -1000,7 +1095,7 @@ mod tests {
body.extend_from_slice(chunk.unwrap().chunk()) body.extend_from_slice(chunk.unwrap().chunk())
} }
Ok::<_, Error>(Response::Ok().body(body)) Ok::<_, Error>(Response::ok().set_body(body.freeze()))
}) })
}) })
} }
@ -1282,14 +1377,30 @@ mod tests {
#[actix_rt::test] #[actix_rt::test]
async fn test_upgrade() { async fn test_upgrade() {
struct TestUpgrade;
impl<T> Service<(Request, Framed<T, Codec>)> for TestUpgrade {
type Response = ();
type Error = Error;
type Future = Ready<Result<Self::Response, Self::Error>>;
actix_service::always_ready!();
fn call(&self, (req, _framed): (Request, Framed<T, Codec>)) -> Self::Future {
assert_eq!(req.method(), Method::GET);
assert!(req.upgrade());
assert_eq!(req.headers().get("upgrade").unwrap(), "websocket");
ready(Ok(()))
}
}
lazy(|cx| { lazy(|cx| {
let mut buf = TestSeqBuffer::empty(); let mut buf = TestSeqBuffer::empty();
let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None); let cfg = ServiceConfig::new(KeepAlive::Disabled, 0, 0, false, None);
let services = let services = HttpFlow::new(ok_service(), ExpectHandler, Some(TestUpgrade));
HttpFlow::new(ok_service(), ExpectHandler, Some(UpgradeHandler));
let h1 = Dispatcher::<_, _, _, _, UpgradeHandler>::new( let h1 = Dispatcher::<_, _, _, _, TestUpgrade>::new(
buf.clone(), buf.clone(),
cfg, cfg,
services, services,

View File

@ -6,14 +6,15 @@ use std::{cmp, io};
use bytes::{BufMut, BytesMut}; use bytes::{BufMut, BytesMut};
use crate::body::BodySize; use crate::{
use crate::config::ServiceConfig; body::BodySize,
use crate::header::{map::Value, HeaderName}; config::ServiceConfig,
use crate::helpers; header::{map::Value, HeaderMap, HeaderName},
use crate::http::header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; header::{CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING},
use crate::http::{HeaderMap, StatusCode, Version}; helpers,
use crate::message::{ConnectionType, RequestHeadType}; message::{ConnectionType, RequestHeadType},
use crate::response::Response; Response, StatusCode, Version,
};
const AVERAGE_HEADER_SIZE: usize = 30; const AVERAGE_HEADER_SIZE: usize = 30;
@ -287,7 +288,7 @@ impl MessageType for RequestHeadType {
let head = self.as_ref(); let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE); dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!( write!(
helpers::Writer(dst), helpers::MutWriter(dst),
"{} {} {}", "{} {} {}",
head.method, head.method,
head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
@ -420,7 +421,7 @@ impl TransferEncoding {
*eof = true; *eof = true;
buf.extend_from_slice(b"0\r\n\r\n"); buf.extend_from_slice(b"0\r\n\r\n");
} else { } else {
writeln!(helpers::Writer(buf), "{:X}\r", msg.len()) writeln!(helpers::MutWriter(buf), "{:X}\r", msg.len())
.map_err(|e| io::Error::new(io::ErrorKind::Other, e))?; .map_err(|e| io::Error::new(io::ErrorKind::Other, e))?;
buf.reserve(msg.len() + 2); buf.reserve(msg.len() + 2);
@ -630,8 +631,7 @@ mod tests {
async fn test_no_content_length() { async fn test_no_content_length() {
let mut bytes = BytesMut::with_capacity(2048); let mut bytes = BytesMut::with_capacity(2048);
let mut res: Response<()> = let mut res = Response::with_body(StatusCode::SWITCHING_PROTOCOLS, ());
Response::new(StatusCode::SWITCHING_PROTOCOLS).into_body::<()>();
res.headers_mut().insert(DATE, HeaderValue::from_static("")); res.headers_mut().insert(DATE, HeaderValue::from_static(""));
res.headers_mut() res.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")); .insert(CONTENT_LENGTH, HeaderValue::from_static("0"));

View File

@ -1,7 +1,5 @@
use std::task::Poll;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready}; use actix_utils::future::{ready, Ready};
use crate::error::Error; use crate::error::Error;
use crate::request::Request; use crate::request::Request;

View File

@ -263,7 +263,7 @@ impl Inner {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use futures_util::future::poll_fn; use actix_utils::future::poll_fn;
#[actix_rt::test] #[actix_rt::test]
async fn test_unread_data() { async fn test_unread_data() {

View File

@ -1,27 +1,29 @@
use std::future::Future; use std::{
use std::marker::PhantomData; error::Error as StdError,
use std::pin::Pin; fmt,
use std::rc::Rc; marker::PhantomData,
use std::task::{Context, Poll}; net,
use std::{fmt, net}; rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{
use futures_core::ready; fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
use futures_util::future::ready; };
use actix_utils::future::ready;
use futures_core::future::LocalBoxFuture;
use crate::body::MessageBody; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::request::Request; error::DispatchError,
use crate::response::Response; service::HttpServiceHandler,
use crate::service::HttpFlow; ConnectCallback, OnConnectData, Request, Response,
use crate::{ConnectCallback, OnConnectData}; };
use super::codec::Codec; use super::{codec::Codec, dispatcher::Dispatcher, ExpectHandler, UpgradeHandler};
use super::dispatcher::Dispatcher;
use super::{ExpectHandler, UpgradeHandler};
/// `ServiceFactory` implementation for HTTP1 transport /// `ServiceFactory` implementation for HTTP1 transport
pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> { pub struct H1Service<T, S, B, X = ExpectHandler, U = UpgradeHandler> {
@ -36,7 +38,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<Error>, S::Error: Into<Response<AnyBody>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
B: MessageBody, B: MessageBody,
@ -60,15 +62,22 @@ where
impl<S, B, X, U> H1Service<TcpStream, S, B, X, U> impl<S, B, X, U> H1Service<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
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::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
@ -81,7 +90,7 @@ where
Error = DispatchError, Error = DispatchError,
InitError = (), InitError = (),
> { > {
pipeline_factory(|io: TcpStream| { fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ready(Ok((io, peer_addr))) ready(Ok((io, peer_addr)))
}) })
@ -94,25 +103,34 @@ mod openssl {
use super::*; use super::*;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; openssl::{Acceptor, SslAcceptor, SslError, TlsStream},
TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>), (Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create openssl based service /// Create openssl based service
@ -126,16 +144,14 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| {
) let peer_addr = io.get_ref().peer_addr().ok();
.and_then(|io: TlsStream<TcpStream>| { ready(Ok((io, peer_addr)))
let peer_addr = io.get_ref().peer_addr().ok(); })
ready(Ok((io, peer_addr))) .and_then(self.map_err(TlsError::Service))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -143,27 +159,38 @@ mod openssl {
#[cfg(feature = "rustls")] #[cfg(feature = "rustls")]
mod rustls { mod rustls {
use super::*; use super::*;
use std::io;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::rustls::{Acceptor, ServerConfig, TlsStream}; use actix_tls::accept::{
use actix_tls::accept::TlsError; rustls::{Acceptor, ServerConfig, TlsStream},
use std::{fmt, io}; TlsError,
};
impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U> impl<S, B, X, U> H1Service<TlsStream<TcpStream>, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, Codec>), (Request, Framed<TlsStream<TcpStream>, Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
/// Create rustls based service /// Create rustls based service
@ -177,16 +204,14 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| {
) let peer_addr = io.get_ref().0.peer_addr().ok();
.and_then(|io: TlsStream<TcpStream>| { ready(Ok((io, peer_addr)))
let peer_addr = io.get_ref().0.peer_addr().ok(); })
ready(Ok((io, peer_addr))) .and_then(self.map_err(TlsError::Service))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -194,7 +219,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<Error>, S::Error: Into<Response<AnyBody>>,
S::Response: Into<Response<B>>, S::Response: Into<Response<B>>,
S::InitError: fmt::Debug, S::InitError: fmt::Debug,
B: MessageBody, B: MessageBody,
@ -202,7 +227,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<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
{ {
H1Service { H1Service {
@ -241,17 +266,25 @@ where
impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory<(T, Option<net::SocketAddr>)>
for H1Service<T, S, B, X, U> for H1Service<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error>, S::Future: 'static,
S::Error: Into<Response<AnyBody>>,
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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
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::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
{ {
type Response = (); type Response = ();
@ -259,205 +292,75 @@ where
type Config = (); type Config = ();
type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>; type Service = H1ServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = (); type InitError = ();
type Future = H1ServiceResponse<T, S, B, X, U>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H1ServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let expect = self.expect.new_service(());
fut_ex: Some(self.expect.new_service(())), let upgrade = self.upgrade.as_ref().map(|s| s.new_service(()));
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), let on_connect_ext = self.on_connect_ext.clone();
expect: None, let cfg = self.cfg.clone();
upgrade: None,
on_connect_ext: self.on_connect_ext.clone(),
cfg: Some(self.cfg.clone()),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project::pin_project] let expect = expect
pub struct H1ServiceResponse<T, S, B, X, U> .await
where .map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
S: ServiceFactory<Request>,
S::Error: Into<Error>,
S::InitError: fmt::Debug,
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
#[pin]
fut: S::Future,
#[pin]
fut_ex: Option<X::Future>,
#[pin]
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: Option<ServiceConfig>,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Future for H1ServiceResponse<T, S, B, X, U> let upgrade = match upgrade {
where Some(upgrade) => {
T: AsyncRead + AsyncWrite + Unpin, let upgrade = upgrade.await.map_err(|e| {
S: ServiceFactory<Request>, log::error!("Init http upgrade service error: {:?}", e)
S::Error: Into<Error>, })?;
S::Response: Into<Response<B>>, Some(upgrade)
S::InitError: fmt::Debug, }
B: MessageBody, None => None,
X: ServiceFactory<Request, Response = Request>, };
X::Error: Into<Error>,
X::InitError: fmt::Debug,
U: ServiceFactory<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
{
type Output = Result<H1ServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let service = service
let mut this = self.as_mut().project(); .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
if let Some(fut) = this.fut_ex.as_pin_mut() { Ok(H1ServiceHandler::new(
let expect = ready!(fut cfg,
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.expect = Some(expect);
this.fut_ex.set(None);
}
if let Some(fut) = this.fut_upg.as_pin_mut() {
let upgrade = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
}
let result = ready!(this
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
H1ServiceHandler::new(
this.cfg.take().unwrap(),
service, service,
this.expect.take().unwrap(), expect,
this.upgrade.take(), upgrade,
this.on_connect_ext.clone(), on_connect_ext,
) ))
})) })
} }
} }
/// `Service` implementation for HTTP/1 transport /// `Service` implementation for HTTP/1 transport
pub struct H1ServiceHandler<T, S, B, X, U> pub type H1ServiceHandler<T, S, B, X, U> = HttpServiceHandler<T, S, B, X, U>;
where
S: Service<Request>,
X: Service<Request>,
U: Service<(Request, Framed<T, Codec>)>,
{
flow: Rc<HttpFlow<S, X, U>>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> H1ServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> H1ServiceHandler<T, S, B, X, U> {
H1ServiceHandler {
flow: HttpFlow::new(service, expect, upgrade),
cfg,
on_connect_ext,
_phantom: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)> impl<T, S, B, X, U> Service<(T, Option<net::SocketAddr>)>
for H1ServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
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<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, Codec>), Response = ()>, U: Service<(Request, Framed<T, Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Future = Dispatcher<T, S, B, X, U>; type Future = Dispatcher<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self self._poll_ready(cx).map_err(|e| {
.flow log::error!("HTTP/1 service readiness error: {:?}", e);
.expect DispatchError::Service(e)
.poll_ready(cx) })
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = self
.flow
.service
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready;
let ready = if let Some(ref upg) = self.flow.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready
} else {
ready
};
if ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
} }
fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future { fn call(&self, (io, addr): (T, Option<net::SocketAddr>)) -> Self::Future {

View File

@ -1,8 +1,6 @@
use std::task::Poll;
use actix_codec::Framed; use actix_codec::Framed;
use actix_service::{Service, ServiceFactory}; use actix_service::{Service, ServiceFactory};
use futures_util::future::{ready, Ready}; use futures_core::future::LocalBoxFuture;
use crate::error::Error; use crate::error::Error;
use crate::h1::Codec; use crate::h1::Codec;
@ -16,7 +14,7 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Config = (); type Config = ();
type Service = UpgradeHandler; type Service = UpgradeHandler;
type InitError = Error; type InitError = Error;
type Future = Ready<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 {
unimplemented!() unimplemented!()
@ -26,11 +24,11 @@ impl<T> ServiceFactory<(Request, Framed<T, Codec>)> for UpgradeHandler {
impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler { impl<T> Service<(Request, Framed<T, Codec>)> for UpgradeHandler {
type Response = (); type Response = ();
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, _: (Request, Framed<T, Codec>)) -> Self::Future { fn call(&self, _: (Request, Framed<T, Codec>)) -> Self::Future {
ready(Ok(())) unimplemented!()
} }
} }

View File

@ -4,7 +4,7 @@ use std::task::{Context, Poll};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::body::{BodySize, MessageBody};
use crate::error::Error; use crate::error::Error;
use crate::h1::{Codec, Message}; use crate::h1::{Codec, Message};
use crate::response::Response; use crate::response::Response;
@ -14,7 +14,7 @@ use crate::response::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<ResponseBody<B>>, body: Option<B>,
#[pin] #[pin]
framed: Option<Framed<T, Codec>>, framed: Option<Framed<T, Codec>>,
} }
@ -22,6 +22,7 @@ pub struct SendResponse<T, B> {
impl<T, B> SendResponse<T, B> impl<T, B> SendResponse<T, B>
where where
B: MessageBody, B: MessageBody,
B::Error: Into<Error>,
{ {
pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self { pub fn new(framed: Framed<T, Codec>, response: Response<B>) -> Self {
let (res, body) = response.into_parts(); let (res, body) = response.into_parts();
@ -38,6 +39,7 @@ impl<T, B> Future for SendResponse<T, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody + Unpin, B: MessageBody + Unpin,
B::Error: Into<Error>,
{ {
type Output = Result<Framed<T, Codec>, Error>; type Output = Result<Framed<T, Codec>, Error>;
@ -60,7 +62,18 @@ where
.unwrap() .unwrap()
.is_write_buf_full() .is_write_buf_full()
{ {
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx)? { let next =
// TODO: MSRV 1.51: poll_map_err
match this.body.as_mut().as_pin_mut().unwrap().poll_next(cx) {
Poll::Ready(Some(Ok(item))) => Poll::Ready(Some(item)),
Poll::Ready(Some(Err(err))) => {
return Poll::Ready(Err(err.into()))
}
Poll::Ready(None) => Poll::Ready(None),
Poll::Pending => Poll::Pending,
};
match next {
Poll::Ready(item) => { Poll::Ready(item) => {
// body is done when item is None // body is done when item is None
body_done = item.is_none(); body_done = item.is_none();
@ -68,7 +81,9 @@ where
let _ = this.body.take(); let _ = this.body.take();
} }
let framed = this.framed.as_mut().as_pin_mut().unwrap(); let framed = this.framed.as_mut().as_pin_mut().unwrap();
framed.write(Message::Chunk(item))?; framed.write(Message::Chunk(item)).map_err(|err| {
Error::new_send_response().with_cause(err)
})?;
} }
Poll::Pending => body_ready = false, Poll::Pending => body_ready = false,
} }
@ -79,7 +94,10 @@ where
// flush write buffer // flush write buffer
if !framed.is_write_buf_empty() { if !framed.is_write_buf_empty() {
match framed.flush(cx)? { match framed
.flush(cx)
.map_err(|err| Error::new_send_response().with_cause(err))?
{
Poll::Ready(_) => { Poll::Ready(_) => {
if body_ready { if body_ready {
continue; continue;
@ -93,7 +111,9 @@ where
// send response // send response
if let Some(res) = this.res.take() { if let Some(res) = this.res.take() {
framed.write(res)?; framed
.write(res)
.map_err(|err| Error::new_send_response().with_cause(err))?;
continue; continue;
} }

View File

@ -1,51 +1,46 @@
use std::task::{Context, Poll}; use std::{
use std::{cmp, future::Future, marker::PhantomData, net, pin::Pin, rc::Rc}; cmp,
error::Error as StdError,
future::Future,
marker::PhantomData,
net,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_service::Service; use actix_service::Service;
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};
use h2::SendStream;
use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING}; use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING};
use log::{error, trace}; use log::{error, trace};
use pin_project_lite::pin_project;
use crate::body::{BodySize, MessageBody, ResponseBody}; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, BodySize, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::message::ResponseHead; service::HttpFlow,
use crate::payload::Payload; OnConnectData, Payload, Request, Response, ResponseHead,
use crate::request::Request; };
use crate::response::Response;
use crate::service::HttpFlow;
use crate::OnConnectData;
const CHUNK_SIZE: usize = 16_384; const CHUNK_SIZE: usize = 16_384;
/// Dispatcher for HTTP/2 protocol. pin_project! {
#[pin_project::pin_project] /// Dispatcher for HTTP/2 protocol.
pub struct Dispatcher<T, S, B, X, U> pub struct Dispatcher<T, S, B, X, U> {
where flow: Rc<HttpFlow<S, X, U>>,
T: AsyncRead + AsyncWrite + Unpin, connection: Connection<T, Bytes>,
S: Service<Request>, on_connect_data: OnConnectData,
B: MessageBody, config: ServiceConfig,
{ peer_addr: Option<net::SocketAddr>,
flow: Rc<HttpFlow<S, X, U>>, _phantom: PhantomData<B>,
connection: Connection<T, Bytes>, }
on_connect_data: OnConnectData,
config: ServiceConfig,
peer_addr: Option<net::SocketAddr>,
_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,
S: Service<Request>,
S::Error: Into<Error>,
S::Response: Into<Response<B>>,
B: MessageBody,
{
pub(crate) fn new( pub(crate) fn new(
flow: Rc<HttpFlow<S, X, U>>, flow: Rc<HttpFlow<S, X, U>>,
connection: Connection<T, Bytes>, connection: Connection<T, Bytes>,
@ -53,7 +48,7 @@ where
config: ServiceConfig, config: ServiceConfig,
peer_addr: Option<net::SocketAddr>, peer_addr: Option<net::SocketAddr>,
) -> Self { ) -> Self {
Dispatcher { Self {
flow, flow,
config, config,
peer_addr, peer_addr,
@ -67,269 +62,208 @@ where
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<Error> + 'static, S::Error: Into<Response<AnyBody>>,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>>,
B: MessageBody + 'static,
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{ {
type Output = Result<(), DispatchError>; type Output = Result<(), crate::error::DispatchError>;
#[inline] #[inline]
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();
loop { while let Some((req, tx)) =
match ready!(Pin::new(&mut this.connection).poll_accept(cx)) { ready!(Pin::new(&mut this.connection).poll_accept(cx)?)
None => return Poll::Ready(Ok(())), {
let (parts, body) = req.into_parts();
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
Some(Err(err)) => return Poll::Ready(Err(err.into())), let head = req.head_mut();
head.uri = parts.uri;
head.method = parts.method;
head.version = parts.version;
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
Some(Ok((req, res))) => { // merge on_connect_ext data into request extensions
let (parts, body) = req.into_parts(); this.on_connect_data.merge_into(&mut req);
let pl = crate::h2::Payload::new(body);
let pl = Payload::<crate::payload::PayloadStream>::H2(pl);
let mut req = Request::with_payload(pl);
let head = req.head_mut(); let fut = this.flow.service.call(req);
head.uri = parts.uri; let config = this.config.clone();
head.method = parts.method;
head.version = parts.version;
head.headers = parts.headers.into();
head.peer_addr = this.peer_addr;
// merge on_connect_ext data into request extensions // multiplex request handling with spawn task
this.on_connect_data.merge_into(&mut req); actix_rt::spawn(async move {
// resolve service call and send response.
let res = match fut.await {
Ok(res) => handle_response(res.into(), tx, config).await,
Err(err) => {
let res: Response<AnyBody> = err.into();
handle_response(res, tx, config).await
}
};
let svc = ServiceResponse { // log error.
state: ServiceResponseState::ServiceCall( if let Err(err) = res {
this.flow.service.call(req), match err {
Some(res), DispatchError::SendResponse(err) => {
), trace!("Error sending HTTP/2 response: {:?}", err)
config: this.config.clone(), }
buffer: None, DispatchError::SendData(err) => warn!("{:?}", err),
_phantom: PhantomData, DispatchError::ResponseBody(err) => {
}; error!("Response payload stream error: {:?}", err)
}
}
}
});
}
actix_rt::spawn(svc); Poll::Ready(Ok(()))
}
}
enum DispatchError {
SendResponse(h2::Error),
SendData(h2::Error),
ResponseBody(Box<dyn StdError>),
}
async fn handle_response<B>(
res: Response<B>,
mut tx: SendResponse<Bytes>,
config: ServiceConfig,
) -> Result<(), DispatchError>
where
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
{
let (res, body) = res.replace_body(());
// prepare response.
let mut size = body.size();
let res = prepare_response(config, res.head(), &mut size);
let eof = size.is_eof();
// send response head and return on eof.
let mut stream = tx
.send_response(res, eof)
.map_err(DispatchError::SendResponse)?;
if eof {
return Ok(());
}
// poll response body and send chunks to client.
actix_rt::pin!(body);
while let Some(res) = poll_fn(|cx| body.as_mut().poll_next(cx)).await {
let mut chunk = res.map_err(|err| DispatchError::ResponseBody(err.into()))?;
'send: loop {
// reserve enough space and wait for stream ready.
stream.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
match poll_fn(|cx| stream.poll_capacity(cx)).await {
// No capacity left. drop body and return.
None => return Ok(()),
Some(res) => {
// Split chuck to writeable size and send to client.
let cap = res.map_err(DispatchError::SendData)?;
let len = chunk.len();
let bytes = chunk.split_to(cmp::min(cap, len));
stream
.send_data(bytes, false)
.map_err(DispatchError::SendData)?;
// Current chuck completely sent. break send loop and poll next one.
if chunk.is_empty() {
break 'send;
}
} }
} }
} }
} }
// response body streaming finished. send end of stream and return.
stream
.send_data(Bytes::new(), true)
.map_err(DispatchError::SendData)?;
Ok(())
} }
#[pin_project::pin_project] fn prepare_response(
struct ServiceResponse<F, I, E, B> {
#[pin]
state: ServiceResponseState<F, B>,
config: ServiceConfig, config: ServiceConfig,
buffer: Option<Bytes>, head: &ResponseHead,
_phantom: PhantomData<(I, E)>, size: &mut BodySize,
} ) -> http::Response<()> {
let mut has_date = false;
let mut skip_len = size != &BodySize::Stream;
#[pin_project::pin_project(project = ServiceResponseStateProj)] let mut res = http::Response::new(());
enum ServiceResponseState<F, B> { *res.status_mut() = head.status;
ServiceCall(#[pin] F, Option<SendResponse<Bytes>>), *res.version_mut() = http::Version::HTTP_2;
SendPayload(SendStream<Bytes>, #[pin] ResponseBody<B>),
}
impl<F, I, E, B> ServiceResponse<F, I, E, B> // Content length
where match head.status {
F: Future<Output = Result<I, E>>, http::StatusCode::NO_CONTENT
E: Into<Error>, | http::StatusCode::CONTINUE
I: Into<Response<B>>, | http::StatusCode::PROCESSING => *size = BodySize::None,
B: MessageBody, http::StatusCode::SWITCHING_PROTOCOLS => {
{ skip_len = true;
fn prepare_response( *size = BodySize::Stream;
&self, }
head: &ResponseHead, _ => {}
size: &mut BodySize, }
) -> http::Response<()> {
let mut has_date = false;
let mut skip_len = size != &BodySize::Stream;
let mut res = http::Response::new(()); let _ = match size {
*res.status_mut() = head.status; BodySize::None | BodySize::Stream => None,
*res.version_mut() = http::Version::HTTP_2; BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
// Content length res.headers_mut().insert(
match head.status { CONTENT_LENGTH,
http::StatusCode::NO_CONTENT HeaderValue::from_str(buf.format(*len)).unwrap(),
| http::StatusCode::CONTINUE )
| http::StatusCode::PROCESSING => *size = BodySize::None, }
http::StatusCode::SWITCHING_PROTOCOLS => { };
skip_len = true;
*size = BodySize::Stream; // copy headers
} for (key, value) in head.headers.iter() {
match *key {
// TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {} _ => {}
} }
let _ = match size { res.headers_mut().append(key, value.clone());
BodySize::None | BodySize::Stream => None,
BodySize::Empty => res
.headers_mut()
.insert(CONTENT_LENGTH, HeaderValue::from_static("0")),
BodySize::Sized(len) => {
let mut buf = itoa::Buffer::new();
res.headers_mut().insert(
CONTENT_LENGTH,
HeaderValue::from_str(buf.format(*len)).unwrap(),
)
}
};
// copy headers
for (key, value) in head.headers.iter() {
match *key {
// TODO: consider skipping other headers according to:
// https://tools.ietf.org/html/rfc7540#section-8.1.2.2
// omit HTTP/1.x only headers
CONNECTION | TRANSFER_ENCODING => continue,
CONTENT_LENGTH if skip_len => continue,
DATE => has_date = true,
_ => {}
}
res.headers_mut().append(key, value.clone());
}
// set date header
if !has_date {
let mut bytes = BytesMut::with_capacity(29);
self.config.set_date_header(&mut bytes);
res.headers_mut().insert(
DATE,
// SAFETY: serialized date-times are known ASCII strings
unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
);
}
res
} }
}
impl<F, I, E, B> Future for ServiceResponse<F, I, E, B> // set date header
where if !has_date {
F: Future<Output = Result<I, E>>, let mut bytes = BytesMut::with_capacity(29);
E: Into<Error>, config.set_date_header(&mut bytes);
I: Into<Response<B>>, res.headers_mut().insert(
B: MessageBody, DATE,
{ // SAFETY: serialized date-times are known ASCII strings
type Output = (); unsafe { HeaderValue::from_maybe_shared_unchecked(bytes.freeze()) },
);
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let mut this = self.as_mut().project();
match this.state.project() {
ServiceResponseStateProj::ServiceCall(call, send) => {
match ready!(call.poll(cx)) {
Ok(res) => {
let (res, body) = res.into().replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state
.set(ServiceResponseState::SendPayload(stream, body));
self.poll(cx)
}
}
Err(e) => {
let res: Response = e.into().into();
let (res, body) = res.replace_body(());
let mut send = send.take().unwrap();
let mut size = body.size();
let h2_res =
self.as_mut().prepare_response(res.head(), &mut size);
this = self.as_mut().project();
let stream = match send.send_response(h2_res, size.is_eof()) {
Err(e) => {
trace!("Error sending HTTP/2 response: {:?}", e);
return Poll::Ready(());
}
Ok(stream) => stream,
};
if size.is_eof() {
Poll::Ready(())
} else {
this.state.set(ServiceResponseState::SendPayload(
stream,
body.into_body(),
));
self.poll(cx)
}
}
}
}
ServiceResponseStateProj::SendPayload(ref mut stream, ref mut body) => {
loop {
match this.buffer {
Some(ref mut buffer) => match ready!(stream.poll_capacity(cx)) {
None => return Poll::Ready(()),
Some(Ok(cap)) => {
let len = buffer.len();
let bytes = buffer.split_to(cmp::min(cap, len));
if let Err(e) = stream.send_data(bytes, false) {
warn!("{:?}", e);
return Poll::Ready(());
} else if !buffer.is_empty() {
let cap = cmp::min(buffer.len(), CHUNK_SIZE);
stream.reserve_capacity(cap);
} else {
this.buffer.take();
}
}
Some(Err(e)) => {
warn!("{:?}", e);
return Poll::Ready(());
}
},
None => match ready!(body.as_mut().poll_next(cx)) {
None => {
if let Err(e) = stream.send_data(Bytes::new(), true) {
warn!("{:?}", e);
}
return Poll::Ready(());
}
Some(Ok(chunk)) => {
stream
.reserve_capacity(cmp::min(chunk.len(), CHUNK_SIZE));
*this.buffer = Some(chunk);
}
Some(Err(e)) => {
error!("Response payload stream error: {:?}", e);
return Poll::Ready(());
}
},
}
}
}
}
} }
res
} }

View File

@ -1,28 +1,32 @@
use std::future::Future; use std::{
use std::marker::PhantomData; error::Error as StdError,
use std::pin::Pin; future::Future,
use std::task::{Context, Poll}; marker::PhantomData,
use std::{net, rc::Rc}; net,
pin::Pin,
rc::Rc,
task::{Context, Poll},
};
use actix_codec::{AsyncRead, AsyncWrite}; use actix_codec::{AsyncRead, AsyncWrite};
use actix_rt::net::TcpStream; use actix_rt::net::TcpStream;
use actix_service::{ use actix_service::{
fn_factory, fn_service, pipeline_factory, IntoServiceFactory, Service, fn_factory, fn_service, IntoServiceFactory, Service, ServiceFactory,
ServiceFactory, ServiceFactoryExt as _,
}; };
use actix_utils::future::ready;
use bytes::Bytes; use bytes::Bytes;
use futures_core::ready; use futures_core::{future::LocalBoxFuture, ready};
use futures_util::future::ok; use h2::server::{handshake as h2_handshake, Handshake as H2Handshake};
use h2::server::{self, Handshake};
use log::error; use log::error;
use crate::body::MessageBody; use crate::{
use crate::config::ServiceConfig; body::{AnyBody, MessageBody},
use crate::error::{DispatchError, Error}; config::ServiceConfig,
use crate::request::Request; error::DispatchError,
use crate::response::Response; service::HttpFlow,
use crate::service::HttpFlow; ConnectCallback, OnConnectData, Request, Response,
use crate::{ConnectCallback, OnConnectData}; };
use super::dispatcher::Dispatcher; use super::dispatcher::Dispatcher;
@ -37,10 +41,12 @@ 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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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>>(
@ -65,10 +71,13 @@ where
impl<S, B> H2Service<TcpStream, S, B> impl<S, B> H2Service<TcpStream, S, B>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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(
@ -80,12 +89,12 @@ where
Error = DispatchError, Error = DispatchError,
InitError = S::InitError, InitError = S::InitError,
> { > {
pipeline_factory(fn_factory(|| async { fn_factory(|| {
Ok::<_, S::InitError>(fn_service(|io: TcpStream| { ready(Ok::<_, S::InitError>(fn_service(|io: TcpStream| {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
ok::<_, DispatchError>((io, peer_addr)) ready(Ok::<_, DispatchError>((io, peer_addr)))
})) })))
})) })
.and_then(self) .and_then(self)
} }
} }
@ -101,10 +110,13 @@ mod openssl {
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::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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(
@ -117,18 +129,18 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(fn_factory(|| {
) ready(Ok::<_, S::InitError>(fn_service(
.and_then(fn_factory(|| { |io: TlsStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|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)))
ok((io, peer_addr)) },
)))
})) }))
})) .and_then(self.map_err(TlsError::Service))
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -144,10 +156,13 @@ mod rustls {
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::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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(
@ -160,85 +175,54 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = S::InitError, InitError = S::InitError,
> { > {
let protos = vec!["h2".to_string().into()]; let mut protos = vec![b"h2".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(fn_factory(|| {
) ready(Ok::<_, S::InitError>(fn_service(
.and_then(fn_factory(|| { |io: TlsStream<TcpStream>| {
ok::<_, S::InitError>(fn_service(|io: TlsStream<TcpStream>| { let peer_addr = io.get_ref().0.peer_addr().ok();
let peer_addr = io.get_ref().0.peer_addr().ok(); ready(Ok((io, peer_addr)))
ok((io, peer_addr)) },
)))
})) }))
})) .and_then(self.map_err(TlsError::Service))
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B> impl<T, S, B> ServiceFactory<(T, Option<net::SocketAddr>)> for H2Service<T, S, B>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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;
type Config = (); type Config = ();
type Service = H2ServiceHandler<T, S::Service, B>; type Service = H2ServiceHandler<T, S::Service, B>;
type InitError = S::InitError; type InitError = S::InitError;
type Future = H2ServiceResponse<T, S, B>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
H2ServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let cfg = self.cfg.clone();
cfg: Some(self.cfg.clone()), let on_connect_ext = self.on_connect_ext.clone();
on_connect_ext: self.on_connect_ext.clone(),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project::pin_project] let service = service.await?;
pub struct H2ServiceResponse<T, S, B> Ok(H2ServiceHandler::new(cfg, on_connect_ext, service))
where
S: ServiceFactory<Request>,
{
#[pin]
fut: S::Future,
cfg: Option<ServiceConfig>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>,
}
impl<T, S, B> Future for H2ServiceResponse<T, S, B>
where
T: AsyncRead + AsyncWrite + Unpin,
S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
{
type Output = Result<H2ServiceHandler<T, S::Service, B>, S::InitError>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
let this = self.as_mut().project();
this.fut.poll(cx).map_ok(|service| {
let this = self.as_mut().project();
H2ServiceHandler::new(
this.cfg.take().unwrap(),
this.on_connect_ext.clone(),
service,
)
}) })
} }
} }
@ -257,7 +241,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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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,
@ -280,10 +264,11 @@ 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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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;
@ -307,7 +292,7 @@ where
Some(self.cfg.clone()), Some(self.cfg.clone()),
addr, addr,
on_connect_data, on_connect_data,
server::handshake(io), h2_handshake(io),
), ),
} }
} }
@ -324,7 +309,7 @@ where
Option<ServiceConfig>, Option<ServiceConfig>,
Option<net::SocketAddr>, Option<net::SocketAddr>,
OnConnectData, OnConnectData,
Handshake<T, Bytes>, H2Handshake<T, Bytes>,
), ),
} }
@ -332,7 +317,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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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,
@ -344,10 +329,11 @@ 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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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>;

View File

@ -8,40 +8,42 @@ use http::header::{HeaderName, InvalidHeaderName};
pub trait AsHeaderName: Sealed {} pub trait AsHeaderName: Sealed {}
pub struct Seal;
pub trait Sealed { pub trait Sealed {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>; fn try_as_name(&self, seal: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName>;
} }
impl Sealed for HeaderName { impl Sealed for HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(self)) Ok(Cow::Borrowed(self))
} }
} }
impl AsHeaderName for HeaderName {} impl AsHeaderName for HeaderName {}
impl Sealed for &HeaderName { impl Sealed for &HeaderName {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
Ok(Cow::Borrowed(*self)) Ok(Cow::Borrowed(*self))
} }
} }
impl AsHeaderName for &HeaderName {} impl AsHeaderName for &HeaderName {}
impl Sealed for &str { impl Sealed for &str {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
} }
impl AsHeaderName for &str {} impl AsHeaderName for &str {}
impl Sealed for String { impl Sealed for String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
} }
impl AsHeaderName for String {} impl AsHeaderName for String {}
impl Sealed for &String { impl Sealed for &String {
fn try_as_name(&self) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> { fn try_as_name(&self, _: Seal) -> Result<Cow<'_, HeaderName>, InvalidHeaderName> {
HeaderName::from_str(self).map(Cow::Owned) HeaderName::from_str(self).map(Cow::Owned)
} }
} }

View File

@ -213,7 +213,7 @@ impl HeaderMap {
} }
fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> { fn get_value(&self, key: impl AsHeaderName) -> Option<&Value> {
match key.try_as_name().ok()? { match key.try_as_name(super::as_name::Seal).ok()? {
Cow::Borrowed(name) => self.inner.get(name), Cow::Borrowed(name) => self.inner.get(name),
Cow::Owned(name) => self.inner.get(&name), Cow::Owned(name) => self.inner.get(&name),
} }
@ -279,7 +279,7 @@ impl HeaderMap {
/// assert!(map.get("INVALID HEADER NAME").is_none()); /// assert!(map.get("INVALID HEADER NAME").is_none());
/// ``` /// ```
pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> { pub fn get_mut(&mut self, key: impl AsHeaderName) -> Option<&mut HeaderValue> {
match key.try_as_name().ok()? { match key.try_as_name(super::as_name::Seal).ok()? {
Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()), Cow::Borrowed(name) => self.inner.get_mut(name).map(|v| v.first_mut()),
Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()), Cow::Owned(name) => self.inner.get_mut(&name).map(|v| v.first_mut()),
} }
@ -327,7 +327,7 @@ impl HeaderMap {
/// assert!(map.contains_key(header::ACCEPT)); /// assert!(map.contains_key(header::ACCEPT));
/// ``` /// ```
pub fn contains_key(&self, key: impl AsHeaderName) -> bool { pub fn contains_key(&self, key: impl AsHeaderName) -> bool {
match key.try_as_name() { match key.try_as_name(super::as_name::Seal) {
Ok(Cow::Borrowed(name)) => self.inner.contains_key(name), Ok(Cow::Borrowed(name)) => self.inner.contains_key(name),
Ok(Cow::Owned(name)) => self.inner.contains_key(&name), Ok(Cow::Owned(name)) => self.inner.contains_key(&name),
Err(_) => false, Err(_) => false,
@ -410,7 +410,7 @@ impl HeaderMap {
/// ///
/// assert!(map.is_empty()); /// assert!(map.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() { 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),
Ok(Cow::Owned(name)) => self.inner.remove(&name), Ok(Cow::Owned(name)) => self.inner.remove(&name),
Err(_) => None, Err(_) => None,

View File

@ -1,12 +1,33 @@
//! Typed HTTP headers, pre-defined `HeaderName`s, traits for parsing and conversion, and other //! Pre-defined `HeaderName`s, traits for parsing and conversion, and other header utility methods.
//! header utility methods.
use std::fmt;
use bytes::{Bytes, BytesMut};
use percent_encoding::{AsciiSet, CONTROLS}; use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; // re-export from http except header map related items
pub use http::header::{
HeaderName, HeaderValue, InvalidHeaderName, InvalidHeaderValue, ToStrError,
};
// re-export const header names
pub use http::header::{
ACCEPT, ACCEPT_CHARSET, ACCEPT_ENCODING, ACCEPT_LANGUAGE, ACCEPT_RANGES,
ACCESS_CONTROL_ALLOW_CREDENTIALS, ACCESS_CONTROL_ALLOW_HEADERS,
ACCESS_CONTROL_ALLOW_METHODS, ACCESS_CONTROL_ALLOW_ORIGIN,
ACCESS_CONTROL_EXPOSE_HEADERS, ACCESS_CONTROL_MAX_AGE,
ACCESS_CONTROL_REQUEST_HEADERS, ACCESS_CONTROL_REQUEST_METHOD, AGE, ALLOW, ALT_SVC,
AUTHORIZATION, CACHE_CONTROL, CONNECTION, CONTENT_DISPOSITION, CONTENT_ENCODING,
CONTENT_LANGUAGE, CONTENT_LENGTH, CONTENT_LOCATION, CONTENT_RANGE,
CONTENT_SECURITY_POLICY, CONTENT_SECURITY_POLICY_REPORT_ONLY, CONTENT_TYPE, COOKIE,
DATE, DNT, ETAG, EXPECT, EXPIRES, FORWARDED, FROM, HOST, IF_MATCH,
IF_MODIFIED_SINCE, IF_NONE_MATCH, IF_RANGE, IF_UNMODIFIED_SINCE, LAST_MODIFIED,
LINK, LOCATION, MAX_FORWARDS, ORIGIN, PRAGMA, PROXY_AUTHENTICATE,
PROXY_AUTHORIZATION, PUBLIC_KEY_PINS, PUBLIC_KEY_PINS_REPORT_ONLY, RANGE, REFERER,
REFERRER_POLICY, REFRESH, RETRY_AFTER, SEC_WEBSOCKET_ACCEPT,
SEC_WEBSOCKET_EXTENSIONS, SEC_WEBSOCKET_KEY, SEC_WEBSOCKET_PROTOCOL,
SEC_WEBSOCKET_VERSION, SERVER, SET_COOKIE, STRICT_TRANSPORT_SECURITY, TE, TRAILER,
TRANSFER_ENCODING, UPGRADE, UPGRADE_INSECURE_REQUESTS, USER_AGENT, VARY, VIA,
WARNING, WWW_AUTHENTICATE, X_CONTENT_TYPE_OPTIONS, X_DNS_PREFETCH_CONTROL,
X_FRAME_OPTIONS, X_XSS_PROTECTION,
};
use crate::error::ParseError; use crate::error::ParseError;
use crate::HttpMessage; use crate::HttpMessage;
@ -16,11 +37,9 @@ mod into_pair;
mod into_value; mod into_value;
mod utils; mod utils;
mod common;
pub(crate) mod map; pub(crate) mod map;
mod shared; mod shared;
pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
@ -41,34 +60,6 @@ pub trait Header: IntoHeaderValue {
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>; fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError>;
} }
#[derive(Debug, Default)]
pub(crate) struct Writer {
buf: BytesMut,
}
impl Writer {
fn new() -> Writer {
Writer::default()
}
fn take(&mut self) -> Bytes {
self.buf.split().freeze()
}
}
impl fmt::Write for Writer {
#[inline]
fn write_str(&mut self, s: &str) -> fmt::Result {
self.buf.extend_from_slice(s.as_bytes());
Ok(())
}
#[inline]
fn write_fmt(&mut self, args: fmt::Arguments<'_>) -> fmt::Result {
fmt::write(self, args)
}
}
/// Convert `http::HeaderMap` to our `HeaderMap`. /// Convert `http::HeaderMap` to our `HeaderMap`.
impl From<http::HeaderMap> for HeaderMap { impl From<http::HeaderMap> for HeaderMap {
fn from(mut map: http::HeaderMap) -> HeaderMap { fn from(mut map: http::HeaderMap) -> HeaderMap {

View File

@ -104,7 +104,7 @@ impl Display for Charset {
impl FromStr for Charset { impl FromStr for Charset {
type Err = crate::Error; type Err = crate::Error;
fn from_str(s: &str) -> crate::Result<Charset> { fn from_str(s: &str) -> Result<Charset, crate::Error> {
Ok(match s.to_ascii_uppercase().as_ref() { Ok(match s.to_ascii_uppercase().as_ref() {
"US-ASCII" => Us_Ascii, "US-ASCII" => Us_Ascii,
"ISO-8859-1" => Iso_8859_1, "ISO-8859-1" => Iso_8859_1,

View File

@ -23,6 +23,9 @@ pub enum ContentEncoding {
/// Gzip algorithm. /// Gzip algorithm.
Gzip, Gzip,
// Zstd algorithm.
Zstd,
/// Indicates the identity function (i.e. no compression, nor modification). /// Indicates the identity function (i.e. no compression, nor modification).
Identity, Identity,
} }
@ -41,6 +44,7 @@ impl ContentEncoding {
ContentEncoding::Br => "br", ContentEncoding::Br => "br",
ContentEncoding::Gzip => "gzip", ContentEncoding::Gzip => "gzip",
ContentEncoding::Deflate => "deflate", ContentEncoding::Deflate => "deflate",
ContentEncoding::Zstd => "zstd",
ContentEncoding::Identity | ContentEncoding::Auto => "identity", ContentEncoding::Identity | ContentEncoding::Auto => "identity",
} }
} }
@ -53,6 +57,7 @@ impl ContentEncoding {
ContentEncoding::Gzip => 1.0, ContentEncoding::Gzip => 1.0,
ContentEncoding::Deflate => 0.9, ContentEncoding::Deflate => 0.9,
ContentEncoding::Identity | ContentEncoding::Auto => 0.1, ContentEncoding::Identity | ContentEncoding::Auto => 0.1,
ContentEncoding::Zstd => 0.0,
} }
} }
} }
@ -81,6 +86,8 @@ impl From<&str> for ContentEncoding {
ContentEncoding::Gzip ContentEncoding::Gzip
} else if val.eq_ignore_ascii_case("deflate") { } else if val.eq_ignore_ascii_case("deflate") {
ContentEncoding::Deflate ContentEncoding::Deflate
} else if val.eq_ignore_ascii_case("zstd") {
ContentEncoding::Zstd
} else { } else {
ContentEncoding::default() ContentEncoding::default()
} }

View File

@ -88,9 +88,9 @@ pub fn parse_extended_value(
}; };
Ok(ExtendedValue { Ok(ExtendedValue {
value,
charset, charset,
language_tag, language_tag,
value,
}) })
} }

View File

@ -1,18 +1,20 @@
use std::fmt::{self, Display}; use std::{
use std::io::Write; fmt,
use std::str::FromStr; io::Write,
use std::time::{SystemTime, UNIX_EPOCH}; str::FromStr,
time::{SystemTime, UNIX_EPOCH},
};
use bytes::buf::BufMut; use bytes::buf::BufMut;
use bytes::BytesMut; use bytes::BytesMut;
use http::header::{HeaderValue, InvalidHeaderValue}; use http::header::{HeaderValue, InvalidHeaderValue};
use time::{offset, OffsetDateTime, PrimitiveDateTime}; use time::{OffsetDateTime, PrimitiveDateTime, UtcOffset};
use crate::error::ParseError; use crate::error::ParseError;
use crate::header::IntoHeaderValue; use crate::header::IntoHeaderValue;
use crate::time_parser; use crate::time_parser;
/// A timestamp with HTTP formatting and parsing /// A timestamp with HTTP formatting and parsing.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)] #[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord)]
pub struct HttpDate(OffsetDateTime); pub struct HttpDate(OffsetDateTime);
@ -27,18 +29,12 @@ impl FromStr for HttpDate {
} }
} }
impl Display for HttpDate { impl fmt::Display for HttpDate {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f) fmt::Display::fmt(&self.0.format("%a, %d %b %Y %H:%M:%S GMT"), f)
} }
} }
impl From<OffsetDateTime> for HttpDate {
fn from(dt: OffsetDateTime) -> HttpDate {
HttpDate(dt)
}
}
impl From<SystemTime> for HttpDate { impl From<SystemTime> for HttpDate {
fn from(sys: SystemTime) -> HttpDate { fn from(sys: SystemTime) -> HttpDate {
HttpDate(PrimitiveDateTime::from(sys).assume_utc()) HttpDate(PrimitiveDateTime::from(sys).assume_utc())
@ -54,7 +50,7 @@ impl IntoHeaderValue for HttpDate {
wrt, wrt,
"{}", "{}",
self.0 self.0
.to_offset(offset!(UTC)) .to_offset(UtcOffset::UTC)
.format("%a, %d %b %Y %H:%M:%S GMT") .format("%a, %d %b %Y %H:%M:%S GMT")
) )
.unwrap(); .unwrap();

View File

@ -1,15 +1,13 @@
//! Originally taken from `hyper::header::shared`. //! Originally taken from `hyper::header::shared`.
mod charset; mod charset;
mod encoding; mod content_encoding;
mod entity;
mod extended; mod extended;
mod httpdate; mod httpdate;
mod quality_item; mod quality_item;
pub use self::charset::Charset; pub use self::charset::Charset;
pub use self::encoding::Encoding; pub use self::content_encoding::ContentEncoding;
pub use self::entity::EntityTag;
pub use self::extended::{parse_extended_value, ExtendedValue}; pub use self::extended::{parse_extended_value, ExtendedValue};
pub use self::httpdate::HttpDate; pub use self::httpdate::HttpDate;
pub use self::quality_item::{q, qitem, Quality, QualityItem}; pub use self::quality_item::{q, qitem, Quality, QualityItem};

View File

@ -193,21 +193,69 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::super::encoding::*;
use super::*; use super::*;
// copy of encoding from actix-web headers
#[derive(Clone, PartialEq, Debug)]
pub enum Encoding {
Chunked,
Brotli,
Gzip,
Deflate,
Compress,
Identity,
Trailers,
EncodingExt(String),
}
impl fmt::Display for Encoding {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use Encoding::*;
f.write_str(match *self {
Chunked => "chunked",
Brotli => "br",
Gzip => "gzip",
Deflate => "deflate",
Compress => "compress",
Identity => "identity",
Trailers => "trailers",
EncodingExt(ref s) => s.as_ref(),
})
}
}
impl str::FromStr for Encoding {
type Err = crate::error::ParseError;
fn from_str(s: &str) -> Result<Encoding, crate::error::ParseError> {
use Encoding::*;
match s {
"chunked" => Ok(Chunked),
"br" => Ok(Brotli),
"deflate" => Ok(Deflate),
"gzip" => Ok(Gzip),
"compress" => Ok(Compress),
"identity" => Ok(Identity),
"trailers" => Ok(Trailers),
_ => Ok(EncodingExt(s.to_owned())),
}
}
}
#[test] #[test]
fn test_quality_item_fmt_q_1() { fn test_quality_item_fmt_q_1() {
use Encoding::*;
let x = qitem(Chunked); let x = qitem(Chunked);
assert_eq!(format!("{}", x), "chunked"); assert_eq!(format!("{}", x), "chunked");
} }
#[test] #[test]
fn test_quality_item_fmt_q_0001() { fn test_quality_item_fmt_q_0001() {
use Encoding::*;
let x = QualityItem::new(Chunked, Quality(1)); let x = QualityItem::new(Chunked, Quality(1));
assert_eq!(format!("{}", x), "chunked; q=0.001"); assert_eq!(format!("{}", x), "chunked; q=0.001");
} }
#[test] #[test]
fn test_quality_item_fmt_q_05() { fn test_quality_item_fmt_q_05() {
use Encoding::*;
// Custom value // Custom value
let x = QualityItem { let x = QualityItem {
item: EncodingExt("identity".to_owned()), item: EncodingExt("identity".to_owned()),
@ -218,6 +266,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_fmt_q_0() { fn test_quality_item_fmt_q_0() {
use Encoding::*;
// Custom value // Custom value
let x = QualityItem { let x = QualityItem {
item: EncodingExt("identity".to_owned()), item: EncodingExt("identity".to_owned()),
@ -228,6 +277,7 @@ mod tests {
#[test] #[test]
fn test_quality_item_from_str1() { fn test_quality_item_from_str1() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "chunked".parse(); let x: Result<QualityItem<Encoding>, _> = "chunked".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -237,8 +287,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str2() { fn test_quality_item_from_str2() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse(); let x: Result<QualityItem<Encoding>, _> = "chunked; q=1".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -248,8 +300,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str3() { fn test_quality_item_from_str3() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.5".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -259,8 +313,10 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str4() { fn test_quality_item_from_str4() {
use Encoding::*;
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.273".parse();
assert_eq!( assert_eq!(
x.unwrap(), x.unwrap(),
@ -270,16 +326,19 @@ mod tests {
} }
); );
} }
#[test] #[test]
fn test_quality_item_from_str5() { fn test_quality_item_from_str5() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=0.2739999".parse();
assert!(x.is_err()); assert!(x.is_err());
} }
#[test] #[test]
fn test_quality_item_from_str6() { fn test_quality_item_from_str6() {
let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse(); let x: Result<QualityItem<Encoding>, _> = "gzip; q=2".parse();
assert!(x.is_err()); assert!(x.is_err());
} }
#[test] #[test]
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();

View File

@ -1,7 +1,6 @@
use std::{fmt, str::FromStr}; use std::{fmt, str::FromStr};
use http::HeaderValue; use super::HeaderValue;
use crate::{error::ParseError, header::HTTP_VALUE}; use crate::{error::ParseError, header::HTTP_VALUE};
/// Reads a comma-delimited raw header into a Vec. /// Reads a comma-delimited raw header into a Vec.

View File

@ -1,15 +1,15 @@
use std::io; use std::io;
use bytes::{BufMut, BytesMut}; use bytes::BufMut;
use http::Version; use http::Version;
const DIGITS_START: u8 = b'0'; const DIGITS_START: u8 = b'0';
pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut) { pub(crate) fn write_status_line<B: BufMut>(version: Version, n: u16, buf: &mut B) {
match version { match version {
Version::HTTP_11 => bytes.put_slice(b"HTTP/1.1 "), Version::HTTP_11 => buf.put_slice(b"HTTP/1.1 "),
Version::HTTP_10 => bytes.put_slice(b"HTTP/1.0 "), Version::HTTP_10 => buf.put_slice(b"HTTP/1.0 "),
Version::HTTP_09 => bytes.put_slice(b"HTTP/0.9 "), Version::HTTP_09 => buf.put_slice(b"HTTP/0.9 "),
_ => { _ => {
// other HTTP version handlers do not use this method // other HTTP version handlers do not use this method
} }
@ -19,33 +19,44 @@ pub(crate) fn write_status_line(version: Version, n: u16, bytes: &mut BytesMut)
let d10 = ((n / 10) % 10) as u8; let d10 = ((n / 10) % 10) as u8;
let d1 = (n % 10) as u8; let d1 = (n % 10) as u8;
bytes.put_u8(DIGITS_START + d100); buf.put_u8(DIGITS_START + d100);
bytes.put_u8(DIGITS_START + d10); buf.put_u8(DIGITS_START + d10);
bytes.put_u8(DIGITS_START + d1); buf.put_u8(DIGITS_START + d1);
// trailing space before reason // trailing space before reason
bytes.put_u8(b' '); buf.put_u8(b' ');
} }
/// NOTE: bytes object has to contain enough space /// Write out content length header.
pub fn write_content_length(n: u64, bytes: &mut BytesMut) { ///
/// Buffer must to contain enough space or be implicitly extendable.
pub fn write_content_length<B: BufMut>(n: u64, buf: &mut B) {
if n == 0 { if n == 0 {
bytes.put_slice(b"\r\ncontent-length: 0\r\n"); buf.put_slice(b"\r\ncontent-length: 0\r\n");
return; return;
} }
let mut buf = itoa::Buffer::new(); let mut buffer = itoa::Buffer::new();
bytes.put_slice(b"\r\ncontent-length: "); buf.put_slice(b"\r\ncontent-length: ");
bytes.put_slice(buf.format(n).as_bytes()); buf.put_slice(buffer.format(n).as_bytes());
bytes.put_slice(b"\r\n"); buf.put_slice(b"\r\n");
} }
pub(crate) struct Writer<'a>(pub &'a mut BytesMut); /// An `io::Write`r that only requires mutable reference and assumes that there is space available
/// in the buffer for every write operation or that it can be extended implicitly (like
/// `bytes::BytesMut`, for example).
///
/// This is slightly faster (~10%) than `bytes::buf::Writer` in such cases because it does not
/// perform a remaining length check before writing.
pub(crate) struct MutWriter<'a, B>(pub(crate) &'a mut B);
impl<'a> io::Write for Writer<'a> { impl<'a, B> io::Write for MutWriter<'a, B>
where
B: BufMut,
{
fn write(&mut self, buf: &[u8]) -> io::Result<usize> { fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
self.0.extend_from_slice(buf); self.0.put_slice(buf);
Ok(buf.len()) Ok(buf.len())
} }
@ -58,6 +69,8 @@ impl<'a> io::Write for Writer<'a> {
mod tests { mod tests {
use std::str::from_utf8; use std::str::from_utf8;
use bytes::BytesMut;
use super::*; use super::*;
#[test] #[test]

View File

@ -1,19 +1,18 @@
use std::cell::{Ref, RefMut}; use std::{
use std::str; cell::{Ref, RefMut},
str,
};
use encoding_rs::{Encoding, UTF_8}; use encoding_rs::{Encoding, UTF_8};
use http::header; use http::header;
use mime::Mime; use mime::Mime;
use crate::error::{ContentTypeError, ParseError}; use crate::{
use crate::extensions::Extensions; error::{ContentTypeError, ParseError},
use crate::header::{Header, HeaderMap}; header::{Header, HeaderMap},
use crate::payload::Payload; payload::Payload,
#[cfg(feature = "cookies")] Extensions,
use crate::{cookie::Cookie, error::CookieParseError}; };
#[cfg(feature = "cookies")]
struct Cookies(Vec<Cookie<'static>>);
/// Trait that implements general purpose operations on HTTP messages. /// Trait that implements general purpose operations on HTTP messages.
pub trait HttpMessage: Sized { pub trait HttpMessage: Sized {
@ -104,41 +103,6 @@ pub trait HttpMessage: Sized {
Ok(false) Ok(false)
} }
} }
/// Load request cookies.
#[cfg(feature = "cookies")]
fn cookies(&self) -> Result<Ref<'_, Vec<Cookie<'static>>>, CookieParseError> {
if self.extensions().get::<Cookies>().is_none() {
let mut cookies = Vec::new();
for hdr in self.headers().get_all(header::COOKIE) {
let s =
str::from_utf8(hdr.as_bytes()).map_err(CookieParseError::from)?;
for cookie_str in s.split(';').map(|s| s.trim()) {
if !cookie_str.is_empty() {
cookies.push(Cookie::parse_encoded(cookie_str)?.into_owned());
}
}
}
self.extensions_mut().insert(Cookies(cookies));
}
Ok(Ref::map(self.extensions(), |ext| {
&ext.get::<Cookies>().unwrap().0
}))
}
/// Return request cookie.
#[cfg(feature = "cookies")]
fn cookie(&self, name: &str) -> Option<Cookie<'static>> {
if let Ok(cookies) = self.cookies() {
for cookie in cookies.iter() {
if cookie.name() == name {
return Some(cookie.to_owned());
}
}
}
None
}
} }
impl<'a, T> HttpMessage for &'a mut T impl<'a, T> HttpMessage for &'a mut T

View File

@ -6,13 +6,10 @@
//! | `openssl` | TLS support via [OpenSSL]. | //! | `openssl` | TLS support via [OpenSSL]. |
//! | `rustls` | TLS support via [rustls]. | //! | `rustls` | TLS support via [rustls]. |
//! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) | //! | `compress` | Payload compression support. (Deflate, Gzip & Brotli) |
//! | `cookies` | Support for cookies backed by the [cookie] crate. |
//! | `secure-cookies` | Adds for secure cookies. Enables `cookies` feature. |
//! | `trust-dns` | Use [trust-dns] as the client DNS resolver. | //! | `trust-dns` | Use [trust-dns] as the client DNS resolver. |
//! //!
//! [OpenSSL]: https://crates.io/crates/openssl //! [OpenSSL]: https://crates.io/crates/openssl
//! [rustls]: https://crates.io/crates/rustls //! [rustls]: https://crates.io/crates/rustls
//! [cookie]: https://crates.io/crates/cookie
//! [trust-dns]: https://crates.io/crates/trust-dns //! [trust-dns]: https://crates.io/crates/trust-dns
#![deny(rust_2018_idioms, nonstandard_style)] #![deny(rust_2018_idioms, nonstandard_style)]
@ -38,14 +35,14 @@ mod config;
#[cfg(feature = "compress")] #[cfg(feature = "compress")]
pub mod encoding; pub mod encoding;
mod extensions; mod extensions;
mod header; pub mod header;
mod helpers; mod helpers;
mod http_codes;
mod http_message; mod http_message;
mod message; mod message;
mod payload; mod payload;
mod request; mod request;
mod response; mod response;
mod response_builder;
mod service; mod service;
mod time_parser; mod time_parser;
@ -55,20 +52,24 @@ pub mod h2;
pub mod test; pub mod test;
pub mod ws; pub mod ws;
#[cfg(feature = "cookies")]
pub use cookie;
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, ResponseError, Result}; pub use self::error::Error;
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
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::{Message, RequestHead, RequestHeadType, ResponseHead}; pub use self::message::{Message, RequestHead, RequestHeadType, ResponseHead};
pub use self::payload::{Payload, PayloadStream}; pub use self::payload::{Payload, PayloadStream};
pub use self::request::Request; pub use self::request::Request;
pub use self::response::{Response, ResponseBuilder}; pub use self::response::Response;
pub use self::response_builder::ResponseBuilder;
pub use self::service::HttpService; pub use self::service::HttpService;
pub use ::http::{uri, uri::Uri};
pub use ::http::{Method, StatusCode, Version};
// TODO: deprecate this mish-mash of random items
pub mod http { pub mod http {
//! Various HTTP related types. //! Various HTTP related types.
@ -78,8 +79,6 @@ pub mod http {
pub use http::{uri, Error, Uri}; pub use http::{uri, Error, Uri};
pub use http::{Method, StatusCode, Version}; pub use http::{Method, StatusCode, Version};
#[cfg(feature = "cookies")]
pub use crate::cookie::{Cookie, CookieBuilder};
pub use crate::header::HeaderMap; pub use crate::header::HeaderMap;
/// A collection of HTTP headers and helpers. /// A collection of HTTP headers and helpers.

View File

@ -1,4 +1,5 @@
#[macro_export] #[macro_export]
#[doc(hidden)]
macro_rules! downcast_get_type_id { macro_rules! downcast_get_type_id {
() => { () => {
/// A helper method to get the type ID of the type /// A helper method to get the type ID of the type
@ -14,8 +15,15 @@ macro_rules! downcast_get_type_id {
/// making it impossible for safe code to construct outside of /// making it impossible for safe code to construct outside of
/// this module. This ensures that safe code cannot violate /// this module. This ensures that safe code cannot violate
/// type-safety by implementing this method. /// type-safety by implementing this method.
///
/// We also take `PrivateHelper` as a parameter, to ensure that
/// safe code cannot obtain a `PrivateHelper` instance by
/// delegating to an existing implementation of `__private_get_type_id__`
#[doc(hidden)] #[doc(hidden)]
fn __private_get_type_id__(&self) -> (std::any::TypeId, PrivateHelper) fn __private_get_type_id__(
&self,
_: PrivateHelper,
) -> (std::any::TypeId, PrivateHelper)
where where
Self: 'static, Self: 'static,
{ {
@ -25,6 +33,7 @@ macro_rules! downcast_get_type_id {
} }
//Generate implementation for dyn $name //Generate implementation for dyn $name
#[doc(hidden)]
#[macro_export] #[macro_export]
macro_rules! downcast { macro_rules! downcast {
($name:ident) => { ($name:ident) => {
@ -37,7 +46,9 @@ macro_rules! downcast {
impl dyn $name + 'static { impl dyn $name + 'static {
/// Downcasts generic body to a specific type. /// Downcasts generic body to a specific type.
pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> { pub fn downcast_ref<T: $name + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() { if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore // it requires returning a private type. We can therefore
@ -51,7 +62,9 @@ macro_rules! downcast {
/// Downcasts a generic body to a mutable specific type. /// Downcasts a generic body to a mutable specific type.
pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> { pub fn downcast_mut<T: $name + 'static>(&mut self) -> Option<&mut T> {
if self.__private_get_type_id__().0 == std::any::TypeId::of::<T>() { if self.__private_get_type_id__(PrivateHelper(())).0
== std::any::TypeId::of::<T>()
{
// SAFETY: external crates cannot override the default // SAFETY: external crates cannot override the default
// implementation of `__private_get_type_id__`, since // implementation of `__private_get_type_id__`, since
// it requires returning a private type. We can therefore // it requires returning a private type. We can therefore
@ -70,6 +83,7 @@ macro_rules! downcast {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
#![allow(clippy::upper_case_acronyms)]
trait MB { trait MB {
downcast_get_type_id!(); downcast_get_type_id!();

View File

@ -1,12 +1,15 @@
use std::cell::{Ref, RefCell, RefMut}; use std::{
use std::net; cell::{Ref, RefCell, RefMut},
use std::rc::Rc; net,
rc::Rc,
};
use bitflags::bitflags; use bitflags::bitflags;
use crate::extensions::Extensions; use crate::{
use crate::header::HeaderMap; header::{self, HeaderMap},
use crate::http::{header, Method, StatusCode, Uri, Version}; Extensions, Method, StatusCode, Uri, Version,
};
/// Represents various types of connection /// Represents various types of connection
#[derive(Copy, Clone, PartialEq, Debug)] #[derive(Copy, Clone, PartialEq, Debug)]
@ -290,14 +293,14 @@ impl ResponseHead {
} }
} }
#[inline]
/// Check if keep-alive is enabled /// Check if keep-alive is enabled
#[inline]
pub fn keep_alive(&self) -> bool { pub fn keep_alive(&self) -> bool {
self.connection_type() == ConnectionType::KeepAlive self.connection_type() == ConnectionType::KeepAlive
} }
#[inline]
/// Check upgrade status of this message /// Check upgrade status of this message
#[inline]
pub fn upgrade(&self) -> bool { pub fn upgrade(&self) -> bool {
self.connection_type() == ConnectionType::Upgrade self.connection_type() == ConnectionType::Upgrade
} }
@ -345,8 +348,8 @@ impl ResponseHead {
} }
pub struct Message<T: Head> { pub struct Message<T: Head> {
// Rc here should not be cloned by anyone. /// Rc here should not be cloned by anyone.
// It's used to reuse allocation of T and no shared ownership is allowed. /// It's used to reuse allocation of T and no shared ownership is allowed.
head: Rc<T>, head: Rc<T>,
} }
@ -386,12 +389,6 @@ impl BoxedResponseHead {
pub fn new(status: StatusCode) -> Self { pub fn new(status: StatusCode) -> Self {
RESPONSE_POOL.with(|p| p.get_message(status)) RESPONSE_POOL.with(|p| p.get_message(status))
} }
pub(crate) fn take(&mut self) -> Self {
BoxedResponseHead {
head: self.head.take(),
}
}
} }
impl std::ops::Deref for BoxedResponseHead { impl std::ops::Deref for BoxedResponseHead {

View File

@ -2,16 +2,18 @@
use std::{ use std::{
cell::{Ref, RefMut}, cell::{Ref, RefMut},
fmt, net, fmt, net, str,
}; };
use http::{header, Method, Uri, Version}; use http::{header, Method, Uri, Version};
use crate::extensions::Extensions; use crate::{
use crate::header::HeaderMap; extensions::Extensions,
use crate::message::{Message, RequestHead}; header::HeaderMap,
use crate::payload::{Payload, PayloadStream}; message::{Message, RequestHead},
use crate::HttpMessage; payload::{Payload, PayloadStream},
HttpMessage,
};
/// Request /// Request
pub struct Request<P = PayloadStream> { pub struct Request<P = PayloadStream> {

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,464 @@
//! HTTP response builder.
use std::{
cell::{Ref, RefMut},
error::Error as StdError,
fmt,
future::Future,
pin::Pin,
str,
task::{Context, Poll},
};
use bytes::Bytes;
use futures_core::Stream;
use crate::{
body::{AnyBody, BodyStream},
error::{Error, HttpError},
header::{self, IntoHeaderPair, IntoHeaderValue},
message::{BoxedResponseHead, ConnectionType, ResponseHead},
Extensions, Response, StatusCode,
};
/// An HTTP response builder.
///
/// Used to construct an instance of `Response` using a builder pattern. Response builders are often
/// created using [`Response::build`].
///
/// # Examples
/// ```
/// use actix_http::{Response, ResponseBuilder, body, http::StatusCode, http::header};
///
/// # actix_rt::System::new().block_on(async {
/// let mut res: Response<_> = Response::build(StatusCode::OK)
/// .content_type(mime::APPLICATION_JSON)
/// .insert_header((header::SERVER, "my-app/1.0"))
/// .append_header((header::SET_COOKIE, "a=1"))
/// .append_header((header::SET_COOKIE, "b=2"))
/// .body("1234");
///
/// assert_eq!(res.status(), StatusCode::OK);
///
/// assert!(res.headers().contains_key("server"));
/// assert_eq!(res.headers().get_all("set-cookie").count(), 2);
///
/// assert_eq!(body::to_bytes(res.into_body()).await.unwrap(), &b"1234"[..]);
/// # })
/// ```
pub struct ResponseBuilder {
head: Option<BoxedResponseHead>,
err: Option<HttpError>,
}
impl ResponseBuilder {
/// Create response builder
///
/// # Examples
/// ```
/// use actix_http::{Response, ResponseBuilder, http::StatusCode};
///
/// let res: Response<_> = ResponseBuilder::default().finish();
/// assert_eq!(res.status(), StatusCode::OK);
/// ```
#[inline]
pub fn new(status: StatusCode) -> Self {
ResponseBuilder {
head: Some(BoxedResponseHead::new(status)),
err: None,
}
}
/// Set HTTP status code of this response.
///
/// # Examples
/// ```
/// use actix_http::{ResponseBuilder, http::StatusCode};
///
/// let res = ResponseBuilder::default().status(StatusCode::NOT_FOUND).finish();
/// assert_eq!(res.status(), StatusCode::NOT_FOUND);
/// ```
#[inline]
pub fn status(&mut self, status: StatusCode) -> &mut Self {
if let Some(parts) = self.inner() {
parts.status = status;
}
self
}
/// Insert a header, replacing any that were set with an equivalent field name.
///
/// # Examples
/// ```
/// use actix_http::{ResponseBuilder, http::header};
///
/// let res = ResponseBuilder::default()
/// .insert_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
/// .insert_header(("X-TEST", "value"))
/// .finish();
///
/// assert!(res.headers().contains_key("content-type"));
/// assert!(res.headers().contains_key("x-test"));
/// ```
pub fn insert_header<H>(&mut self, header: H) -> &mut Self
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() {
match header.try_into_header_pair() {
Ok((key, value)) => {
parts.headers.insert(key, value);
}
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Append a header, keeping any that were set with an equivalent field name.
///
/// # Examples
/// ```
/// use actix_http::{ResponseBuilder, http::header};
///
/// let res = ResponseBuilder::default()
/// .append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON))
/// .append_header(("X-TEST", "value1"))
/// .append_header(("X-TEST", "value2"))
/// .finish();
///
/// assert_eq!(res.headers().get_all("content-type").count(), 1);
/// assert_eq!(res.headers().get_all("x-test").count(), 2);
/// ```
pub fn append_header<H>(&mut self, header: H) -> &mut Self
where
H: IntoHeaderPair,
{
if let Some(parts) = self.inner() {
match header.try_into_header_pair() {
Ok((key, value)) => parts.headers.append(key, value),
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Set the custom reason for the response.
#[inline]
pub fn reason(&mut self, reason: &'static str) -> &mut Self {
if let Some(parts) = self.inner() {
parts.reason = Some(reason);
}
self
}
/// Set connection type to KeepAlive
#[inline]
pub fn keep_alive(&mut self) -> &mut Self {
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::KeepAlive);
}
self
}
/// Set connection type to Upgrade
#[inline]
pub fn upgrade<V>(&mut self, value: V) -> &mut Self
where
V: IntoHeaderValue,
{
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Upgrade);
}
if let Ok(value) = value.try_into_value() {
self.insert_header((header::UPGRADE, value));
}
self
}
/// Force close connection, even if it is marked as keep-alive
#[inline]
pub fn force_close(&mut self) -> &mut Self {
if let Some(parts) = self.inner() {
parts.set_connection_type(ConnectionType::Close);
}
self
}
/// Disable chunked transfer encoding for HTTP/1.1 streaming responses.
#[inline]
pub fn no_chunking(&mut self, len: u64) -> &mut Self {
let mut buf = itoa::Buffer::new();
self.insert_header((header::CONTENT_LENGTH, buf.format(len)));
if let Some(parts) = self.inner() {
parts.no_chunking(true);
}
self
}
/// Set response content type.
#[inline]
pub fn content_type<V>(&mut self, value: V) -> &mut Self
where
V: IntoHeaderValue,
{
if let Some(parts) = self.inner() {
match value.try_into_value() {
Ok(value) => {
parts.headers.insert(header::CONTENT_TYPE, value);
}
Err(e) => self.err = Some(e.into()),
};
}
self
}
/// Responses extensions
#[inline]
pub fn extensions(&self) -> Ref<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow()
}
/// Mutable reference to a the response's extensions
#[inline]
pub fn extensions_mut(&mut self) -> RefMut<'_, Extensions> {
let head = self.head.as_ref().expect("cannot reuse response builder");
head.extensions.borrow_mut()
}
/// Generate response with a wrapped body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn body<B: Into<AnyBody>>(&mut self, body: B) -> Response<AnyBody> {
self.message_body(body.into())
.unwrap_or_else(Response::from)
}
/// Generate response with a body.
///
/// This `ResponseBuilder` will be left in a useless state.
pub fn message_body<B>(&mut self, body: B) -> Result<Response<B>, Error> {
if let Some(err) = self.err.take() {
return Err(Error::new_http().with_cause(err));
}
let head = self.head.take().expect("cannot reuse response builder");
Ok(Response { head, body })
}
/// Generate response with a streaming body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn streaming<S, E>(&mut self, stream: S) -> Response<AnyBody>
where
S: Stream<Item = Result<Bytes, E>> + 'static,
E: Into<Box<dyn StdError>> + 'static,
{
self.body(AnyBody::from_message(BodyStream::new(stream)))
}
/// Generate response with an empty body.
///
/// This `ResponseBuilder` will be left in a useless state.
#[inline]
pub fn finish(&mut self) -> Response<AnyBody> {
self.body(AnyBody::Empty)
}
/// Create an owned `ResponseBuilder`, leaving the original in a useless state.
pub fn take(&mut self) -> ResponseBuilder {
ResponseBuilder {
head: self.head.take(),
err: self.err.take(),
}
}
/// Get access to the inner response head if there has been no error.
fn inner(&mut self) -> Option<&mut ResponseHead> {
if self.err.is_some() {
return None;
}
self.head.as_deref_mut()
}
}
impl Default for ResponseBuilder {
fn default() -> Self {
Self::new(StatusCode::OK)
}
}
/// Convert `Response` to a `ResponseBuilder`. Body get dropped.
impl<B> From<Response<B>> for ResponseBuilder {
fn from(res: Response<B>) -> ResponseBuilder {
ResponseBuilder {
head: Some(res.head),
err: None,
}
}
}
/// Convert `ResponseHead` to a `ResponseBuilder`
impl<'a> From<&'a ResponseHead> for ResponseBuilder {
fn from(head: &'a ResponseHead) -> ResponseBuilder {
let mut msg = BoxedResponseHead::new(head.status);
msg.version = head.version;
msg.reason = head.reason;
for (k, v) in head.headers.iter() {
msg.headers.append(k.clone(), v.clone());
}
msg.no_chunking(!head.chunked());
ResponseBuilder {
head: Some(msg),
err: None,
}
}
}
impl Future for ResponseBuilder {
type Output = Result<Response<AnyBody>, Error>;
fn poll(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Self::Output> {
Poll::Ready(Ok(self.finish()))
}
}
impl fmt::Debug for ResponseBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let head = self.head.as_ref().unwrap();
let res = writeln!(
f,
"\nResponseBuilder {:?} {}{}",
head.version,
head.status,
head.reason.unwrap_or(""),
);
let _ = writeln!(f, " headers:");
for (key, val) in head.headers.iter() {
let _ = writeln!(f, " {:?}: {:?}", key, val);
}
res
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::body::Body;
use crate::http::header::{HeaderName, HeaderValue, CONTENT_TYPE};
#[test]
fn test_basic_builder() {
let resp = Response::build(StatusCode::OK)
.insert_header(("X-TEST", "value"))
.finish();
assert_eq!(resp.status(), StatusCode::OK);
}
#[test]
fn test_upgrade() {
let resp = Response::build(StatusCode::OK)
.upgrade("websocket")
.finish();
assert!(resp.upgrade());
assert_eq!(
resp.headers().get(header::UPGRADE).unwrap(),
HeaderValue::from_static("websocket")
);
}
#[test]
fn test_force_close() {
let resp = Response::build(StatusCode::OK).force_close().finish();
assert!(!resp.keep_alive())
}
#[test]
fn test_content_type() {
let resp = Response::build(StatusCode::OK)
.content_type("text/plain")
.body(Body::Empty);
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "text/plain")
}
#[test]
fn test_into_builder() {
let mut resp: Response<Body> = "test".into();
assert_eq!(resp.status(), StatusCode::OK);
resp.headers_mut().insert(
HeaderName::from_static("cookie"),
HeaderValue::from_static("cookie1=val100"),
);
let mut builder: ResponseBuilder = resp.into();
let resp = builder.status(StatusCode::BAD_REQUEST).finish();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let cookie = resp.headers().get_all("Cookie").next().unwrap();
assert_eq!(cookie.to_str().unwrap(), "cookie1=val100");
}
#[test]
fn response_builder_header_insert_kv() {
let mut res = Response::build(StatusCode::OK);
res.insert_header(("Content-Type", "application/octet-stream"));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_insert_typed() {
let mut res = Response::build(StatusCode::OK);
res.insert_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
let res = res.finish();
assert_eq!(
res.headers().get("Content-Type"),
Some(&HeaderValue::from_static("application/octet-stream"))
);
}
#[test]
fn response_builder_header_append_kv() {
let mut res = Response::build(StatusCode::OK);
res.append_header(("Content-Type", "application/octet-stream"));
res.append_header(("Content-Type", "application/json"));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
#[test]
fn response_builder_header_append_typed() {
let mut res = Response::build(StatusCode::OK);
res.append_header((header::CONTENT_TYPE, mime::APPLICATION_OCTET_STREAM));
res.append_header((header::CONTENT_TYPE, mime::APPLICATION_JSON));
let res = res.finish();
let headers: Vec<_> = res.headers().get_all("Content-Type").cloned().collect();
assert_eq!(headers.len(), 2);
assert!(headers.contains(&HeaderValue::from_static("application/octet-stream")));
assert!(headers.contains(&HeaderValue::from_static("application/json")));
}
}

View File

@ -1,23 +1,31 @@
use std::marker::PhantomData; use std::{
use std::pin::Pin; error::Error as StdError,
use std::task::{Context, Poll}; fmt,
use std::{fmt, net, rc::Rc}; future::Future,
marker::PhantomData,
net,
pin::Pin,
rc::Rc,
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::{pipeline_factory, IntoServiceFactory, Service, ServiceFactory}; use actix_service::{
fn_service, IntoServiceFactory, Service, ServiceFactory, ServiceFactoryExt as _,
};
use bytes::Bytes; use bytes::Bytes;
use futures_core::{ready, Future}; use futures_core::{future::LocalBoxFuture, ready};
use h2::server::{self, Handshake};
use pin_project::pin_project; use pin_project::pin_project;
use crate::body::MessageBody; use crate::{
use crate::builder::HttpServiceBuilder; body::{AnyBody, MessageBody},
use crate::config::{KeepAlive, ServiceConfig}; builder::HttpServiceBuilder,
use crate::error::{DispatchError, Error}; config::{KeepAlive, ServiceConfig},
use crate::request::Request; error::DispatchError,
use crate::response::Response; h1, h2, ConnectCallback, OnConnectData, Protocol, Request, Response,
use crate::{h1, h2::Dispatcher, ConnectCallback, OnConnectData, Protocol}; };
/// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol. /// A `ServiceFactory` for HTTP/1.1 or HTTP/2 protocol.
pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> { pub struct HttpService<T, S, B, X = h1::ExpectHandler, U = h1::UpgradeHandler> {
@ -32,7 +40,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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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,
@ -47,11 +55,12 @@ 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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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 {
@ -86,7 +95,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<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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,
@ -100,9 +109,8 @@ 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<Error>, X1::Error: Into<Response<AnyBody>>,
X1::InitError: fmt::Debug, X1::InitError: fmt::Debug,
<X1::Service as Service<Request>>::Future: 'static,
{ {
HttpService { HttpService {
expect, expect,
@ -123,7 +131,6 @@ where
U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U1: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U1::Error: fmt::Display, U1::Error: fmt::Display,
U1::InitError: fmt::Debug, U1::InitError: fmt::Debug,
<U1::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
{ {
HttpService { HttpService {
upgrade, upgrade,
@ -145,23 +152,28 @@ where
impl<S, B, X, U> HttpService<TcpStream, S, B, X, U> impl<S, B, X, U> HttpService<TcpStream, S, B, X, U>
where where
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TcpStream, h1::Codec>), (Request, Framed<TcpStream, h1::Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TcpStream, h1::Codec>)>>::Future: 'static,
{ {
/// Create simple tcp stream service /// Create simple tcp stream service
pub fn tcp( pub fn tcp(
@ -173,7 +185,7 @@ where
Error = DispatchError, Error = DispatchError,
InitError = (), InitError = (),
> { > {
pipeline_factory(|io: TcpStream| async { fn_service(|io: TcpStream| async {
let peer_addr = io.peer_addr().ok(); let peer_addr = io.peer_addr().ok();
Ok((io, Protocol::Http1, peer_addr)) Ok((io, Protocol::Http1, peer_addr))
}) })
@ -183,31 +195,37 @@ where
#[cfg(feature = "openssl")] #[cfg(feature = "openssl")]
mod openssl { mod openssl {
use super::*;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream}; use actix_tls::accept::openssl::{Acceptor, SslAcceptor, SslError, TlsStream};
use actix_tls::accept::TlsError; use actix_tls::accept::TlsError;
use super::*;
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::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>), (Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TlsStream<TcpStream>, h1::Codec>)>>::Future: 'static,
{ {
/// Create openssl based service /// Create openssl based service
pub fn openssl( pub fn openssl(
@ -220,25 +238,23 @@ mod openssl {
Error = TlsError<SslError, DispatchError>, Error = TlsError<SslError, DispatchError>,
InitError = (), InitError = (),
> { > {
pipeline_factory( Acceptor::new(acceptor)
Acceptor::new(acceptor) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| async {
) let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() {
.and_then(|io: TlsStream<TcpStream>| async { if protos.windows(2).any(|window| window == b"h2") {
let proto = if let Some(protos) = io.ssl().selected_alpn_protocol() { Protocol::Http2
if protos.windows(2).any(|window| window == b"h2") { } else {
Protocol::Http2 Protocol::Http1
}
} else { } else {
Protocol::Http1 Protocol::Http1
} };
} else { let peer_addr = io.get_ref().peer_addr().ok();
Protocol::Http1 Ok((io, proto, peer_addr))
}; })
let peer_addr = io.get_ref().peer_addr().ok(); .and_then(self.map_err(TlsError::Service))
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -256,25 +272,30 @@ mod rustls {
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::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory< U: ServiceFactory<
(Request, Framed<TlsStream<TcpStream>, h1::Codec>), (Request, Framed<TlsStream<TcpStream>, h1::Codec>),
Config = (), Config = (),
Response = (), Response = (),
>, >,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<TlsStream<TcpStream>, h1::Codec>)>>::Future: 'static,
{ {
/// Create openssl based service /// Create rustls based service
pub fn rustls( pub fn rustls(
self, self,
mut config: ServerConfig, mut config: ServerConfig,
@ -285,28 +306,28 @@ mod rustls {
Error = TlsError<io::Error, DispatchError>, Error = TlsError<io::Error, DispatchError>,
InitError = (), InitError = (),
> { > {
let protos = vec!["h2".to_string().into(), "http/1.1".to_string().into()]; let mut protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
protos.extend_from_slice(&config.alpn_protocols);
config.set_protocols(&protos); config.set_protocols(&protos);
pipeline_factory( Acceptor::new(config)
Acceptor::new(config) .map_err(TlsError::Tls)
.map_err(TlsError::Tls) .map_init_err(|_| panic!())
.map_init_err(|_| panic!()), .and_then(|io: TlsStream<TcpStream>| async {
) let proto = if let Some(protos) = io.get_ref().1.get_alpn_protocol()
.and_then(|io: TlsStream<TcpStream>| async { {
let proto = if let Some(protos) = io.get_ref().1.get_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 {
Protocol::Http1
}
} else { } else {
Protocol::Http1 Protocol::Http1
} };
} else { let peer_addr = io.get_ref().0.peer_addr().ok();
Protocol::Http1 Ok((io, proto, peer_addr))
}; })
let peer_addr = io.get_ref().0.peer_addr().ok(); .and_then(self.map_err(TlsError::Service))
Ok((io, proto, peer_addr))
})
.and_then(self.map_err(TlsError::Service))
} }
} }
} }
@ -314,137 +335,125 @@ mod rustls {
impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)> impl<T, S, B, X, U> ServiceFactory<(T, Protocol, Option<net::SocketAddr>)>
for HttpService<T, S, B, X, U> for HttpService<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin + 'static,
S: ServiceFactory<Request, Config = ()>, S: ServiceFactory<Request, Config = ()>,
S::Error: Into<Error> + 'static, S::Future: 'static,
S::Error: Into<Response<AnyBody>> + '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::Error: Into<Error>, X::Future: 'static,
X::Error: Into<Response<AnyBody>>,
X::InitError: fmt::Debug, X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>, U: ServiceFactory<(Request, Framed<T, h1::Codec>), Config = (), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Future: 'static,
U::Error: fmt::Display + Into<Response<AnyBody>>,
U::InitError: fmt::Debug, U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Config = (); type Config = ();
type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>; type Service = HttpServiceHandler<T, S::Service, B, X::Service, U::Service>;
type InitError = (); type InitError = ();
type Future = HttpServiceResponse<T, S, B, X, U>; type Future = LocalBoxFuture<'static, Result<Self::Service, Self::InitError>>;
fn new_service(&self, _: ()) -> Self::Future { fn new_service(&self, _: ()) -> Self::Future {
HttpServiceResponse { let service = self.srv.new_service(());
fut: self.srv.new_service(()), let expect = self.expect.new_service(());
fut_ex: Some(self.expect.new_service(())), let upgrade = self.upgrade.as_ref().map(|s| s.new_service(()));
fut_upg: self.upgrade.as_ref().map(|f| f.new_service(())), let on_connect_ext = self.on_connect_ext.clone();
expect: None, let cfg = self.cfg.clone();
upgrade: None,
on_connect_ext: self.on_connect_ext.clone(),
cfg: self.cfg.clone(),
_phantom: PhantomData,
}
}
}
#[doc(hidden)] Box::pin(async move {
#[pin_project] let expect = expect
pub struct HttpServiceResponse<T, S, B, X, U> .await
where .map_err(|e| log::error!("Init http expect service error: {:?}", e))?;
S: ServiceFactory<Request>,
X: ServiceFactory<Request>,
U: ServiceFactory<(Request, Framed<T, h1::Codec>)>,
{
#[pin]
fut: S::Future,
#[pin]
fut_ex: Option<X::Future>,
#[pin]
fut_upg: Option<U::Future>,
expect: Option<X::Service>,
upgrade: Option<U::Service>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
cfg: ServiceConfig,
_phantom: PhantomData<B>,
}
impl<T, S, B, X, U> Future for HttpServiceResponse<T, S, B, X, U> let upgrade = match upgrade {
where Some(upgrade) => {
T: AsyncRead + AsyncWrite + Unpin, let upgrade = upgrade.await.map_err(|e| {
S: ServiceFactory<Request>, log::error!("Init http upgrade service error: {:?}", e)
S::Error: Into<Error> + 'static, })?;
S::InitError: fmt::Debug, Some(upgrade)
S::Response: Into<Response<B>> + 'static, }
<S::Service as Service<Request>>::Future: 'static, None => None,
B: MessageBody + 'static, };
X: ServiceFactory<Request, Response = Request>,
X::Error: Into<Error>,
X::InitError: fmt::Debug,
<X::Service as Service<Request>>::Future: 'static,
U: ServiceFactory<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
U::InitError: fmt::Debug,
<U::Service as Service<(Request, Framed<T, h1::Codec>)>>::Future: 'static,
{
type Output =
Result<HttpServiceHandler<T, S::Service, B, X::Service, U::Service>, ()>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { let service = service
let mut this = self.as_mut().project(); .await
.map_err(|e| log::error!("Init http service error: {:?}", e))?;
if let Some(fut) = this.fut_ex.as_pin_mut() { Ok(HttpServiceHandler::new(
let expect = ready!(fut cfg,
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.expect = Some(expect);
this.fut_ex.set(None);
}
if let Some(fut) = this.fut_upg.as_pin_mut() {
let upgrade = ready!(fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)))?;
this = self.as_mut().project();
*this.upgrade = Some(upgrade);
this.fut_upg.set(None);
}
let result = ready!(this
.fut
.poll(cx)
.map_err(|e| log::error!("Init http service error: {:?}", e)));
Poll::Ready(result.map(|service| {
let this = self.as_mut().project();
HttpServiceHandler::new(
this.cfg.clone(),
service, service,
this.expect.take().unwrap(), expect,
this.upgrade.take(), upgrade,
this.on_connect_ext.clone(), on_connect_ext,
) ))
})) })
} }
} }
/// `Service` implementation for HTTP transport /// `Service` implementation for HTTP/1 and HTTP/2 transport
pub struct HttpServiceHandler<T, S, B, X, U> pub struct HttpServiceHandler<T, S, B, X, U>
where where
S: Service<Request>, S: Service<Request>,
X: Service<Request>, X: Service<Request>,
U: Service<(Request, Framed<T, h1::Codec>)>, U: Service<(Request, Framed<T, h1::Codec>)>,
{ {
flow: Rc<HttpFlow<S, X, U>>, pub(super) flow: Rc<HttpFlow<S, X, U>>,
cfg: ServiceConfig, pub(super) cfg: ServiceConfig,
on_connect_ext: Option<Rc<ConnectCallback<T>>>, pub(super) on_connect_ext: Option<Rc<ConnectCallback<T>>>,
_phantom: PhantomData<B>, _phantom: PhantomData<B>,
} }
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Response<AnyBody>>,
X: Service<Request>,
X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>)>,
U::Error: Into<Response<AnyBody>>,
{
pub(super) fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler {
cfg,
on_connect_ext,
flow: HttpFlow::new(service, expect, upgrade),
_phantom: PhantomData,
}
}
pub(super) fn _poll_ready(
&self,
cx: &mut Context<'_>,
) -> Poll<Result<(), Response<AnyBody>>> {
ready!(self.flow.expect.poll_ready(cx).map_err(Into::into))?;
ready!(self.flow.service.poll_ready(cx).map_err(Into::into))?;
if let Some(ref upg) = self.flow.upgrade {
ready!(upg.poll_ready(cx).map_err(Into::into))?;
};
Poll::Ready(Ok(()))
}
}
/// A collection of services that describe an HTTP request flow. /// A collection of services that describe an HTTP request flow.
pub(super) struct HttpFlow<S, X, U> { pub(super) struct HttpFlow<S, X, U> {
pub(super) service: S, pub(super) service: S,
@ -462,94 +471,34 @@ impl<S, X, U> HttpFlow<S, X, U> {
} }
} }
impl<T, S, B, X, U> HttpServiceHandler<T, S, B, X, U>
where
S: Service<Request>,
S::Error: Into<Error> + 'static,
S::Future: 'static,
S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
X: Service<Request, Response = Request>,
X::Error: Into<Error>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display,
{
fn new(
cfg: ServiceConfig,
service: S,
expect: X,
upgrade: Option<U>,
on_connect_ext: Option<Rc<ConnectCallback<T>>>,
) -> HttpServiceHandler<T, S, B, X, U> {
HttpServiceHandler {
cfg,
on_connect_ext,
flow: HttpFlow::new(service, expect, upgrade),
_phantom: PhantomData,
}
}
}
impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)> impl<T, S, B, X, U> Service<(T, Protocol, Option<net::SocketAddr>)>
for HttpServiceHandler<T, S, B, X, U> for HttpServiceHandler<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + '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<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display + Into<Error>, U::Error: fmt::Display + Into<Response<AnyBody>>,
{ {
type Response = (); type Response = ();
type Error = DispatchError; type Error = DispatchError;
type Future = HttpServiceHandlerResponse<T, S, B, X, U>; type Future = HttpServiceHandlerResponse<T, S, B, X, U>;
fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
let ready = self self._poll_ready(cx).map_err(|e| {
.flow log::error!("HTTP service readiness error: {:?}", e);
.expect DispatchError::Service(e)
.poll_ready(cx) })
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready();
let ready = self
.flow
.service
.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready;
let ready = if let Some(ref upg) = self.flow.upgrade {
upg.poll_ready(cx)
.map_err(|e| {
let e = e.into();
log::error!("Http service readiness error: {:?}", e);
DispatchError::Service(e)
})?
.is_ready()
&& ready
} else {
ready
};
if ready {
Poll::Ready(Ok(()))
} else {
Poll::Pending
}
} }
fn call( fn call(
@ -562,7 +511,7 @@ where
match proto { match proto {
Protocol::Http2 => HttpServiceHandlerResponse { Protocol::Http2 => HttpServiceHandlerResponse {
state: State::H2Handshake(Some(( state: State::H2Handshake(Some((
server::handshake(io), h2_handshake(io),
self.cfg.clone(), self.cfg.clone(),
self.flow.clone(), self.flow.clone(),
on_connect_data, on_connect_data,
@ -588,21 +537,26 @@ where
#[pin_project(project = StateProj)] #[pin_project(project = StateProj)]
enum State<T, S, B, X, U> enum State<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Future: 'static, S::Future: 'static,
S::Error: Into<Error>, S::Error: Into<Response<AnyBody>>,
T: AsyncRead + AsyncWrite + Unpin,
B: MessageBody, B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
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] h1::Dispatcher<T, S, B, X, U>),
H2(#[pin] Dispatcher<T, S, B, X, U>), H2(#[pin] h2::Dispatcher<T, S, B, X, U>),
H2Handshake( H2Handshake(
Option<( Option<(
Handshake<T, Bytes>, H2Handshake<T, Bytes>,
ServiceConfig, ServiceConfig,
Rc<HttpFlow<S, X, U>>, Rc<HttpFlow<S, X, U>>,
OnConnectData, OnConnectData,
@ -615,13 +569,18 @@ where
pub struct HttpServiceHandlerResponse<T, S, B, X, U> pub struct HttpServiceHandlerResponse<T, S, B, X, U>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody + 'static,
B: MessageBody,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -632,13 +591,18 @@ where
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>
where where
T: AsyncRead + AsyncWrite + Unpin, T: AsyncRead + AsyncWrite + Unpin,
S: Service<Request>, S: Service<Request>,
S::Error: Into<Error> + 'static, S::Error: Into<Response<AnyBody>> + 'static,
S::Future: 'static, S::Future: 'static,
S::Response: Into<Response<B>> + 'static, S::Response: Into<Response<B>> + 'static,
B: MessageBody,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
X: Service<Request, Response = Request>, X: Service<Request, Response = Request>,
X::Error: Into<Error>, X::Error: Into<Response<AnyBody>>,
U: Service<(Request, Framed<T, h1::Codec>), Response = ()>, U: Service<(Request, Framed<T, h1::Codec>), Response = ()>,
U::Error: fmt::Display, U::Error: fmt::Display,
{ {
@ -653,13 +617,15 @@ where
Ok(conn) => { Ok(conn) => {
let (_, cfg, srv, on_connect_data, peer_addr) = let (_, cfg, srv, on_connect_data, peer_addr) =
data.take().unwrap(); data.take().unwrap();
self.as_mut().project().state.set(State::H2(Dispatcher::new( self.as_mut().project().state.set(State::H2(
srv, h2::Dispatcher::new(
conn, srv,
on_connect_data, conn,
cfg, on_connect_data,
peer_addr, cfg,
))); peer_addr,
),
));
self.poll(cx) self.poll(cx)
} }
Err(err) => { Err(err) => {

View File

@ -13,11 +13,6 @@ use actix_codec::{AsyncRead, AsyncWrite, ReadBuf};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::{Method, Uri, Version}; use http::{Method, Uri, Version};
#[cfg(feature = "cookies")]
use crate::{
cookie::{Cookie, CookieJar},
header::{self, HeaderValue},
};
use crate::{ use crate::{
header::{HeaderMap, IntoHeaderPair}, header::{HeaderMap, IntoHeaderPair},
payload::Payload, payload::Payload,
@ -26,7 +21,7 @@ use crate::{
/// Test `Request` builder /// Test `Request` builder
/// ///
/// ```rust,ignore /// ```ignore
/// # use http::{header, StatusCode}; /// # use http::{header, StatusCode};
/// # use actix_web::*; /// # use actix_web::*;
/// use actix_web::test::TestRequest; /// use actix_web::test::TestRequest;
@ -54,8 +49,6 @@ struct Inner {
method: Method, method: Method,
uri: Uri, uri: Uri,
headers: HeaderMap, headers: HeaderMap,
#[cfg(feature = "cookies")]
cookies: CookieJar,
payload: Option<Payload>, payload: Option<Payload>,
} }
@ -66,8 +59,6 @@ impl Default for TestRequest {
uri: Uri::from_str("/").unwrap(), uri: Uri::from_str("/").unwrap(),
version: Version::HTTP_11, version: Version::HTTP_11,
headers: HeaderMap::new(), headers: HeaderMap::new(),
#[cfg(feature = "cookies")]
cookies: CookieJar::new(),
payload: None, payload: None,
})) }))
} }
@ -134,13 +125,6 @@ impl TestRequest {
self self
} }
/// Set cookie for this request.
#[cfg(feature = "cookies")]
pub fn cookie<'a>(&mut self, cookie: Cookie<'a>) -> &mut Self {
parts(&mut self.0).cookies.add(cookie.into_owned());
self
}
/// Set request payload. /// Set request payload.
pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self { pub fn set_payload<B: Into<Bytes>>(&mut self, data: B) -> &mut Self {
let mut payload = crate::h1::Payload::empty(); let mut payload = crate::h1::Payload::empty();
@ -169,22 +153,6 @@ impl TestRequest {
head.version = inner.version; head.version = inner.version;
head.headers = inner.headers; head.headers = inner.headers;
#[cfg(feature = "cookies")]
{
let cookie: String = inner
.cookies
.delta()
// ensure only name=value is written to cookie header
.map(|c| Cookie::new(c.name(), c.value()).encoded().to_string())
.collect::<Vec<_>>()
.join("; ");
if !cookie.is_empty() {
head.headers
.insert(header::COOKIE, HeaderValue::from_str(&cookie).unwrap());
}
}
req req
} }
} }

View File

@ -1,7 +1,7 @@
use time::{Date, OffsetDateTime, PrimitiveDateTime}; use time::{Date, OffsetDateTime, PrimitiveDateTime};
/// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime. /// Attempt to parse a `time` string as one of either RFC 1123, RFC 850, or asctime.
pub fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> { pub(crate) fn parse_http_date(time: &str) -> Option<PrimitiveDateTime> {
try_parse_rfc_1123(time) try_parse_rfc_1123(time)
.or_else(|| try_parse_rfc_850(time)) .or_else(|| try_parse_rfc_850(time))
.or_else(|| try_parse_asctime(time)) .or_else(|| try_parse_asctime(time))

View File

@ -4,7 +4,6 @@ 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 actix_utils::dispatcher::{Dispatcher as InnerDispatcher, DispatcherError};
use super::{Codec, Frame, Message}; use super::{Codec, Frame, Message};
@ -15,7 +14,7 @@ where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
#[pin] #[pin]
inner: InnerDispatcher<S, T, Codec, Message>, inner: inner::Dispatcher<S, T, Codec, Message>,
} }
impl<S, T> Dispatcher<S, T> impl<S, T> Dispatcher<S, T>
@ -27,13 +26,13 @@ where
{ {
pub fn new<F: IntoService<S, Frame>>(io: T, service: F) -> Self { pub fn new<F: IntoService<S, Frame>>(io: T, service: F) -> Self {
Dispatcher { Dispatcher {
inner: InnerDispatcher::new(Framed::new(io, Codec::new()), service), inner: inner::Dispatcher::new(Framed::new(io, Codec::new()), service),
} }
} }
pub fn with<F: IntoService<S, Frame>>(framed: Framed<T, Codec>, service: F) -> Self { pub fn with<F: IntoService<S, Frame>>(framed: Framed<T, Codec>, service: F) -> Self {
Dispatcher { Dispatcher {
inner: InnerDispatcher::new(framed, service), inner: inner::Dispatcher::new(framed, service),
} }
} }
} }
@ -45,9 +44,396 @@ where
S::Future: 'static, S::Future: 'static,
S::Error: 'static, S::Error: 'static,
{ {
type Output = Result<(), DispatcherError<S::Error, Codec, Message>>; type Output = Result<(), inner::DispatcherError<S::Error, Codec, Message>>;
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> { fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
self.project().inner.poll(cx) self.project().inner.poll(cx)
} }
} }
/// Framed dispatcher service and related utilities.
mod inner {
// allow dead code since this mod was ripped from actix-utils
#![allow(dead_code)]
use core::{
fmt,
future::Future,
mem,
pin::Pin,
task::{Context, Poll},
};
use actix_service::{IntoService, Service};
use futures_core::stream::Stream;
use local_channel::mpsc;
use log::debug;
use pin_project_lite::pin_project;
use actix_codec::{AsyncRead, AsyncWrite, Decoder, Encoder, Framed};
use crate::{body::AnyBody, Response};
/// Framed transport errors
pub enum DispatcherError<E, U, I>
where
U: Encoder<I> + Decoder,
{
/// Inner service error.
Service(E),
/// Frame encoding error.
Encoder(<U as Encoder<I>>::Error),
/// Frame decoding error.
Decoder(<U as Decoder>::Error),
}
impl<E, U, I> From<E> for DispatcherError<E, U, I>
where
U: Encoder<I> + Decoder,
{
fn from(err: E) -> Self {
DispatcherError::Service(err)
}
}
impl<E, U, I> fmt::Debug for DispatcherError<E, U, I>
where
E: fmt::Debug,
U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
DispatcherError::Service(ref e) => {
write!(fmt, "DispatcherError::Service({:?})", e)
}
DispatcherError::Encoder(ref e) => {
write!(fmt, "DispatcherError::Encoder({:?})", e)
}
DispatcherError::Decoder(ref e) => {
write!(fmt, "DispatcherError::Decoder({:?})", e)
}
}
}
}
impl<E, U, I> fmt::Display for DispatcherError<E, U, I>
where
E: fmt::Display,
U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
fn fmt(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
DispatcherError::Service(ref e) => write!(fmt, "{}", e),
DispatcherError::Encoder(ref e) => write!(fmt, "{:?}", e),
DispatcherError::Decoder(ref e) => write!(fmt, "{:?}", e),
}
}
}
impl<E, U, I> From<DispatcherError<E, U, I>> for Response<AnyBody>
where
E: fmt::Debug + fmt::Display,
U: Encoder<I> + Decoder,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
fn from(err: DispatcherError<E, U, I>) -> Self {
Response::internal_server_error().set_body(AnyBody::from(err.to_string()))
}
}
/// Message type wrapper for signalling end of message stream.
pub enum Message<T> {
/// Message item.
Item(T),
/// Signal from service to flush all messages and stop processing.
Close,
}
pin_project! {
/// A future that reads frames from a [`Framed`] object and passes them to a [`Service`].
pub struct Dispatcher<S, T, U, I>
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead,
T: AsyncWrite,
U: Encoder<I>,
U: Decoder,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
{
service: S,
state: State<S, U, I>,
#[pin]
framed: Framed<T, U>,
rx: mpsc::Receiver<Result<Message<I>, S::Error>>,
tx: mpsc::Sender<Result<Message<I>, S::Error>>,
}
}
enum State<S, U, I>
where
S: Service<<U as Decoder>::Item>,
U: Encoder<I> + Decoder,
{
Processing,
Error(DispatcherError<S::Error, U, I>),
FramedError(DispatcherError<S::Error, U, I>),
FlushAndStop,
Stopping,
}
impl<S, U, I> State<S, U, I>
where
S: Service<<U as Decoder>::Item>,
U: Encoder<I> + Decoder,
{
fn take_error(&mut self) -> DispatcherError<S::Error, U, I> {
match mem::replace(self, State::Processing) {
State::Error(err) => err,
_ => panic!(),
}
}
fn take_framed_error(&mut self) -> DispatcherError<S::Error, U, I> {
match mem::replace(self, State::Processing) {
State::FramedError(err) => err,
_ => panic!(),
}
}
}
impl<S, T, U, I> Dispatcher<S, T, U, I>
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Decoder>::Error: fmt::Debug,
<U as Encoder<I>>::Error: fmt::Debug,
{
/// Create new `Dispatcher`.
pub fn new<F>(framed: Framed<T, U>, service: F) -> Self
where
F: IntoService<S, <U as Decoder>::Item>,
{
let (tx, rx) = mpsc::channel();
Dispatcher {
framed,
rx,
tx,
service: service.into_service(),
state: State::Processing,
}
}
/// Construct new `Dispatcher` instance with customer `mpsc::Receiver`
pub fn with_rx<F>(
framed: Framed<T, U>,
service: F,
rx: mpsc::Receiver<Result<Message<I>, S::Error>>,
) -> Self
where
F: IntoService<S, <U as Decoder>::Item>,
{
let tx = rx.sender();
Dispatcher {
framed,
rx,
tx,
service: service.into_service(),
state: State::Processing,
}
}
/// Get sender handle.
pub fn tx(&self) -> mpsc::Sender<Result<Message<I>, S::Error>> {
self.tx.clone()
}
/// Get reference to a service wrapped by `Dispatcher` instance.
pub fn service(&self) -> &S {
&self.service
}
/// Get mutable reference to a service wrapped by `Dispatcher` instance.
pub fn service_mut(&mut self) -> &mut S {
&mut self.service
}
/// Get reference to a framed instance wrapped by `Dispatcher` instance.
pub fn framed(&self) -> &Framed<T, U> {
&self.framed
}
/// Get mutable reference to a framed instance wrapped by `Dispatcher` instance.
pub fn framed_mut(&mut self) -> &mut Framed<T, U> {
&mut self.framed
}
/// Read from framed object.
fn poll_read(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
{
loop {
let this = self.as_mut().project();
match this.service.poll_ready(cx) {
Poll::Ready(Ok(_)) => {
let item = match this.framed.next_item(cx) {
Poll::Ready(Some(Ok(el))) => el,
Poll::Ready(Some(Err(err))) => {
*this.state =
State::FramedError(DispatcherError::Decoder(err));
return true;
}
Poll::Pending => return false,
Poll::Ready(None) => {
*this.state = State::Stopping;
return true;
}
};
let tx = this.tx.clone();
let fut = this.service.call(item);
actix_rt::spawn(async move {
let item = fut.await;
let _ = tx.send(item.map(Message::Item));
});
}
Poll::Pending => return false,
Poll::Ready(Err(err)) => {
*this.state = State::Error(DispatcherError::Service(err));
return true;
}
}
}
}
/// Write to framed object.
fn poll_write(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> bool
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
{
loop {
let mut this = self.as_mut().project();
while !this.framed.is_write_buf_full() {
match Pin::new(&mut this.rx).poll_next(cx) {
Poll::Ready(Some(Ok(Message::Item(msg)))) => {
if let Err(err) = this.framed.as_mut().write(msg) {
*this.state =
State::FramedError(DispatcherError::Encoder(err));
return true;
}
}
Poll::Ready(Some(Ok(Message::Close))) => {
*this.state = State::FlushAndStop;
return true;
}
Poll::Ready(Some(Err(err))) => {
*this.state = State::Error(DispatcherError::Service(err));
return true;
}
Poll::Ready(None) | Poll::Pending => break,
}
}
if !this.framed.is_write_buf_empty() {
match this.framed.flush(cx) {
Poll::Pending => break,
Poll::Ready(Ok(_)) => {}
Poll::Ready(Err(err)) => {
debug!("Error sending data: {:?}", err);
*this.state =
State::FramedError(DispatcherError::Encoder(err));
return true;
}
}
} else {
break;
}
}
false
}
}
impl<S, T, U, I> Future for Dispatcher<S, T, U, I>
where
S: Service<<U as Decoder>::Item, Response = I>,
S::Error: 'static,
S::Future: 'static,
T: AsyncRead + AsyncWrite,
U: Decoder + Encoder<I>,
I: 'static,
<U as Encoder<I>>::Error: fmt::Debug,
<U as Decoder>::Error: fmt::Debug,
{
type Output = Result<(), DispatcherError<S::Error, U, I>>;
fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
loop {
let this = self.as_mut().project();
return match this.state {
State::Processing => {
if self.as_mut().poll_read(cx) || self.as_mut().poll_write(cx) {
continue;
} else {
Poll::Pending
}
}
State::Error(_) => {
// flush write buffer
if !this.framed.is_write_buf_empty()
&& this.framed.flush(cx).is_pending()
{
return Poll::Pending;
}
Poll::Ready(Err(this.state.take_error()))
}
State::FlushAndStop => {
if !this.framed.is_write_buf_empty() {
this.framed.flush(cx).map(|res| {
if let Err(err) = res {
debug!("Error sending data: {:?}", err);
}
Ok(())
})
} else {
Poll::Ready(Ok(()))
}
}
State::FramedError(_) => {
Poll::Ready(Err(this.state.take_framed_error()))
}
State::Stopping => Poll::Ready(Ok(())),
};
}
}
}
}

View File

@ -9,10 +9,8 @@ use derive_more::{Display, Error, From};
use http::{header, Method, StatusCode}; use http::{header, Method, StatusCode};
use crate::{ use crate::{
error::ResponseError, body::AnyBody, header::HeaderValue, message::RequestHead, response::Response,
header::HeaderValue, ResponseBuilder,
message::RequestHead,
response::{Response, ResponseBuilder},
}; };
mod codec; mod codec;
@ -27,7 +25,7 @@ pub use self::frame::Parser;
pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode}; pub use self::proto::{hash_key, CloseCode, CloseReason, OpCode};
/// WebSocket protocol errors. /// WebSocket protocol errors.
#[derive(Debug, Display, From, Error)] #[derive(Debug, Display, Error, From)]
pub enum ProtocolError { pub enum ProtocolError {
/// Received an unmasked frame from client. /// Received an unmasked frame from client.
#[display(fmt = "Received an unmasked frame from client.")] #[display(fmt = "Received an unmasked frame from client.")]
@ -70,10 +68,8 @@ pub enum ProtocolError {
Io(io::Error), Io(io::Error),
} }
impl ResponseError for ProtocolError {}
/// WebSocket handshake errors /// WebSocket handshake errors
#[derive(PartialEq, Debug, Display)] #[derive(Debug, 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.")]
@ -100,36 +96,55 @@ pub enum HandshakeError {
BadWebsocketKey, BadWebsocketKey,
} }
impl ResponseError for HandshakeError { impl From<&HandshakeError> for Response<AnyBody> {
fn error_response(&self) -> Response { fn from(err: &HandshakeError) -> Self {
match self { match err {
HandshakeError::GetMethodRequired => Response::MethodNotAllowed() HandshakeError::GetMethodRequired => {
.insert_header((header::ALLOW, "GET")) let mut res = Response::new(StatusCode::METHOD_NOT_ALLOWED);
.finish(), res.headers_mut()
.insert(header::ALLOW, HeaderValue::from_static("GET"));
res
}
HandshakeError::NoWebsocketUpgrade => Response::BadRequest() HandshakeError::NoWebsocketUpgrade => {
.reason("No WebSocket Upgrade header found") let mut res = Response::bad_request();
.finish(), res.head_mut().reason = Some("No WebSocket Upgrade header found");
res
}
HandshakeError::NoConnectionUpgrade => Response::BadRequest() HandshakeError::NoConnectionUpgrade => {
.reason("No Connection upgrade") let mut res = Response::bad_request();
.finish(), res.head_mut().reason = Some("No Connection upgrade");
res
}
HandshakeError::NoVersionHeader => Response::BadRequest() HandshakeError::NoVersionHeader => {
.reason("WebSocket version header is required") let mut res = Response::bad_request();
.finish(), res.head_mut().reason = Some("WebSocket version header is required");
res
}
HandshakeError::UnsupportedVersion => Response::BadRequest() HandshakeError::UnsupportedVersion => {
.reason("Unsupported WebSocket version") let mut res = Response::bad_request();
.finish(), res.head_mut().reason = Some("Unsupported WebSocket version");
res
}
HandshakeError::BadWebsocketKey => { HandshakeError::BadWebsocketKey => {
Response::BadRequest().reason("Handshake error").finish() let mut res = Response::bad_request();
res.head_mut().reason = Some("Handshake error");
res
} }
} }
} }
} }
impl From<HandshakeError> for Response<AnyBody> {
fn from(err: HandshakeError) -> Self {
(&err).into()
}
}
/// Verify WebSocket handshake request and create handshake response. /// Verify WebSocket handshake request and create handshake response.
pub fn handshake(req: &RequestHead) -> Result<ResponseBuilder, HandshakeError> { pub fn handshake(req: &RequestHead) -> Result<ResponseBuilder, HandshakeError> {
verify_handshake(req)?; verify_handshake(req)?;
@ -207,7 +222,7 @@ pub fn handshake_response(req: &RequestHead) -> ResponseBuilder {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::test::TestRequest; use crate::{body::AnyBody, test::TestRequest};
use http::{header, Method}; use http::{header, Method};
#[test] #[test]
@ -321,18 +336,18 @@ mod tests {
} }
#[test] #[test]
fn test_wserror_http_response() { fn test_ws_error_http_response() {
let resp: Response = HandshakeError::GetMethodRequired.error_response(); let resp: Response<AnyBody> = HandshakeError::GetMethodRequired.into();
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let resp: Response = HandshakeError::NoWebsocketUpgrade.error_response(); let resp: Response<AnyBody> = HandshakeError::NoWebsocketUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::NoConnectionUpgrade.error_response(); let resp: Response<AnyBody> = HandshakeError::NoConnectionUpgrade.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::NoVersionHeader.error_response(); let resp: Response<AnyBody> = HandshakeError::NoVersionHeader.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::UnsupportedVersion.error_response(); let resp: Response<AnyBody> = HandshakeError::UnsupportedVersion.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
let resp: Response = HandshakeError::BadWebsocketKey.error_response(); let resp: Response<AnyBody> = HandshakeError::BadWebsocketKey.into();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@ -1,13 +1,14 @@
use std::convert::Infallible;
use actix_http::{ use actix_http::{
error, http, http::StatusCode, HttpMessage, HttpService, Request, Response, body::AnyBody, http, http::StatusCode, HttpMessage, HttpService, Request, Response,
}; };
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::ServiceFactoryExt; use actix_service::ServiceFactoryExt;
use actix_utils::future;
use bytes::Bytes; use bytes::Bytes;
use futures_util::{ use derive_more::{Display, Error};
future::{self, ok}, use futures_util::StreamExt as _;
StreamExt,
};
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 \
Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \
@ -35,7 +36,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h1_v2() { async fn test_h1_v2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -63,7 +64,7 @@ async fn test_h1_v2() {
async fn test_connection_close() { async fn test_connection_close() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| future::ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
.map(|_| ()) .map(|_| ())
}) })
@ -77,11 +78,11 @@ async fn test_connection_close() {
async fn test_with_query_parameter() { async fn test_with_query_parameter() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|req: Request| { .finish(|req: Request| async move {
if req.uri().query().unwrap().contains("qp=") { if req.uri().query().unwrap().contains("qp=") {
ok::<_, ()>(Response::Ok().finish()) Ok::<_, Infallible>(Response::ok())
} else { } else {
ok::<_, ()>(Response::BadRequest().finish()) Ok(Response::bad_request())
} }
}) })
.tcp() .tcp()
@ -94,6 +95,16 @@ async fn test_with_query_parameter() {
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
#[derive(Debug, Display, Error)]
#[display(fmt = "expect failed")]
struct ExpectFailed;
impl From<ExpectFailed> for Response<AnyBody> {
fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED)
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_h1_expect() { async fn test_h1_expect() {
let srv = test_server(move || { let srv = test_server(move || {
@ -102,7 +113,7 @@ async fn test_h1_expect() {
if req.headers().contains_key("AUTH") { if req.headers().contains_key("AUTH") {
Ok(req) Ok(req)
} else { } else {
Err(error::ErrorExpectationFailed("expect failed")) Err(ExpectFailed)
} }
}) })
.h1(|req: Request| async move { .h1(|req: Request| async move {
@ -114,7 +125,7 @@ async fn test_h1_expect() {
let str = std::str::from_utf8(&buf).unwrap(); let str = std::str::from_utf8(&buf).unwrap();
assert_eq!(str, "expect body"); assert_eq!(str, "expect body");
Ok::<_, ()>(Response::Ok().finish()) Ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -136,7 +147,7 @@ async fn test_h1_expect() {
let response = request.send_body("expect body").await.unwrap(); let response = request.send_body("expect body").await.unwrap();
assert_eq!(response.status(), StatusCode::EXPECTATION_FAILED); assert_eq!(response.status(), StatusCode::EXPECTATION_FAILED);
// test exepct would continue // test expect would continue
let request = srv let request = srv
.request(http::Method::GET, srv.url("/")) .request(http::Method::GET, srv.url("/"))
.insert_header(("Expect", "100-continue")) .insert_header(("Expect", "100-continue"))

View File

@ -2,18 +2,24 @@
extern crate tls_openssl as openssl; extern crate tls_openssl as openssl;
use std::io; use std::{convert::Infallible, io};
use actix_http::error::{ErrorBadRequest, PayloadError}; use actix_http::{
use actix_http::http::header::{self, HeaderName, HeaderValue}; body::{AnyBody, Body, SizedStream},
use actix_http::http::{Method, StatusCode, Version}; error::PayloadError,
use actix_http::HttpMessage; http::{
use actix_http::{body, Error, HttpService, Request, Response}; header::{self, HeaderName, HeaderValue},
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};
use actix_utils::future::{err, ok, ready};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures_util::future::{err, ok, ready}; use derive_more::{Display, Error};
use futures_util::stream::{once, Stream, StreamExt}; use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _};
use openssl::{ use openssl::{
pkey::PKey, pkey::PKey,
ssl::{SslAcceptor, SslMethod}, ssl::{SslAcceptor, SslMethod},
@ -66,7 +72,7 @@ fn tls_config() -> SslAcceptor {
async fn test_h2() -> io::Result<()> { async fn test_h2() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, Error>(Response::Ok().finish())) .h2(|_| ok::<_, Error>(Response::ok()))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -84,7 +90,7 @@ async fn test_h2_1() -> io::Result<()> {
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::Ok().finish()) ok::<_, Error>(Response::ok())
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -103,7 +109,7 @@ async fn test_h2_body() -> io::Result<()> {
HttpService::build() HttpService::build()
.h2(|mut req: Request<_>| async move { .h2(|mut req: Request<_>| async move {
let body = load_body(req.take_payload()).await?; let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::Ok().body(body)) Ok::<_, Error>(Response::ok().set_body(body))
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -130,7 +136,7 @@ async fn test_h2_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
ok::<_, ()>(Response::new(statuses[idx])) ok::<_, Infallible>(Response::new(statuses[idx]))
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -181,7 +187,7 @@ async fn test_h2_headers() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
let data = data.clone(); let data = data.clone();
HttpService::build().h2(move |_| { HttpService::build().h2(move |_| {
let mut builder = Response::Ok(); let mut builder = Response::build(StatusCode::OK);
for idx in 0..90 { for idx in 0..90 {
builder.insert_header( builder.insert_header(
(format!("X-TEST-{}", idx).as_str(), (format!("X-TEST-{}", idx).as_str(),
@ -200,7 +206,7 @@ async fn test_h2_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
ok::<_, ()>(builder.body(data.clone())) ok::<_, Infallible>(builder.body(data.clone()))
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
@ -240,7 +246,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h2_body2() { async fn test_h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -258,7 +264,7 @@ async fn test_h2_body2() {
async fn test_h2_head_empty() { async fn test_h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -282,7 +288,7 @@ async fn test_h2_head_empty() {
async fn test_h2_head_binary() { async fn test_h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -305,7 +311,7 @@ async fn test_h2_head_binary() {
async fn test_h2_head_binary2() { async fn test_h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -324,10 +330,13 @@ async fn test_h2_head_binary2() {
async fn test_h2_body_length() { async fn test_h2_body_length() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| async {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(async {
ok::<_, ()>( Ok::<_, Infallible>(Bytes::from_static(STR.as_ref()))
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), });
Ok::<_, Infallible>(
Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.openssl(tls_config()) .openssl(tls_config())
@ -349,8 +358,8 @@ async fn test_h2_body_chunked_explicit() {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
) )
@ -377,8 +386,8 @@ async fn test_h2_response_http_error_handling() {
HttpService::build() HttpService::build()
.h2(fn_service(|_| { .h2(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::CONTENT_TYPE, broken_header)) .insert_header((header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
) )
@ -393,14 +402,27 @@ async fn test_h2_response_http_error_handling() {
// 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"failed to parse header value")); assert_eq!(
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
}
#[derive(Debug, Display, Error)]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<AnyBody> {
fn from(err: BadRequest) -> Self {
Response::build(StatusCode::BAD_REQUEST).body(err.to_string())
}
} }
#[actix_rt::test] #[actix_rt::test]
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, Error>(ErrorBadRequest("error"))) .h2(|_| err::<Response<Body>, _>(BadRequest))
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())
}) })
@ -423,7 +445,7 @@ async fn test_h2_on_connect() {
}) })
.h2(|req: Request| { .h2(|req: Request| {
assert!(req.extensions().contains::<isize>()); assert!(req.extensions().contains::<isize>());
ok::<_, ()>(Response::Ok().finish()) ok::<_, Infallible>(Response::ok())
}) })
.openssl(tls_config()) .openssl(tls_config())
.map_err(|_| ()) .map_err(|_| ())

View File

@ -2,22 +2,34 @@
extern crate tls_rustls as rustls; extern crate tls_rustls as rustls;
use actix_http::error::PayloadError; use std::{
use actix_http::http::header::{self, HeaderName, HeaderValue}; convert::Infallible,
use actix_http::http::{Method, StatusCode, Version}; io::{self, BufReader, Write},
use actix_http::{body, error, Error, HttpService, Request, Response}; net::{SocketAddr, TcpStream as StdTcpStream},
use actix_http_test::test_server; sync::Arc,
use actix_service::{fn_factory_with_config, fn_service};
use bytes::{Bytes, BytesMut};
use futures_util::future::{self, err, ok};
use futures_util::stream::{once, Stream, StreamExt};
use rustls::{
internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig as RustlsServerConfig,
}; };
use std::io::{self, BufReader}; use actix_http::{
body::{AnyBody, Body, SizedStream},
error::PayloadError,
http::{
header::{self, HeaderName, HeaderValue},
Method, StatusCode, Version,
},
Error, HttpService, Request, Response,
};
use actix_http_test::test_server;
use actix_service::{fn_factory_with_config, fn_service};
use actix_utils::future::{err, ok};
use bytes::{Bytes, BytesMut};
use derive_more::{Display, Error};
use futures_core::Stream;
use futures_util::stream::{once, StreamExt as _};
use rustls::{
internal::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
@ -46,11 +58,30 @@ fn tls_config() -> RustlsServerConfig {
config config
} }
pub fn get_negotiated_alpn_protocol(
addr: SocketAddr,
client_alpn_protocol: &[u8],
) -> Option<Vec<u8>> {
let mut config = rustls::ClientConfig::new();
config.alpn_protocols.push(client_alpn_protocol.to_vec());
let mut sess = rustls::ClientSession::new(
&Arc::new(config),
DNSNameRef::try_from_ascii_str("localhost").unwrap(),
);
let mut sock = StdTcpStream::connect(addr).unwrap();
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
// certificate, but it doesn't matter here as we are just interested in the negotiated ALPN
// protocol
let _ = stream.flush();
sess.get_alpn_protocol().map(|proto| proto.to_vec())
}
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() -> io::Result<()> { async fn test_h1() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, Error>(Response::Ok().finish())) .h1(|_| ok::<_, Error>(Response::ok()))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -64,7 +95,7 @@ async fn test_h1() -> io::Result<()> {
async fn test_h2() -> io::Result<()> { async fn test_h2() -> io::Result<()> {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| future::ok::<_, Error>(Response::Ok().finish())) .h2(|_| ok::<_, Error>(Response::ok()))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -81,7 +112,7 @@ async fn test_h1_1() -> io::Result<()> {
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_11); assert_eq!(req.version(), Version::HTTP_11);
future::ok::<_, Error>(Response::Ok().finish()) ok::<_, Error>(Response::ok())
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -99,7 +130,7 @@ async fn test_h2_1() -> io::Result<()> {
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_2); assert_eq!(req.version(), Version::HTTP_2);
future::ok::<_, Error>(Response::Ok().finish()) ok::<_, Error>(Response::ok())
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -117,7 +148,7 @@ async fn test_h2_body1() -> io::Result<()> {
HttpService::build() HttpService::build()
.h2(|mut req: Request<_>| async move { .h2(|mut req: Request<_>| async move {
let body = load_body(req.take_payload()).await?; let body = load_body(req.take_payload()).await?;
Ok::<_, Error>(Response::Ok().body(body)) Ok::<_, Error>(Response::ok().set_body(body))
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -143,7 +174,7 @@ async fn test_h2_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
future::ok::<_, ()>(Response::new(statuses[indx])) ok::<_, Infallible>(Response::new(statuses[indx]))
}) })
.rustls(tls_config()) .rustls(tls_config())
}) })
@ -193,7 +224,7 @@ async fn test_h2_headers() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
let data = data.clone(); let data = data.clone();
HttpService::build().h2(move |_| { HttpService::build().h2(move |_| {
let mut config = Response::Ok(); let mut config = Response::build(StatusCode::OK);
for idx in 0..90 { for idx in 0..90 {
config.insert_header(( config.insert_header((
format!("X-TEST-{}", idx).as_str(), format!("X-TEST-{}", idx).as_str(),
@ -212,7 +243,7 @@ async fn test_h2_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
future::ok::<_, ()>(config.body(data.clone())) ok::<_, Infallible>(config.body(data.clone()))
}) })
.rustls(tls_config()) .rustls(tls_config())
}).await; }).await;
@ -251,7 +282,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h2_body2() { async fn test_h2_body2() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| future::ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -268,7 +299,7 @@ async fn test_h2_body2() {
async fn test_h2_head_empty() { async fn test_h2_head_empty() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR))) .finish(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -294,7 +325,7 @@ async fn test_h2_head_empty() {
async fn test_h2_head_binary() { async fn test_h2_head_binary() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -319,7 +350,7 @@ async fn test_h2_head_binary() {
async fn test_h2_head_binary2() { async fn test_h2_head_binary2() {
let srv = test_server(move || { let srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR))) .h2(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -341,9 +372,9 @@ async fn test_h2_body_length() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.rustls(tls_config()) .rustls(tls_config())
@ -364,8 +395,8 @@ async fn test_h2_body_chunked_explicit() {
HttpService::build() HttpService::build()
.h2(|_| { .h2(|_| {
let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Error>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
) )
@ -390,10 +421,10 @@ async fn test_h2_response_http_error_handling() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
HttpService::build() HttpService::build()
.h2(fn_factory_with_config(|_: ()| { .h2(fn_factory_with_config(|_: ()| {
ok::<_, ()>(fn_service(|_| { ok::<_, Infallible>(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((http::header::CONTENT_TYPE, broken_header)) .insert_header((http::header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
) )
@ -408,14 +439,27 @@ async fn test_h2_response_http_error_handling() {
// 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"failed to parse header value")); assert_eq!(
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
}
#[derive(Debug, Display, Error)]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<AnyBody> {
fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(AnyBody::from("error"))
}
} }
#[actix_rt::test] #[actix_rt::test]
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, Error>(error::ErrorBadRequest("error"))) .h2(|_| err::<Response<Body>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -432,7 +476,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, Error>(error::ErrorBadRequest("error"))) .h1(|_| err::<Response<Body>, _>(BadRequest))
.rustls(tls_config()) .rustls(tls_config())
}) })
.await; .await;
@ -444,3 +488,85 @@ async fn test_h1_service_error() {
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"));
} }
const H2_ALPN_PROTOCOL: &[u8] = b"h2";
const HTTP1_1_ALPN_PROTOCOL: &[u8] = b"http/1.1";
const CUSTOM_ALPN_PROTOCOL: &[u8] = b"custom";
#[actix_rt::test]
async fn test_alpn_h1() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h1(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}
#[actix_rt::test]
async fn test_alpn_h2() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.h2(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
Some(H2_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}
#[actix_rt::test]
async fn test_alpn_h2_1() -> io::Result<()> {
let srv = test_server(move || {
let mut config = tls_config();
config.alpn_protocols.push(CUSTOM_ALPN_PROTOCOL.to_vec());
HttpService::build()
.finish(|_| ok::<_, Error>(Response::ok()))
.rustls(config)
})
.await;
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), H2_ALPN_PROTOCOL),
Some(H2_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), HTTP1_1_ALPN_PROTOCOL),
Some(HTTP1_1_ALPN_PROTOCOL.to_vec())
);
assert_eq!(
get_negotiated_alpn_protocol(srv.addr(), CUSTOM_ALPN_PROTOCOL),
Some(CUSTOM_ALPN_PROTOCOL.to_vec())
);
let response = srv.sget("/").send().await.unwrap();
assert!(response.status().is_success());
Ok(())
}

View File

@ -1,19 +1,26 @@
use std::io::{Read, Write}; use std::{
use std::time::Duration; convert::Infallible,
use std::{net, thread}; io::{Read, Write},
net, thread,
time::Duration,
};
use actix_http::{
body::{AnyBody, Body, SizedStream},
header, http, Error, HttpMessage, HttpService, KeepAlive, Request, Response,
StatusCode,
};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_rt::time::sleep; use actix_rt::time::sleep;
use actix_service::fn_service; use actix_service::fn_service;
use actix_utils::future::{err, ok, ready};
use bytes::Bytes; use bytes::Bytes;
use futures_util::future::{self, err, ok, ready, FutureExt}; use derive_more::{Display, Error};
use futures_util::stream::{once, StreamExt}; use futures_util::{
use regex::Regex; stream::{once, StreamExt as _},
FutureExt as _,
use actix_http::HttpMessage;
use actix_http::{
body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response,
}; };
use regex::Regex;
#[actix_rt::test] #[actix_rt::test]
async fn test_h1() { async fn test_h1() {
@ -24,7 +31,7 @@ async fn test_h1() {
.client_disconnect(1000) .client_disconnect(1000)
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
future::ok::<_, ()>(Response::Ok().finish()) ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -44,7 +51,7 @@ async fn test_h1_2() {
.finish(|req: Request| { .finish(|req: Request| {
assert!(req.peer_addr().is_some()); assert!(req.peer_addr().is_some());
assert_eq!(req.version(), http::Version::HTTP_11); assert_eq!(req.version(), http::Version::HTTP_11);
future::ok::<_, ()>(Response::Ok().finish()) ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })
@ -54,6 +61,16 @@ async fn test_h1_2() {
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
#[derive(Debug, Display, Error)]
#[display(fmt = "expect failed")]
struct ExpectFailed;
impl From<ExpectFailed> for Response<AnyBody> {
fn from(_: ExpectFailed) -> Self {
Response::new(StatusCode::EXPECTATION_FAILED)
}
}
#[actix_rt::test] #[actix_rt::test]
async fn test_expect_continue() { async fn test_expect_continue() {
let srv = test_server(|| { let srv = test_server(|| {
@ -62,10 +79,10 @@ async fn test_expect_continue() {
if req.head().uri.query() == Some("yes=") { if req.head().uri.query() == Some("yes=") {
ok(req) ok(req)
} else { } else {
err(error::ErrorPreconditionFailed("error")) err(ExpectFailed)
} }
})) }))
.finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .finish(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -74,7 +91,7 @@ async fn test_expect_continue() {
let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
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 412 Precondition Failed\r\ncontent-length")); assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length"));
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
@ -92,11 +109,11 @@ async fn test_expect_continue_h1() {
if req.head().uri.query() == Some("yes=") { if req.head().uri.query() == Some("yes=") {
ok(req) ok(req)
} else { } else {
err(error::ErrorPreconditionFailed("error")) err(ExpectFailed)
} }
}) })
})) }))
.h1(fn_service(|_| future::ok::<_, ()>(Response::Ok().finish()))) .h1(fn_service(|_| ok::<_, Infallible>(Response::ok())))
.tcp() .tcp()
}) })
.await; .await;
@ -105,7 +122,7 @@ async fn test_expect_continue_h1() {
let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
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 412 Precondition Failed\r\ncontent-length")); assert!(data.starts_with("HTTP/1.1 417 Expectation Failed\r\ncontent-length"));
let mut stream = net::TcpStream::connect(srv.addr()).unwrap(); let mut stream = net::TcpStream::connect(srv.addr()).unwrap();
let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n"); let _ = stream.write_all(b"GET /test?yes= HTTP/1.1\r\nexpect: 100-continue\r\n\r\n");
@ -130,7 +147,9 @@ async fn test_chunked_payload() {
}) })
.fold(0usize, |acc, chunk| ready(acc + chunk.len())) .fold(0usize, |acc, chunk| ready(acc + chunk.len()))
.map(|req_size| { .map(|req_size| {
Ok::<_, Error>(Response::Ok().body(format!("size={}", req_size))) Ok::<_, Error>(
Response::ok().set_body(format!("size={}", req_size)),
)
}) })
})) }))
.tcp() .tcp()
@ -175,7 +194,7 @@ async fn test_slow_request() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.client_timeout(100) .client_timeout(100)
.finish(|_| future::ok::<_, ()>(Response::Ok().finish())) .finish(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -191,7 +210,7 @@ async fn test_slow_request() {
async fn test_http1_malformed_request() { async fn test_http1_malformed_request() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -207,7 +226,7 @@ async fn test_http1_malformed_request() {
async fn test_http1_keepalive() { async fn test_http1_keepalive() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -229,7 +248,7 @@ async fn test_http1_keepalive_timeout() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(1) .keep_alive(1)
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -250,7 +269,7 @@ async fn test_http1_keepalive_timeout() {
async fn test_http1_keepalive_close() { async fn test_http1_keepalive_close() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -271,7 +290,7 @@ async fn test_http1_keepalive_close() {
async fn test_http10_keepalive_default_close() { async fn test_http10_keepalive_default_close() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -291,7 +310,7 @@ async fn test_http10_keepalive_default_close() {
async fn test_http10_keepalive() { async fn test_http10_keepalive() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -319,7 +338,7 @@ async fn test_http1_keepalive_disabled() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.keep_alive(KeepAlive::Disabled) .keep_alive(KeepAlive::Disabled)
.h1(|_| future::ok::<_, ()>(Response::Ok().finish())) .h1(|_| ok::<_, Infallible>(Response::ok()))
.tcp() .tcp()
}) })
.await; .await;
@ -354,7 +373,7 @@ async fn test_content_length() {
StatusCode::OK, StatusCode::OK,
StatusCode::NOT_FOUND, StatusCode::NOT_FOUND,
]; ];
future::ok::<_, ()>(Response::new(statuses[indx])) ok::<_, Infallible>(Response::new(statuses[indx]))
}) })
.tcp() .tcp()
}) })
@ -390,7 +409,7 @@ async fn test_h1_headers() {
let mut srv = test_server(move || { let mut srv = test_server(move || {
let data = data.clone(); let data = data.clone();
HttpService::build().h1(move |_| { HttpService::build().h1(move |_| {
let mut builder = Response::Ok(); let mut builder = Response::build(StatusCode::OK);
for idx in 0..90 { for idx in 0..90 {
builder.insert_header(( builder.insert_header((
format!("X-TEST-{}", idx).as_str(), format!("X-TEST-{}", idx).as_str(),
@ -409,7 +428,7 @@ async fn test_h1_headers() {
TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ", TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST TEST ",
)); ));
} }
future::ok::<_, ()>(builder.body(data.clone())) ok::<_, Infallible>(builder.body(data.clone()))
}).tcp() }).tcp()
}).await; }).await;
@ -447,7 +466,7 @@ const STR: &str = "Hello World Hello World Hello World Hello World Hello World \
async fn test_h1_body() { async fn test_h1_body() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -464,7 +483,7 @@ async fn test_h1_body() {
async fn test_h1_head_empty() { async fn test_h1_head_empty() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -489,7 +508,7 @@ async fn test_h1_head_empty() {
async fn test_h1_head_binary() { async fn test_h1_head_binary() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -514,7 +533,7 @@ async fn test_h1_head_binary() {
async fn test_h1_head_binary2() { async fn test_h1_head_binary2() {
let srv = test_server(|| { let srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| ok::<_, ()>(Response::Ok().body(STR))) .h1(|_| ok::<_, Infallible>(Response::ok().set_body(STR)))
.tcp() .tcp()
}) })
.await; .await;
@ -536,9 +555,9 @@ async fn test_h1_body_length() {
let mut srv = test_server(|| { let mut srv = test_server(|| {
HttpService::build() HttpService::build()
.h1(|_| { .h1(|_| {
let body = once(ok(Bytes::from_static(STR.as_ref()))); let body = once(ok::<_, Infallible>(Bytes::from_static(STR.as_ref())));
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok().body(body::SizedStream::new(STR.len() as u64, body)), Response::ok().set_body(SizedStream::new(STR.len() as u64, body)),
) )
}) })
.tcp() .tcp()
@ -559,8 +578,8 @@ async fn test_h1_body_chunked_explicit() {
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::<_, ()>( ok::<_, Infallible>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((header::TRANSFER_ENCODING, "chunked")) .insert_header((header::TRANSFER_ENCODING, "chunked"))
.streaming(body), .streaming(body),
) )
@ -594,7 +613,7 @@ 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::<_, ()>(Response::Ok().streaming(body)) ok::<_, Infallible>(Response::build(StatusCode::OK).streaming(body))
}) })
.tcp() .tcp()
}) })
@ -623,8 +642,8 @@ async fn test_h1_response_http_error_handling() {
HttpService::build() HttpService::build()
.h1(fn_service(|_| { .h1(fn_service(|_| {
let broken_header = Bytes::from_static(b"\0\0\0"); let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>( ok::<_, Infallible>(
Response::Ok() Response::build(StatusCode::OK)
.insert_header((http::header::CONTENT_TYPE, broken_header)) .insert_header((http::header::CONTENT_TYPE, broken_header))
.body(STR), .body(STR),
) )
@ -638,14 +657,27 @@ async fn test_h1_response_http_error_handling() {
// 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"failed to parse header value")); assert_eq!(
bytes,
Bytes::from_static(b"error processing HTTP: failed to parse header value")
);
}
#[derive(Debug, Display, Error)]
#[display(fmt = "error")]
struct BadRequest;
impl From<BadRequest> for Response<AnyBody> {
fn from(_: BadRequest) -> Self {
Response::bad_request().set_body(AnyBody::from("error"))
}
} }
#[actix_rt::test] #[actix_rt::test]
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(|_| future::err::<Response, Error>(error::ErrorBadRequest("error"))) .h1(|_| err::<Response<Body>, _>(BadRequest))
.tcp() .tcp()
}) })
.await; .await;
@ -667,7 +699,7 @@ async fn test_h1_on_connect() {
}) })
.h1(|req: Request| { .h1(|req: Request| {
assert!(req.extensions().contains::<isize>()); assert!(req.extensions().contains::<isize>());
future::ok::<_, ()>(Response::Ok().finish()) ok::<_, Infallible>(Response::ok())
}) })
.tcp() .tcp()
}) })

View File

@ -1,192 +1,196 @@
use std::cell::Cell; use std::{
use std::future::Future; cell::Cell,
use std::marker::PhantomData; convert::Infallible,
use std::pin::Pin; task::{Context, Poll},
use std::sync::{Arc, Mutex}; };
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
use actix_http::{body, h1, ws, Error, HttpService, Request, Response}; use actix_http::{
body::{AnyBody, BodySize},
h1,
ws::{self, CloseCode, Frame, Item, Message},
Error, HttpService, Request, Response,
};
use actix_http_test::test_server; use actix_http_test::test_server;
use actix_service::{fn_factory, Service}; use actix_service::{fn_factory, Service};
use actix_utils::dispatcher::Dispatcher;
use bytes::Bytes; use bytes::Bytes;
use futures_util::future; use derive_more::{Display, Error, From};
use futures_util::task::{Context, Poll}; use futures_core::future::LocalBoxFuture;
use futures_util::{SinkExt, StreamExt}; use futures_util::{SinkExt as _, StreamExt as _};
struct WsService<T>(Arc<Mutex<(PhantomData<T>, Cell<bool>)>>); #[derive(Clone)]
struct WsService(Cell<bool>);
impl<T> WsService<T> { impl WsService {
fn new() -> Self { fn new() -> Self {
WsService(Arc::new(Mutex::new((PhantomData, Cell::new(false))))) WsService(Cell::new(false))
} }
fn set_polled(&self) { fn set_polled(&self) {
*self.0.lock().unwrap().1.get_mut() = true; self.0.set(true);
} }
fn was_polled(&self) -> bool { fn was_polled(&self) -> bool {
self.0.lock().unwrap().1.get() self.0.get()
} }
} }
impl<T> Clone for WsService<T> { #[derive(Debug, Display, Error, From)]
fn clone(&self) -> Self { enum WsServiceError {
WsService(self.0.clone()) #[display(fmt = "http error")]
Http(actix_http::Error),
#[display(fmt = "ws handshake error")]
Ws(actix_http::ws::HandshakeError),
#[display(fmt = "io error")]
Io(std::io::Error),
#[display(fmt = "dispatcher error")]
Dispatcher,
}
impl From<WsServiceError> for Response<AnyBody> {
fn from(err: WsServiceError) -> Self {
match err {
WsServiceError::Http(err) => err.into(),
WsServiceError::Ws(err) => err.into(),
WsServiceError::Io(_err) => unreachable!(),
WsServiceError::Dispatcher => Response::internal_server_error()
.set_body(AnyBody::from(format!("{}", err))),
}
} }
} }
impl<T> Service<(Request, Framed<T, h1::Codec>)> for WsService<T> impl<T> Service<(Request, Framed<T, h1::Codec>)> for WsService
where where
T: AsyncRead + AsyncWrite + Unpin + 'static, T: AsyncRead + AsyncWrite + Unpin + 'static,
{ {
type Response = (); type Response = ();
type Error = Error; type Error = WsServiceError;
type Future = Pin<Box<dyn Future<Output = Result<(), Error>>>>; type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(&self, _ctx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> { fn poll_ready(&self, _: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
self.set_polled(); self.set_polled();
Poll::Ready(Ok(())) Poll::Ready(Ok(()))
} }
fn call(&self, (req, mut framed): (Request, Framed<T, h1::Codec>)) -> Self::Future { fn call(&self, (req, mut framed): (Request, Framed<T, h1::Codec>)) -> Self::Future {
let fut = async move { assert!(self.was_polled());
let res = ws::handshake(req.head()).unwrap().message_body(());
framed Box::pin(async move {
.send((res, body::BodySize::None).into()) let res = ws::handshake(req.head())?.message_body(())?;
framed.send((res, BodySize::None).into()).await?;
let framed = framed.replace_codec(ws::Codec::new());
ws::Dispatcher::with(framed, service)
.await .await
.unwrap(); .map_err(|_| WsServiceError::Dispatcher)?;
Dispatcher::new(framed.replace_codec(ws::Codec::new()), service) Ok(())
.await })
.map_err(|_| panic!())
};
Box::pin(fut)
} }
} }
async fn service(msg: ws::Frame) -> Result<ws::Message, Error> { async fn service(msg: Frame) -> Result<Message, Error> {
let msg = match msg { let msg = match msg {
ws::Frame::Ping(msg) => ws::Message::Pong(msg), Frame::Ping(msg) => Message::Pong(msg),
ws::Frame::Text(text) => { Frame::Text(text) => {
ws::Message::Text(String::from_utf8_lossy(&text).into_owned().into()) Message::Text(String::from_utf8_lossy(&text).into_owned().into())
} }
ws::Frame::Binary(bin) => ws::Message::Binary(bin), Frame::Binary(bin) => Message::Binary(bin),
ws::Frame::Continuation(item) => ws::Message::Continuation(item), Frame::Continuation(item) => Message::Continuation(item),
ws::Frame::Close(reason) => ws::Message::Close(reason), Frame::Close(reason) => Message::Close(reason),
_ => panic!(), _ => return Err(ws::ProtocolError::BadOpCode.into()),
}; };
Ok(msg) Ok(msg)
} }
#[actix_rt::test] #[actix_rt::test]
async fn test_simple() { async fn test_simple() {
let ws_service = WsService::new(); let mut srv = test_server(|| {
let mut srv = test_server({ HttpService::build()
let ws_service = ws_service.clone(); .upgrade(fn_factory(|| async {
move || { Ok::<_, Infallible>(WsService::new())
let ws_service = ws_service.clone(); }))
HttpService::build() .finish(|_| async { Ok::<_, Infallible>(Response::not_found()) })
.upgrade(fn_factory(move || future::ok::<_, ()>(ws_service.clone()))) .tcp()
.finish(|_| future::ok::<_, ()>(Response::NotFound()))
.tcp()
}
}) })
.await; .await;
// client service // client service
let mut framed = srv.ws().await.unwrap(); let mut framed = srv.ws().await.unwrap();
framed.send(ws::Message::Text("text".into())).await.unwrap(); framed.send(Message::Text("text".into())).await.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!( let item = framed.next().await.unwrap().unwrap();
item.unwrap().unwrap(), assert_eq!(item, Frame::Text(Bytes::from_static(b"text")));
ws::Frame::Text(Bytes::from_static(b"text"))
); framed.send(Message::Binary("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, Frame::Binary(Bytes::from_static(&b"text"[..])));
framed.send(Message::Ping("text".into())).await.unwrap();
let item = framed.next().await.unwrap().unwrap();
assert_eq!(item, Frame::Pong("text".to_string().into()));
framed framed
.send(ws::Message::Binary("text".into())) .send(Message::Continuation(Item::FirstText("text".into())))
.await .await
.unwrap(); .unwrap();
let (item, mut framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(
item.unwrap().unwrap(), item,
ws::Frame::Binary(Bytes::from_static(&b"text"[..])) Frame::Continuation(Item::FirstText(Bytes::from_static(b"text")))
);
framed.send(ws::Message::Ping("text".into())).await.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Pong("text".to_string().into())
);
framed
.send(ws::Message::Continuation(ws::Item::FirstText(
"text".into(),
)))
.await
.unwrap();
let (item, mut framed) = framed.into_future().await;
assert_eq!(
item.unwrap().unwrap(),
ws::Frame::Continuation(ws::Item::FirstText(Bytes::from_static(b"text")))
); );
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::FirstText( .send(Message::Continuation(Item::FirstText("text".into())))
"text".into()
)))
.await .await
.is_err()); .is_err());
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::FirstBinary( .send(Message::Continuation(Item::FirstBinary("text".into())))
"text".into()
)))
.await .await
.is_err()); .is_err());
framed framed
.send(ws::Message::Continuation(ws::Item::Continue("text".into()))) .send(Message::Continuation(Item::Continue("text".into())))
.await .await
.unwrap(); .unwrap();
let (item, mut framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(
item.unwrap().unwrap(), item,
ws::Frame::Continuation(ws::Item::Continue(Bytes::from_static(b"text"))) Frame::Continuation(Item::Continue(Bytes::from_static(b"text")))
); );
framed framed
.send(ws::Message::Continuation(ws::Item::Last("text".into()))) .send(Message::Continuation(Item::Last("text".into())))
.await .await
.unwrap(); .unwrap();
let (item, mut framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(
item.unwrap().unwrap(), item,
ws::Frame::Continuation(ws::Item::Last(Bytes::from_static(b"text"))) Frame::Continuation(Item::Last(Bytes::from_static(b"text")))
); );
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::Continue("text".into()))) .send(Message::Continuation(Item::Continue("text".into())))
.await .await
.is_err()); .is_err());
assert!(framed assert!(framed
.send(ws::Message::Continuation(ws::Item::Last("text".into()))) .send(Message::Continuation(Item::Last("text".into())))
.await .await
.is_err()); .is_err());
framed framed
.send(ws::Message::Close(Some(ws::CloseCode::Normal.into()))) .send(Message::Close(Some(CloseCode::Normal.into())))
.await .await
.unwrap(); .unwrap();
let (item, _framed) = framed.into_future().await; let item = framed.next().await.unwrap().unwrap();
assert_eq!( assert_eq!(item, Frame::Close(Some(CloseCode::Normal.into())));
item.unwrap().unwrap(),
ws::Frame::Close(Some(ws::CloseCode::Normal.into()))
);
assert!(ws_service.was_polled());
} }

View File

@ -3,6 +3,14 @@
## Unreleased - 2021-xx-xx ## Unreleased - 2021-xx-xx
## 0.4.0-beta.5 - 2021-06-17
* No notable changes.
## 0.4.0-beta.4 - 2021-04-02
* No notable changes.
## 0.4.0-beta.3 - 2021-03-09 ## 0.4.0-beta.3 - 2021-03-09
* No notable changes. * No notable changes.

View File

@ -1,13 +1,13 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.4.0-beta.3" version = "0.4.0-beta.5"
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"
readme = "README.md" readme = "README.md"
keywords = ["http", "web", "framework", "async", "futures"] keywords = ["http", "web", "framework", "async", "futures"]
homepage = "https://actix.rs" homepage = "https://actix.rs"
repository = "https://github.com/actix/actix-web.git" repository = "https://github.com/actix/actix-web.git"
documentation = "https://docs.rs/actix-multipart/" documentation = "https://docs.rs/actix-multipart"
license = "MIT OR Apache-2.0" license = "MIT OR Apache-2.0"
edition = "2018" edition = "2018"
@ -16,19 +16,21 @@ name = "actix_multipart"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "4.0.0-beta.4", default-features = false } actix-web = { version = "4.0.0-beta.7", default-features = false }
actix-utils = "3.0.0-beta.2" actix-utils = "3.0.0"
bytes = "1" bytes = "1"
derive_more = "0.99.5" derive_more = "0.99.5"
httparse = "1.3" futures-core = { version = "0.3.7", default-features = false, features = ["alloc"] }
futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] } futures-util = { version = "0.3.7", default-features = false, features = ["alloc"] }
httparse = "1.3"
local-waker = "0.1"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
twoway = "0.2" twoway = "0.2"
[dev-dependencies] [dev-dependencies]
actix-rt = "2.1" actix-rt = "2.2"
actix-http = "3.0.0-beta.4" actix-http = "3.0.0-beta.7"
tokio = { version = "1", features = ["sync"] } tokio = { version = "1", features = ["sync"] }
tokio-stream = "0.1" tokio-stream = "0.1"

View File

@ -3,11 +3,11 @@
> 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.3)](https://docs.rs/actix-multipart/0.4.0-beta.3) [![Documentation](https://docs.rs/actix-multipart/badge.svg?version=0.4.0-beta.5)](https://docs.rs/actix-multipart/0.4.0-beta.5)
[![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html) [![Version](https://img.shields.io/badge/rustc-1.46+-ab6000.svg)](https://blog.rust-lang.org/2020/03/12/Rust-1.46.html)
![MIT or Apache 2.0 licensed](https://img.shields.io/crates/l/actix-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.3/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.3) [![dependency status](https://deps.rs/crate/actix-multipart/0.4.0-beta.5/status.svg)](https://deps.rs/crate/actix-multipart/0.4.0-beta.5)
[![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)
## Documentation & Resources ## Documentation & Resources

View File

@ -45,11 +45,10 @@ impl ResponseError for MultipartError {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use actix_web::HttpResponse;
#[test] #[test]
fn test_multipart_error() { fn test_multipart_error() {
let resp: HttpResponse = MultipartError::Boundary.error_response(); let resp = MultipartError::Boundary.error_response();
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
} }

View File

@ -1,21 +1,22 @@
//! Multipart payload support //! Multipart payload support
use actix_utils::future::{ready, Ready};
use actix_web::{dev::Payload, Error, FromRequest, HttpRequest}; use actix_web::{dev::Payload, Error, FromRequest, HttpRequest};
use futures_util::future::{ok, Ready};
use crate::server::Multipart; use crate::server::Multipart;
/// Get request's payload as multipart stream /// Get request's payload as multipart stream.
/// ///
/// Content-type: multipart/form-data; /// Content-type: multipart/form-data;
/// ///
/// ## Server example /// ## Server example
/// ///
/// ```rust /// ```
/// use futures_util::stream::{Stream, StreamExt};
/// use actix_web::{web, HttpResponse, Error}; /// use actix_web::{web, HttpResponse, Error};
/// use actix_multipart as mp; /// use actix_multipart::Multipart;
/// use futures_util::stream::StreamExt as _;
/// ///
/// async fn index(mut payload: mp::Multipart) -> Result<HttpResponse, Error> { /// async fn index(mut payload: Multipart) -> Result<HttpResponse, Error> {
/// // iterate over multipart stream /// // iterate over multipart stream
/// while let Some(item) = payload.next().await { /// while let Some(item) = payload.next().await {
/// let mut field = item?; /// let mut field = item?;
@ -25,9 +26,9 @@ use crate::server::Multipart;
/// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk?)); /// println!("-- CHUNK: \n{:?}", std::str::from_utf8(&chunk?));
/// } /// }
/// } /// }
///
/// Ok(HttpResponse::Ok().into()) /// Ok(HttpResponse::Ok().into())
/// } /// }
/// # fn main() {}
/// ``` /// ```
impl FromRequest for Multipart { impl FromRequest for Multipart {
type Error = Error; type Error = Error;
@ -36,9 +37,9 @@ impl FromRequest for Multipart {
#[inline] #[inline]
fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future { fn from_request(req: &HttpRequest, payload: &mut Payload) -> Self::Future {
ok(match Multipart::boundary(req.headers()) { ready(Ok(match Multipart::boundary(req.headers()) {
Ok(boundary) => Multipart::from_boundary(boundary, payload.take()), Ok(boundary) => Multipart::from_boundary(boundary, payload.take()),
Err(err) => Multipart::from_error(err), Err(err) => Multipart::from_error(err),
}) }))
} }
} }

View File

@ -1,4 +1,4 @@
//! Multipart payload support //! Multipart response payload support.
use std::cell::{Cell, RefCell, RefMut}; use std::cell::{Cell, RefCell, RefMut};
use std::convert::TryFrom; use std::convert::TryFrom;
@ -8,12 +8,12 @@ use std::rc::Rc;
use std::task::{Context, Poll}; use std::task::{Context, Poll};
use std::{cmp, fmt}; use std::{cmp, fmt};
use bytes::{Bytes, BytesMut};
use futures_util::stream::{LocalBoxStream, Stream, StreamExt};
use actix_utils::task::LocalWaker;
use actix_web::error::{ParseError, PayloadError}; use actix_web::error::{ParseError, PayloadError};
use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue}; use actix_web::http::header::{self, ContentDisposition, HeaderMap, HeaderName, HeaderValue};
use bytes::{Bytes, BytesMut};
use futures_core::stream::{LocalBoxStream, Stream};
use futures_util::stream::StreamExt as _;
use local_waker::LocalWaker;
use crate::error::MultipartError; use crate::error::MultipartError;

13
actix-test/CHANGES.md Normal file
View File

@ -0,0 +1,13 @@
# Changes
## Unreleased - 2021-xx-xx
## 0.1.0-beta.2 - 2021-04-17
* No significant changes from `0.1.0-beta.1`.
## 0.1.0-beta.1 - 2021-04-02
* Move integration testing structs from `actix-web`. [#2112]
[#2112]: https://github.com/actix/actix-web/pull/2112

38
actix-test/Cargo.toml Normal file
View File

@ -0,0 +1,38 @@
[package]
name = "actix-test"
version = "0.1.0-beta.2"
authors = [
"Nikolay Kim <fafhrd91@gmail.com>",
"Rob Ede <robjtede@icloud.com>",
]
edition = "2018"
description = "Integration testing tools for Actix Web applications"
license = "MIT OR Apache-2.0"
[features]
default = []
# rustls
rustls = ["tls-rustls", "actix-http/rustls"]
# openssl
openssl = ["tls-openssl", "actix-http/openssl"]
[dependencies]
actix-codec = "0.4.0"
actix-http = "3.0.0-beta.7"
actix-http-test = { version = "3.0.0-beta.4", features = [] }
actix-service = "2.0.0"
actix-utils = "3.0.0"
actix-web = { version = "4.0.0-beta.7", default-features = false, features = ["cookies"] }
actix-rt = "2.1"
awc = { version = "3.0.0-beta.6", default-features = false, features = ["cookies"] }
futures-core = { version = "0.3.7", default-features = false, features = ["std"] }
futures-util = { version = "0.3.7", default-features = false, features = [] }
log = "0.4"
serde = { version = "1", features = ["derive"] }
serde_json = "1"
serde_urlencoded = "0.7"
tls-openssl = { package = "openssl", version = "0.10.9", optional = true }
tls-rustls = { package = "rustls", version = "0.19.0", optional = true }

1
actix-test/LICENSE-APACHE Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-APACHE

1
actix-test/LICENSE-MIT Symbolic link
View File

@ -0,0 +1 @@
../LICENSE-MIT

519
actix-test/src/lib.rs Normal file
View File

@ -0,0 +1,519 @@
//! Integration testing tools for Actix Web applications.
//!
//! The main integration testing tool is [`TestServer`]. It spawns a real HTTP server on an
//! unused port and provides methods that use a real HTTP client. Therefore, it is much closer to
//! real-world cases than using `init_service`, which skips HTTP encoding and decoding.
//!
//! # Examples
//! ```
//! use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
//!
//! #[get("/")]
//! async fn my_handler() -> Result<impl Responder, Error> {
//! Ok(HttpResponse::Ok())
//! }
//!
//! #[actix_rt::test]
//! async fn test_example() {
//! let srv = actix_test::start(||
//! App::new().service(my_handler)
//! );
//!
//! let req = srv.get("/");
//! let res = req.send().await.unwrap();
//!
//! assert!(res.status().is_success());
//! }
//! ```
#[cfg(feature = "openssl")]
extern crate tls_openssl as openssl;
#[cfg(feature = "rustls")]
extern crate tls_rustls as rustls;
use std::{error::Error as StdError, fmt, net, sync::mpsc, thread, time};
use actix_codec::{AsyncRead, AsyncWrite, Framed};
pub use actix_http::test::TestBuffer;
use actix_http::{
http::{HeaderMap, Method},
ws, HttpService, Request, Response,
};
use actix_service::{map_config, IntoServiceFactory, ServiceFactory, ServiceFactoryExt as _};
use actix_web::{
dev::{AppConfig, MessageBody, Server, Service},
rt, web, Error,
};
use awc::{error::PayloadError, Client, ClientRequest, ClientResponse, Connector};
use futures_core::Stream;
pub use actix_http_test::unused_addr;
pub use actix_web::test::{
call_service, default_service, init_service, load_stream, ok_service, read_body,
read_body_json, read_response, read_response_json, TestRequest,
};
/// Start default [`TestServer`].
///
/// # Examples
/// ```
/// use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
///
/// #[get("/")]
/// async fn my_handler() -> Result<impl Responder, Error> {
/// Ok(HttpResponse::Ok())
/// }
///
/// #[actix_rt::test]
/// async fn test_example() {
/// let srv = actix_test::start(||
/// App::new().service(my_handler)
/// );
///
/// let req = srv.get("/");
/// let res = req.send().await.unwrap();
///
/// assert!(res.status().is_success());
/// }
/// ```
pub fn start<F, I, S, B>(factory: F) -> TestServer
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{
start_with(TestServerConfig::default(), factory)
}
/// Start test server with custom configuration
///
/// Check [`TestServerConfig`] docs for configuration options.
///
/// # Examples
/// ```
/// use actix_web::{get, web, test, App, HttpResponse, Error, Responder};
///
/// #[get("/")]
/// async fn my_handler() -> Result<impl Responder, Error> {
/// Ok(HttpResponse::Ok())
/// }
///
/// #[actix_rt::test]
/// async fn test_example() {
/// let srv = actix_test::start_with(actix_test::config().h1(), ||
/// App::new().service(my_handler)
/// );
///
/// let req = srv.get("/");
/// let res = req.send().await.unwrap();
///
/// assert!(res.status().is_success());
/// }
/// ```
pub fn start_with<F, I, S, B>(cfg: TestServerConfig, factory: F) -> TestServer
where
F: Fn() -> I + Send + Clone + 'static,
I: IntoServiceFactory<S, Request>,
S: ServiceFactory<Request, Config = AppConfig> + 'static,
S::Error: Into<Error> + 'static,
S::InitError: fmt::Debug,
S::Response: Into<Response<B>> + 'static,
<S::Service as Service<Request>>::Future: 'static,
B: MessageBody + 'static,
B::Error: Into<Box<dyn StdError>>,
{
let (tx, rx) = mpsc::channel();
let tls = match cfg.stream {
StreamType::Tcp => false,
#[cfg(feature = "openssl")]
StreamType::Openssl(_) => true,
#[cfg(feature = "rustls")]
StreamType::Rustls(_) => true,
};
// run server in separate thread
thread::spawn(move || {
let sys = rt::System::new();
let tcp = net::TcpListener::bind("127.0.0.1:0").unwrap();
let local_addr = tcp.local_addr().unwrap();
let factory = factory.clone();
let srv_cfg = cfg.clone();
let timeout = cfg.client_timeout;
let builder = Server::build().workers(1).disable_signals();
let srv = match srv_cfg.stream {
StreamType::Tcp => match srv_cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.h1(map_config(fac, move |_| app_cfg.clone()))
.tcp()
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.h2(map_config(fac, move |_| app_cfg.clone()))
.tcp()
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.finish(map_config(fac, move |_| app_cfg.clone()))
.tcp()
}),
},
#[cfg(feature = "openssl")]
StreamType::Openssl(acceptor) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.h1(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone())
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.h2(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone())
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.finish(map_config(fac, move |_| app_cfg.clone()))
.openssl(acceptor.clone())
}),
},
#[cfg(feature = "rustls")]
StreamType::Rustls(config) => match cfg.tp {
HttpVer::Http1 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.h1(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone())
}),
HttpVer::Http2 => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.h2(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone())
}),
HttpVer::Both => builder.listen("test", tcp, move || {
let app_cfg =
AppConfig::__priv_test_new(false, local_addr.to_string(), local_addr);
let fac = factory()
.into_factory()
.map_err(|err| err.into().error_response());
HttpService::build()
.client_timeout(timeout)
.finish(map_config(fac, move |_| app_cfg.clone()))
.rustls(config.clone())
}),
},
}
.unwrap();
sys.block_on(async {
let srv = srv.run();
tx.send((rt::System::current(), srv, local_addr)).unwrap();
});
sys.run()
});
let (system, server, addr) = rx.recv().unwrap();
let client = {
let connector = {
#[cfg(feature = "openssl")]
{
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
builder.set_verify(SslVerifyMode::NONE);
let _ = builder
.set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
.ssl(builder.build())
}
#[cfg(not(feature = "openssl"))]
{
Connector::new()
.conn_lifetime(time::Duration::from_secs(0))
.timeout(time::Duration::from_millis(30000))
}
};
Client::builder().connector(connector).finish()
};
TestServer {
addr,
client,
system,
tls,
server,
}
}
#[derive(Debug, Clone)]
enum HttpVer {
Http1,
Http2,
Both,
}
#[derive(Clone)]
enum StreamType {
Tcp,
#[cfg(feature = "openssl")]
Openssl(openssl::ssl::SslAcceptor),
#[cfg(feature = "rustls")]
Rustls(rustls::ServerConfig),
}
/// Create default test server config.
pub fn config() -> TestServerConfig {
TestServerConfig::default()
}
#[derive(Clone)]
pub struct TestServerConfig {
tp: HttpVer,
stream: StreamType,
client_timeout: u64,
}
impl Default for TestServerConfig {
fn default() -> Self {
TestServerConfig::new()
}
}
impl TestServerConfig {
/// Create default server configuration
pub(crate) fn new() -> TestServerConfig {
TestServerConfig {
tp: HttpVer::Both,
stream: StreamType::Tcp,
client_timeout: 5000,
}
}
/// Accept HTTP/1.1 only.
pub fn h1(mut self) -> Self {
self.tp = HttpVer::Http1;
self
}
/// Accept HTTP/2 only.
pub fn h2(mut self) -> Self {
self.tp = HttpVer::Http2;
self
}
/// Accept secure connections via OpenSSL.
#[cfg(feature = "openssl")]
pub fn openssl(mut self, acceptor: openssl::ssl::SslAcceptor) -> Self {
self.stream = StreamType::Openssl(acceptor);
self
}
/// Accept secure connections via Rustls.
#[cfg(feature = "rustls")]
pub fn rustls(mut self, config: rustls::ServerConfig) -> Self {
self.stream = StreamType::Rustls(config);
self
}
/// Set client timeout in milliseconds for first request.
pub fn client_timeout(mut self, val: u64) -> Self {
self.client_timeout = val;
self
}
}
/// A basic HTTP server controller that simplifies the process of writing integration tests for
/// Actix Web applications.
///
/// See [`start`] for usage example.
pub struct TestServer {
addr: net::SocketAddr,
client: awc::Client,
system: rt::System,
tls: bool,
server: Server,
}
impl TestServer {
/// Construct test server url
pub fn addr(&self) -> net::SocketAddr {
self.addr
}
/// Construct test server url
pub fn url(&self, uri: &str) -> String {
let scheme = if self.tls { "https" } else { "http" };
if uri.starts_with('/') {
format!("{}://localhost:{}{}", scheme, self.addr.port(), uri)
} else {
format!("{}://localhost:{}/{}", scheme, self.addr.port(), uri)
}
}
/// Create `GET` request.
pub fn get(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.get(self.url(path.as_ref()).as_str())
}
/// Create `POST` request.
pub fn post(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.post(self.url(path.as_ref()).as_str())
}
/// Create `HEAD` request.
pub fn head(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.head(self.url(path.as_ref()).as_str())
}
/// Create `PUT` request.
pub fn put(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.put(self.url(path.as_ref()).as_str())
}
/// Create `PATCH` request.
pub fn patch(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.patch(self.url(path.as_ref()).as_str())
}
/// Create `DELETE` request.
pub fn delete(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.delete(self.url(path.as_ref()).as_str())
}
/// Create `OPTIONS` request.
pub fn options(&self, path: impl AsRef<str>) -> ClientRequest {
self.client.options(self.url(path.as_ref()).as_str())
}
/// Connect request with given method and path.
pub fn request(&self, method: Method, path: impl AsRef<str>) -> ClientRequest {
self.client.request(method, path.as_ref())
}
pub async fn load_body<S>(
&mut self,
mut response: ClientResponse<S>,
) -> Result<web::Bytes, PayloadError>
where
S: Stream<Item = Result<web::Bytes, PayloadError>> + Unpin + 'static,
{
response.body().limit(10_485_760).await
}
/// Connect to WebSocket server at a given path.
pub async fn ws_at(
&mut self,
path: &str,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
let url = self.url(path);
let connect = self.client.ws(url).connect();
connect.await.map(|(_, framed)| framed)
}
/// Connect to a WebSocket server.
pub async fn ws(
&mut self,
) -> Result<Framed<impl AsyncRead + AsyncWrite, ws::Codec>, awc::error::WsClientError> {
self.ws_at("/").await
}
/// Get default HeaderMap of Client.
///
/// Returns Some(&mut HeaderMap) when Client object is unique
/// (No other clone of client exists at the same time).
pub fn client_headers(&mut self) -> Option<&mut HeaderMap> {
self.client.headers()
}
/// Gracefully stop HTTP server.
pub async fn stop(self) {
self.server.stop(true).await;
self.system.stop();
rt::time::sleep(time::Duration::from_millis(100)).await;
}
}
impl Drop for TestServer {
fn drop(&mut self) {
self.system.stop()
}
}

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