1
0
mirror of https://github.com/fafhrd91/actix-web synced 2025-07-04 09:56:22 +02:00

Compare commits

...

90 Commits

Author SHA1 Message Date
b2934ad8d2 prep actix-file release 2019-11-06 11:25:26 -08:00
f7f410d033 fix test order dep 2019-11-06 11:20:47 -08:00
885ff7396e prepare actox-http release 2019-11-06 10:35:13 -08:00
61b38e8d0d Increase timeouts in test-server (#1153) 2019-11-06 06:09:22 -08:00
edcde67076 Fix escaping/encoding problems in Content-Disposition header (#1151)
* Fix filename encoding in Content-Disposition of acitx_files::NamedFile

* Add more comments on how to use Content-Disposition header properly & Fix some trivial problems

* Improve Content-Disposition filename(*) parameters of actix_files::NamedFile

* Tweak Content-Disposition parse to accept empty param value in quoted-string

* Fix typos in comments in .../content_disposition.rs (pointed out by @JohnTitor)

* Update CHANGES.md

* Update CHANGES.md again
2019-11-06 06:08:37 -08:00
f0612f7570 awc: Add support for setting query from Serialize type for client request (#1130)
Signed-off-by: Jonathas-Conceicao <jadoliveira@inf.ufpel.edu.br>
2019-10-26 08:27:14 +03:00
ace98e3a1e support Host guards when Host header is unset (#1129) 2019-10-15 05:05:54 +06:00
1ca9d87f0a prep actix-web-codegen release 2019-10-14 21:35:53 +06:00
967f965405 Update syn & quote to 1.0 (#1133)
* chore(actix-web-codegen): Upgrade syn and quote to 1.0

* feat(actix-web-codegen): Generate better error message

* doc(actix-web-codegen): Update CHANGES.md

* fix: Build with stable rust
2019-10-14 21:34:17 +06:00
062e51e8ce prep actix-file release 2019-10-14 21:26:26 +06:00
a48e616def feat(files): add possibility to redirect to slash-ended path (#1134)
When accessing to a folder without a final slash, the index file will be loaded ok, but if it has
references (like a css or an image in an html file) these resources won't be loaded correctly if
they are using relative paths. In order to solve this, this PR adds the possibility to detect
folders without a final slash and make a 302 redirect to mitigate this issue. The behavior is off by
default. We're adding a new method called `redirect_to_slash_directory` which can be used to enable
this behavior.
2019-10-14 21:23:15 +06:00
effa96f5e4 Removed httpcode 'MovedPermanenty'. (#1128) 2019-10-12 06:45:12 +06:00
cc0b4be5b7 Fix typo in response.rs body() comment (#1126)
Fixes https://github.com/actix/actix-web/issues/1125
2019-10-09 19:11:55 +06:00
a464ffc23d prepare actix-files release 2019-10-08 10:13:16 +06:00
4de2e8a898 [actix-files] Allow user defined guards for NamedFile (actix#1113) (#1115)
* [actix-files] remove request method checks from NamedFile

* [actix-files] added custom guard checks to FilesService

* [actix-files] modify method check tests (NamedFile -> Files)

* [actix-files] add test for custom guards in Files

* [actix-files] update changelog
2019-10-08 10:09:40 +06:00
0f09415469 Convert documentation examples to Rust 2018 edition (#1120)
* Convert types::query examples to rust-2018 edition

* Convert types::json examples to rust-2018 edition

* Convert types::path examples to rust-2018 edition

* Convert types::form examples to rust-2018 edition

* Convert rest of the examples to rust-2018 edition.
2019-10-07 11:29:11 +06:00
f089cf185b Let ResponseError render w/ 'text/plain; charset=utf-8' header (#1118) (#1119)
* Let ResponseError render w/ 'text/plain; charset=utf-8' header (#1118)

Trait ResponseError originally render Error messages with header
`text/plain` , which causes browsers (i.e. Firefox 70.0) with
Non-English locale unable to render UTF-8 responses with non-English
characters correctly. i.e. emoji.

This fix solved this problem by specifying the charset of `text/plain`
as utf-8, which is the default charset in rust.

Before actix-web consider to support other charsets, this hotfix is
 enough.

Test case:

fn test() -> Result<String, actix_web::Error> {
    Err(actix_web::error::ErrorForbidden("😋test"))
}

* Update actix-http/CHANGES.md for #1118
2019-10-07 10:56:24 +06:00
15d3c1ae81 Update docs of guard.rs (#1116)
* Update guard.rs
2019-10-07 12:05:17 +09:00
fba31d4e0a Expose ContentDisposition in actix-multipart to fix broken doc link (#1114)
* Expose ContentDisposition in actix-multipart to fix broken doc link

* Revert "Expose ContentDisposition in actix-multipart to fix broken doc link"

This reverts commit e90d71d16c.

* Unhide actix-http::header::common docs

These types are used in other exported documented interfaces and create
broken links if not documented.
See `actix_multipart::Field.content_disposition`
2019-10-02 09:48:25 +06:00
f81ae37677 Add From<Payload> for crate::dev::Payload (#1110)
* Add From<Payload> for crate::dev::Payload

* Make dev::Payload field of Payload public and add into_inner method

* Add changelog entry
2019-10-01 14:05:38 +06:00
5169d306ae update ConnectionInfo.remote() doc string 2019-09-27 07:03:12 +06:00
4f3e97fff8 prepare actix-web release 2019-09-25 15:39:09 +06:00
3ff01a9fc4 Add changelog entry for #1101 (#1102) 2019-09-25 15:35:28 +06:00
3d4e45a0e5 prepare release 2019-09-25 15:30:20 +06:00
c659c33919 Feature uds: Add listen_uds to ServerBuilder (#1085)
Allows using an existing Unix Listener instead of binding to a path.
Useful for when running as a daemon under systemd.

Change-Id: I54a0e78c321d8b7a9ded381083217af590e9a7fa
2019-09-25 15:16:51 +06:00
959f7754b2 Merge pull request #1101 from actix/add-awc-get-head-methods
Add remaining getter methods from private head field
2019-09-25 10:23:23 +02:00
23f04c4f38 Add remaining getter methods from private head field 2019-09-25 08:50:45 +02:00
d9af8f66ba Use actix-testing for testing utils 2019-09-25 10:28:41 +06:00
aa39b8ca6f Add support for serde_json::Value to be passed as argument to ResponseBuilder.body() (#1096)
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()

* Update actix-http/CHANGES.md
2019-09-25 09:33:52 +06:00
58c7065f08 Implement register_data method on Resource and Scope. (#1094)
* Implement `register_data` method on `Resource` and `Scope`.

* Split Scope::register_data tests out from Scope::data tests.

* CHANGES.md: Mention {Scope,Resource}::register_data.
2019-09-18 06:36:39 +06:00
b3783b403e Merge branch 'master' of github.com:actix/actix-web 2019-09-17 21:46:45 +06:00
e4503046de Do not override current System 2019-09-17 21:45:06 +06:00
32a1c36597 Make UrlencodedError::Overflow more informative (#1089) 2019-09-17 06:58:04 +06:00
7c9f9afc46 Add ability to use Infallible as HttpResponse error type (#1093)
* Add `std::convert::Infallible` implementantion for `ResponseError`

* Add from `std::convert::Infallible` to `Error`

* Remove `ResponseError` implementantion for `Infallible`

* Remove useless docs

* Better comment

* Update changelog

* Update actix_http::changelog
2019-09-17 06:57:38 +06:00
c1f99e0775 Remove mem::uninitialized() (#1090) 2019-09-16 07:52:23 +09:00
a32573bb58 Allow to re-construct ServiceRequest from HttpRequest and Payload #1088 2019-09-13 11:56:24 +06:00
e35d930ef9 prepare releases 2019-09-12 21:58:08 +06:00
60b7aebd0a fmt & clippy 2019-09-12 21:52:46 +06:00
45d2fd4299 export frozen request related types; refactor code layout 2019-09-12 10:40:56 +06:00
71f8577713 prepare awc release 2019-09-11 20:13:28 +06:00
043f763c51 prepare actix-http release 2019-09-11 20:07:39 +06:00
8873e9b39e Added FrozenClientRequest for easier retrying HTTP calls (#1064)
* Initial commit

* Added extra_headers

* Added freeze() method to ClientRequest which produces a 'read-only' copy of a request suitable for retrying the send operation

* Additional methods for FrozenClientRequest

* Fix

* Increased crates versions

* Fixed a unit test. Added one more unit test.

* Added RequestHeaderWrapper

* Small fixes

* Renamed RequestHeadWrapper->RequestHeadType

* Updated CHANGES.md files

* Small fix

* Small changes

* Removed *_extra methods from Connection trait

* Added FrozenSendBuilder

* Added FrozenSendBuilder

* Minor fix

* Replaced impl Future with concrete Future implementation

* Small renaming

* Renamed Send->SendBody
2019-09-10 10:29:32 +06:00
5e8f1c338c fix h2 not using error response (#1080)
* fix h2 not using error response

* add fix change log

* fix h2 service error tests
2019-09-09 16:24:57 +06:00
1d96ae9bc3 actix-multipart: Correctly parse multipart body which does not end in CRLF (#1042)
* Correctly parse multipart body which does not end in CRLF

* Add in an eof guard for extra safety
2019-09-09 13:58:00 +06:00
8d61fe0925 Ensure that awc::ws::WebsocketsRequest sets the Host header (#1070)
* Ensure that awc::ws::WebsocketsRequest sets the Host header before connecting.

* Make sure to check if headers already have a HOST value before setting

* Update CHANGES.md to reflect WebSocket client update.
2019-09-09 12:27:13 +06:00
8a9fcddb3c Condition middleware (#1075)
* add condition middleware

* write tests

* update changes

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>

* Update src/middleware/condition.rs

Co-Authored-By: Yuki Okushi <huyuumi.dev@gmail.com>
2019-09-09 12:26:38 +06:00
c9400456f6 update actix-connect ver 2019-09-02 15:20:28 -07:00
63ddd30ee4 on_connect result isnt added to request extensions for http2 requests #1009 2019-09-01 13:15:02 +06:00
bae29897d6 prep actix-web release 2019-08-29 09:36:16 +06:00
616981ecf9 clear extensions before reclaiming HttpRequests in their pool (#1063)
Issue #1062
2019-08-29 09:35:05 +06:00
98bf8ab098 enable rust-tls feature for actix_web::client #1045 2019-08-28 21:40:24 +06:00
c193137905 actix_web::test::TestRequest::set_form (#1058) 2019-08-28 21:32:17 +06:00
a07cdd6533 Data::into_inner 2019-08-27 17:25:25 +01:00
61e492e7e3 Prepare actix-multipart 0.1.3 release 2019-08-18 10:39:22 +09:00
23d768a77b Add explicit dyns (#1041)
* Add explicit `dyn`s

* Remove unnecessary lines
2019-08-17 02:45:44 +09:00
87b7162473 chore(readme): fix copy paste error (#1040)
Fix actix-cors README
2019-08-16 09:21:30 +09:00
979c4d44f4 update awc dep 2019-08-13 12:41:26 -07:00
5d248cad89 prep release 2019-08-13 12:28:05 -07:00
b1cb72d088 update url crate 2019-08-13 11:03:24 -07:00
55179d6ab2 update dependencies 2019-08-13 10:48:11 -07:00
192dfff680 prepare actix-http 0.2.9 release 2019-08-13 15:20:29 +02:00
915010e733 Fixes a bug in OpenWaitingConnection where the h2 flow would panic a future (#1031) 2019-08-13 14:55:04 +02:00
dbe4c9ffb5 Replace deprecated methods in actix_files (#1027)
* Bump up mime_guess to 2.0.1

* Replace deprecated methods

* Update CHANGE.md
2019-08-12 05:43:29 +09:00
0ee69671ba Update nightly to 2019-08-10 (#1028) 2019-08-12 04:00:13 +09:00
80e1d16ab8 Merge pull request #1023 from lukaslueg/byteorder_removed
Remove byteorder-dependency
2019-08-07 12:28:23 -03:00
b70de5b991 Update CHANGES.md 2019-08-07 16:43:03 +02:00
0b9e692298 Remove byteorder-dependency 2019-08-06 18:32:36 +02:00
cf1a60cb3a prepare awc release 2019-08-01 15:41:14 -07:00
0d15861e23 prepare actix-http release 2019-08-01 15:26:30 -07:00
cb19ebfe0c add rustls support for actix-http and awc (#998)
* add rustls support for actix-http and awc

* fix features conflict

* remove unnecessary duplication

* test server with rust-tls

* fix

* test rustls

* awc rustls test

* format

* tests

* fix dependencies

* fixes and add changes

* remove test-server and Cargo.toml dev-dependencies changes

* cargo fmt
2019-07-31 13:02:56 -07:00
0d9ea41047 update min rust version 2019-07-31 06:49:46 -07:00
e9b4aa205f Merge branch 'master' of github.com:actix/actix-web 2019-07-30 08:00:57 -07:00
7674f1173c fix awc client panic #1016 2019-07-30 08:00:46 -07:00
511026cab0 Allow HeaderMap to be cloned (#1014)
* Allow HeaderMap to be cloned

* Add entry to changelog
2019-07-29 08:11:23 +04:00
81ab37f235 Fix two dyn warnings (#1015) 2019-07-29 08:10:33 +04:00
6f2049ba9b Fix typo 2019-07-25 12:54:59 +01:00
52372fcbea actix-files: "Specified path is not a directory" error now includes the path (#1004) 2019-07-23 06:41:58 +06:00
f3751d83f8 Modify response body only if encoder is not None #997 2019-07-22 11:35:00 +06:00
b0b462581b update CHANGES.md for Form impl Responder 2019-07-20 14:46:46 +01:00
8f48ed2597 impl Responder for Form 2019-07-20 14:46:46 +01:00
c96068e78d bump version 2019-07-20 11:46:21 +06:00
7bca1f7d8d Allow to disable Content-Disposition header #686 2019-07-20 11:43:49 +06:00
3618a84164 update changes 2019-07-20 11:27:21 +06:00
03ca408e94 add support for specifying protocols on websocket handshake (#835)
* add support for specifying protocols on websocket handshake

* separated the handshake function with and without protocols
changed protocols type from Vec<&str> to [&str]
2019-07-20 11:22:06 +06:00
e53e9c8ba3 Add the start_with_addr() function, to obtain an addr to the target websocket actor (#988) 2019-07-20 11:17:58 +06:00
941241c5f0 Remove unneeded actix-utils dependency 2019-07-20 10:50:36 +06:00
f8320fedd8 add note about Query decoding (#992) 2019-07-19 17:37:49 +06:00
c808364c07 make Query payload public (#991) 2019-07-19 15:47:44 +06:00
cccd829656 update changes 2019-07-19 11:07:52 +06:00
3650f6d7b8 Re-implement Host predicate (#989)
* update HostGuard implementation

* update/add tests for new HostGuard implementation
2019-07-19 10:28:43 +06:00
102 changed files with 4281 additions and 1485 deletions

View File

@ -10,9 +10,9 @@ matrix:
include: include:
- rust: stable - rust: stable
- rust: beta - rust: beta
- rust: nightly-2019-04-02 - rust: nightly-2019-08-10
allow_failures: allow_failures:
- rust: nightly-2019-04-02 - rust: nightly-2019-08-10
env: env:
global: global:
@ -25,7 +25,7 @@ before_install:
- sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev - sudo apt-get install -y openssl libssl-dev libelf-dev libdw-dev cmake gcc binutils-dev libiberty-dev
before_cache: | before_cache: |
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-04-02" ]]; then if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-08-10" ]]; then
RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --version 0.6.11 cargo-tarpaulin RUSTFLAGS="--cfg procmacro2_semver_exempt" cargo install --version 0.6.11 cargo-tarpaulin
fi fi
@ -37,6 +37,8 @@ script:
- cargo update - cargo update
- cargo check --all --no-default-features - cargo check --all --no-default-features
- cargo test --all-features --all -- --nocapture - cargo test --all-features --all -- --nocapture
- cd actix-http; cargo test --no-default-features --features="rust-tls" -- --nocapture; cd ..
- cd awc; cargo test --no-default-features --features="rust-tls" -- --nocapture; cd ..
# Upload docs # Upload docs
after_success: after_success:
@ -49,7 +51,7 @@ after_success:
echo "Uploaded documentation" echo "Uploaded documentation"
fi fi
- | - |
if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-04-02" ]]; then if [[ "$TRAVIS_RUST_VERSION" == "nightly-2019-08-10" ]]; then
taskset -c 0 cargo tarpaulin --out Xml --all --all-features taskset -c 0 cargo tarpaulin --out Xml --all --all-features
bash <(curl -s https://codecov.io/bash) bash <(curl -s https://codecov.io/bash)
echo "Uploaded code coverage" echo "Uploaded code coverage"

View File

@ -1,5 +1,67 @@
# Changes # Changes
## [1.0.9] - 2019-xx-xx
### Added
* Add `Payload::into_inner` method and make stored `def::Payload` public. (#1110)
### Changed
* Support `Host` guards when the `Host` header is unset (e.g. HTTP/2 requests) (#1129)
## [1.0.8] - 2019-09-25
### Added
* Add `Scope::register_data` and `Resource::register_data` methods, parallel to
`App::register_data`.
* Add `middleware::Condition` that conditionally enables another middleware
* Allow to re-construct `ServiceRequest` from `HttpRequest` and `Payload`
* Add `HttpServer::listen_uds` for ability to listen on UDS FD rather than path,
which is useful for example with systemd.
### Changed
* Make UrlEncodedError::Overflow more informativve
* Use actix-testing for testing utils
## [1.0.7] - 2019-08-29
### Fixed
* Request Extensions leak #1062
## [1.0.6] - 2019-08-28
### Added
* Re-implement Host predicate (#989)
* Form immplements Responder, returning a `application/x-www-form-urlencoded` response
* Add `into_inner` to `Data`
* Add `test::TestRequest::set_form()` convenience method to automatically serialize data and set
the header in test requests.
### Changed
* `Query` payload made `pub`. Allows user to pattern-match the payload.
* Enable `rust-tls` feature for client #1045
* Update serde_urlencoded to 0.6.1
* Update url to 2.1
## [1.0.5] - 2019-07-18 ## [1.0.5] - 2019-07-18
### Added ### Added

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web" name = "actix-web"
version = "1.0.5" version = "1.0.8"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust." description = "Actix web is a simple, pragmatic and extremely fast web framework for Rust."
readme = "README.md" readme = "README.md"
@ -66,7 +66,7 @@ fail = ["actix-http/fail"]
ssl = ["openssl", "actix-server/ssl", "awc/ssl"] ssl = ["openssl", "actix-server/ssl", "awc/ssl"]
# rustls # rustls
rust-tls = ["rustls", "actix-server/rust-tls"] rust-tls = ["rustls", "actix-server/rust-tls", "awc/rust-tls"]
# unix domain sockets support # unix domain sockets support
uds = ["actix-server/uds"] uds = ["actix-server/uds"]
@ -78,17 +78,18 @@ actix-utils = "0.4.4"
actix-router = "0.1.5" actix-router = "0.1.5"
actix-rt = "0.2.4" actix-rt = "0.2.4"
actix-web-codegen = "0.1.2" actix-web-codegen = "0.1.2"
actix-http = "0.2.7" actix-http = "0.2.9"
actix-server = "0.6.0" actix-server = "0.6.1"
actix-server-config = "0.1.2" actix-server-config = "0.1.2"
actix-testing = "0.1.0"
actix-threadpool = "0.1.1" actix-threadpool = "0.1.1"
awc = { version = "0.2.2", optional = true } awc = { version = "0.2.7", optional = true }
bytes = "0.4" bytes = "0.4"
derive_more = "0.15.0" derive_more = "0.15.0"
encoding_rs = "0.8" encoding_rs = "0.8"
futures = "0.1.25" futures = "0.1.25"
hashbrown = "0.5.0" hashbrown = "0.6.3"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
net2 = "0.2.33" net2 = "0.2.33"
@ -96,18 +97,18 @@ parking_lot = "0.9"
regex = "1.0" regex = "1.0"
serde = { version = "1.0", features=["derive"] } serde = { version = "1.0", features=["derive"] }
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.5.3" serde_urlencoded = "0.6.1"
time = "0.1.42" time = "0.1.42"
url = { version="1.7", features=["query_encoding"] } url = "2.1"
# ssl support # ssl support
openssl = { version="0.10", optional = true } openssl = { version="0.10", optional = true }
rustls = { version = "0.15", optional = true } rustls = { version = "0.15", optional = true }
[dev-dependencies] [dev-dependencies]
actix = { version = "0.8.3" } actix = "0.8.3"
actix-http = { version = "0.2.5", features=["ssl", "brotli", "flate2-zlib"] } actix-connect = "0.2.2"
actix-http-test = { version = "0.2.4", features=["ssl"] } actix-http-test = "0.2.4"
rand = "0.7" rand = "0.7"
env_logger = "0.6" env_logger = "0.6"
serde_derive = "1.0" serde_derive = "1.0"

View File

@ -22,7 +22,7 @@ Actix web is a simple, pragmatic and extremely fast web framework for Rust.
* [API Documentation (0.7)](https://docs.rs/actix-web/0.7.19/actix_web/) * [API Documentation (0.7)](https://docs.rs/actix-web/0.7.19/actix_web/)
* [Chat on gitter](https://gitter.im/actix/actix) * [Chat on gitter](https://gitter.im/actix/actix)
* Cargo package: [actix-web](https://crates.io/crates/actix-web) * Cargo package: [actix-web](https://crates.io/crates/actix-web)
* Minimum supported Rust version: 1.34 or later * Minimum supported Rust version: 1.36 or later
## Example ## Example

View File

@ -1,4 +1,4 @@
# Identity service for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-identity)](https://crates.io/crates/actix-identity) [![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) # Cors Middleware for actix web framework [![Build Status](https://travis-ci.org/actix/actix-web.svg?branch=master)](https://travis-ci.org/actix/actix-web) [![codecov](https://codecov.io/gh/actix/actix-web/branch/master/graph/badge.svg)](https://codecov.io/gh/actix/actix-web) [![crates.io](https://meritbadge.herokuapp.com/actix-cors)](https://crates.io/crates/actix-cors) [![Join the chat at https://gitter.im/actix/actix](https://badges.gitter.im/actix/actix.svg)](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
## Documentation & community resources ## Documentation & community resources

View File

@ -1,10 +1,29 @@
# Changes # Changes
## [0.1.7] - 2019-11-06
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
## [0.1.6] - 2019-10-14
* Add option to redirect to a slash-ended path `Files` #1132
## [0.1.5] - 2019-10-08
* Bump up `mime_guess` crate version to 2.0.1
* Bump up `percent-encoding` crate version to 2.1
* Allow user defined request guards for `Files` #1113
## [0.1.4] - 2019-07-20
* Allow to disable `Content-Disposition` header #686
## [0.1.3] - 2019-06-28 ## [0.1.3] - 2019-06-28
* Do not set `Content-Length` header, let actix-http set it #930 * Do not set `Content-Length` header, let actix-http set it #930
## [0.1.2] - 2019-06-13 ## [0.1.2] - 2019-06-13
* Content-Length is 0 for NamedFile HEAD request #914 * Content-Length is 0 for NamedFile HEAD request #914

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-files" name = "actix-files"
version = "0.1.3" version = "0.1.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Static files support for actix web." description = "Static files support for actix web."
readme = "README.md" readme = "README.md"
@ -18,8 +18,8 @@ name = "actix_files"
path = "src/lib.rs" path = "src/lib.rs"
[dependencies] [dependencies]
actix-web = { version = "1.0.2", default-features = false } actix-web = { version = "1.0.8", default-features = false }
actix-http = "0.2.4" actix-http = "0.2.11"
actix-service = "0.4.1" actix-service = "0.4.1"
bitflags = "1" bitflags = "1"
bytes = "0.4" bytes = "0.4"
@ -27,9 +27,9 @@ futures = "0.1.25"
derive_more = "0.15.0" derive_more = "0.15.0"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
mime_guess = "2.0.0-alpha" mime_guess = "2.0.1"
percent-encoding = "1.0" percent-encoding = "2.1"
v_htmlescape = "0.4" v_htmlescape = "0.4"
[dev-dependencies] [dev-dependencies]
actix-web = { version = "1.0.2", features=["ssl"] } actix-web = { version = "1.0.8", features=["ssl"] }

View File

@ -16,14 +16,16 @@ use actix_web::dev::{
ServiceResponse, ServiceResponse,
}; };
use actix_web::error::{BlockingError, Error, ErrorInternalServerError}; use actix_web::error::{BlockingError, Error, ErrorInternalServerError};
use actix_web::http::header::DispositionType; use actix_web::guard::Guard;
use actix_web::http::header::{self, DispositionType};
use actix_web::http::Method;
use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder}; use actix_web::{web, FromRequest, HttpRequest, HttpResponse, Responder};
use bytes::Bytes; use bytes::Bytes;
use futures::future::{ok, Either, FutureResult}; use futures::future::{ok, Either, FutureResult};
use futures::{Async, Future, Poll, Stream}; use futures::{Async, Future, Poll, Stream};
use mime; use mime;
use mime_guess::get_mime_type; use mime_guess::from_ext;
use percent_encoding::{utf8_percent_encode, DEFAULT_ENCODE_SET}; use percent_encoding::{utf8_percent_encode, CONTROLS};
use v_htmlescape::escape as escape_html_entity; use v_htmlescape::escape as escape_html_entity;
mod error; mod error;
@ -42,7 +44,7 @@ type HttpNewService = BoxedNewService<(), ServiceRequest, ServiceResponse, Error
/// the type `application/octet-stream`. /// the type `application/octet-stream`.
#[inline] #[inline]
pub fn file_extension_to_mime(ext: &str) -> mime::Mime { pub fn file_extension_to_mime(ext: &str) -> mime::Mime {
get_mime_type(ext) from_ext(ext).first_or_octet_stream()
} }
#[doc(hidden)] #[doc(hidden)]
@ -144,7 +146,7 @@ impl Directory {
// show file url as relative to static path // show file url as relative to static path
macro_rules! encode_file_url { macro_rules! encode_file_url {
($path:ident) => { ($path:ident) => {
utf8_percent_encode(&$path.to_string_lossy(), DEFAULT_ENCODE_SET) utf8_percent_encode(&$path.to_string_lossy(), CONTROLS)
}; };
} }
@ -231,10 +233,12 @@ pub struct Files {
directory: PathBuf, directory: PathBuf,
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
redirect_to_slash: bool,
default: Rc<RefCell<Option<Rc<HttpNewService>>>>, default: Rc<RefCell<Option<Rc<HttpNewService>>>>,
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<Box<dyn Guard>>>,
} }
impl Clone for Files { impl Clone for Files {
@ -243,11 +247,13 @@ impl Clone for Files {
directory: self.directory.clone(), directory: self.directory.clone(),
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: self.default.clone(), default: self.default.clone(),
renderer: self.renderer.clone(), renderer: self.renderer.clone(),
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(),
guards: self.guards.clone(),
} }
} }
} }
@ -261,7 +267,7 @@ impl Files {
pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files { pub fn new<T: Into<PathBuf>>(path: &str, dir: T) -> Files {
let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new()); let dir = dir.into().canonicalize().unwrap_or_else(|_| PathBuf::new());
if !dir.is_dir() { if !dir.is_dir() {
log::error!("Specified path is not a directory"); log::error!("Specified path is not a directory: {:?}", dir);
} }
Files { Files {
@ -269,10 +275,12 @@ impl Files {
directory: dir, directory: dir,
index: None, index: None,
show_index: false, show_index: false,
redirect_to_slash: false,
default: Rc::new(RefCell::new(None)), default: Rc::new(RefCell::new(None)),
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,
} }
} }
@ -284,6 +292,14 @@ impl Files {
self self
} }
/// Redirects to a slash-ended path when browsing a directory.
///
/// By default never redirect.
pub fn redirect_to_slash_directory(mut self) -> Self {
self.redirect_to_slash = true;
self
}
/// Set custom directory renderer /// Set custom directory renderer
pub fn files_listing_renderer<F>(mut self, f: F) -> Self pub fn files_listing_renderer<F>(mut self, f: F) -> Self
where where
@ -314,23 +330,41 @@ impl Files {
} }
#[inline] #[inline]
///Specifies whether to use ETag or not. /// Specifies whether to use ETag or not.
/// ///
///Default is true. /// Default is true.
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
} }
#[inline] #[inline]
///Specifies whether to use Last-Modified or not. /// Specifies whether to use Last-Modified or not.
/// ///
///Default is true. /// Default is true.
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
} }
/// Specifies custom guards to use for directory listings and files.
///
/// Default behaviour allows GET and HEAD.
#[inline]
pub fn use_guards<G: Guard + 'static>(mut self, guards: G) -> Self {
self.guards = Some(Rc::new(Box::new(guards)));
self
}
/// Disable `Content-Disposition` header.
///
/// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self {
self.file_flags.remove(named::Flags::CONTENT_DISPOSITION);
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.
pub fn default_handler<F, U>(mut self, f: F) -> Self pub fn default_handler<F, U>(mut self, f: F) -> Self
where where
@ -366,10 +400,10 @@ impl HttpServiceFactory for Files {
} }
impl NewService for Files { impl NewService for Files {
type Config = ();
type Request = ServiceRequest; type Request = ServiceRequest;
type Response = ServiceResponse; type Response = ServiceResponse;
type Error = Error; type Error = Error;
type Config = ();
type Service = FilesService; type Service = FilesService;
type InitError = (); type InitError = ();
type Future = Box<dyn Future<Item = Self::Service, Error = Self::InitError>>; type Future = Box<dyn Future<Item = Self::Service, Error = Self::InitError>>;
@ -379,10 +413,12 @@ impl NewService for Files {
directory: self.directory.clone(), directory: self.directory.clone(),
index: self.index.clone(), index: self.index.clone(),
show_index: self.show_index, show_index: self.show_index,
redirect_to_slash: self.redirect_to_slash,
default: None, default: None,
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(),
}; };
if let Some(ref default) = *self.default.borrow() { if let Some(ref default) = *self.default.borrow() {
@ -405,10 +441,12 @@ pub struct FilesService {
directory: PathBuf, directory: PathBuf,
index: Option<String>, index: Option<String>,
show_index: bool, show_index: bool,
redirect_to_slash: bool,
default: Option<HttpService>, default: Option<HttpService>,
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<Box<dyn Guard>>>,
} }
impl FilesService { impl FilesService {
@ -445,6 +483,25 @@ impl Service for FilesService {
fn call(&mut self, req: ServiceRequest) -> Self::Future { fn call(&mut self, req: ServiceRequest) -> Self::Future {
// let (req, pl) = req.into_parts(); // let (req, pl) = req.into_parts();
let is_method_valid = if let Some(guard) = &self.guards {
// execute user defined guards
(**guard).check(req.head())
} else {
// default behaviour
match *req.method() {
Method::HEAD | Method::GET => true,
_ => false,
}
};
if !is_method_valid {
return Either::A(ok(req.into_response(
actix_web::HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.body("Request did not meet this resource's requirements."),
)));
}
let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) { let real_path = match PathBufWrp::get_pathbuf(req.match_info().path()) {
Ok(item) => item, Ok(item) => item,
Err(e) => return Either::A(ok(req.error_response(e))), Err(e) => return Either::A(ok(req.error_response(e))),
@ -458,6 +515,16 @@ impl Service for FilesService {
if path.is_dir() { if path.is_dir() {
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::A(ok(req.into_response(
HttpResponse::Found()
.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) {
@ -567,6 +634,7 @@ mod tests {
use bytes::BytesMut; use bytes::BytesMut;
use super::*; use super::*;
use actix_web::guard;
use actix_web::http::header::{ use actix_web::http::header::{
self, ContentDisposition, DispositionParam, DispositionType, self, ContentDisposition, DispositionParam, DispositionType,
}; };
@ -638,6 +706,58 @@ mod tests {
); );
} }
#[test]
fn test_named_file_content_disposition() {
assert!(NamedFile::open("test--").is_err());
let mut file = NamedFile::open("Cargo.toml").unwrap();
{
file.file();
let _f: &File = &file;
}
{
let _f: &mut File = &mut file;
}
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"Cargo.toml\""
);
let file = NamedFile::open("Cargo.toml")
.unwrap()
.disable_content_disposition();
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).unwrap();
assert!(resp.headers().get(header::CONTENT_DISPOSITION).is_none());
}
#[test]
fn test_named_file_non_ascii_file_name() {
let mut file =
NamedFile::from_file(File::open("Cargo.toml").unwrap(), "貨物.toml")
.unwrap();
{
file.file();
let _f: &File = &file;
}
{
let _f: &mut File = &mut file;
}
let req = TestRequest::default().to_http_request();
let resp = file.respond_to(&req).unwrap();
assert_eq!(
resp.headers().get(header::CONTENT_TYPE).unwrap(),
"text/x-toml"
);
assert_eq!(
resp.headers().get(header::CONTENT_DISPOSITION).unwrap(),
"inline; filename=\"貨物.toml\"; filename*=UTF-8''%E8%B2%A8%E7%89%A9.toml"
);
}
#[test] #[test]
fn test_named_file_set_content_type() { fn test_named_file_set_content_type() {
let mut file = NamedFile::open("Cargo.toml") let mut file = NamedFile::open("Cargo.toml")
@ -974,20 +1094,41 @@ mod tests {
} }
#[test] #[test]
fn test_named_file_not_allowed() { fn test_files_not_allowed() {
let file = NamedFile::open("Cargo.toml").unwrap(); let mut srv = test::init_service(App::new().service(Files::new("/", ".")));
let req = TestRequest::default() let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST) .method(Method::POST)
.to_http_request(); .to_request();
let resp = file.respond_to(&req).unwrap();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
let file = NamedFile::open("Cargo.toml").unwrap(); let mut srv = test::init_service(App::new().service(Files::new("/", ".")));
let req = TestRequest::default().method(Method::PUT).to_http_request(); let req = TestRequest::default()
let resp = file.respond_to(&req).unwrap(); .method(Method::PUT)
.uri("/Cargo.toml")
.to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED); assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
} }
#[test]
fn test_files_guards() {
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").use_guards(guard::Post())),
);
let req = TestRequest::default()
.uri("/Cargo.toml")
.method(Method::POST)
.to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test] #[test]
fn test_named_file_content_encoding() { fn test_named_file_content_encoding() {
let mut srv = test::init_service(App::new().wrap(Compress::default()).service( let mut srv = test::init_service(App::new().wrap(Compress::default()).service(
@ -1076,6 +1217,34 @@ mod tests {
assert!(format!("{:?}", bytes).contains("/tests/test.png")); assert!(format!("{:?}", bytes).contains("/tests/test.png"));
} }
#[test]
fn test_redirect_to_slash_directory() {
// should not redirect if no index
let mut srv = test::init_service(
App::new().service(Files::new("/", ".").redirect_to_slash_directory()),
);
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
// should redirect if index present
let mut srv = test::init_service(
App::new().service(
Files::new("/", ".")
.index_file("test.png")
.redirect_to_slash_directory(),
),
);
let req = TestRequest::with_uri("/tests").to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::FOUND);
// should not redirect if the path is wrong
let req = TestRequest::with_uri("/not_existing").to_request();
let resp = test::call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::NOT_FOUND);
}
#[test] #[test]
fn test_static_files_bad_directory() { fn test_static_files_bad_directory() {
let _st: Files = Files::new("/", "missing"); let _st: Files = Files::new("/", "missing");

View File

@ -9,13 +9,13 @@ use std::os::unix::fs::MetadataExt;
use bitflags::bitflags; use bitflags::bitflags;
use mime; use mime;
use mime_guess::guess_mime_type; use mime_guess::from_path;
use actix_http::body::SizedStream; use actix_http::body::SizedStream;
use actix_web::http::header::{ use actix_web::http::header::{
self, ContentDisposition, DispositionParam, DispositionType, self, Charset, ContentDisposition, DispositionParam, DispositionType, ExtendedValue,
}; };
use actix_web::http::{ContentEncoding, Method, StatusCode}; use actix_web::http::{ContentEncoding, StatusCode};
use actix_web::middleware::BodyEncoding; use actix_web::middleware::BodyEncoding;
use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder}; use actix_web::{Error, HttpMessage, HttpRequest, HttpResponse, Responder};
@ -23,9 +23,10 @@ use crate::range::HttpRange;
use crate::ChunkedReadFile; use crate::ChunkedReadFile;
bitflags! { bitflags! {
pub(crate) struct Flags: u32 { pub(crate) struct Flags: u8 {
const ETAG = 0b0000_0001; const ETAG = 0b0000_0001;
const LAST_MD = 0b0000_0010; const LAST_MD = 0b0000_0010;
const CONTENT_DISPOSITION = 0b0000_0100;
} }
} }
@ -40,13 +41,13 @@ impl Default for Flags {
pub struct NamedFile { pub struct NamedFile {
path: PathBuf, path: PathBuf,
file: File, file: File,
modified: Option<SystemTime>,
pub(crate) md: Metadata,
pub(crate) flags: Flags,
pub(crate) status_code: StatusCode,
pub(crate) content_type: mime::Mime, pub(crate) content_type: mime::Mime,
pub(crate) content_disposition: header::ContentDisposition, pub(crate) content_disposition: header::ContentDisposition,
pub(crate) md: Metadata,
modified: Option<SystemTime>,
pub(crate) encoding: Option<ContentEncoding>, pub(crate) encoding: Option<ContentEncoding>,
pub(crate) status_code: StatusCode,
pub(crate) flags: Flags,
} }
impl NamedFile { impl NamedFile {
@ -87,14 +88,23 @@ impl NamedFile {
} }
}; };
let ct = guess_mime_type(&path); let ct = from_path(&path).first_or_octet_stream();
let disposition_type = match ct.type_() { let disposition_type = match ct.type_() {
mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline, mime::IMAGE | mime::TEXT | mime::VIDEO => DispositionType::Inline,
_ => DispositionType::Attachment, _ => DispositionType::Attachment,
}; };
let mut parameters =
vec![DispositionParam::Filename(String::from(filename.as_ref()))];
if !filename.is_ascii() {
parameters.push(DispositionParam::FilenameExt(ExtendedValue {
charset: Charset::Ext(String::from("UTF-8")),
language_tag: None,
value: filename.into_owned().into_bytes(),
}))
}
let cd = ContentDisposition { let cd = ContentDisposition {
disposition: disposition_type, disposition: disposition_type,
parameters: vec![DispositionParam::Filename(filename.into_owned())], parameters: parameters,
}; };
(ct, cd) (ct, cd)
}; };
@ -172,11 +182,21 @@ impl NamedFile {
/// sent to the peer. By default the disposition is `inline` for text, /// sent to the peer. By default the disposition is `inline` for text,
/// image, and video content types, and `attachment` otherwise, and /// image, and video content types, and `attachment` otherwise, and
/// the filename is taken from the path provided in the `open` method /// 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.
/// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy). /// [to_string_lossy](https://doc.rust-lang.org/std/ffi/struct.OsStr.html#method.to_string_lossy).
#[inline] #[inline]
pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self { pub fn set_content_disposition(mut self, cd: header::ContentDisposition) -> Self {
self.content_disposition = cd; self.content_disposition = cd;
self.flags.insert(Flags::CONTENT_DISPOSITION);
self
}
/// Disable `Content-Disposition` header.
///
/// By default Content-Disposition` header is enabled.
#[inline]
pub fn disable_content_disposition(mut self) -> Self {
self.flags.remove(Flags::CONTENT_DISPOSITION);
self self
} }
@ -294,10 +314,12 @@ impl Responder for NamedFile {
if self.status_code != StatusCode::OK { if self.status_code != StatusCode::OK {
let mut resp = HttpResponse::build(self.status_code); let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone())) resp.set(header::ContentType(self.content_type.clone()))
.header( .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
header::CONTENT_DISPOSITION, res.header(
self.content_disposition.to_string(), header::CONTENT_DISPOSITION,
); self.content_disposition.to_string(),
);
});
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); resp.encoding(current_encoding);
} }
@ -311,16 +333,6 @@ impl Responder for NamedFile {
return Ok(resp.streaming(reader)); return Ok(resp.streaming(reader));
} }
match *req.method() {
Method::HEAD | Method::GET => (),
_ => {
return Ok(HttpResponse::MethodNotAllowed()
.header(header::CONTENT_TYPE, "text/plain")
.header(header::ALLOW, "GET, HEAD")
.body("This resource only supports GET and HEAD."));
}
}
let etag = if self.flags.contains(Flags::ETAG) { let etag = if self.flags.contains(Flags::ETAG) {
self.etag() self.etag()
} else { } else {
@ -368,10 +380,12 @@ impl Responder for NamedFile {
let mut resp = HttpResponse::build(self.status_code); let mut resp = HttpResponse::build(self.status_code);
resp.set(header::ContentType(self.content_type.clone())) resp.set(header::ContentType(self.content_type.clone()))
.header( .if_true(self.flags.contains(Flags::CONTENT_DISPOSITION), |res| {
header::CONTENT_DISPOSITION, res.header(
self.content_disposition.to_string(), header::CONTENT_DISPOSITION,
); self.content_disposition.to_string(),
);
});
// default compressing // default compressing
if let Some(current_encoding) = self.encoding { if let Some(current_encoding) = self.encoding {
resp.encoding(current_encoding); resp.encoding(current_encoding);

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-framed" name = "actix-framed"
version = "0.2.0" version = "0.2.1"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix framed app server" description = "Actix framed app server"
readme = "README.md" readme = "README.md"
@ -21,11 +21,10 @@ path = "src/lib.rs"
[dependencies] [dependencies]
actix-codec = "0.1.2" actix-codec = "0.1.2"
actix-service = "0.4.0" actix-service = "0.4.1"
actix-utils = "0.4.0"
actix-router = "0.1.2" actix-router = "0.1.2"
actix-rt = "0.2.2" actix-rt = "0.2.2"
actix-http = "0.2.0" actix-http = "0.2.7"
actix-server-config = "0.1.2" actix-server-config = "0.1.2"
bytes = "0.4" bytes = "0.4"
@ -36,3 +35,4 @@ log = "0.4"
actix-server = { version = "0.6.0", features=["ssl"] } actix-server = { version = "0.6.0", features=["ssl"] }
actix-connect = { version = "0.2.0", features=["ssl"] } actix-connect = { version = "0.2.0", features=["ssl"] }
actix-http-test = { version = "0.2.4", features=["ssl"] } actix-http-test = { version = "0.2.4", features=["ssl"] }
actix-utils = "0.4.4"

View File

@ -1,5 +1,10 @@
# Changes # Changes
## [0.2.1] - 2019-07-20
* Remove unneeded actix-utils dependency
## [0.2.0] - 2019-05-12 ## [0.2.0] - 2019-05-12
* Update dependencies * Update dependencies

View File

@ -6,7 +6,6 @@ use actix_http::{Error, Request, Response};
use actix_router::{Path, Router, Url}; use actix_router::{Path, Router, Url};
use actix_server_config::ServerConfig; use actix_server_config::ServerConfig;
use actix_service::{IntoNewService, NewService, Service}; use actix_service::{IntoNewService, NewService, Service};
use actix_utils::cloneable::CloneableService;
use futures::{Async, Future, Poll}; use futures::{Async, Future, Poll};
use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService}; use crate::helpers::{BoxedHttpNewService, BoxedHttpService, HttpNewService};
@ -100,7 +99,7 @@ where
type Response = (); type Response = ();
type Error = Error; type Error = Error;
type InitError = (); type InitError = ();
type Service = CloneableService<FramedAppService<T, S>>; type Service = FramedAppService<T, S>;
type Future = CreateService<T, S>; type Future = CreateService<T, S>;
fn new_service(&self, _: &ServerConfig) -> Self::Future { fn new_service(&self, _: &ServerConfig) -> Self::Future {
@ -138,7 +137,7 @@ impl<S: 'static, T: 'static> Future for CreateService<T, S>
where where
T: AsyncRead + AsyncWrite, T: AsyncRead + AsyncWrite,
{ {
type Item = CloneableService<FramedAppService<T, S>>; type Item = FramedAppService<T, S>;
type Error = (); type Error = ();
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
@ -177,10 +176,10 @@ where
} }
router router
}); });
Ok(Async::Ready(CloneableService::new(FramedAppService { Ok(Async::Ready(FramedAppService {
router: router.finish(), router: router.finish(),
state: self.state.clone(), state: self.state.clone(),
}))) }))
} else { } else {
Ok(Async::NotReady) Ok(Async::NotReady)
} }

View File

@ -1,9 +1,4 @@
#![allow( #![allow(clippy::type_complexity, clippy::new_without_default, dead_code)]
clippy::type_complexity,
clippy::new_without_default,
dead_code,
deprecated
)]
mod app; mod app;
mod helpers; mod helpers;
mod request; mod request;

View File

@ -1,5 +1,63 @@
# Changes # Changes
## [0.2.11] - 2019-11-06
### Added
* Add support for serde_json::Value to be passed as argument to ResponseBuilder.body()
* Add an additional `filename*` param in the `Content-Disposition` header of `actix_files::NamedFile` to be more compatible. (#1151)
* Allow to use `std::convert::Infallible` as `actix_http::error::Error`
### Fixed
* To be compatible with non-English error responses, `ResponseError` rendered with `text/plain; charset=utf-8` header #1118
## [0.2.10] - 2019-09-11
### Added
* Add support for sending HTTP requests with `Rc<RequestHead>` in addition to sending HTTP requests with `RequestHead`
### Fixed
* h2 will use error response #1080
* on_connect result isn't added to request extensions for http2 requests #1009
## [0.2.9] - 2019-08-13
### Changed
* Dropped the `byteorder`-dependency in favor of `stdlib`-implementation
* Update percent-encoding to 2.1
* Update serde_urlencoded to 0.6.1
### Fixed
* Fixed a panic in the HTTP2 handshake in client HTTP requests (#1031)
## [0.2.8] - 2019-08-01
### Added
* Add `rustls` support
* Add `Clone` impl for `HeaderMap`
### Fixed
* awc client panic #1016
* Invalid response with compression middleware enabled, but compression-related features disabled #997
## [0.2.7] - 2019-07-18 ## [0.2.7] - 2019-07-18
### Added ### Added

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http" name = "actix-http"
version = "0.2.7" version = "0.2.11"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http primitives" description = "Actix http primitives"
readme = "README.md" readme = "README.md"
@ -28,6 +28,9 @@ default = []
# openssl # openssl
ssl = ["openssl", "actix-connect/ssl"] ssl = ["openssl", "actix-connect/ssl"]
# rustls support
rust-tls = ["rustls", "webpki-roots", "actix-connect/rust-tls"]
# brotli encoding, requires c compiler # brotli encoding, requires c compiler
brotli = ["brotli2"] brotli = ["brotli2"]
@ -46,7 +49,7 @@ secure-cookies = ["ring"]
[dependencies] [dependencies]
actix-service = "0.4.1" actix-service = "0.4.1"
actix-codec = "0.1.2" actix-codec = "0.1.2"
actix-connect = "0.2.1" actix-connect = "0.2.4"
actix-utils = "0.4.4" actix-utils = "0.4.4"
actix-server-config = "0.1.2" actix-server-config = "0.1.2"
actix-threadpool = "0.1.1" actix-threadpool = "0.1.1"
@ -54,29 +57,28 @@ actix-threadpool = "0.1.1"
base64 = "0.10" base64 = "0.10"
bitflags = "1.0" bitflags = "1.0"
bytes = "0.4" bytes = "0.4"
byteorder = "1.2"
copyless = "0.1.4" copyless = "0.1.4"
derive_more = "0.15.0" derive_more = "0.15.0"
either = "1.5.2" either = "1.5.2"
encoding_rs = "0.8" encoding_rs = "0.8"
futures = "0.1.25" futures = "0.1.25"
hashbrown = "0.5.0" hashbrown = "0.6.3"
h2 = "0.1.16" h2 = "0.1.16"
http = "0.1.17" http = "0.1.17"
httparse = "1.3" httparse = "1.3"
indexmap = "1.0" indexmap = "1.2"
lazy_static = "1.0" lazy_static = "1.0"
language-tags = "0.2" language-tags = "0.2"
log = "0.4" log = "0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "1.0" percent-encoding = "2.1"
rand = "0.7" rand = "0.7"
regex = "1.0" regex = "1.0"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
sha1 = "0.6" sha1 = "0.6"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.5.5" serde_urlencoded = "0.6.1"
time = "0.1.42" time = "0.1.42"
tokio-tcp = "0.1.3" tokio-tcp = "0.1.3"
tokio-timer = "0.2.8" tokio-timer = "0.2.8"
@ -93,11 +95,13 @@ flate2 = { version="1.0.7", optional = true, default-features = false }
# optional deps # optional deps
failure = { version = "0.1.5", optional = true } failure = { version = "0.1.5", optional = true }
openssl = { version="0.10", optional = true } openssl = { version="0.10", optional = true }
rustls = { version = "0.15.2", optional = true }
webpki-roots = { version = "0.16", optional = true }
chrono = "0.4.6" chrono = "0.4.6"
[dev-dependencies] [dev-dependencies]
actix-rt = "0.2.2" actix-rt = "0.2.2"
actix-server = { version = "0.6.0", features=["ssl"] } actix-server = { version = "0.6.0", features=["ssl", "rust-tls"] }
actix-connect = { version = "0.2.0", features=["ssl"] } actix-connect = { version = "0.2.0", features=["ssl"] }
actix-http-test = { version = "0.2.4", features=["ssl"] } actix-http-test = { version = "0.2.4", features=["ssl"] }
env_logger = "0.6" env_logger = "0.6"

View File

@ -234,6 +234,12 @@ impl From<BytesMut> for Body {
} }
} }
impl From<serde_json::Value> for Body {
fn from(v: serde_json::Value) -> Body {
Body::Bytes(v.to_string().into())
}
}
impl<S> From<SizedStream<S>> for Body impl<S> From<SizedStream<S>> for Body
where where
S: Stream<Item = Bytes, Error = Error> + 'static, S: Stream<Item = Bytes, Error = Error> + 'static,
@ -548,4 +554,17 @@ mod tests {
assert!(format!("{:?}", Body::Empty).contains("Body::Empty")); assert!(format!("{:?}", Body::Empty).contains("Body::Empty"));
assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1")); assert!(format!("{:?}", Body::Bytes(Bytes::from_static(b"1"))).contains("1"));
} }
#[test]
fn test_serde_json() {
use serde_json::json;
assert_eq!(
Body::from(serde_json::Value::String("test".into())).size(),
BodySize::Sized(6)
);
assert_eq!(
Body::from(json!({"test-key":"test-value"})).size(),
BodySize::Sized(25)
);
}
} }

View File

@ -199,6 +199,7 @@ where
self.client_disconnect, self.client_disconnect,
); );
H2Service::with_config(cfg, service.into_new_service()) H2Service::with_config(cfg, service.into_new_service())
.on_connect(self.on_connect)
} }
/// Finish service configuration and create `HttpService` instance. /// Finish service configuration and create `HttpService` instance.

View File

@ -8,7 +8,7 @@ use h2::client::SendRequest;
use crate::body::MessageBody; use crate::body::MessageBody;
use crate::h1::ClientCodec; use crate::h1::ClientCodec;
use crate::message::{RequestHead, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::error::SendRequestError; use super::error::SendRequestError;
@ -27,9 +27,9 @@ pub trait Connection {
fn protocol(&self) -> Protocol; fn protocol(&self) -> Protocol;
/// Send request and body /// Send request and body
fn send_request<B: MessageBody + 'static>( fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
self, self,
head: RequestHead, head: H,
body: B, body: B,
) -> Self::Future; ) -> Self::Future;
@ -39,7 +39,7 @@ pub trait Connection {
>; >;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel(self, head: RequestHead) -> Self::TunnelFuture; fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture;
} }
pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static { pub(crate) trait ConnectionLifetime: AsyncRead + AsyncWrite + 'static {
@ -105,22 +105,22 @@ where
} }
} }
fn send_request<B: MessageBody + 'static>( fn send_request<B: MessageBody + 'static, H: Into<RequestHeadType>>(
mut self, mut self,
head: RequestHead, head: H,
body: B, body: B,
) -> Self::Future { ) -> Self::Future {
match self.io.take().unwrap() { match self.io.take().unwrap() {
ConnectionType::H1(io) => Box::new(h1proto::send_request( ConnectionType::H1(io) => Box::new(h1proto::send_request(
io, io,
head, head.into(),
body, body,
self.created, self.created,
self.pool, self.pool,
)), )),
ConnectionType::H2(io) => Box::new(h2proto::send_request( ConnectionType::H2(io) => Box::new(h2proto::send_request(
io, io,
head, head.into(),
body, body,
self.created, self.created,
self.pool, self.pool,
@ -139,10 +139,10 @@ where
>; >;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel(mut self, head: RequestHead) -> Self::TunnelFuture { fn open_tunnel<H: Into<RequestHeadType>>(mut self, head: H) -> Self::TunnelFuture {
match self.io.take().unwrap() { match self.io.take().unwrap() {
ConnectionType::H1(io) => { ConnectionType::H1(io) => {
Either::A(Box::new(h1proto::open_tunnel(io, head))) Either::A(Box::new(h1proto::open_tunnel(io, head.into())))
} }
ConnectionType::H2(io) => { ConnectionType::H2(io) => {
if let Some(mut pool) = self.pool.take() { if let Some(mut pool) = self.pool.take() {
@ -180,9 +180,9 @@ where
} }
} }
fn send_request<RB: MessageBody + 'static>( fn send_request<RB: MessageBody + 'static, H: Into<RequestHeadType>>(
self, self,
head: RequestHead, head: H,
body: RB, body: RB,
) -> Self::Future { ) -> Self::Future {
match self { match self {
@ -199,7 +199,7 @@ where
>; >;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel(self, head: RequestHead) -> Self::TunnelFuture { fn open_tunnel<H: Into<RequestHeadType>>(self, head: H) -> Self::TunnelFuture {
match self { match self {
EitherConnection::A(con) => Box::new( EitherConnection::A(con) => Box::new(
con.open_tunnel(head) con.open_tunnel(head)

View File

@ -17,9 +17,21 @@ use super::pool::{ConnectionPool, Protocol};
use super::Connect; use super::Connect;
#[cfg(feature = "ssl")] #[cfg(feature = "ssl")]
use openssl::ssl::SslConnector; use openssl::ssl::SslConnector as OpensslConnector;
#[cfg(not(feature = "ssl"))] #[cfg(feature = "rust-tls")]
use rustls::ClientConfig;
#[cfg(feature = "rust-tls")]
use std::sync::Arc;
#[cfg(any(feature = "ssl", feature = "rust-tls"))]
enum SslConnector {
#[cfg(feature = "ssl")]
Openssl(OpensslConnector),
#[cfg(feature = "rust-tls")]
Rustls(Arc<ClientConfig>),
}
#[cfg(not(any(feature = "ssl", feature = "rust-tls")))]
type SslConnector = (); type SslConnector = ();
/// Manages http client network connectivity /// Manages http client network connectivity
@ -46,6 +58,9 @@ pub struct Connector<T, U> {
_t: PhantomData<U>, _t: PhantomData<U>,
} }
trait Io: AsyncRead + AsyncWrite {}
impl<T: AsyncRead + AsyncWrite> Io for T {}
impl Connector<(), ()> { impl Connector<(), ()> {
#[allow(clippy::new_ret_no_self)] #[allow(clippy::new_ret_no_self)]
pub fn new() -> Connector< pub fn new() -> Connector<
@ -61,13 +76,23 @@ impl Connector<(), ()> {
{ {
use openssl::ssl::SslMethod; use openssl::ssl::SslMethod;
let mut ssl = SslConnector::builder(SslMethod::tls()).unwrap(); let mut ssl = OpensslConnector::builder(SslMethod::tls()).unwrap();
let _ = ssl let _ = ssl
.set_alpn_protos(b"\x02h2\x08http/1.1") .set_alpn_protos(b"\x02h2\x08http/1.1")
.map_err(|e| error!("Can not set alpn protocol: {:?}", e)); .map_err(|e| error!("Can not set alpn protocol: {:?}", e));
ssl.build() SslConnector::Openssl(ssl.build())
} }
#[cfg(not(feature = "ssl"))] #[cfg(all(not(feature = "ssl"), feature = "rust-tls"))]
{
let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
let mut config = ClientConfig::new();
config.set_protocols(&protos);
config
.root_store
.add_server_trust_anchors(&webpki_roots::TLS_SERVER_ROOTS);
SslConnector::Rustls(Arc::new(config))
}
#[cfg(not(any(feature = "ssl", feature = "rust-tls")))]
{} {}
}; };
@ -127,8 +152,14 @@ where
#[cfg(feature = "ssl")] #[cfg(feature = "ssl")]
/// Use custom `SslConnector` instance. /// Use custom `SslConnector` instance.
pub fn ssl(mut self, connector: SslConnector) -> Self { pub fn ssl(mut self, connector: OpensslConnector) -> Self {
self.ssl = connector; self.ssl = SslConnector::Openssl(connector);
self
}
#[cfg(feature = "rust-tls")]
pub fn rustls(mut self, connector: Arc<ClientConfig>) -> Self {
self.ssl = SslConnector::Rustls(connector);
self self
} }
@ -182,7 +213,7 @@ where
self, self,
) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError> ) -> impl Service<Request = Connect, Response = impl Connection, Error = ConnectError>
+ Clone { + Clone {
#[cfg(not(feature = "ssl"))] #[cfg(not(any(feature = "ssl", feature = "rust-tls")))]
{ {
let connector = TimeoutService::new( let connector = TimeoutService::new(
self.timeout, self.timeout,
@ -207,10 +238,16 @@ where
), ),
} }
} }
#[cfg(feature = "ssl")] #[cfg(any(feature = "ssl", feature = "rust-tls"))]
{ {
const H2: &[u8] = b"h2"; const H2: &[u8] = b"h2";
#[cfg(feature = "ssl")]
use actix_connect::ssl::OpensslConnector; use actix_connect::ssl::OpensslConnector;
#[cfg(feature = "rust-tls")]
use actix_connect::ssl::RustlsConnector;
use actix_service::boxed::service;
#[cfg(feature = "rust-tls")]
use rustls::Session;
let ssl_service = TimeoutService::new( let ssl_service = TimeoutService::new(
self.timeout, self.timeout,
@ -218,24 +255,46 @@ where
srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr)) srv.call(TcpConnect::new(msg.uri).set_addr(msg.addr))
}) })
.map_err(ConnectError::from) .map_err(ConnectError::from)
.and_then( .and_then(match self.ssl {
OpensslConnector::service(self.ssl) #[cfg(feature = "ssl")]
.map_err(ConnectError::from) SslConnector::Openssl(ssl) => service(
.map(|stream| { OpensslConnector::service(ssl)
let sock = stream.into_parts().0; .map_err(ConnectError::from)
let h2 = sock .map(|stream| {
.get_ref() let sock = stream.into_parts().0;
.ssl() let h2 = sock
.selected_alpn_protocol() .get_ref()
.map(|protos| protos.windows(2).any(|w| w == H2)) .ssl()
.unwrap_or(false); .selected_alpn_protocol()
if h2 { .map(|protos| protos.windows(2).any(|w| w == H2))
(sock, Protocol::Http2) .unwrap_or(false);
} else { if h2 {
(sock, Protocol::Http1) (Box::new(sock) as Box<dyn Io>, Protocol::Http2)
} } else {
}), (Box::new(sock) as Box<dyn Io>, Protocol::Http1)
), }
}),
),
#[cfg(feature = "rust-tls")]
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 { .map_err(|e| match e {
TimeoutError::Service(e) => e, TimeoutError::Service(e) => e,
@ -275,7 +334,7 @@ where
} }
} }
#[cfg(not(feature = "ssl"))] #[cfg(not(any(feature = "ssl", feature = "rust-tls")))]
mod connect_impl { mod connect_impl {
use futures::future::{err, Either, FutureResult}; use futures::future::{err, Either, FutureResult};
use futures::Poll; use futures::Poll;
@ -337,7 +396,7 @@ mod connect_impl {
} }
} }
#[cfg(feature = "ssl")] #[cfg(any(feature = "ssl", feature = "rust-tls"))]
mod connect_impl { mod connect_impl {
use std::marker::PhantomData; use std::marker::PhantomData;

View File

@ -128,3 +128,23 @@ impl ResponseError for SendRequestError {
.into() .into()
} }
} }
/// A set of errors that can occur during freezing a request
#[derive(Debug, Display, From)]
pub enum FreezeRequestError {
/// Invalid URL
#[display(fmt = "Invalid URL: {}", _0)]
Url(InvalidUrl),
/// Http error
#[display(fmt = "{}", _0)]
Http(HttpError),
}
impl From<FreezeRequestError> for SendRequestError {
fn from(e: FreezeRequestError) -> Self {
match e {
FreezeRequestError::Url(e) => e.into(),
FreezeRequestError::Http(e) => e.into(),
}
}
}

View File

@ -8,8 +8,9 @@ use futures::{Async, Future, Poll, Sink, Stream};
use crate::error::PayloadError; use crate::error::PayloadError;
use crate::h1; use crate::h1;
use crate::header::HeaderMap;
use crate::http::header::{IntoHeaderValue, HOST}; use crate::http::header::{IntoHeaderValue, HOST};
use crate::message::{RequestHead, ResponseHead}; use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::{Payload, PayloadStream}; use crate::payload::{Payload, PayloadStream};
use super::connection::{ConnectionLifetime, ConnectionType, IoConnection}; use super::connection::{ConnectionLifetime, ConnectionType, IoConnection};
@ -19,7 +20,7 @@ use crate::body::{BodySize, MessageBody};
pub(crate) fn send_request<T, B>( pub(crate) fn send_request<T, B>(
io: T, io: T,
mut head: RequestHead, mut head: RequestHeadType,
body: B, body: B,
created: time::Instant, created: time::Instant,
pool: Option<Acquired<T>>, pool: Option<Acquired<T>>,
@ -29,22 +30,28 @@ where
B: MessageBody, B: MessageBody,
{ {
// set request host header // set request host header
if !head.headers.contains_key(HOST) { if !head.as_ref().headers.contains_key(HOST)
if let Some(host) = head.uri.host() { && !head.extra_headers().iter().any(|h| h.contains_key(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.uri.port_u16() { let _ = match head.as_ref().uri.port_u16() {
None | Some(80) | Some(443) => write!(wrt, "{}", host), None | Some(80) | Some(443) => write!(wrt, "{}", host),
Some(port) => write!(wrt, "{}:{}", host, port), Some(port) => write!(wrt, "{}:{}", host, port),
}; };
match wrt.get_mut().take().freeze().try_into() { match wrt.get_mut().take().freeze().try_into() {
Ok(value) => { Ok(value) => match head {
head.headers.insert(HOST, value); RequestHeadType::Owned(ref mut head) => {
} head.headers.insert(HOST, value)
Err(e) => { }
log::error!("Can not set HOST header {}", e); RequestHeadType::Rc(_, ref mut extra_headers) => {
} let headers = extra_headers.get_or_insert(HeaderMap::new());
headers.insert(HOST, value)
}
},
Err(e) => log::error!("Can not set HOST header {}", e),
} }
} }
} }
@ -57,7 +64,7 @@ where
let len = body.size(); let len = body.size();
// create Framed and send reqest // create Framed and send request
Framed::new(io, h1::ClientCodec::default()) Framed::new(io, h1::ClientCodec::default())
.send((head, len).into()) .send((head, len).into())
.from_err() .from_err()
@ -95,12 +102,12 @@ where
pub(crate) fn open_tunnel<T>( pub(crate) fn open_tunnel<T>(
io: T, io: T,
head: RequestHead, head: RequestHeadType,
) -> impl Future<Item = (ResponseHead, Framed<T, h1::ClientCodec>), Error = SendRequestError> ) -> impl Future<Item = (ResponseHead, Framed<T, h1::ClientCodec>), Error = SendRequestError>
where where
T: AsyncRead + AsyncWrite + 'static, T: AsyncRead + AsyncWrite + 'static,
{ {
// create Framed and send reqest // create Framed and send request
Framed::new(io, h1::ClientCodec::default()) Framed::new(io, h1::ClientCodec::default())
.send((head, BodySize::None).into()) .send((head, BodySize::None).into())
.from_err() .from_err()

View File

@ -9,7 +9,8 @@ use http::header::{HeaderValue, CONNECTION, CONTENT_LENGTH, TRANSFER_ENCODING};
use http::{request::Request, HttpTryFrom, Method, Version}; use http::{request::Request, HttpTryFrom, Method, Version};
use crate::body::{BodySize, MessageBody}; use crate::body::{BodySize, MessageBody};
use crate::message::{RequestHead, ResponseHead}; use crate::header::HeaderMap;
use crate::message::{RequestHeadType, ResponseHead};
use crate::payload::Payload; use crate::payload::Payload;
use super::connection::{ConnectionType, IoConnection}; use super::connection::{ConnectionType, IoConnection};
@ -18,7 +19,7 @@ use super::pool::Acquired;
pub(crate) fn send_request<T, B>( pub(crate) fn send_request<T, B>(
io: SendRequest<Bytes>, io: SendRequest<Bytes>,
head: RequestHead, head: RequestHeadType,
body: B, body: B,
created: time::Instant, created: time::Instant,
pool: Option<Acquired<T>>, pool: Option<Acquired<T>>,
@ -28,7 +29,7 @@ where
B: MessageBody, B: MessageBody,
{ {
trace!("Sending client request: {:?} {:?}", head, body.size()); trace!("Sending client request: {:?} {:?}", head, body.size());
let head_req = head.method == Method::HEAD; let head_req = head.as_ref().method == Method::HEAD;
let length = body.size(); let length = body.size();
let eof = match length { let eof = match length {
BodySize::None | BodySize::Empty | BodySize::Sized(0) => true, BodySize::None | BodySize::Empty | BodySize::Sized(0) => true,
@ -39,8 +40,8 @@ where
.map_err(SendRequestError::from) .map_err(SendRequestError::from)
.and_then(move |mut io| { .and_then(move |mut io| {
let mut req = Request::new(()); let mut req = Request::new(());
*req.uri_mut() = head.uri; *req.uri_mut() = head.as_ref().uri.clone();
*req.method_mut() = head.method; *req.method_mut() = head.as_ref().method.clone();
*req.version_mut() = Version::HTTP_2; *req.version_mut() = Version::HTTP_2;
let mut skip_len = true; let mut skip_len = true;
@ -66,8 +67,27 @@ where
), ),
}; };
// Extracting extra headers from RequestHeadType. HeaderMap::new() does not allocate.
let (head, extra_headers) = match head {
RequestHeadType::Owned(head) => {
(RequestHeadType::Owned(head), HeaderMap::new())
}
RequestHeadType::Rc(head, extra_headers) => (
RequestHeadType::Rc(head, None),
extra_headers.unwrap_or_else(HeaderMap::new),
),
};
// merging headers from head and extra headers.
let headers = head
.as_ref()
.headers
.iter()
.filter(|(name, _)| !extra_headers.contains_key(*name))
.chain(extra_headers.iter());
// copy headers // copy headers
for (key, value) in head.headers.iter() { for (key, value) in headers {
match *key { match *key {
CONNECTION | TRANSFER_ENCODING => continue, // http2 specific CONNECTION | TRANSFER_ENCODING => continue, // http2 specific
CONTENT_LENGTH if skip_len => continue, CONTENT_LENGTH if skip_len => continue,

View File

@ -10,7 +10,7 @@ mod pool;
pub use self::connection::Connection; pub use self::connection::Connection;
pub use self::connector::Connector; pub use self::connector::Connector;
pub use self::error::{ConnectError, InvalidUrl, SendRequestError}; pub use self::error::{ConnectError, FreezeRequestError, InvalidUrl, SendRequestError};
pub use self::pool::Protocol; pub use self::pool::Protocol;
#[derive(Clone)] #[derive(Clone)]

View File

@ -305,10 +305,12 @@ pub(crate) struct Inner<Io> {
limit: usize, limit: usize,
acquired: usize, acquired: usize,
available: HashMap<Key, VecDeque<AvailableConnection<Io>>>, available: HashMap<Key, VecDeque<AvailableConnection<Io>>>,
waiters: Slab<( waiters: Slab<
Connect, Option<(
oneshot::Sender<Result<IoConnection<Io>, ConnectError>>, Connect,
)>, oneshot::Sender<Result<IoConnection<Io>, ConnectError>>,
)>,
>,
waiters_queue: IndexSet<(Key, usize)>, waiters_queue: IndexSet<(Key, usize)>,
task: Option<AtomicTask>, task: Option<AtomicTask>,
} }
@ -324,7 +326,7 @@ impl<Io> Inner<Io> {
fn release_waiter(&mut self, key: &Key, token: usize) { fn release_waiter(&mut self, key: &Key, token: usize) {
self.waiters.remove(token); self.waiters.remove(token);
self.waiters_queue.remove(&(key.clone(), token)); let _ = self.waiters_queue.shift_remove(&(key.clone(), token));
} }
} }
@ -346,7 +348,7 @@ where
let key: Key = connect.uri.authority_part().unwrap().clone().into(); let key: Key = connect.uri.authority_part().unwrap().clone().into();
let entry = self.waiters.vacant_entry(); let entry = self.waiters.vacant_entry();
let token = entry.key(); let token = entry.key();
entry.insert((connect, tx)); entry.insert(Some((connect, tx)));
assert!(self.waiters_queue.insert((key, token))); assert!(self.waiters_queue.insert((key, token)));
(rx, token, self.task.is_some()) (rx, token, self.task.is_some())
@ -499,10 +501,14 @@ where
break; break;
} }
}; };
if inner.waiters.get(token).unwrap().is_none() {
continue;
}
match inner.acquire(&key) { match inner.acquire(&key) {
Acquire::NotAvailable => break, Acquire::NotAvailable => break,
Acquire::Acquired(io, created) => { Acquire::Acquired(io, created) => {
let (_, tx) = inner.waiters.remove(token); let tx = inner.waiters.get_mut(token).unwrap().take().unwrap().1;
if let Err(conn) = tx.send(Ok(IoConnection::new( if let Err(conn) = tx.send(Ok(IoConnection::new(
io, io,
created, created,
@ -513,7 +519,8 @@ where
} }
} }
Acquire::Available => { Acquire::Available => {
let (connect, tx) = inner.waiters.remove(token); let (connect, tx) =
inner.waiters.get_mut(token).unwrap().take().unwrap();
OpenWaitingConnection::spawn( OpenWaitingConnection::spawn(
key.clone(), key.clone(),
tx, tx,
@ -583,6 +590,29 @@ where
type Error = (); type Error = ();
fn poll(&mut self) -> Poll<Self::Item, Self::Error> { fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if let Some(ref mut h2) = self.h2 {
return match h2.poll() {
Ok(Async::Ready((snd, connection))) => {
tokio_current_thread::spawn(connection.map_err(|_| ()));
let rx = self.rx.take().unwrap();
let _ = rx.send(Ok(IoConnection::new(
ConnectionType::H2(snd),
Instant::now(),
Some(Acquired(self.key.clone(), self.inner.take())),
)));
Ok(Async::Ready(()))
}
Ok(Async::NotReady) => Ok(Async::NotReady),
Err(err) => {
let _ = self.inner.take();
if let Some(rx) = self.rx.take() {
let _ = rx.send(Err(ConnectError::H2(err)));
}
Err(())
}
};
}
match self.fut.poll() { match self.fut.poll() {
Err(err) => { Err(err) => {
let _ = self.inner.take(); let _ = self.inner.take();

View File

@ -66,7 +66,7 @@ use std::fmt;
use std::str::FromStr; use std::str::FromStr;
use chrono::Duration; use chrono::Duration;
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use percent_encoding::{percent_encode, AsciiSet, CONTROLS};
use time::Tm; use time::Tm;
pub use self::builder::CookieBuilder; pub use self::builder::CookieBuilder;
@ -75,6 +75,25 @@ pub use self::jar::{CookieJar, Delta, Iter};
use self::parse::parse_cookie; use self::parse::parse_cookie;
pub use self::parse::ParseError; pub use self::parse::ParseError;
/// https://url.spec.whatwg.org/#fragment-percent-encode-set
const FRAGMENT: &AsciiSet = &CONTROLS.add(b' ').add(b'"').add(b'<').add(b'>').add(b'`');
/// https://url.spec.whatwg.org/#path-percent-encode-set
const PATH: &AsciiSet = &FRAGMENT.add(b'#').add(b'?').add(b'{').add(b'}');
/// https://url.spec.whatwg.org/#userinfo-percent-encode-set
pub const USERINFO: &AsciiSet = &PATH
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'|');
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
enum CookieStr { enum CookieStr {
/// An string derived from indexes (start, end). /// An string derived from indexes (start, end).
@ -910,8 +929,8 @@ pub struct EncodedCookie<'a, 'c: 'a>(&'a Cookie<'c>);
impl<'a, 'c: 'a> fmt::Display for EncodedCookie<'a, 'c> { impl<'a, 'c: 'a> fmt::Display for EncodedCookie<'a, 'c> {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// Percent-encode the name and value. // Percent-encode the name and value.
let name = percent_encode(self.0.name().as_bytes(), USERINFO_ENCODE_SET); let name = percent_encode(self.0.name().as_bytes(), USERINFO);
let value = percent_encode(self.0.value().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(self.0.value().as_bytes(), USERINFO);
// Write out the name/value pair and the cookie's parameters. // Write out the name/value pair and the cookie's parameters.
write!(f, "{}={}", name, value)?; write!(f, "{}={}", name, value)?;

View File

@ -54,22 +54,24 @@ impl<B: MessageBody> Encoder<B> {
}; };
if can_encode { if can_encode {
update_head(encoding, head); // Modify response body only if encoder is not None
head.no_chunking(false); if let Some(enc) = ContentEncoder::encoder(encoding) {
ResponseBody::Body(Encoder { update_head(encoding, head);
body, head.no_chunking(false);
eof: false, return ResponseBody::Body(Encoder {
fut: None, body,
encoder: ContentEncoder::encoder(encoding), eof: false,
}) fut: None,
} else { encoder: Some(enc),
ResponseBody::Body(Encoder { });
body, }
eof: false,
fut: None,
encoder: None,
})
} }
ResponseBody::Body(Encoder {
body,
eof: false,
fut: None,
encoder: None,
})
} }
} }

View File

@ -75,7 +75,7 @@ pub trait ResponseError: fmt::Debug + fmt::Display {
let _ = write!(Writer(&mut buf), "{}", self); let _ = write!(Writer(&mut buf), "{}", self);
resp.headers_mut().insert( resp.headers_mut().insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"), header::HeaderValue::from_static("text/plain; charset=utf-8"),
); );
resp.set_body(Body::from(buf)) resp.set_body(Body::from(buf))
} }
@ -89,11 +89,11 @@ pub trait ResponseError: fmt::Debug + fmt::Display {
} }
} }
impl ResponseError + 'static { impl dyn ResponseError + 'static {
/// Downcasts a response error to a specific type. /// Downcasts a response error to a specific type.
pub fn downcast_ref<T: ResponseError + 'static>(&self) -> Option<&T> { pub fn downcast_ref<T: ResponseError + 'static>(&self) -> Option<&T> {
if self.__private_get_type_id__() == TypeId::of::<T>() { if self.__private_get_type_id__() == TypeId::of::<T>() {
unsafe { Some(&*(self as *const ResponseError as *const T)) } unsafe { Some(&*(self as *const dyn ResponseError as *const T)) }
} else { } else {
None None
} }
@ -132,6 +132,14 @@ impl std::error::Error for Error {
} }
} }
impl From<std::convert::Infallible> for Error {
fn from(_: std::convert::Infallible) -> Self {
// `std::convert::Infallible` indicates an error
// that will never happen
unreachable!()
}
}
/// Convert `Error` to a `Response` instance /// Convert `Error` to a `Response` instance
impl From<Error> for Response { impl From<Error> for Response {
fn from(err: Error) -> Self { fn from(err: Error) -> Self {
@ -528,7 +536,7 @@ where
let _ = write!(Writer(&mut buf), "{}", self); let _ = write!(Writer(&mut buf), "{}", self);
res.headers_mut().insert( res.headers_mut().insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
header::HeaderValue::from_static("text/plain"), header::HeaderValue::from_static("text/plain; charset=utf-8"),
); );
res.set_body(Body::from(buf)) res.set_body(Body::from(buf))
} }
@ -1072,7 +1080,7 @@ mod tests {
#[test] #[test]
fn test_error_casting() { fn test_error_casting() {
let err = PayloadError::Overflow; let err = PayloadError::Overflow;
let resp_err: &ResponseError = &err; let resp_err: &dyn ResponseError = &err;
let err = resp_err.downcast_ref::<PayloadError>().unwrap(); let err = resp_err.downcast_ref::<PayloadError>().unwrap();
assert_eq!(err.to_string(), "A payload reached size limit."); assert_eq!(err.to_string(), "A payload reached size limit.");
let not_err = resp_err.downcast_ref::<ContentTypeError>(); let not_err = resp_err.downcast_ref::<ContentTypeError>();

View File

@ -1,5 +1,6 @@
#![allow(unused_imports, unused_variables, dead_code)] #![allow(unused_imports, unused_variables, dead_code)]
use std::io::{self, Write}; use std::io::{self, Write};
use std::rc::Rc;
use actix_codec::{Decoder, Encoder}; use actix_codec::{Decoder, Encoder};
use bitflags::bitflags; use bitflags::bitflags;
@ -15,8 +16,11 @@ use super::{Message, MessageType};
use crate::body::BodySize; use crate::body::BodySize;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{ParseError, PayloadError}; use crate::error::{ParseError, PayloadError};
use crate::header::HeaderMap;
use crate::helpers; use crate::helpers;
use crate::message::{ConnectionType, Head, MessagePool, RequestHead, ResponseHead}; use crate::message::{
ConnectionType, Head, MessagePool, RequestHead, RequestHeadType, ResponseHead,
};
bitflags! { bitflags! {
struct Flags: u8 { struct Flags: u8 {
@ -48,7 +52,7 @@ struct ClientCodecInner {
// encoder part // encoder part
flags: Flags, flags: Flags,
headers_size: u32, headers_size: u32,
encoder: encoder::MessageEncoder<RequestHead>, encoder: encoder::MessageEncoder<RequestHeadType>,
} }
impl Default for ClientCodec { impl Default for ClientCodec {
@ -183,7 +187,7 @@ impl Decoder for ClientPayloadCodec {
} }
impl Encoder for ClientCodec { impl Encoder for ClientCodec {
type Item = Message<(RequestHead, BodySize)>; type Item = Message<(RequestHeadType, BodySize)>;
type Error = io::Error; type Error = io::Error;
fn encode( fn encode(
@ -192,13 +196,15 @@ impl Encoder for ClientCodec {
dst: &mut BytesMut, dst: &mut BytesMut,
) -> Result<(), Self::Error> { ) -> Result<(), Self::Error> {
match item { match item {
Message::Item((mut msg, length)) => { Message::Item((mut head, length)) => {
let inner = &mut self.inner; let inner = &mut self.inner;
inner.version = msg.version; inner.version = head.as_ref().version;
inner.flags.set(Flags::HEAD, msg.method == Method::HEAD); inner
.flags
.set(Flags::HEAD, head.as_ref().method == Method::HEAD);
// connection status // connection status
inner.ctype = match msg.connection_type() { inner.ctype = match head.as_ref().connection_type() {
ConnectionType::KeepAlive => { ConnectionType::KeepAlive => {
if inner.flags.contains(Flags::KEEPALIVE_ENABLED) { if inner.flags.contains(Flags::KEEPALIVE_ENABLED) {
ConnectionType::KeepAlive ConnectionType::KeepAlive
@ -212,7 +218,7 @@ impl Encoder for ClientCodec {
inner.encoder.encode( inner.encoder.encode(
dst, dst,
&mut msg, &mut head,
false, false,
false, false,
inner.version, inner.version,

View File

@ -1,5 +1,6 @@
use std::io;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::{io, mem}; use std::mem::MaybeUninit;
use actix_codec::Decoder; use actix_codec::Decoder;
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
@ -186,11 +187,12 @@ impl MessageType for Request {
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> { fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
// Unsafe: we read only this data only after httparse parses headers into. // Unsafe: we read only this data only after httparse parses headers into.
// performance bump for pipeline benchmarks. // performance bump for pipeline benchmarks.
let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
let (len, method, uri, ver, h_len) = { let (len, method, uri, ver, h_len) = {
let mut parsed: [httparse::Header; MAX_HEADERS] = let mut parsed: [httparse::Header; MAX_HEADERS] =
unsafe { mem::uninitialized() }; unsafe { MaybeUninit::uninit().assume_init() };
let mut req = httparse::Request::new(&mut parsed); let mut req = httparse::Request::new(&mut parsed);
match req.parse(src)? { match req.parse(src)? {
@ -260,11 +262,12 @@ impl MessageType for ResponseHead {
fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> { fn decode(src: &mut BytesMut) -> Result<Option<(Self, PayloadType)>, ParseError> {
// Unsafe: we read only this data only after httparse parses headers into. // Unsafe: we read only this data only after httparse parses headers into.
// performance bump for pipeline benchmarks. // performance bump for pipeline benchmarks.
let mut headers: [HeaderIndex; MAX_HEADERS] = unsafe { mem::uninitialized() }; let mut headers: [HeaderIndex; MAX_HEADERS] =
unsafe { MaybeUninit::uninit().assume_init() };
let (len, ver, status, h_len) = { let (len, ver, status, h_len) = {
let mut parsed: [httparse::Header; MAX_HEADERS] = let mut parsed: [httparse::Header; MAX_HEADERS] =
unsafe { mem::uninitialized() }; unsafe { MaybeUninit::uninit().assume_init() };
let mut res = httparse::Response::new(&mut parsed); let mut res = httparse::Response::new(&mut parsed);
match res.parse(src)? { match res.parse(src)? {

View File

@ -502,7 +502,7 @@ where
let pl = self.codec.message_type(); let pl = self.codec.message_type();
req.head_mut().peer_addr = self.peer_addr; req.head_mut().peer_addr = self.peer_addr;
// on_connect data // set on_connect data
if let Some(ref on_connect) = self.on_connect { if let Some(ref on_connect) = self.on_connect {
on_connect.set(&mut req.extensions_mut()); on_connect.set(&mut req.extensions_mut());
} }

View File

@ -2,6 +2,7 @@
use std::fmt::Write as FmtWrite; use std::fmt::Write as FmtWrite;
use std::io::Write; use std::io::Write;
use std::marker::PhantomData; use std::marker::PhantomData;
use std::rc::Rc;
use std::str::FromStr; use std::str::FromStr;
use std::{cmp, fmt, io, mem}; use std::{cmp, fmt, io, mem};
@ -15,7 +16,7 @@ use crate::http::header::{
HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING, HeaderValue, ACCEPT_ENCODING, CONNECTION, CONTENT_LENGTH, DATE, TRANSFER_ENCODING,
}; };
use crate::http::{HeaderMap, Method, StatusCode, Version}; use crate::http::{HeaderMap, Method, StatusCode, Version};
use crate::message::{ConnectionType, Head, RequestHead, ResponseHead}; use crate::message::{ConnectionType, Head, RequestHead, RequestHeadType, ResponseHead};
use crate::request::Request; use crate::request::Request;
use crate::response::Response; use crate::response::Response;
@ -43,6 +44,8 @@ pub(crate) trait MessageType: Sized {
fn headers(&self) -> &HeaderMap; fn headers(&self) -> &HeaderMap;
fn extra_headers(&self) -> Option<&HeaderMap>;
fn camel_case(&self) -> bool { fn camel_case(&self) -> bool {
false false
} }
@ -128,12 +131,22 @@ pub(crate) trait MessageType: Sized {
_ => (), _ => (),
} }
// merging headers from head and extra headers. HeaderMap::new() does not allocate.
let empty_headers = HeaderMap::new();
let extra_headers = self.extra_headers().unwrap_or(&empty_headers);
let headers = self
.headers()
.inner
.iter()
.filter(|(name, _)| !extra_headers.contains_key(*name))
.chain(extra_headers.inner.iter());
// write headers // write headers
let mut pos = 0; let mut pos = 0;
let mut has_date = false; let mut has_date = false;
let mut remaining = dst.remaining_mut(); let mut remaining = dst.remaining_mut();
let mut buf = unsafe { &mut *(dst.bytes_mut() as *mut [u8]) }; let mut buf = unsafe { &mut *(dst.bytes_mut() as *mut [u8]) };
for (key, value) in self.headers().inner.iter() { for (key, value) in headers {
match *key { match *key {
CONNECTION => continue, CONNECTION => continue,
TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue, TRANSFER_ENCODING | CONTENT_LENGTH if skip_len => continue,
@ -235,6 +248,10 @@ impl MessageType for Response<()> {
&self.head().headers &self.head().headers
} }
fn extra_headers(&self) -> Option<&HeaderMap> {
None
}
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
let head = self.head(); let head = self.head();
let reason = head.reason().as_bytes(); let reason = head.reason().as_bytes();
@ -247,31 +264,36 @@ impl MessageType for Response<()> {
} }
} }
impl MessageType for RequestHead { impl MessageType for RequestHeadType {
fn status(&self) -> Option<StatusCode> { fn status(&self) -> Option<StatusCode> {
None None
} }
fn chunked(&self) -> bool { fn chunked(&self) -> bool {
self.chunked() self.as_ref().chunked()
} }
fn camel_case(&self) -> bool { fn camel_case(&self) -> bool {
RequestHead::camel_case_headers(self) self.as_ref().camel_case_headers()
} }
fn headers(&self) -> &HeaderMap { fn headers(&self) -> &HeaderMap {
&self.headers self.as_ref().headers()
}
fn extra_headers(&self) -> Option<&HeaderMap> {
self.extra_headers()
} }
fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> { fn encode_status(&mut self, dst: &mut BytesMut) -> io::Result<()> {
dst.reserve(256 + self.headers.len() * AVERAGE_HEADER_SIZE); let head = self.as_ref();
dst.reserve(256 + head.headers.len() * AVERAGE_HEADER_SIZE);
write!( write!(
Writer(dst), Writer(dst),
"{} {} {}", "{} {} {}",
self.method, head.method,
self.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"), head.uri.path_and_query().map(|u| u.as_str()).unwrap_or("/"),
match self.version { match head.version {
Version::HTTP_09 => "HTTP/0.9", Version::HTTP_09 => "HTTP/0.9",
Version::HTTP_10 => "HTTP/1.0", Version::HTTP_10 => "HTTP/1.0",
Version::HTTP_11 => "HTTP/1.1", Version::HTTP_11 => "HTTP/1.1",
@ -488,9 +510,11 @@ fn write_camel_case(value: &[u8], buffer: &mut [u8]) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes; use bytes::Bytes;
//use std::rc::Rc;
use super::*; use super::*;
use crate::http::header::{HeaderValue, CONTENT_TYPE}; use crate::http::header::{HeaderValue, CONTENT_TYPE};
use http::header::AUTHORIZATION;
#[test] #[test]
fn test_chunked_te() { fn test_chunked_te() {
@ -515,6 +539,8 @@ mod tests {
head.headers head.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text")); .insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
let mut head = RequestHeadType::Owned(head);
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
@ -522,10 +548,11 @@ mod tests {
ConnectionType::Close, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("Content-Length: 0\r\n"));
Bytes::from_static(b"\r\nContent-Length: 0\r\nConnection: close\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("Connection: close\r\n"));
); assert!(data.contains("Content-Type: plain/text\r\n"));
assert!(data.contains("Date: date\r\n"));
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
@ -534,10 +561,10 @@ mod tests {
ConnectionType::KeepAlive, ConnectionType::KeepAlive,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("Transfer-Encoding: chunked\r\n"));
Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("Content-Type: plain/text\r\n"));
); assert!(data.contains("Date: date\r\n"));
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
@ -546,13 +573,20 @@ mod tests {
ConnectionType::KeepAlive, ConnectionType::KeepAlive,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("Content-Length: 100\r\n"));
Bytes::from_static(b"\r\nContent-Length: 100\r\nDate: date\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("Content-Type: plain/text\r\n"));
); assert!(data.contains("Date: date\r\n"));
let mut head = RequestHead::default();
head.set_camel_case_headers(false);
head.headers.insert(DATE, HeaderValue::from_static("date"));
head.headers
.insert(CONTENT_TYPE, HeaderValue::from_static("plain/text"));
head.headers head.headers
.append(CONTENT_TYPE, HeaderValue::from_static("xml")); .append(CONTENT_TYPE, HeaderValue::from_static("xml"));
let mut head = RequestHeadType::Owned(head);
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
@ -560,22 +594,43 @@ mod tests {
ConnectionType::KeepAlive, ConnectionType::KeepAlive,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("transfer-encoding: chunked\r\n"));
Bytes::from_static(b"\r\nTransfer-Encoding: chunked\r\nDate: date\r\nContent-Type: xml\r\nContent-Type: plain/text\r\n\r\n") assert!(data.contains("content-type: xml\r\n"));
assert!(data.contains("content-type: plain/text\r\n"));
assert!(data.contains("date: date\r\n"));
}
#[test]
fn test_extra_headers() {
let mut bytes = BytesMut::with_capacity(2048);
let mut head = RequestHead::default();
head.headers.insert(
AUTHORIZATION,
HeaderValue::from_static("some authorization"),
); );
head.set_camel_case_headers(false); let mut extra_headers = HeaderMap::new();
extra_headers.insert(
AUTHORIZATION,
HeaderValue::from_static("another authorization"),
);
extra_headers.insert(DATE, HeaderValue::from_static("date"));
let mut head = RequestHeadType::Rc(Rc::new(head), Some(extra_headers));
let _ = head.encode_headers( let _ = head.encode_headers(
&mut bytes, &mut bytes,
Version::HTTP_11, Version::HTTP_11,
BodySize::Stream, BodySize::Empty,
ConnectionType::KeepAlive, ConnectionType::Close,
&ServiceConfig::default(), &ServiceConfig::default(),
); );
assert_eq!( let data = String::from_utf8(Vec::from(bytes.take().freeze().as_ref())).unwrap();
bytes.take().freeze(), assert!(data.contains("content-length: 0\r\n"));
Bytes::from_static(b"\r\ntransfer-encoding: chunked\r\ndate: date\r\ncontent-type: xml\r\ncontent-type: plain/text\r\n\r\n") assert!(data.contains("connection: close\r\n"));
); assert!(data.contains("authorization: another authorization\r\n"));
assert!(data.contains("date: date\r\n"));
} }
} }

View File

@ -23,6 +23,7 @@ use crate::cloneable::CloneableService;
use crate::config::ServiceConfig; use crate::config::ServiceConfig;
use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError}; use crate::error::{DispatchError, Error, ParseError, PayloadError, ResponseError};
use crate::helpers::DataFactory; use crate::helpers::DataFactory;
use crate::httpmessage::HttpMessage;
use crate::message::ResponseHead; use crate::message::ResponseHead;
use crate::payload::Payload; use crate::payload::Payload;
use crate::request::Request; use crate::request::Request;
@ -122,6 +123,12 @@ where
head.version = parts.version; head.version = parts.version;
head.headers = parts.headers.into(); head.headers = parts.headers.into();
head.peer_addr = self.peer_addr; head.peer_addr = self.peer_addr;
// set on_connect data
if let Some(ref on_connect) = self.on_connect {
on_connect.set(&mut req.extensions_mut());
}
tokio_current_thread::spawn(ServiceResponse::<S::Future, B> { tokio_current_thread::spawn(ServiceResponse::<S::Future, B> {
state: ServiceResponseState::ServiceCall( state: ServiceResponseState::ServiceCall(
self.service.call(req), self.service.call(req),
@ -250,8 +257,8 @@ where
} }
} }
Ok(Async::NotReady) => Ok(Async::NotReady), Ok(Async::NotReady) => Ok(Async::NotReady),
Err(_e) => { Err(e) => {
let res: Response = Response::InternalServerError().finish(); let res: Response = e.into().into();
let (res, body) = res.replace_body(()); let (res, body) = res.replace_body(());
let mut send = send.take().unwrap(); let mut send = send.take().unwrap();

View File

@ -76,6 +76,11 @@ pub enum DispositionParam {
/// the form. /// the form.
Name(String), Name(String),
/// A plain file name. /// A plain file name.
///
/// It is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// [`FilenameExt`](DispositionParam::FilenameExt) with charset UTF-8 may be used instead
/// in case there are Unicode characters in file names.
Filename(String), Filename(String),
/// An extended file name. It must not exist for `ContentType::Formdata` according to /// An extended file name. It must not exist for `ContentType::Formdata` according to
/// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2). /// [RFC7578 Section 4.2](https://tools.ietf.org/html/rfc7578#section-4.2).
@ -220,7 +225,16 @@ impl DispositionParam {
/// ext-token = <the characters in token, followed by "*"> /// ext-token = <the characters in token, followed by "*">
/// ``` /// ```
/// ///
/// **Note**: filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within /// # Note
///
/// filename is [not supposed](https://tools.ietf.org/html/rfc6266#appendix-D) to contain any
/// non-ASCII characters when used in a *Content-Disposition* HTTP response header, where
/// filename* with charset UTF-8 may be used instead in case there are Unicode characters in file
/// names.
/// filename is [acceptable](https://tools.ietf.org/html/rfc7578#section-4.2) to be UTF-8 encoded
/// directly in a *Content-Disposition* header for *multipart/form-data*, though.
///
/// filename* [must not](https://tools.ietf.org/html/rfc7578#section-4.2) be used within
/// *multipart/form-data*. /// *multipart/form-data*.
/// ///
/// # Example /// # Example
@ -251,6 +265,22 @@ impl DispositionParam {
/// }; /// };
/// assert_eq!(cd2.get_name(), Some("file")); // field name /// assert_eq!(cd2.get_name(), Some("file")); // field name
/// assert_eq!(cd2.get_filename(), Some("bill.odt")); /// assert_eq!(cd2.get_filename(), Some("bill.odt"));
///
/// // HTTP response header with Unicode characters in file names
/// let cd3 = ContentDisposition {
/// disposition: DispositionType::Attachment,
/// parameters: vec![
/// DispositionParam::FilenameExt(ExtendedValue {
/// charset: Charset::Ext(String::from("UTF-8")),
/// language_tag: None,
/// value: String::from("\u{1f600}.svg").into_bytes(),
/// }),
/// // fallback for better compatibility
/// DispositionParam::Filename(String::from("Grinning-Face-Emoji.svg"))
/// ],
/// };
/// assert_eq!(cd3.get_filename_ext().map(|ev| ev.value.as_ref()),
/// Some("\u{1f600}.svg".as_bytes()));
/// ``` /// ```
/// ///
/// # WARN /// # WARN
@ -333,15 +363,17 @@ impl ContentDisposition {
// token: won't contains semicolon according to RFC 2616 Section 2.2 // token: won't contains semicolon according to RFC 2616 Section 2.2
let (token, new_left) = split_once_and_trim(left, ';'); let (token, new_left) = split_once_and_trim(left, ';');
left = new_left; left = new_left;
if token.is_empty() {
// quoted-string can be empty, but token cannot be empty
return Err(crate::error::ParseError::Header);
}
token.to_owned() token.to_owned()
}; };
if value.is_empty() {
return Err(crate::error::ParseError::Header);
}
let param = if param_name.eq_ignore_ascii_case("name") { let param = if param_name.eq_ignore_ascii_case("name") {
DispositionParam::Name(value) DispositionParam::Name(value)
} else if param_name.eq_ignore_ascii_case("filename") { } else if param_name.eq_ignore_ascii_case("filename") {
// See also comments in test_from_raw_uncessary_percent_decode.
DispositionParam::Filename(value) DispositionParam::Filename(value)
} else { } else {
DispositionParam::Unknown(param_name.to_owned(), value) DispositionParam::Unknown(param_name.to_owned(), value)
@ -466,11 +498,40 @@ impl fmt::Display for DispositionType {
impl fmt::Display for DispositionParam { impl fmt::Display for DispositionParam {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
// All ASCII control charaters (0-30, 127) excepting horizontal tab, double quote, and // All ASCII control characters (0-30, 127) including horizontal tab, double quote, and
// backslash should be escaped in quoted-string (i.e. "foobar"). // backslash should be escaped in quoted-string (i.e. "foobar").
// Ref: RFC6266 S4.1 -> RFC2616 S2.2; RFC 7578 S4.2 -> RFC2183 S2 -> ... . // Ref: RFC6266 S4.1 -> RFC2616 S3.6
// filename-parm = "filename" "=" value
// value = token | quoted-string
// quoted-string = ( <"> *(qdtext | quoted-pair ) <"> )
// qdtext = <any TEXT except <">>
// quoted-pair = "\" CHAR
// TEXT = <any OCTET except CTLs,
// but including LWS>
// LWS = [CRLF] 1*( SP | HT )
// OCTET = <any 8-bit sequence of data>
// CHAR = <any US-ASCII character (octets 0 - 127)>
// CTL = <any US-ASCII control character
// (octets 0 - 31) and DEL (127)>
//
// Ref: RFC7578 S4.2 -> RFC2183 S2 -> RFC2045 S5.1
// parameter := attribute "=" value
// attribute := token
// ; Matching of attributes
// ; is ALWAYS case-insensitive.
// value := token / quoted-string
// token := 1*<any (US-ASCII) CHAR except SPACE, CTLs,
// or tspecials>
// tspecials := "(" / ")" / "<" / ">" / "@" /
// "," / ";" / ":" / "\" / <">
// "/" / "[" / "]" / "?" / "="
// ; Must be in quoted-string,
// ; to use within parameter values
//
//
// See also comments in test_from_raw_uncessary_percent_decode.
lazy_static! { lazy_static! {
static ref RE: Regex = Regex::new("[\x01-\x08\x10\x1F\x7F\"\\\\]").unwrap(); static ref RE: Regex = Regex::new("[\x00-\x08\x10-\x1F\x7F\"\\\\]").unwrap();
} }
match self { match self {
DispositionParam::Name(ref value) => write!(f, "name={}", value), DispositionParam::Name(ref value) => write!(f, "name={}", value),
@ -774,8 +835,18 @@ mod tests {
#[test] #[test]
fn test_from_raw_uncessary_percent_decode() { fn test_from_raw_uncessary_percent_decode() {
// In fact, RFC7578 (multipart/form-data) Section 2 and 4.2 suggests that filename with
// non-ASCII characters MAY be percent-encoded.
// On the contrary, RFC6266 or other RFCs related to Content-Disposition response header
// do not mention such percent-encoding.
// So, it appears to be undecidable whether to percent-decode or not without
// knowing the usage scenario (multipart/form-data v.s. HTTP response header) and
// inevitable to unnecessarily percent-decode filename with %XX in the former scenario.
// Fortunately, it seems that almost all mainstream browsers just send UTF-8 encoded file
// names in quoted-string format (tested on Edge, IE11, Chrome and Firefox) without
// percent-encoding. So we do not bother to attempt to percent-decode.
let a = HeaderValue::from_static( let a = HeaderValue::from_static(
"form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"", // Should not be decoded! "form-data; name=photo; filename=\"%74%65%73%74%2e%70%6e%67\"",
); );
let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap(); let a: ContentDisposition = ContentDisposition::from_raw(&a).unwrap();
let b = ContentDisposition { let b = ContentDisposition {
@ -811,6 +882,9 @@ mod tests {
let a = HeaderValue::from_static("inline; filename= "); let a = HeaderValue::from_static("inline; filename= ");
assert!(ContentDisposition::from_raw(&a).is_err()); assert!(ContentDisposition::from_raw(&a).is_err());
let a = HeaderValue::from_static("inline; filename=\"\"");
assert!(ContentDisposition::from_raw(&a).expect("parse cd").get_filename().expect("filename").is_empty());
} }
#[test] #[test]

View File

@ -9,12 +9,12 @@ use http::HttpTryFrom;
/// `HeaderMap` is an multimap of [`HeaderName`] to values. /// `HeaderMap` is an multimap of [`HeaderName`] to values.
/// ///
/// [`HeaderName`]: struct.HeaderName.html /// [`HeaderName`]: struct.HeaderName.html
#[derive(Debug)] #[derive(Debug, Clone)]
pub struct HeaderMap { pub struct HeaderMap {
pub(crate) inner: HashMap<HeaderName, Value>, pub(crate) inner: HashMap<HeaderName, Value>,
} }
#[derive(Debug)] #[derive(Debug, Clone)]
pub(crate) enum Value { pub(crate) enum Value {
One(HeaderValue), One(HeaderValue),
Multi(Vec<HeaderValue>), Multi(Vec<HeaderValue>),

View File

@ -6,6 +6,7 @@ use std::{fmt, str::FromStr};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use http::Error as HttpError; use http::Error as HttpError;
use mime::Mime; use mime::Mime;
use percent_encoding::{AsciiSet, CONTROLS};
pub use http::header::*; pub use http::header::*;
@ -15,7 +16,6 @@ use crate::httpmessage::HttpMessage;
mod common; mod common;
pub(crate) mod map; pub(crate) mod map;
mod shared; mod shared;
#[doc(hidden)]
pub use self::common::*; pub use self::common::*;
#[doc(hidden)] #[doc(hidden)]
pub use self::shared::*; pub use self::shared::*;
@ -361,10 +361,8 @@ pub fn parse_extended_value(
impl fmt::Display for ExtendedValue { impl fmt::Display for ExtendedValue {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let encoded_value = percent_encoding::percent_encode( let encoded_value =
&self.value[..], percent_encoding::percent_encode(&self.value[..], HTTP_VALUE);
self::percent_encoding_http::HTTP_VALUE,
);
if let Some(ref lang) = self.language_tag { if let Some(ref lang) = self.language_tag {
write!(f, "{}'{}'{}", self.charset, lang, encoded_value) write!(f, "{}'{}'{}", self.charset, lang, encoded_value)
} else { } else {
@ -378,8 +376,7 @@ impl fmt::Display for ExtendedValue {
/// ///
/// [url]: https://tools.ietf.org/html/rfc5987#section-3.2 /// [url]: https://tools.ietf.org/html/rfc5987#section-3.2
pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result { pub fn http_percent_encode(f: &mut fmt::Formatter, bytes: &[u8]) -> fmt::Result {
let encoded = let encoded = percent_encoding::percent_encode(bytes, HTTP_VALUE);
percent_encoding::percent_encode(bytes, self::percent_encoding_http::HTTP_VALUE);
fmt::Display::fmt(&encoded, f) fmt::Display::fmt(&encoded, f)
} }
@ -394,20 +391,29 @@ impl From<http::HeaderMap> for HeaderMap {
} }
} }
mod percent_encoding_http { // This encode set is used for HTTP header values and is defined at
use percent_encoding::{self, define_encode_set}; // https://tools.ietf.org/html/rfc5987#section-3.2
pub(crate) const HTTP_VALUE: &AsciiSet = &CONTROLS
// internal module because macro is hard-coded to make a public item .add(b' ')
// but we don't want to public export this item .add(b'"')
define_encode_set! { .add(b'%')
// This encode set is used for HTTP header values and is defined at .add(b'\'')
// https://tools.ietf.org/html/rfc5987#section-3.2 .add(b'(')
pub HTTP_VALUE = [percent_encoding::SIMPLE_ENCODE_SET] | { .add(b')')
' ', '"', '%', '\'', '(', ')', '*', ',', '/', ':', ';', '<', '-', '>', '?', .add(b'*')
'[', '\\', ']', '{', '}' .add(b',')
} .add(b'/')
} .add(b':')
} .add(b';')
.add(b'<')
.add(b'-')
.add(b'>')
.add(b'?')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'{')
.add(b'}');
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {

View File

@ -115,7 +115,7 @@ pub fn write_content_length(mut n: usize, bytes: &mut BytesMut) {
pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) { pub(crate) fn convert_usize(mut n: usize, bytes: &mut BytesMut) {
let mut curr: isize = 39; let mut curr: isize = 39;
let mut buf: [u8; 41] = unsafe { mem::uninitialized() }; let mut buf: [u8; 41] = unsafe { mem::MaybeUninit::uninit().assume_init() };
buf[39] = b'\r'; buf[39] = b'\r';
buf[40] = b'\n'; buf[40] = b'\n';
let buf_ptr = buf.as_mut_ptr(); let buf_ptr = buf.as_mut_ptr();

View File

@ -29,7 +29,6 @@ impl Response {
STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED); STATIC_RESP!(AlreadyReported, StatusCode::ALREADY_REPORTED);
STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES); STATIC_RESP!(MultipleChoices, StatusCode::MULTIPLE_CHOICES);
STATIC_RESP!(MovedPermanenty, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY); STATIC_RESP!(MovedPermanently, StatusCode::MOVED_PERMANENTLY);
STATIC_RESP!(Found, StatusCode::FOUND); STATIC_RESP!(Found, StatusCode::FOUND);
STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER); STATIC_RESP!(SeeOther, StatusCode::SEE_OTHER);

View File

@ -39,7 +39,7 @@ pub use self::config::{KeepAlive, ServiceConfig};
pub use self::error::{Error, ResponseError, Result}; pub use self::error::{Error, ResponseError, Result};
pub use self::extensions::Extensions; pub use self::extensions::Extensions;
pub use self::httpmessage::HttpMessage; pub use self::httpmessage::HttpMessage;
pub use self::message::{Message, RequestHead, 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, ResponseBuilder};

View File

@ -181,6 +181,36 @@ impl RequestHead {
} }
} }
#[derive(Debug)]
pub enum RequestHeadType {
Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>),
}
impl RequestHeadType {
pub fn extra_headers(&self) -> Option<&HeaderMap> {
match self {
RequestHeadType::Owned(_) => None,
RequestHeadType::Rc(_, headers) => headers.as_ref(),
}
}
}
impl AsRef<RequestHead> for RequestHeadType {
fn as_ref(&self) -> &RequestHead {
match self {
RequestHeadType::Owned(head) => &head,
RequestHeadType::Rc(head, _) => head.as_ref(),
}
}
}
impl From<RequestHead> for RequestHeadType {
fn from(head: RequestHead) -> Self {
RequestHeadType::Owned(head)
}
}
#[derive(Debug)] #[derive(Debug)]
pub struct ResponseHead { pub struct ResponseHead {
pub version: Version, pub version: Version,

View File

@ -194,7 +194,7 @@ impl<B> Response<B> {
self.head.extensions.borrow_mut() self.head.extensions.borrow_mut()
} }
/// Get body os this response /// Get body of this response
#[inline] #[inline]
pub fn body(&self) -> &ResponseBody<B> { pub fn body(&self) -> &ResponseBody<B> {
&self.body &self.body
@ -992,6 +992,14 @@ mod tests {
assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]"); assert_eq!(resp.body().get_ref(), b"[\"v1\",\"v2\",\"v3\"]");
} }
#[test]
fn test_serde_json_in_body() {
use serde_json::json;
let resp =
Response::build(StatusCode::OK).body(json!({"test-key":"test-value"}));
assert_eq!(resp.body().get_ref(), br#"{"test-key":"test-value"}"#);
}
#[test] #[test]
fn test_into_response() { fn test_into_response() {
let resp: Response = "test".into(); let resp: Response = "test".into();

View File

@ -9,9 +9,9 @@ use bytes::{Buf, Bytes, BytesMut};
use futures::{Async, Poll}; use futures::{Async, Poll};
use http::header::{self, HeaderName, HeaderValue}; use http::header::{self, HeaderName, HeaderValue};
use http::{HttpTryFrom, Method, Uri, Version}; use http::{HttpTryFrom, Method, Uri, Version};
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use percent_encoding::percent_encode;
use crate::cookie::{Cookie, CookieJar}; use crate::cookie::{Cookie, CookieJar, USERINFO};
use crate::header::HeaderMap; use crate::header::HeaderMap;
use crate::header::{Header, IntoHeaderValue}; use crate::header::{Header, IntoHeaderValue};
use crate::payload::Payload; use crate::payload::Payload;
@ -150,7 +150,7 @@ impl TestRequest {
/// Complete request creation and generate `Request` instance /// Complete request creation and generate `Request` instance
pub fn finish(&mut self) -> Request { pub fn finish(&mut self) -> Request {
let inner = self.0.take().expect("cannot reuse test request builder");; let inner = self.0.take().expect("cannot reuse test request builder");
let mut req = if let Some(pl) = inner.payload { let mut req = if let Some(pl) = inner.payload {
Request::with_payload(pl) Request::with_payload(pl)
@ -166,8 +166,8 @@ impl TestRequest {
let mut cookie = String::new(); let mut cookie = String::new();
for c in inner.cookies.delta() { for c in inner.cookies.delta() {
let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); let name = percent_encode(c.name().as_bytes(), USERINFO);
let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(c.value().as_bytes(), USERINFO);
let _ = write!(&mut cookie, "; {}={}", name, value); let _ = write!(&mut cookie, "; {}={}", name, value);
} }
if !cookie.is_empty() { if !cookie.is_empty() {

View File

@ -1,4 +1,5 @@
use byteorder::{ByteOrder, LittleEndian, NetworkEndian}; use std::convert::TryFrom;
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut, Bytes, BytesMut};
use log::debug; use log::debug;
use rand; use rand;
@ -48,14 +49,16 @@ impl Parser {
if chunk_len < 4 { if chunk_len < 4 {
return Ok(None); return Ok(None);
} }
let len = NetworkEndian::read_uint(&src[idx..], 2) as usize; let len = usize::from(u16::from_be_bytes(
TryFrom::try_from(&src[idx..idx + 2]).unwrap(),
));
idx += 2; idx += 2;
len len
} else if len == 127 { } else if len == 127 {
if chunk_len < 10 { if chunk_len < 10 {
return Ok(None); return Ok(None);
} }
let len = NetworkEndian::read_uint(&src[idx..], 8); let len = u64::from_be_bytes(TryFrom::try_from(&src[idx..idx + 8]).unwrap());
if len > max_size as u64 { if len > max_size as u64 {
return Err(ProtocolError::Overflow); return Err(ProtocolError::Overflow);
} }
@ -75,10 +78,10 @@ impl Parser {
return Ok(None); return Ok(None);
} }
let mask: &[u8] = &src[idx..idx + 4]; let mask =
let mask_u32 = LittleEndian::read_u32(mask); u32::from_le_bytes(TryFrom::try_from(&src[idx..idx + 4]).unwrap());
idx += 4; idx += 4;
Some(mask_u32) Some(mask)
} else { } else {
None None
}; };
@ -137,7 +140,7 @@ impl Parser {
/// Parse the payload of a close frame. /// Parse the payload of a close frame.
pub fn parse_close_payload(payload: &[u8]) -> Option<CloseReason> { pub fn parse_close_payload(payload: &[u8]) -> Option<CloseReason> {
if payload.len() >= 2 { if payload.len() >= 2 {
let raw_code = NetworkEndian::read_u16(payload); let raw_code = u16::from_be_bytes(TryFrom::try_from(&payload[..2]).unwrap());
let code = CloseCode::from(raw_code); let code = CloseCode::from(raw_code);
let description = if payload.len() > 2 { let description = if payload.len() > 2 {
Some(String::from_utf8_lossy(&payload[2..]).into()) Some(String::from_utf8_lossy(&payload[2..]).into())
@ -201,10 +204,7 @@ impl Parser {
let payload = match reason { let payload = match reason {
None => Vec::new(), None => Vec::new(),
Some(reason) => { Some(reason) => {
let mut code_bytes = [0; 2]; let mut payload = Into::<u16>::into(reason.code).to_be_bytes().to_vec();
NetworkEndian::write_u16(&mut code_bytes, reason.code.into());
let mut payload = Vec::from(&code_bytes[..]);
if let Some(description) = reason.description { if let Some(description) = reason.description {
payload.extend(description.as_bytes()); payload.extend(description.as_bytes());
} }

View File

@ -105,7 +105,6 @@ fn align_buf(buf: &mut [u8]) -> (ShortSlice, &mut [u64], ShortSlice) {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::apply_mask; use super::apply_mask;
use byteorder::{ByteOrder, LittleEndian};
/// A safe unoptimized mask application. /// A safe unoptimized mask application.
fn apply_mask_fallback(buf: &mut [u8], mask: &[u8; 4]) { fn apply_mask_fallback(buf: &mut [u8], mask: &[u8; 4]) {
@ -117,7 +116,7 @@ mod tests {
#[test] #[test]
fn test_apply_mask() { fn test_apply_mask() {
let mask = [0x6d, 0xb6, 0xb2, 0x80]; let mask = [0x6d, 0xb6, 0xb2, 0x80];
let mask_u32: u32 = LittleEndian::read_u32(&mask); let mask_u32 = u32::from_le_bytes(mask);
let unmasked = vec![ let unmasked = vec![
0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17, 0xf3, 0x00, 0x01, 0x02, 0x03, 0x80, 0x81, 0x82, 0xff, 0xfe, 0x00, 0x17,

View File

@ -1,20 +0,0 @@
-----BEGIN CERTIFICATE-----
MIIDPjCCAiYCCQCmkoCBehOyYTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV
UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMRAwDgYDVQQKDAdDb21wYW55MQww
CgYDVQQLDANPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xOTAzMjky
MzE5MDlaFw0yMDAzMjgyMzE5MDlaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD
QTELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBhbnkxDDAKBgNVBAsMA09yZzEY
MBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A
MIIBCgKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmmbt8r
YNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2ZBro
AuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo4YI4
xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN124x
giFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ+K/Y
p/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABMA0GCSqGSIb3DQEBCwUAA4IB
AQAEWn3WAwAbd64f5jo2w4076s2qFiCJjPWoxO6bO75FgFFtw/NNev8pxGVw1ehg
HiTO6VRYolL5S/RKOchjA83AcDEBjgf8fKtvTmE9kxZSUIo4kIvv8V9ZM72gJhDN
8D/lXduTZ9JMwLOa1NUB8/I6CbaU3VzWkfodArKKpQF3M+LLgK03i12PD0KPQ5zv
bwaNoQo6cTmPNIdsVZETRvPqONiCUaQV57G74dGtjeirCh/DO5EYRtb1thgS7TGm
+Xg8OC5vZ6g0+xsrSqDBmWNtlI7S3bsL5C3LIEOOAL1ZJHRy2KvIGQ9ipb3XjnKS
N7/wlQduRyPH7oaD/o4xf5Gt
-----END CERTIFICATE-----

View File

@ -1,27 +0,0 @@
-----BEGIN RSA PRIVATE KEY-----
MIIEpQIBAAKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmm
bt8rYNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2
ZBroAuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo
4YI4xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN
124xgiFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ
+K/Yp/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABAoIBAQC4lzyQd+ITEbi+
dTxJuQj94hgHB1htgKqU888SLI5F9nP6n67y9hb5N9WygSp6UWbGqYTFYwxlPMKr
22p2WjL5NTsTcm+XdIKQZW/3y06Mn4qFefsT9XURaZriCjihfU2BRaCCNARSUzwd
ZH4I6n9mM7KaH71aa7v6ZVoahE9tXPR6hM+SHQEySW4pWkEu98VpNNeIt6vP7WF9
ONGbRa+0En4xgkuaxem2ZYa/GZFFtdQRkroNMhIRlfcPpkjy8DCc8E5RAkOzKC3O
lnxQwt+tdNNkGZz02ed2hx/YHPwFYy76y6hK5dxq74iKIaOc8U5t0HjB1zVfwiR0
5mcxMncxAoGBAP+RivwXZ4FcxDY1uyziF+rwlC/1RujQFEWXIxsXCnba5DH3yKul
iKEIZPZtGhpsnQe367lcXcn7tztuoVjpAnk5L+hQY64tLwYbHeRcOMJ75C2y8FFC
NeG5sQsrk3IU1+jhGvrbE7UgOeAuWJmv0M1vPNB/+hGoZBW5W5uU1x89AoGBANtA
AhLtAcqQ/Qh2SpVhLljh7U85Be9tbCGua09clkYFzh3bcoBolXKH18Veww0TP0yF
0748CKw1A+ITbTVFV+vKvi4jzIxS7mr4wYtVCMssbttQN7y3l30IDxJwa9j3zTJx
IUn5OMMLv1JyitLId8HdOy1AdU3MkpJzdLyi1mFfAoGBAL3kL4fGABM/kU7SN6RO
zgS0AvdrYOeljBp1BRGg2hab58g02vamxVEZgqMTR7zwjPDqOIz+03U7wda4Ccyd
PUhDNJSB/r6xNepshZZi642eLlnCRguqjYyNw72QADtYv2B6uehAlXEUY8xtw0lW
OGgcSeyF2pH6M3tswWNlgT3lAoGAQ/BttBelOnP7NKgTLH7Usc4wjyAIaszpePZn
Ykw6dLBP0oixzoCZ7seRYSOgJWkVcEz39Db+KP60mVWTvbIjMHm+vOVy+Pip0JQM
xXQwKWU3ZNZSrzPkyWW55ejYQn9nIn5T5mxH3ojBXHcJ9Y8RLQ20zKzwrI77zE3i
mqGK9NkCgYEAq3dzHI0DGAJrR19sWl2LcqI19sj5a91tHx4cl1dJXS/iApOLLieU
zyUGkwfsqjHPAZ7GacICeBojIn/7KdPdlSKAbGVAU3d4qzvFS0qmWzObplBz3niT
Xnep2XLaVXqwlFJZZ6AHeKzYmMH0d0raiou2bpEUBqYizy2fi3NI4mA=
-----END RSA PRIVATE KEY-----

View File

@ -0,0 +1,462 @@
#![cfg(feature = "rust-tls")]
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http::error::PayloadError;
use actix_http::http::header::{self, HeaderName, HeaderValue};
use actix_http::http::{Method, StatusCode, Version};
use actix_http::{body, error, Error, HttpService, Request, Response};
use actix_http_test::TestServer;
use actix_server::ssl::RustlsAcceptor;
use actix_server_config::ServerConfig;
use actix_service::{new_service_cfg, NewService};
use bytes::{Bytes, BytesMut};
use futures::future::{self, ok, Future};
use futures::stream::{once, Stream};
use rustls::{
internal::pemfile::{certs, pkcs8_private_keys},
NoClientAuth, ServerConfig as RustlsServerConfig,
};
use std::fs::File;
use std::io::{BufReader, Result};
fn load_body<S>(stream: S) -> impl Future<Item = BytesMut, Error = PayloadError>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
stream.fold(BytesMut::new(), move |mut body, chunk| {
body.extend_from_slice(&chunk);
Ok::<_, PayloadError>(body)
})
}
fn ssl_acceptor<T: AsyncRead + AsyncWrite>() -> Result<RustlsAcceptor<T, ()>> {
// load ssl keys
let mut config = RustlsServerConfig::new(NoClientAuth::new());
let cert_file = &mut BufReader::new(File::open("../tests/cert.pem").unwrap());
let key_file = &mut BufReader::new(File::open("../tests/key.pem").unwrap());
let cert_chain = certs(cert_file).unwrap();
let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
let protos = vec![b"h2".to_vec()];
config.set_protocols(&protos);
Ok(RustlsAcceptor::new(config))
}
#[test]
fn test_h2() -> Result<()> {
let rustls = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| future::ok::<_, Error>(Response::Ok().finish()))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
Ok(())
}
#[test]
fn test_h2_1() -> Result<()> {
let rustls = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.finish(|req: Request| {
assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_2);
future::ok::<_, Error>(Response::Ok().finish())
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
Ok(())
}
#[test]
fn test_h2_body() -> Result<()> {
let data = "HELLOWORLD".to_owned().repeat(64 * 1024);
let rustls = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|mut req: Request<_>| {
load_body(req.take_payload())
.and_then(|body| Ok(Response::Ok().body(body)))
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send_body(data.clone())).unwrap();
assert!(response.status().is_success());
let body = srv.load_body(response).unwrap();
assert_eq!(&body, data.as_bytes());
Ok(())
}
#[test]
fn test_h2_content_length() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|req: Request| {
let indx: usize = req.uri().path()[1..].parse().unwrap();
let statuses = [
StatusCode::NO_CONTENT,
StatusCode::CONTINUE,
StatusCode::SWITCHING_PROTOCOLS,
StatusCode::PROCESSING,
StatusCode::OK,
StatusCode::NOT_FOUND,
];
future::ok::<_, ()>(Response::new(statuses[indx]))
})
.map_err(|_| ()),
)
});
let header = HeaderName::from_static("content-length");
let value = HeaderValue::from_static("0");
{
for i in 0..4 {
let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), None);
let req = srv
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), None);
}
for i in 4..6 {
let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), Some(&value));
}
}
}
#[test]
fn test_h2_headers() {
let data = STR.repeat(10);
let data2 = data.clone();
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
let data = data.clone();
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build().h2(move |_| {
let mut config = Response::Ok();
for idx in 0..90 {
config.header(
format!("X-TEST-{}", idx).as_str(),
"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 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 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 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 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 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 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()))
}).map_err(|_| ()))
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from(data2));
}
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 \
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 \
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 \
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 \
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 \
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 \
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";
#[test]
fn test_h2_body2() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| future::ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test]
fn test_h2_head_empty() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
{
let len = response
.headers()
.get(http::header::CONTENT_LENGTH)
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
}
#[test]
fn test_h2_head_binary() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
ok::<_, ()>(
Response::Ok().content_length(STR.len() as u64).body(STR),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
{
let len = response
.headers()
.get(http::header::CONTENT_LENGTH)
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
}
#[test]
fn test_h2_head_binary2() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
{
let len = response
.headers()
.get(http::header::CONTENT_LENGTH)
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
}
#[test]
fn test_h2_body_length() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
let body = once(Ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(
Response::Ok()
.body(body::SizedStream::new(STR.len() as u64, body)),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test]
fn test_h2_body_chunked_explicit() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
let body =
once::<_, Error>(Ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(
Response::Ok()
.header(header::TRANSFER_ENCODING, "chunked")
.streaming(body),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
assert!(!response.headers().contains_key(header::TRANSFER_ENCODING));
// read response
let bytes = srv.load_body(response).unwrap();
// decode
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test]
fn test_h2_response_http_error_handling() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(new_service_cfg(|_: &ServerConfig| {
Ok::<_, ()>(|_| {
let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>(
Response::Ok()
.header(http::header::CONTENT_TYPE, broken_header)
.body(STR),
)
})
}))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR);
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(b"failed to parse header value"));
}
#[test]
fn test_h2_service_error() {
let rustls = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
rustls
.clone()
.map_err(|e| println!("Rustls error: {}", e))
.and_then(
HttpService::build()
.h2(|_| Err::<Response, Error>(error::ErrorBadRequest("error")))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert_eq!(response.status(), http::StatusCode::BAD_REQUEST);
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(b"error"));
}

View File

@ -2,32 +2,20 @@ use std::io::{Read, Write};
use std::time::Duration; use std::time::Duration;
use std::{net, thread}; use std::{net, thread};
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http_test::TestServer; use actix_http_test::TestServer;
use actix_server_config::ServerConfig; use actix_server_config::ServerConfig;
use actix_service::{new_service_cfg, service_fn, NewService}; use actix_service::{new_service_cfg, service_fn, NewService};
use bytes::{Bytes, BytesMut}; use bytes::Bytes;
use futures::future::{self, ok, Future}; use futures::future::{self, ok, Future};
use futures::stream::{once, Stream}; use futures::stream::{once, Stream};
use regex::Regex; use regex::Regex;
use tokio_timer::sleep; use tokio_timer::sleep;
use actix_http::error::PayloadError; use actix_http::httpmessage::HttpMessage;
use actix_http::{ use actix_http::{
body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response, body, error, http, http::header, Error, HttpService, KeepAlive, Request, Response,
}; };
#[cfg(feature = "ssl")]
fn load_body<S>(stream: S) -> impl Future<Item = BytesMut, Error = PayloadError>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
stream.fold(BytesMut::new(), move |mut body, chunk| {
body.extend_from_slice(&chunk);
Ok::<_, PayloadError>(body)
})
}
#[test] #[test]
fn test_h1() { fn test_h1() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -64,101 +52,6 @@ fn test_h1_2() {
assert!(response.status().is_success()); assert!(response.status().is_success());
} }
#[cfg(feature = "ssl")]
fn ssl_acceptor<T: AsyncRead + AsyncWrite>(
) -> std::io::Result<actix_server::ssl::OpensslAcceptor<T, ()>> {
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
// load ssl keys
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder
.set_private_key_file("tests/key.pem", SslFiletype::PEM)
.unwrap();
builder
.set_certificate_chain_file("tests/cert.pem")
.unwrap();
builder.set_alpn_select_callback(|_, protos| {
const H2: &[u8] = b"\x02h2";
if protos.windows(3).any(|window| window == H2) {
Ok(b"h2")
} else {
Err(openssl::ssl::AlpnError::NOACK)
}
});
builder.set_alpn_protos(b"\x02h2")?;
Ok(actix_server::ssl::OpensslAcceptor::new(builder.build()))
}
#[cfg(feature = "ssl")]
#[test]
fn test_h2() -> std::io::Result<()> {
let openssl = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| future::ok::<_, Error>(Response::Ok().finish()))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
Ok(())
}
#[cfg(feature = "ssl")]
#[test]
fn test_h2_1() -> std::io::Result<()> {
let openssl = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.finish(|req: Request| {
assert!(req.peer_addr().is_some());
assert_eq!(req.version(), http::Version::HTTP_2);
future::ok::<_, Error>(Response::Ok().finish())
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
Ok(())
}
#[cfg(feature = "ssl")]
#[test]
fn test_h2_body() -> std::io::Result<()> {
let data = "HELLOWORLD".to_owned().repeat(64 * 1024);
let openssl = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|mut req: Request<_>| {
load_body(req.take_payload())
.and_then(|body| Ok(Response::Ok().body(body)))
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send_body(data.clone())).unwrap();
assert!(response.status().is_success());
let body = srv.load_body(response).unwrap();
assert_eq!(&body, data.as_bytes());
Ok(())
}
#[test] #[test]
fn test_expect_continue() { fn test_expect_continue() {
let srv = TestServer::new(|| { let srv = TestServer::new(|| {
@ -457,65 +350,6 @@ fn test_content_length() {
} }
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_content_length() {
use actix_http::http::{
header::{HeaderName, HeaderValue},
StatusCode,
};
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|req: Request| {
let indx: usize = req.uri().path()[1..].parse().unwrap();
let statuses = [
StatusCode::NO_CONTENT,
StatusCode::CONTINUE,
StatusCode::SWITCHING_PROTOCOLS,
StatusCode::PROCESSING,
StatusCode::OK,
StatusCode::NOT_FOUND,
];
future::ok::<_, ()>(Response::new(statuses[indx]))
})
.map_err(|_| ()),
)
});
let header = HeaderName::from_static("content-length");
let value = HeaderValue::from_static("0");
{
for i in 0..4 {
let req = srv
.request(http::Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), None);
let req = srv
.request(http::Method::HEAD, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), None);
}
for i in 4..6 {
let req = srv
.request(http::Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), Some(&value));
}
}
}
#[test] #[test]
fn test_h1_headers() { fn test_h1_headers() {
let data = STR.repeat(10); let data = STR.repeat(10);
@ -555,51 +389,6 @@ fn test_h1_headers() {
assert_eq!(bytes, Bytes::from(data2)); assert_eq!(bytes, Bytes::from(data2));
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_headers() {
let data = STR.repeat(10);
let data2 = data.clone();
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
let data = data.clone();
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build().h2(move |_| {
let mut builder = Response::Ok();
for idx in 0..90 {
builder.header(
format!("X-TEST-{}", idx).as_str(),
"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 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 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 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 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 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 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()))
}).map_err(|_| ()))
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from(data2));
}
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 \
Hello World Hello World Hello World Hello World Hello World \ Hello World Hello World Hello World Hello World Hello World \
@ -636,29 +425,6 @@ fn test_h1_body() {
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_body2() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| future::ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test] #[test]
fn test_h1_head_empty() { fn test_h1_head_empty() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -681,38 +447,6 @@ fn test_h1_head_empty() {
assert!(bytes.is_empty()); assert!(bytes.is_empty());
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_head_empty() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), http::Version::HTTP_2);
{
let len = response
.headers()
.get(http::header::CONTENT_LENGTH)
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
}
#[test] #[test]
fn test_h1_head_binary() { fn test_h1_head_binary() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -737,41 +471,6 @@ fn test_h1_head_binary() {
assert!(bytes.is_empty()); assert!(bytes.is_empty());
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_head_binary() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
ok::<_, ()>(
Response::Ok().content_length(STR.len() as u64).body(STR),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
{
let len = response
.headers()
.get(http::header::CONTENT_LENGTH)
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
}
#[test] #[test]
fn test_h1_head_binary2() { fn test_h1_head_binary2() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -790,33 +489,6 @@ fn test_h1_head_binary2() {
} }
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_head_binary2() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
{
let len = response
.headers()
.get(http::header::CONTENT_LENGTH)
.unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
}
#[test] #[test]
fn test_h1_body_length() { fn test_h1_body_length() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -836,35 +508,6 @@ fn test_h1_body_length() {
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_body_length() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
let body = once(Ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(
Response::Ok()
.body(body::SizedStream::new(STR.len() as u64, body)),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test] #[test]
fn test_h1_body_chunked_explicit() { fn test_h1_body_chunked_explicit() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -897,40 +540,6 @@ fn test_h1_body_chunked_explicit() {
assert_eq!(bytes, Bytes::from_static(STR.as_ref())); assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_body_chunked_explicit() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
let body =
once::<_, Error>(Ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(
Response::Ok()
.header(header::TRANSFER_ENCODING, "chunked")
.streaming(body),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
assert!(!response.headers().contains_key(header::TRANSFER_ENCODING));
// read response
let bytes = srv.load_body(response).unwrap();
// decode
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test] #[test]
fn test_h1_body_chunked_implicit() { fn test_h1_body_chunked_implicit() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -980,39 +589,6 @@ fn test_h1_response_http_error_handling() {
assert_eq!(bytes, Bytes::from_static(b"failed to parse header value")); assert_eq!(bytes, Bytes::from_static(b"failed to parse header value"));
} }
#[cfg(feature = "ssl")]
#[test]
fn test_h2_response_http_error_handling() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(new_service_cfg(|_: &ServerConfig| {
Ok::<_, ()>(|_| {
let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>(
Response::Ok()
.header(http::header::CONTENT_TYPE, broken_header)
.body(STR),
)
})
}))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR);
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(b"failed to parse header value"));
}
#[test] #[test]
fn test_h1_service_error() { fn test_h1_service_error() {
let mut srv = TestServer::new(|| { let mut srv = TestServer::new(|| {
@ -1028,26 +604,17 @@ fn test_h1_service_error() {
assert_eq!(bytes, Bytes::from_static(b"error")); assert_eq!(bytes, Bytes::from_static(b"error"));
} }
#[cfg(feature = "ssl")]
#[test] #[test]
fn test_h2_service_error() { fn test_h1_on_connect() {
let openssl = ssl_acceptor().unwrap(); let mut srv = TestServer::new(|| {
HttpService::build()
let mut srv = TestServer::new(move || { .on_connect(|_| 10usize)
openssl .h1(|req: Request| {
.clone() assert!(req.extensions().contains::<usize>());
.map_err(|e| println!("Openssl error: {}", e)) future::ok::<_, ()>(Response::Ok().finish())
.and_then( })
HttpService::build()
.h2(|_| Err::<Response, Error>(error::ErrorBadRequest("error")))
.map_err(|_| ()),
)
}); });
let response = srv.block_on(srv.sget("/").send()).unwrap(); let response = srv.block_on(srv.get("/").send()).unwrap();
assert_eq!(response.status(), http::StatusCode::INTERNAL_SERVER_ERROR); assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
} }

View File

@ -0,0 +1,480 @@
#![cfg(feature = "ssl")]
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http_test::TestServer;
use actix_server::ssl::OpensslAcceptor;
use actix_server_config::ServerConfig;
use actix_service::{new_service_cfg, NewService};
use bytes::{Bytes, BytesMut};
use futures::future::{ok, Future};
use futures::stream::{once, Stream};
use openssl::ssl::{AlpnError, SslAcceptor, SslFiletype, SslMethod};
use std::io::Result;
use actix_http::error::{ErrorBadRequest, PayloadError};
use actix_http::http::header::{self, HeaderName, HeaderValue};
use actix_http::http::{Method, StatusCode, Version};
use actix_http::httpmessage::HttpMessage;
use actix_http::{body, Error, HttpService, Request, Response};
fn load_body<S>(stream: S) -> impl Future<Item = BytesMut, Error = PayloadError>
where
S: Stream<Item = Bytes, Error = PayloadError>,
{
stream.fold(BytesMut::new(), move |mut body, chunk| {
body.extend_from_slice(&chunk);
Ok::<_, PayloadError>(body)
})
}
fn ssl_acceptor<T: AsyncRead + AsyncWrite>() -> Result<OpensslAcceptor<T, ()>> {
// load ssl keys
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder
.set_private_key_file("../tests/key.pem", SslFiletype::PEM)
.unwrap();
builder
.set_certificate_chain_file("../tests/cert.pem")
.unwrap();
builder.set_alpn_select_callback(|_, protos| {
const H2: &[u8] = b"\x02h2";
if protos.windows(3).any(|window| window == H2) {
Ok(b"h2")
} else {
Err(AlpnError::NOACK)
}
});
builder.set_alpn_protos(b"\x02h2")?;
Ok(OpensslAcceptor::new(builder.build()))
}
#[test]
fn test_h2() -> Result<()> {
let openssl = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| ok::<_, Error>(Response::Ok().finish()))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
Ok(())
}
#[test]
fn test_h2_1() -> Result<()> {
let openssl = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.finish(|req: Request| {
assert!(req.peer_addr().is_some());
assert_eq!(req.version(), Version::HTTP_2);
ok::<_, Error>(Response::Ok().finish())
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
Ok(())
}
#[test]
fn test_h2_body() -> Result<()> {
let data = "HELLOWORLD".to_owned().repeat(64 * 1024);
let openssl = ssl_acceptor()?;
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|mut req: Request<_>| {
load_body(req.take_payload())
.and_then(|body| Ok(Response::Ok().body(body)))
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send_body(data.clone())).unwrap();
assert!(response.status().is_success());
let body = srv.load_body(response).unwrap();
assert_eq!(&body, data.as_bytes());
Ok(())
}
#[test]
fn test_h2_content_length() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|req: Request| {
let indx: usize = req.uri().path()[1..].parse().unwrap();
let statuses = [
StatusCode::NO_CONTENT,
StatusCode::CONTINUE,
StatusCode::SWITCHING_PROTOCOLS,
StatusCode::PROCESSING,
StatusCode::OK,
StatusCode::NOT_FOUND,
];
ok::<_, ()>(Response::new(statuses[indx]))
})
.map_err(|_| ()),
)
});
let header = HeaderName::from_static("content-length");
let value = HeaderValue::from_static("0");
{
for i in 0..4 {
let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), None);
let req = srv
.request(Method::HEAD, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), None);
}
for i in 4..6 {
let req = srv
.request(Method::GET, srv.surl(&format!("/{}", i)))
.send();
let response = srv.block_on(req).unwrap();
assert_eq!(response.headers().get(&header), Some(&value));
}
}
}
#[test]
fn test_h2_headers() {
let data = STR.repeat(10);
let data2 = data.clone();
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
let data = data.clone();
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build().h2(move |_| {
let mut builder = Response::Ok();
for idx in 0..90 {
builder.header(
format!("X-TEST-{}", idx).as_str(),
"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 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 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 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 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 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 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()))
}).map_err(|_| ()))
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from(data2));
}
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 \
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 \
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 \
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 \
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 \
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 \
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";
#[test]
fn test_h2_body2() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test]
fn test_h2_head_empty() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.finish(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
{
let len = response.headers().get(header::CONTENT_LENGTH).unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
}
#[test]
fn test_h2_head_binary() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
ok::<_, ()>(
Response::Ok().content_length(STR.len() as u64).body(STR),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
{
let len = response.headers().get(header::CONTENT_LENGTH).unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
// read response
let bytes = srv.load_body(response).unwrap();
assert!(bytes.is_empty());
}
#[test]
fn test_h2_head_binary2() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| ok::<_, ()>(Response::Ok().body(STR)))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.shead("/").send()).unwrap();
assert!(response.status().is_success());
{
let len = response.headers().get(header::CONTENT_LENGTH).unwrap();
assert_eq!(format!("{}", STR.len()), len.to_str().unwrap());
}
}
#[test]
fn test_h2_body_length() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
let body = once(Ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(
Response::Ok()
.body(body::SizedStream::new(STR.len() as u64, body)),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test]
fn test_h2_body_chunked_explicit() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| {
let body =
once::<_, Error>(Ok(Bytes::from_static(STR.as_ref())));
ok::<_, ()>(
Response::Ok()
.header(header::TRANSFER_ENCODING, "chunked")
.streaming(body),
)
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
assert!(!response.headers().contains_key(header::TRANSFER_ENCODING));
// read response
let bytes = srv.load_body(response).unwrap();
// decode
assert_eq!(bytes, Bytes::from_static(STR.as_ref()));
}
#[test]
fn test_h2_response_http_error_handling() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(new_service_cfg(|_: &ServerConfig| {
Ok::<_, ()>(|_| {
let broken_header = Bytes::from_static(b"\0\0\0");
ok::<_, ()>(
Response::Ok()
.header(header::CONTENT_TYPE, broken_header)
.body(STR),
)
})
}))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert_eq!(response.status(), StatusCode::INTERNAL_SERVER_ERROR);
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(b"failed to parse header value"));
}
#[test]
fn test_h2_service_error() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.h2(|_| Err::<Response, Error>(ErrorBadRequest("error")))
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert_eq!(response.status(), StatusCode::BAD_REQUEST);
// read response
let bytes = srv.load_body(response).unwrap();
assert_eq!(bytes, Bytes::from_static(b"error"));
}
#[test]
fn test_h2_on_connect() {
let openssl = ssl_acceptor().unwrap();
let mut srv = TestServer::new(move || {
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e))
.and_then(
HttpService::build()
.on_connect(|_| 10usize)
.h2(|req: Request| {
assert!(req.extensions().contains::<usize>());
ok::<_, ()>(Response::Ok().finish())
})
.map_err(|_| ()),
)
});
let response = srv.block_on(srv.sget("/").send()).unwrap();
assert!(response.status().is_success());
}

View File

@ -1,6 +1,10 @@
# Changes # Changes
## [0.1.3] - 2019-06-06 ## [0.1.4] - 2019-09-12
* Multipart handling now parses requests which do not end in CRLF #1038
## [0.1.3] - 2019-08-18
* Fix ring dependency from actix-web default features for #741. * Fix ring dependency from actix-web default features for #741.

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-multipart" name = "actix-multipart"
version = "0.1.3" version = "0.1.4"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Multipart support for actix web framework." description = "Multipart support for actix web framework."
readme = "README.md" readme = "README.md"

View File

@ -167,7 +167,7 @@ impl InnerMultipart {
boundary: &str, boundary: &str,
) -> Result<Option<bool>, MultipartError> { ) -> Result<Option<bool>, MultipartError> {
// TODO: need to read epilogue // TODO: need to read epilogue
match payload.readline()? { match payload.readline_or_eof()? {
None => { None => {
if payload.eof { if payload.eof {
Ok(Some(true)) Ok(Some(true))
@ -176,15 +176,16 @@ impl InnerMultipart {
} }
} }
Some(chunk) => { Some(chunk) => {
if chunk.len() == boundary.len() + 4 if chunk.len() < boundary.len() + 4
&& &chunk[..2] == b"--" || &chunk[..2] != b"--"
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes() || &chunk[2..boundary.len() + 2] != boundary.as_bytes()
{ {
Err(MultipartError::Boundary)
} else if &chunk[boundary.len() + 2..] == b"\r\n" {
Ok(Some(false)) Ok(Some(false))
} else if chunk.len() == boundary.len() + 6 } else if &chunk[boundary.len() + 2..boundary.len() + 4] == b"--"
&& &chunk[..2] == b"--" && (chunk.len() == boundary.len() + 4
&& &chunk[2..boundary.len() + 2] == boundary.as_bytes() || &chunk[boundary.len() + 4..] == b"\r\n")
&& &chunk[boundary.len() + 2..boundary.len() + 4] == b"--"
{ {
Ok(Some(true)) Ok(Some(true))
} else { } else {
@ -779,6 +780,16 @@ impl PayloadBuffer {
self.read_until(b"\n") self.read_until(b"\n")
} }
/// Read bytes until new line delimiter or eof
pub fn readline_or_eof(&mut self) -> Result<Option<Bytes>, MultipartError> {
match self.readline() {
Err(MultipartError::Incomplete) if self.eof => {
Ok(Some(self.buf.take().freeze()))
}
line => line,
}
}
/// Put unprocessed data back to the buffer /// Put unprocessed data back to the buffer
pub fn unprocessed(&mut self, data: Bytes) { pub fn unprocessed(&mut self, data: Bytes) {
let buf = BytesMut::from(data); let buf = BytesMut::from(data);
@ -849,32 +860,65 @@ mod tests {
(tx, rx.map_err(|_| panic!()).and_then(|res| res)) (tx, rx.map_err(|_| panic!()).and_then(|res| res))
} }
fn create_simple_request_with_header() -> (Bytes, HeaderMap) {
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
);
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
(bytes, headers)
}
#[test]
fn test_multipart_no_end_crlf() {
run_on(|| {
let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
let bytes_stripped = bytes.slice_to(bytes.len()); // strip crlf
sender.unbounded_send(Ok(bytes_stripped)).unwrap();
drop(sender); // eof
let mut multipart = Multipart::new(&headers, payload);
match multipart.poll().unwrap() {
Async::Ready(Some(_)) => (),
_ => unreachable!(),
}
match multipart.poll().unwrap() {
Async::Ready(Some(_)) => (),
_ => unreachable!(),
}
match multipart.poll().unwrap() {
Async::Ready(None) => (),
_ => unreachable!(),
}
})
}
#[test] #[test]
fn test_multipart() { fn test_multipart() {
run_on(|| { run_on(|| {
let (sender, payload) = create_stream(); let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\nContent-Length: 4\r\n\r\n\
data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
);
sender.unbounded_send(Ok(bytes)).unwrap(); sender.unbounded_send(Ok(bytes)).unwrap();
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let mut multipart = Multipart::new(&headers, payload); let mut multipart = Multipart::new(&headers, payload);
match multipart.poll().unwrap() { match multipart.poll().unwrap() {
Async::Ready(Some(mut field)) => { Async::Ready(Some(mut field)) => {
@ -925,28 +969,10 @@ mod tests {
fn test_stream() { fn test_stream() {
run_on(|| { run_on(|| {
let (sender, payload) = create_stream(); let (sender, payload) = create_stream();
let (bytes, headers) = create_simple_request_with_header();
let bytes = Bytes::from(
"testasdadsad\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Disposition: form-data; name=\"file\"; filename=\"fn.txt\"\r\n\
Content-Type: text/plain; charset=utf-8\r\n\r\n\
test\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0\r\n\
Content-Type: text/plain; charset=utf-8\r\n\r\n\
data\r\n\
--abbc761f78ff4d7cb7573b5a23f96ef0--\r\n",
);
sender.unbounded_send(Ok(bytes)).unwrap(); sender.unbounded_send(Ok(bytes)).unwrap();
let mut headers = HeaderMap::new();
headers.insert(
header::CONTENT_TYPE,
header::HeaderValue::from_static(
"multipart/mixed; boundary=\"abbc761f78ff4d7cb7573b5a23f96ef0\"",
),
);
let mut multipart = Multipart::new(&headers, payload); let mut multipart = Multipart::new(&headers, payload);
match multipart.poll().unwrap() { match multipart.poll().unwrap() {
Async::Ready(Some(mut field)) => { Async::Ready(Some(mut field)) => {

View File

@ -1,5 +1,12 @@
# Changes # Changes
## [1.0.2] - 2019-07-20
* Add `ws::start_with_addr()`, returning the address of the created actor, along
with the `HttpResponse`.
* Add support for specifying protocols on websocket handshake #835
## [1.0.1] - 2019-06-28 ## [1.0.1] - 2019-06-28
* Allow to use custom ws codec with `WebsocketContext` #925 * Allow to use custom ws codec with `WebsocketContext` #925

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-actors" name = "actix-web-actors"
version = "1.0.1" version = "1.0.2"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix actors support for actix web framework." description = "Actix actors support for actix web framework."
readme = "README.md" readme = "README.md"
@ -27,4 +27,4 @@ futures = "0.1.25"
[dev-dependencies] [dev-dependencies]
env_logger = "0.6" env_logger = "0.6"
actix-http-test = { version = "0.2.0", features=["ssl"] } actix-http-test = { version = "0.2.4", features=["ssl"] }

View File

@ -35,15 +35,68 @@ where
Ok(res.streaming(WebsocketContext::create(actor, stream))) Ok(res.streaming(WebsocketContext::create(actor, stream)))
} }
/// Do websocket handshake and start ws actor.
///
/// `req` is an HTTP Request that should be requesting a websocket protocol
/// change. `stream` should be a `Bytes` stream (such as
/// `actix_web::web::Payload`) that contains a stream of the body request.
///
/// If there is a problem with the handshake, an error is returned.
///
/// If successful, returns a pair where the first item is an address for the
/// created actor and the second item is the response that should be returned
/// from the websocket request.
pub fn start_with_addr<A, T>(
actor: A,
req: &HttpRequest,
stream: T,
) -> Result<(Addr<A>, HttpResponse), Error>
where
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Message, ProtocolError>,
T: Stream<Item = Bytes, Error = PayloadError> + 'static,
{
let mut res = handshake(req)?;
let (addr, out_stream) = WebsocketContext::create_with_addr(actor, stream);
Ok((addr, res.streaming(out_stream)))
}
/// Do websocket handshake and start ws actor.
///
/// `protocols` is a sequence of known protocols.
pub fn start_with_protocols<A, T>(
actor: A,
protocols: &[&str],
req: &HttpRequest,
stream: T,
) -> Result<HttpResponse, Error>
where
A: Actor<Context = WebsocketContext<A>> + StreamHandler<Message, ProtocolError>,
T: Stream<Item = Bytes, Error = PayloadError> + 'static,
{
let mut res = handshake_with_protocols(req, protocols)?;
Ok(res.streaming(WebsocketContext::create(actor, stream)))
}
/// Prepare `WebSocket` handshake response.
///
/// This function returns handshake `HttpResponse`, ready to send to peer.
/// It does not perform any IO.
pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeError> {
handshake_with_protocols(req, &[])
}
/// Prepare `WebSocket` handshake response. /// Prepare `WebSocket` handshake response.
/// ///
/// This function returns handshake `HttpResponse`, ready to send to peer. /// This function returns handshake `HttpResponse`, ready to send to peer.
/// It does not perform any IO. /// It does not perform any IO.
/// ///
// /// `protocols` is a sequence of known protocols. On successful handshake, /// `protocols` is a sequence of known protocols. On successful handshake,
// /// the returned response headers contain the first protocol in this list /// the returned response headers contain the first protocol in this list
// /// which the server also knows. /// which the server also knows.
pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeError> { pub fn handshake_with_protocols(
req: &HttpRequest,
protocols: &[&str],
) -> Result<HttpResponseBuilder, HandshakeError> {
// WebSocket accepts only GET // WebSocket accepts only GET
if *req.method() != Method::GET { if *req.method() != Method::GET {
return Err(HandshakeError::GetMethodRequired); return Err(HandshakeError::GetMethodRequired);
@ -92,11 +145,28 @@ pub fn handshake(req: &HttpRequest) -> Result<HttpResponseBuilder, HandshakeErro
hash_key(key.as_ref()) hash_key(key.as_ref())
}; };
Ok(HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS) // check requested protocols
let protocol =
req.headers()
.get(&header::SEC_WEBSOCKET_PROTOCOL)
.and_then(|req_protocols| {
let req_protocols = req_protocols.to_str().ok()?;
req_protocols
.split(", ")
.find(|req_p| protocols.iter().any(|p| p == req_p))
});
let mut response = HttpResponse::build(StatusCode::SWITCHING_PROTOCOLS)
.upgrade("websocket") .upgrade("websocket")
.header(header::TRANSFER_ENCODING, "chunked") .header(header::TRANSFER_ENCODING, "chunked")
.header(header::SEC_WEBSOCKET_ACCEPT, key.as_str()) .header(header::SEC_WEBSOCKET_ACCEPT, key.as_str())
.take()) .take();
if let Some(protocol) = protocol {
response.header(&header::SEC_WEBSOCKET_PROTOCOL, protocol);
}
Ok(response)
} }
/// Execution context for `WebSockets` actors /// Execution context for `WebSockets` actors
@ -168,6 +238,24 @@ where
#[inline] #[inline]
/// Create a new Websocket context from a request and an actor /// Create a new Websocket context from a request and an actor
pub fn create<S>(actor: A, stream: S) -> impl Stream<Item = Bytes, Error = Error> pub fn create<S>(actor: A, stream: S) -> impl Stream<Item = Bytes, Error = Error>
where
A: StreamHandler<Message, ProtocolError>,
S: Stream<Item = Bytes, Error = PayloadError> + 'static,
{
let (_, stream) = WebsocketContext::create_with_addr(actor, stream);
stream
}
#[inline]
/// Create a new Websocket context from a request and an actor.
///
/// Returns a pair, where the first item is an addr for the created actor,
/// and the second item is a stream intended to be set as part of the
/// response via `HttpResponseBuilder::streaming()`.
pub fn create_with_addr<S>(
actor: A,
stream: S,
) -> (Addr<A>, impl Stream<Item = Bytes, Error = Error>)
where where
A: StreamHandler<Message, ProtocolError>, A: StreamHandler<Message, ProtocolError>,
S: Stream<Item = Bytes, Error = PayloadError> + 'static, S: Stream<Item = Bytes, Error = PayloadError> + 'static,
@ -179,7 +267,9 @@ where
}; };
ctx.add_stream(WsStream::new(stream, Codec::new())); ctx.add_stream(WsStream::new(stream, Codec::new()));
WebsocketContextFut::new(ctx, actor, mb, Codec::new()) let addr = ctx.address();
(addr, WebsocketContextFut::new(ctx, actor, mb, Codec::new()))
} }
#[inline] #[inline]
@ -564,5 +654,87 @@ mod tests {
StatusCode::SWITCHING_PROTOCOLS, StatusCode::SWITCHING_PROTOCOLS,
handshake(&req).unwrap().finish().status() handshake(&req).unwrap().finish().status()
); );
let req = TestRequest::default()
.header(
header::UPGRADE,
header::HeaderValue::from_static("websocket"),
)
.header(
header::CONNECTION,
header::HeaderValue::from_static("upgrade"),
)
.header(
header::SEC_WEBSOCKET_VERSION,
header::HeaderValue::from_static("13"),
)
.header(
header::SEC_WEBSOCKET_KEY,
header::HeaderValue::from_static("13"),
)
.header(
header::SEC_WEBSOCKET_PROTOCOL,
header::HeaderValue::from_static("graphql"),
)
.to_http_request();
let protocols = ["graphql"];
assert_eq!(
StatusCode::SWITCHING_PROTOCOLS,
handshake_with_protocols(&req, &protocols)
.unwrap()
.finish()
.status()
);
assert_eq!(
Some(&header::HeaderValue::from_static("graphql")),
handshake_with_protocols(&req, &protocols)
.unwrap()
.finish()
.headers()
.get(&header::SEC_WEBSOCKET_PROTOCOL)
);
let req = TestRequest::default()
.header(
header::UPGRADE,
header::HeaderValue::from_static("websocket"),
)
.header(
header::CONNECTION,
header::HeaderValue::from_static("upgrade"),
)
.header(
header::SEC_WEBSOCKET_VERSION,
header::HeaderValue::from_static("13"),
)
.header(
header::SEC_WEBSOCKET_KEY,
header::HeaderValue::from_static("13"),
)
.header(
header::SEC_WEBSOCKET_PROTOCOL,
header::HeaderValue::from_static("p1, p2, p3"),
)
.to_http_request();
let protocols = vec!["p3", "p2"];
assert_eq!(
StatusCode::SWITCHING_PROTOCOLS,
handshake_with_protocols(&req, &protocols)
.unwrap()
.finish()
.status()
);
assert_eq!(
Some(&header::HeaderValue::from_static("p2")),
handshake_with_protocols(&req, &protocols)
.unwrap()
.finish()
.headers()
.get(&header::SEC_WEBSOCKET_PROTOCOL)
);
} }
} }

View File

@ -1,5 +1,11 @@
# Changes # Changes
## [0.1.3] - 2019-10-14
* Bump up `syn` & `quote` to 1.0
* Provide better error message
## [0.1.2] - 2019-06-04 ## [0.1.2] - 2019-06-04
* Add macros for head, options, trace, connect and patch http methods * Add macros for head, options, trace, connect and patch http methods

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-codegen" name = "actix-web-codegen"
version = "0.1.2" version = "0.1.3"
description = "Actix web proc macros" description = "Actix web proc macros"
readme = "README.md" readme = "README.md"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
@ -12,8 +12,9 @@ workspace = ".."
proc-macro = true proc-macro = true
[dependencies] [dependencies]
quote = "0.6.12" quote = "1"
syn = { version = "0.15.34", features = ["full", "parsing", "extra-traits"] } syn = { version = "1", features = ["full", "parsing"] }
proc-macro2 = "1"
[dev-dependencies] [dev-dependencies]
actix-web = { version = "1.0.0" } actix-web = { version = "1.0.0" }

View File

@ -58,7 +58,10 @@ use syn::parse_macro_input;
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn get(args: TokenStream, input: TokenStream) -> TokenStream { pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Get); let gen = match route::Route::new(args, input, route::GuardType::Get) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -70,7 +73,10 @@ pub fn get(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn post(args: TokenStream, input: TokenStream) -> TokenStream { pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Post); let gen = match route::Route::new(args, input, route::GuardType::Post) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -82,7 +88,10 @@ pub fn post(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn put(args: TokenStream, input: TokenStream) -> TokenStream { pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Put); let gen = match route::Route::new(args, input, route::GuardType::Put) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -94,7 +103,10 @@ pub fn put(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream { pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Delete); let gen = match route::Route::new(args, input, route::GuardType::Delete) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -106,7 +118,10 @@ pub fn delete(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn head(args: TokenStream, input: TokenStream) -> TokenStream { pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Head); let gen = match route::Route::new(args, input, route::GuardType::Head) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -118,7 +133,10 @@ pub fn head(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream { pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Connect); let gen = match route::Route::new(args, input, route::GuardType::Connect) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -130,7 +148,10 @@ pub fn connect(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn options(args: TokenStream, input: TokenStream) -> TokenStream { pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Options); let gen = match route::Route::new(args, input, route::GuardType::Options) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -142,7 +163,10 @@ pub fn options(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream { pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Trace); let gen = match route::Route::new(args, input, route::GuardType::Trace) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }
@ -154,6 +178,9 @@ pub fn trace(args: TokenStream, input: TokenStream) -> TokenStream {
#[proc_macro_attribute] #[proc_macro_attribute]
pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream { pub fn patch(args: TokenStream, input: TokenStream) -> TokenStream {
let args = parse_macro_input!(args as syn::AttributeArgs); let args = parse_macro_input!(args as syn::AttributeArgs);
let gen = route::Args::new(&args, input, route::GuardType::Patch); let gen = match route::Route::new(args, input, route::GuardType::Patch) {
Ok(gen) => gen,
Err(err) => return err.to_compile_error().into(),
};
gen.generate() gen.generate()
} }

View File

@ -1,21 +1,23 @@
extern crate proc_macro; extern crate proc_macro;
use std::fmt;
use proc_macro::TokenStream; use proc_macro::TokenStream;
use quote::quote; use proc_macro2::{Span, TokenStream as TokenStream2};
use quote::{quote, ToTokens, TokenStreamExt};
use syn::{AttributeArgs, Ident, NestedMeta};
enum ResourceType { enum ResourceType {
Async, Async,
Sync, Sync,
} }
impl fmt::Display for ResourceType { impl ToTokens for ResourceType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn to_tokens(&self, stream: &mut TokenStream2) {
match *self { let ident = match self {
ResourceType::Async => write!(f, "to_async"), ResourceType::Async => "to_async",
ResourceType::Sync => write!(f, "to"), ResourceType::Sync => "to",
} };
let ident = Ident::new(ident, Span::call_site());
stream.append(ident);
} }
} }
@ -32,63 +34,89 @@ pub enum GuardType {
Patch, Patch,
} }
impl fmt::Display for GuardType { impl GuardType {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { fn as_str(&self) -> &'static str {
match *self { match self {
GuardType::Get => write!(f, "Get"), GuardType::Get => "Get",
GuardType::Post => write!(f, "Post"), GuardType::Post => "Post",
GuardType::Put => write!(f, "Put"), GuardType::Put => "Put",
GuardType::Delete => write!(f, "Delete"), GuardType::Delete => "Delete",
GuardType::Head => write!(f, "Head"), GuardType::Head => "Head",
GuardType::Connect => write!(f, "Connect"), GuardType::Connect => "Connect",
GuardType::Options => write!(f, "Options"), GuardType::Options => "Options",
GuardType::Trace => write!(f, "Trace"), GuardType::Trace => "Trace",
GuardType::Patch => write!(f, "Patch"), GuardType::Patch => "Patch",
} }
} }
} }
pub struct Args { impl ToTokens for GuardType {
name: syn::Ident, fn to_tokens(&self, stream: &mut TokenStream2) {
path: String, let ident = self.as_str();
ast: syn::ItemFn, let ident = Ident::new(ident, Span::call_site());
resource_type: ResourceType, stream.append(ident);
pub guard: GuardType, }
pub extra_guards: Vec<String>,
} }
impl fmt::Display for Args { struct Args {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { path: syn::LitStr,
let ast = &self.ast; guards: Vec<Ident>,
let guards = format!(".guard(actix_web::guard::{}())", self.guard); }
let guards = self.extra_guards.iter().fold(guards, |acc, val| {
format!("{}.guard(actix_web::guard::fn_guard({}))", acc, val)
});
write!( impl Args {
f, fn new(args: AttributeArgs) -> syn::Result<Self> {
" let mut path = None;
#[allow(non_camel_case_types)] let mut guards = Vec::new();
pub struct {name}; for arg in args {
match arg {
impl actix_web::dev::HttpServiceFactory for {name} {{ NestedMeta::Lit(syn::Lit::Str(lit)) => match path {
fn register(self, config: &mut actix_web::dev::AppService) {{ None => {
{ast} path = Some(lit);
}
let resource = actix_web::Resource::new(\"{path}\"){guards}.{to}({name}); _ => {
return Err(syn::Error::new_spanned(
actix_web::dev::HttpServiceFactory::register(resource, config) lit,
}} "Multiple paths specified! Should be only one!",
}}", ));
name = self.name, }
ast = quote!(#ast), },
path = self.path, NestedMeta::Meta(syn::Meta::NameValue(nv)) => {
guards = guards, if nv.path.is_ident("guard") {
to = self.resource_type if let syn::Lit::Str(lit) = nv.lit {
) guards.push(Ident::new(&lit.value(), Span::call_site()));
} else {
return Err(syn::Error::new_spanned(
nv.lit,
"Attribute guard expects literal string!",
));
}
} else {
return Err(syn::Error::new_spanned(
nv.path,
"Unknown attribute key is specified. Allowed: guard",
));
}
}
arg => {
return Err(syn::Error::new_spanned(arg, "Unknown attribute"));
}
}
}
Ok(Args {
path: path.unwrap(),
guards,
})
} }
} }
pub struct Route {
name: syn::Ident,
args: Args,
ast: syn::ItemFn,
resource_type: ResourceType,
guard: GuardType,
}
fn guess_resource_type(typ: &syn::Type) -> ResourceType { fn guess_resource_type(typ: &syn::Type) -> ResourceType {
let mut guess = ResourceType::Sync; let mut guess = ResourceType::Sync;
@ -111,75 +139,73 @@ fn guess_resource_type(typ: &syn::Type) -> ResourceType {
guess guess
} }
impl Args { impl Route {
pub fn new(args: &[syn::NestedMeta], input: TokenStream, guard: GuardType) -> Self { pub fn new(
args: AttributeArgs,
input: TokenStream,
guard: GuardType,
) -> syn::Result<Self> {
if args.is_empty() { if args.is_empty() {
panic!( return Err(syn::Error::new(
"invalid server definition, expected: #[{}(\"some path\")]", Span::call_site(),
guard format!(
); r#"invalid server definition, expected #[{}("<some path>")]"#,
guard.as_str().to_ascii_lowercase()
),
));
} }
let ast: syn::ItemFn = syn::parse(input)?;
let name = ast.sig.ident.clone();
let ast: syn::ItemFn = syn::parse(input).expect("Parse input as function"); let args = Args::new(args)?;
let name = ast.ident.clone();
let mut extra_guards = Vec::new(); let resource_type = if ast.sig.asyncness.is_some() {
let mut path = None;
for arg in args {
match arg {
syn::NestedMeta::Literal(syn::Lit::Str(ref fname)) => {
if path.is_some() {
panic!("Multiple paths specified! Should be only one!")
}
let fname = quote!(#fname).to_string();
path = Some(fname.as_str()[1..fname.len() - 1].to_owned())
}
syn::NestedMeta::Meta(syn::Meta::NameValue(ident)) => {
match ident.ident.to_string().to_lowercase().as_str() {
"guard" => match ident.lit {
syn::Lit::Str(ref text) => extra_guards.push(text.value()),
_ => panic!("Attribute guard expects literal string!"),
},
attr => panic!(
"Unknown attribute key is specified: {}. Allowed: guard",
attr
),
}
}
attr => panic!("Unknown attribute{:?}", attr),
}
}
let resource_type = if ast.asyncness.is_some() {
ResourceType::Async ResourceType::Async
} else { } else {
match ast.decl.output { match ast.sig.output {
syn::ReturnType::Default => panic!( syn::ReturnType::Default => {
"Function {} has no return type. Cannot be used as handler", return Err(syn::Error::new_spanned(
name ast,
), "Function has no return type. Cannot be used as handler",
));
}
syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()), syn::ReturnType::Type(_, ref typ) => guess_resource_type(typ.as_ref()),
} }
}; };
let path = path.unwrap(); Ok(Self {
Self {
name, name,
path, args,
ast, ast,
resource_type, resource_type,
guard, guard,
extra_guards, })
}
} }
pub fn generate(&self) -> TokenStream { pub fn generate(&self) -> TokenStream {
let text = self.to_string(); let name = &self.name;
let guard = &self.guard;
let ast = &self.ast;
let path = &self.args.path;
let extra_guards = &self.args.guards;
let resource_type = &self.resource_type;
let stream = quote! {
#[allow(non_camel_case_types)]
pub struct #name;
match text.parse() { impl actix_web::dev::HttpServiceFactory for #name {
Ok(res) => res, fn register(self, config: &mut actix_web::dev::AppService) {
Err(error) => panic!("Error: {:?}\nGenerated code: {}", error, text), #ast
}
let resource = actix_web::Resource::new(#path)
.guard(actix_web::guard::#guard())
#(.guard(actix_web::guard::fn_guard(#extra_guards)))*
.#resource_type(#name);
actix_web::dev::HttpServiceFactory::register(resource, config)
}
}
};
stream.into()
} }
} }

View File

@ -142,7 +142,6 @@ fn test_body() {
assert!(response.status().is_success()); assert!(response.status().is_success());
assert_eq!(response.status(), http::StatusCode::NO_CONTENT); assert_eq!(response.status(), http::StatusCode::NO_CONTENT);
let mut srv = TestServer::new(|| HttpService::new(App::new().service(auto_sync)));
let request = srv.request(http::Method::GET, srv.url("/test")); let request = srv.request(http::Method::GET, srv.url("/test"));
let response = srv.block_on(request.send()).unwrap(); let response = srv.block_on(request.send()).unwrap();
assert!(response.status().is_success()); assert!(response.status().is_success());

View File

@ -1,5 +1,50 @@
# Changes # Changes
## [0.2.8] - 2019-10-24
* Add support for setting query from Serialize type for client request.
## [0.2.7] - 2019-09-25
### Added
* Remaining getter methods for `ClientRequest`'s private `head` field #1101
## [0.2.6] - 2019-09-12
### Added
* Export frozen request related types.
## [0.2.5] - 2019-09-11
### Added
* Add `FrozenClientRequest` to support retries for sending HTTP requests
### Changed
* Ensure that the `Host` header is set when initiating a WebSocket client connection.
## [0.2.4] - 2019-08-13
### Changed
* Update percent-encoding to "2.1"
* Update serde_urlencoded to "0.6.1"
## [0.2.3] - 2019-08-01
### Added
* Add `rustls` support
## [0.2.2] - 2019-07-01 ## [0.2.2] - 2019-07-01
### Changed ### Changed

View File

@ -1,6 +1,6 @@
[package] [package]
name = "awc" name = "awc"
version = "0.2.2" version = "0.2.7"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http client." description = "Actix http client."
readme = "README.md" readme = "README.md"
@ -29,6 +29,9 @@ default = ["brotli", "flate2-zlib"]
# openssl # openssl
ssl = ["openssl", "actix-http/ssl"] ssl = ["openssl", "actix-http/ssl"]
# rustls
rust-tls = ["rustls", "actix-http/rust-tls"]
# brotli encoding, requires c compiler # brotli encoding, requires c compiler
brotli = ["actix-http/brotli"] brotli = ["actix-http/brotli"]
@ -41,30 +44,33 @@ flate2-rust = ["actix-http/flate2-rust"]
[dependencies] [dependencies]
actix-codec = "0.1.2" actix-codec = "0.1.2"
actix-service = "0.4.1" actix-service = "0.4.1"
actix-http = "0.2.4" actix-http = "0.2.10"
base64 = "0.10.1" base64 = "0.10.1"
bytes = "0.4" bytes = "0.4"
derive_more = "0.15.0" derive_more = "0.15.0"
futures = "0.1.25" futures = "0.1.25"
log =" 0.4" log =" 0.4"
mime = "0.3" mime = "0.3"
percent-encoding = "1.0" percent-encoding = "2.1"
rand = "0.7" rand = "0.7"
serde = "1.0" serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
serde_urlencoded = "0.5.3" serde_urlencoded = "0.6.1"
tokio-timer = "0.2.8" tokio-timer = "0.2.8"
openssl = { version="0.10", optional = true } openssl = { version="0.10", optional = true }
rustls = { version = "0.15.2", optional = true }
[dev-dependencies] [dev-dependencies]
actix-rt = "0.2.2" actix-rt = "0.2.2"
actix-web = { version = "1.0.0", features=["ssl"] } actix-web = { version = "1.0.0", features=["ssl"] }
actix-http = { version = "0.2.4", features=["ssl"] } actix-http = { version = "0.2.10", features=["ssl"] }
actix-http-test = { version = "0.2.0", features=["ssl"] } actix-http-test = { version = "0.2.0", features=["ssl"] }
actix-utils = "0.4.1" actix-utils = "0.4.1"
actix-server = { version = "0.5.1", features=["ssl"] } actix-server = { version = "0.6.0", features=["ssl", "rust-tls"] }
brotli2 = { version="0.3.2" } brotli2 = { version="0.3.2" }
flate2 = { version="1.0.2" } flate2 = { version="1.0.2" }
env_logger = "0.6" env_logger = "0.6"
rand = "0.7" rand = "0.7"
tokio-tcp = "0.1" tokio-tcp = "0.1"
webpki = "0.19"
rustls = { version = "0.15.2", features = ["dangerous_configuration"] }

View File

@ -1,3 +1,4 @@
use std::rc::Rc;
use std::{fmt, io, net}; use std::{fmt, io, net};
use actix_codec::{AsyncRead, AsyncWrite, Framed}; use actix_codec::{AsyncRead, AsyncWrite, Framed};
@ -6,7 +7,8 @@ use actix_http::client::{
Connect as ClientConnect, ConnectError, Connection, SendRequestError, Connect as ClientConnect, ConnectError, Connection, SendRequestError,
}; };
use actix_http::h1::ClientCodec; use actix_http::h1::ClientCodec;
use actix_http::{RequestHead, ResponseHead}; use actix_http::http::HeaderMap;
use actix_http::{RequestHead, RequestHeadType, ResponseHead};
use actix_service::Service; use actix_service::Service;
use futures::{Future, Poll}; use futures::{Future, Poll};
@ -22,6 +24,14 @@ pub(crate) trait Connect {
addr: Option<net::SocketAddr>, addr: Option<net::SocketAddr>,
) -> Box<dyn Future<Item = ClientResponse, Error = SendRequestError>>; ) -> Box<dyn Future<Item = ClientResponse, Error = SendRequestError>>;
fn send_request_extra(
&mut self,
head: Rc<RequestHead>,
extra_headers: Option<HeaderMap>,
body: Body,
addr: Option<net::SocketAddr>,
) -> Box<dyn Future<Item = ClientResponse, Error = SendRequestError>>;
/// Send request, returns Response and Framed /// Send request, returns Response and Framed
fn open_tunnel( fn open_tunnel(
&mut self, &mut self,
@ -33,6 +43,19 @@ pub(crate) trait Connect {
Error = SendRequestError, Error = SendRequestError,
>, >,
>; >;
/// Send request and extra headers, returns Response and Framed
fn open_tunnel_extra(
&mut self,
head: Rc<RequestHead>,
extra_headers: Option<HeaderMap>,
addr: Option<net::SocketAddr>,
) -> Box<
dyn Future<
Item = (ResponseHead, Framed<BoxedSocket, ClientCodec>),
Error = SendRequestError,
>,
>;
} }
impl<T> Connect for ConnectorWrapper<T> impl<T> Connect for ConnectorWrapper<T>
@ -59,7 +82,33 @@ where
}) })
.from_err() .from_err()
// send request // send request
.and_then(move |connection| connection.send_request(head, body)) .and_then(move |connection| {
connection.send_request(RequestHeadType::from(head), body)
})
.map(|(head, payload)| ClientResponse::new(head, payload)),
)
}
fn send_request_extra(
&mut self,
head: Rc<RequestHead>,
extra_headers: Option<HeaderMap>,
body: Body,
addr: Option<net::SocketAddr>,
) -> Box<dyn Future<Item = ClientResponse, Error = SendRequestError>> {
Box::new(
self.0
// connect to the host
.call(ClientConnect {
uri: head.uri.clone(),
addr,
})
.from_err()
// send request
.and_then(move |connection| {
connection
.send_request(RequestHeadType::Rc(head, extra_headers), body)
})
.map(|(head, payload)| ClientResponse::new(head, payload)), .map(|(head, payload)| ClientResponse::new(head, payload)),
) )
} }
@ -83,7 +132,39 @@ where
}) })
.from_err() .from_err()
// send request // send request
.and_then(move |connection| connection.open_tunnel(head)) .and_then(move |connection| {
connection.open_tunnel(RequestHeadType::from(head))
})
.map(|(head, framed)| {
let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io))));
(head, framed)
}),
)
}
fn open_tunnel_extra(
&mut self,
head: Rc<RequestHead>,
extra_headers: Option<HeaderMap>,
addr: Option<net::SocketAddr>,
) -> Box<
dyn Future<
Item = (ResponseHead, Framed<BoxedSocket, ClientCodec>),
Error = SendRequestError,
>,
> {
Box::new(
self.0
// connect to the host
.call(ClientConnect {
uri: head.uri.clone(),
addr,
})
.from_err()
// send request
.and_then(move |connection| {
connection.open_tunnel(RequestHeadType::Rc(head, extra_headers))
})
.map(|(head, framed)| { .map(|(head, framed)| {
let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io)))); let framed = framed.map_io(|io| BoxedSocket(Box::new(Socket(io))));
(head, framed) (head, framed)

View File

@ -1,5 +1,7 @@
//! Http client errors //! Http client errors
pub use actix_http::client::{ConnectError, InvalidUrl, SendRequestError}; pub use actix_http::client::{
ConnectError, FreezeRequestError, InvalidUrl, SendRequestError,
};
pub use actix_http::error::PayloadError; pub use actix_http::error::PayloadError;
pub use actix_http::ws::HandshakeError as WsHandshakeError; pub use actix_http::ws::HandshakeError as WsHandshakeError;
pub use actix_http::ws::ProtocolError as WsProtocolError; pub use actix_http::ws::ProtocolError as WsProtocolError;

235
awc/src/frozen.rs Normal file
View File

@ -0,0 +1,235 @@
use std::net;
use std::rc::Rc;
use std::time::Duration;
use bytes::Bytes;
use futures::Stream;
use serde::Serialize;
use actix_http::body::Body;
use actix_http::http::header::IntoHeaderValue;
use actix_http::http::{
Error as HttpError, HeaderMap, HeaderName, HttpTryFrom, Method, Uri,
};
use actix_http::{Error, RequestHead};
use crate::sender::{RequestSender, SendClientRequest};
use crate::ClientConfig;
/// `FrozenClientRequest` struct represents clonable client request.
/// It could be used to send same request multiple times.
#[derive(Clone)]
pub struct FrozenClientRequest {
pub(crate) head: Rc<RequestHead>,
pub(crate) addr: Option<net::SocketAddr>,
pub(crate) response_decompress: bool,
pub(crate) timeout: Option<Duration>,
pub(crate) config: Rc<ClientConfig>,
}
impl FrozenClientRequest {
/// Get HTTP URI of request
pub fn get_uri(&self) -> &Uri {
&self.head.uri
}
/// Get HTTP method of this request
pub fn get_method(&self) -> &Method {
&self.head.method
}
/// Returns request's headers.
pub fn headers(&self) -> &HeaderMap {
&self.head.headers
}
/// Send a body.
pub fn send_body<B>(&self, body: B) -> SendClientRequest
where
B: Into<Body>,
{
RequestSender::Rc(self.head.clone(), None).send_body(
self.addr,
self.response_decompress,
self.timeout,
self.config.as_ref(),
body,
)
}
/// Send a json body.
pub fn send_json<T: Serialize>(&self, value: &T) -> SendClientRequest {
RequestSender::Rc(self.head.clone(), None).send_json(
self.addr,
self.response_decompress,
self.timeout,
self.config.as_ref(),
value,
)
}
/// Send an urlencoded body.
pub fn send_form<T: Serialize>(&self, value: &T) -> SendClientRequest {
RequestSender::Rc(self.head.clone(), None).send_form(
self.addr,
self.response_decompress,
self.timeout,
self.config.as_ref(),
value,
)
}
/// Send a streaming body.
pub fn send_stream<S, E>(&self, stream: S) -> SendClientRequest
where
S: Stream<Item = Bytes, Error = E> + 'static,
E: Into<Error> + 'static,
{
RequestSender::Rc(self.head.clone(), None).send_stream(
self.addr,
self.response_decompress,
self.timeout,
self.config.as_ref(),
stream,
)
}
/// Send an empty body.
pub fn send(&self) -> SendClientRequest {
RequestSender::Rc(self.head.clone(), None).send(
self.addr,
self.response_decompress,
self.timeout,
self.config.as_ref(),
)
}
/// Create a `FrozenSendBuilder` with extra headers
pub fn extra_headers(&self, extra_headers: HeaderMap) -> FrozenSendBuilder {
FrozenSendBuilder::new(self.clone(), extra_headers)
}
/// Create a `FrozenSendBuilder` with an extra header
pub fn extra_header<K, V>(&self, key: K, value: V) -> FrozenSendBuilder
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
self.extra_headers(HeaderMap::new())
.extra_header(key, value)
}
}
/// Builder that allows to modify extra headers.
pub struct FrozenSendBuilder {
req: FrozenClientRequest,
extra_headers: HeaderMap,
err: Option<HttpError>,
}
impl FrozenSendBuilder {
pub(crate) fn new(req: FrozenClientRequest, extra_headers: HeaderMap) -> Self {
Self {
req,
extra_headers,
err: None,
}
}
/// Insert a header, it overrides existing header in `FrozenClientRequest`.
pub fn extra_header<K, V>(mut self, key: K, value: V) -> Self
where
HeaderName: HttpTryFrom<K>,
V: IntoHeaderValue,
{
match HeaderName::try_from(key) {
Ok(key) => match value.try_into() {
Ok(value) => self.extra_headers.insert(key, value),
Err(e) => self.err = Some(e.into()),
},
Err(e) => self.err = Some(e.into()),
}
self
}
/// Complete request construction and send a body.
pub fn send_body<B>(self, body: B) -> SendClientRequest
where
B: Into<Body>,
{
if let Some(e) = self.err {
return e.into();
}
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_body(
self.req.addr,
self.req.response_decompress,
self.req.timeout,
self.req.config.as_ref(),
body,
)
}
/// Complete request construction and send a json body.
pub fn send_json<T: Serialize>(self, value: &T) -> SendClientRequest {
if let Some(e) = self.err {
return e.into();
}
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_json(
self.req.addr,
self.req.response_decompress,
self.req.timeout,
self.req.config.as_ref(),
value,
)
}
/// Complete request construction and send an urlencoded body.
pub fn send_form<T: Serialize>(self, value: &T) -> SendClientRequest {
if let Some(e) = self.err {
return e.into();
}
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_form(
self.req.addr,
self.req.response_decompress,
self.req.timeout,
self.req.config.as_ref(),
value,
)
}
/// Complete request construction and send a streaming body.
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
where
S: Stream<Item = Bytes, Error = E> + 'static,
E: Into<Error> + 'static,
{
if let Some(e) = self.err {
return e.into();
}
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send_stream(
self.req.addr,
self.req.response_decompress,
self.req.timeout,
self.req.config.as_ref(),
stream,
)
}
/// Complete request construction and send an empty body.
pub fn send(self) -> SendClientRequest {
if let Some(e) = self.err {
return e.into();
}
RequestSender::Rc(self.req.head, Some(self.extra_headers)).send(
self.req.addr,
self.req.response_decompress,
self.req.timeout,
self.req.config.as_ref(),
)
}
}

View File

@ -33,15 +33,19 @@ use actix_http::RequestHead;
mod builder; mod builder;
mod connect; mod connect;
pub mod error; pub mod error;
mod frozen;
mod request; mod request;
mod response; mod response;
mod sender;
pub mod test; pub mod test;
pub mod ws; pub mod ws;
pub use self::builder::ClientBuilder; pub use self::builder::ClientBuilder;
pub use self::connect::BoxedSocket; pub use self::connect::BoxedSocket;
pub use self::frozen::{FrozenClientRequest, FrozenSendBuilder};
pub use self::request::ClientRequest; pub use self::request::ClientRequest;
pub use self::response::{ClientResponse, JsonBody, MessageBody}; pub use self::response::{ClientResponse, JsonBody, MessageBody};
pub use self::sender::SendClientRequest;
use self::connect::{Connect, ConnectorWrapper}; use self::connect::{Connect, ConnectorWrapper};

View File

@ -5,25 +5,22 @@ use std::time::Duration;
use std::{fmt, net}; use std::{fmt, net};
use bytes::{BufMut, Bytes, BytesMut}; use bytes::{BufMut, Bytes, BytesMut};
use futures::future::{err, Either}; use futures::Stream;
use futures::{Future, Stream}; use percent_encoding::percent_encode;
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET};
use serde::Serialize; use serde::Serialize;
use serde_json;
use tokio_timer::Timeout;
use actix_http::body::{Body, BodyStream}; use actix_http::body::Body;
use actix_http::cookie::{Cookie, CookieJar}; use actix_http::cookie::{Cookie, CookieJar, USERINFO};
use actix_http::encoding::Decoder; use actix_http::http::header::{self, Header, IntoHeaderValue};
use actix_http::http::header::{self, ContentEncoding, Header, IntoHeaderValue};
use actix_http::http::{ use actix_http::http::{
uri, ConnectionType, Error as HttpError, HeaderMap, HeaderName, HeaderValue, uri, ConnectionType, Error as HttpError, HeaderMap, HeaderName, HeaderValue,
HttpTryFrom, Method, Uri, Version, HttpTryFrom, Method, Uri, Version,
}; };
use actix_http::{Error, Payload, RequestHead}; use actix_http::{Error, RequestHead};
use crate::error::{InvalidUrl, PayloadError, SendRequestError}; use crate::error::{FreezeRequestError, InvalidUrl};
use crate::response::ClientResponse; use crate::frozen::FrozenClientRequest;
use crate::sender::{PrepForSendingError, RequestSender, SendClientRequest};
use crate::ClientConfig; use crate::ClientConfig;
#[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))] #[cfg(any(feature = "brotli", feature = "flate2-zlib", feature = "flate2-rust"))]
@ -99,6 +96,11 @@ impl ClientRequest {
self self
} }
/// Get HTTP URI of request.
pub fn get_uri(&self) -> &Uri {
&self.head.uri
}
/// Set socket address of the server. /// Set socket address of the server.
/// ///
/// This address is used for connection. If address is not /// This address is used for connection. If address is not
@ -115,6 +117,11 @@ impl ClientRequest {
self self
} }
/// Get HTTP method of this request
pub fn get_method(&self) -> &Method {
&self.head.method
}
#[doc(hidden)] #[doc(hidden)]
/// Set HTTP version of this request. /// Set HTTP version of this request.
/// ///
@ -125,6 +132,16 @@ impl ClientRequest {
self self
} }
/// Get HTTP version of this request.
pub fn get_version(&self) -> &Version {
&self.head.version
}
/// Get peer address of this request.
pub fn get_peer_addr(&self) -> &Option<net::SocketAddr> {
&self.head.peer_addr
}
#[inline] #[inline]
/// Returns request's headers. /// Returns request's headers.
pub fn headers(&self) -> &HeaderMap { pub fn headers(&self) -> &HeaderMap {
@ -365,42 +382,160 @@ impl ClientRequest {
} }
} }
/// Complete request construction and send body. /// Sets the query part of the request
pub fn send_body<B>( pub fn query<T: Serialize>(
mut self, mut self,
body: B, query: &T,
) -> impl Future< ) -> Result<Self, serde_urlencoded::ser::Error> {
Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>, let mut parts = self.head.uri.clone().into_parts();
Error = SendRequestError,
> if let Some(path_and_query) = parts.path_and_query {
let query = serde_urlencoded::to_string(query)?;
let path = path_and_query.path();
parts.path_and_query = format!("{}?{}", path, query).parse().ok();
match Uri::from_parts(parts) {
Ok(uri) => self.head.uri = uri,
Err(e) => self.err = Some(e.into()),
}
}
Ok(self)
}
/// Freeze request builder and construct `FrozenClientRequest`,
/// which could be used for sending same request multiple times.
pub fn freeze(self) -> Result<FrozenClientRequest, FreezeRequestError> {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return Err(e.into()),
};
let request = FrozenClientRequest {
head: Rc::new(slf.head),
addr: slf.addr,
response_decompress: slf.response_decompress,
timeout: slf.timeout,
config: slf.config,
};
Ok(request)
}
/// Complete request construction and send body.
pub fn send_body<B>(self, body: B) -> SendClientRequest
where where
B: Into<Body>, B: Into<Body>,
{ {
if let Some(e) = self.err.take() { let slf = match self.prep_for_sending() {
return Either::A(err(e.into())); Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestSender::Owned(slf.head).send_body(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config.as_ref(),
body,
)
}
/// Set a JSON body and generate `ClientRequest`
pub fn send_json<T: Serialize>(self, value: &T) -> SendClientRequest {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestSender::Owned(slf.head).send_json(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config.as_ref(),
value,
)
}
/// Set a urlencoded body and generate `ClientRequest`
///
/// `ClientRequestBuilder` can not be used after this call.
pub fn send_form<T: Serialize>(self, value: &T) -> SendClientRequest {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestSender::Owned(slf.head).send_form(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config.as_ref(),
value,
)
}
/// Set an streaming body and generate `ClientRequest`.
pub fn send_stream<S, E>(self, stream: S) -> SendClientRequest
where
S: Stream<Item = Bytes, Error = E> + 'static,
E: Into<Error> + 'static,
{
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestSender::Owned(slf.head).send_stream(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config.as_ref(),
stream,
)
}
/// Set an empty body and generate `ClientRequest`.
pub fn send(self) -> SendClientRequest {
let slf = match self.prep_for_sending() {
Ok(slf) => slf,
Err(e) => return e.into(),
};
RequestSender::Owned(slf.head).send(
slf.addr,
slf.response_decompress,
slf.timeout,
slf.config.as_ref(),
)
}
fn prep_for_sending(mut self) -> Result<Self, PrepForSendingError> {
if let Some(e) = self.err {
return Err(e.into());
} }
// validate uri // validate uri
let uri = &self.head.uri; let uri = &self.head.uri;
if uri.host().is_none() { if uri.host().is_none() {
return Either::A(err(InvalidUrl::MissingHost.into())); return Err(InvalidUrl::MissingHost.into());
} else if uri.scheme_part().is_none() { } else if uri.scheme_part().is_none() {
return Either::A(err(InvalidUrl::MissingScheme.into())); return Err(InvalidUrl::MissingScheme.into());
} else if let Some(scheme) = uri.scheme_part() { } else if let Some(scheme) = uri.scheme_part() {
match scheme.as_str() { match scheme.as_str() {
"http" | "ws" | "https" | "wss" => (), "http" | "ws" | "https" | "wss" => (),
_ => return Either::A(err(InvalidUrl::UnknownScheme.into())), _ => return Err(InvalidUrl::UnknownScheme.into()),
} }
} else { } else {
return Either::A(err(InvalidUrl::UnknownScheme.into())); return Err(InvalidUrl::UnknownScheme.into());
} }
// set cookies // set cookies
if let Some(ref mut jar) = self.cookies { if let Some(ref mut jar) = self.cookies {
let mut cookie = String::new(); let mut cookie = String::new();
for c in jar.delta() { for c in jar.delta() {
let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); let name = percent_encode(c.name().as_bytes(), USERINFO);
let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(c.value().as_bytes(), USERINFO);
let _ = write!(&mut cookie, "; {}={}", name, value); let _ = write!(&mut cookie, "; {}={}", name, value);
} }
self.head.headers.insert( self.head.headers.insert(
@ -438,104 +573,7 @@ impl ClientRequest {
} }
} }
let head = slf.head; Ok(slf)
let config = slf.config.as_ref();
let response_decompress = slf.response_decompress;
let fut = config
.connector
.borrow_mut()
.send_request(head, body.into(), slf.addr)
.map(move |res| {
res.map_body(|head, payload| {
if response_decompress {
Payload::Stream(Decoder::from_headers(payload, &head.headers))
} else {
Payload::Stream(Decoder::new(payload, ContentEncoding::Identity))
}
})
});
// set request timeout
if let Some(timeout) = slf.timeout.or_else(|| config.timeout) {
Either::B(Either::A(Timeout::new(fut, timeout).map_err(|e| {
if let Some(e) = e.into_inner() {
e
} else {
SendRequestError::Timeout
}
})))
} else {
Either::B(Either::B(fut))
}
}
/// Set a JSON body and generate `ClientRequest`
pub fn send_json<T: Serialize>(
self,
value: &T,
) -> impl Future<
Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>,
Error = SendRequestError,
> {
let body = match serde_json::to_string(value) {
Ok(body) => body,
Err(e) => return Either::A(err(Error::from(e).into())),
};
// set content-type
let slf = self.set_header_if_none(header::CONTENT_TYPE, "application/json");
Either::B(slf.send_body(Body::Bytes(Bytes::from(body))))
}
/// Set a urlencoded body and generate `ClientRequest`
///
/// `ClientRequestBuilder` can not be used after this call.
pub fn send_form<T: Serialize>(
self,
value: &T,
) -> impl Future<
Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>,
Error = SendRequestError,
> {
let body = match serde_urlencoded::to_string(value) {
Ok(body) => body,
Err(e) => return Either::A(err(Error::from(e).into())),
};
// set content-type
let slf = self.set_header_if_none(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
);
Either::B(slf.send_body(Body::Bytes(Bytes::from(body))))
}
/// Set an streaming body and generate `ClientRequest`.
pub fn send_stream<S, E>(
self,
stream: S,
) -> impl Future<
Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>,
Error = SendRequestError,
>
where
S: Stream<Item = Bytes, Error = E> + 'static,
E: Into<Error> + 'static,
{
self.send_body(Body::from_message(BodyStream::new(stream)))
}
/// Set an empty body and generate `ClientRequest`.
pub fn send(
self,
) -> impl Future<
Item = ClientResponse<impl Stream<Item = Bytes, Error = PayloadError>>,
Error = SendRequestError,
> {
self.send_body(Body::Empty)
} }
} }
@ -673,4 +711,13 @@ mod tests {
"Bearer someS3cr3tAutht0k3n" "Bearer someS3cr3tAutht0k3n"
); );
} }
#[test]
fn client_query() {
let req = Client::new()
.get("/")
.query(&[("key1", "val1"), ("key2", "val2")])
.unwrap();
assert_eq!(req.get_uri().query().unwrap(), "key1=val1&key2=val2");
}
} }

282
awc/src/sender.rs Normal file
View File

@ -0,0 +1,282 @@
use std::net;
use std::rc::Rc;
use std::time::{Duration, Instant};
use bytes::Bytes;
use derive_more::From;
use futures::{try_ready, Async, Future, Poll, Stream};
use serde::Serialize;
use serde_json;
use tokio_timer::Delay;
use actix_http::body::{Body, BodyStream};
use actix_http::encoding::Decoder;
use actix_http::http::header::{self, ContentEncoding, IntoHeaderValue};
use actix_http::http::{Error as HttpError, HeaderMap, HeaderName};
use actix_http::{Error, Payload, PayloadStream, RequestHead};
use crate::error::{FreezeRequestError, InvalidUrl, SendRequestError};
use crate::response::ClientResponse;
use crate::ClientConfig;
#[derive(Debug, From)]
pub(crate) enum PrepForSendingError {
Url(InvalidUrl),
Http(HttpError),
}
impl Into<FreezeRequestError> for PrepForSendingError {
fn into(self) -> FreezeRequestError {
match self {
PrepForSendingError::Url(e) => FreezeRequestError::Url(e),
PrepForSendingError::Http(e) => FreezeRequestError::Http(e),
}
}
}
impl Into<SendRequestError> for PrepForSendingError {
fn into(self) -> SendRequestError {
match self {
PrepForSendingError::Url(e) => SendRequestError::Url(e),
PrepForSendingError::Http(e) => SendRequestError::Http(e),
}
}
}
/// Future that sends request's payload and resolves to a server response.
#[must_use = "futures do nothing unless polled"]
pub enum SendClientRequest {
Fut(
Box<dyn Future<Item = ClientResponse, Error = SendRequestError>>,
Option<Delay>,
bool,
),
Err(Option<SendRequestError>),
}
impl SendClientRequest {
pub(crate) fn new(
send: Box<dyn Future<Item = ClientResponse, Error = SendRequestError>>,
response_decompress: bool,
timeout: Option<Duration>,
) -> SendClientRequest {
let delay = timeout.map(|t| Delay::new(Instant::now() + t));
SendClientRequest::Fut(send, delay, response_decompress)
}
}
impl Future for SendClientRequest {
type Item = ClientResponse<Decoder<Payload<PayloadStream>>>;
type Error = SendRequestError;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
match self {
SendClientRequest::Fut(send, delay, response_decompress) => {
if delay.is_some() {
match delay.poll() {
Ok(Async::NotReady) => (),
_ => return Err(SendRequestError::Timeout),
}
}
let res = try_ready!(send.poll()).map_body(|head, payload| {
if *response_decompress {
Payload::Stream(Decoder::from_headers(payload, &head.headers))
} else {
Payload::Stream(Decoder::new(payload, ContentEncoding::Identity))
}
});
Ok(Async::Ready(res))
}
SendClientRequest::Err(ref mut e) => match e.take() {
Some(e) => Err(e),
None => panic!("Attempting to call completed future"),
},
}
}
}
impl From<SendRequestError> for SendClientRequest {
fn from(e: SendRequestError) -> Self {
SendClientRequest::Err(Some(e))
}
}
impl From<Error> for SendClientRequest {
fn from(e: Error) -> Self {
SendClientRequest::Err(Some(e.into()))
}
}
impl From<HttpError> for SendClientRequest {
fn from(e: HttpError) -> Self {
SendClientRequest::Err(Some(e.into()))
}
}
impl From<PrepForSendingError> for SendClientRequest {
fn from(e: PrepForSendingError) -> Self {
SendClientRequest::Err(Some(e.into()))
}
}
#[derive(Debug)]
pub(crate) enum RequestSender {
Owned(RequestHead),
Rc(Rc<RequestHead>, Option<HeaderMap>),
}
impl RequestSender {
pub(crate) fn send_body<B>(
self,
addr: Option<net::SocketAddr>,
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
body: B,
) -> SendClientRequest
where
B: Into<Body>,
{
let mut connector = config.connector.borrow_mut();
let fut = match self {
RequestSender::Owned(head) => {
connector.send_request(head, body.into(), addr)
}
RequestSender::Rc(head, extra_headers) => {
connector.send_request_extra(head, extra_headers, body.into(), addr)
}
};
SendClientRequest::new(
fut,
response_decompress,
timeout.or_else(|| config.timeout),
)
}
pub(crate) fn send_json<T: Serialize>(
mut self,
addr: Option<net::SocketAddr>,
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
value: &T,
) -> SendClientRequest {
let body = match serde_json::to_string(value) {
Ok(body) => body,
Err(e) => return Error::from(e).into(),
};
if let Err(e) = self.set_header_if_none(header::CONTENT_TYPE, "application/json")
{
return e.into();
}
self.send_body(
addr,
response_decompress,
timeout,
config,
Body::Bytes(Bytes::from(body)),
)
}
pub(crate) fn send_form<T: Serialize>(
mut self,
addr: Option<net::SocketAddr>,
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
value: &T,
) -> SendClientRequest {
let body = match serde_urlencoded::to_string(value) {
Ok(body) => body,
Err(e) => return Error::from(e).into(),
};
// set content-type
if let Err(e) = self.set_header_if_none(
header::CONTENT_TYPE,
"application/x-www-form-urlencoded",
) {
return e.into();
}
self.send_body(
addr,
response_decompress,
timeout,
config,
Body::Bytes(Bytes::from(body)),
)
}
pub(crate) fn send_stream<S, E>(
self,
addr: Option<net::SocketAddr>,
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
stream: S,
) -> SendClientRequest
where
S: Stream<Item = Bytes, Error = E> + 'static,
E: Into<Error> + 'static,
{
self.send_body(
addr,
response_decompress,
timeout,
config,
Body::from_message(BodyStream::new(stream)),
)
}
pub(crate) fn send(
self,
addr: Option<net::SocketAddr>,
response_decompress: bool,
timeout: Option<Duration>,
config: &ClientConfig,
) -> SendClientRequest {
self.send_body(addr, response_decompress, timeout, config, Body::Empty)
}
fn set_header_if_none<V>(
&mut self,
key: HeaderName,
value: V,
) -> Result<(), HttpError>
where
V: IntoHeaderValue,
{
match self {
RequestSender::Owned(head) => {
if !head.headers.contains_key(&key) {
match value.try_into() {
Ok(value) => head.headers.insert(key, value),
Err(e) => return Err(e.into()),
}
}
}
RequestSender::Rc(head, extra_headers) => {
if !head.headers.contains_key(&key)
&& !extra_headers.iter().any(|h| h.contains_key(&key))
{
match value.try_into() {
Ok(v) => {
let h = extra_headers.get_or_insert(HeaderMap::new());
h.insert(key, v)
}
Err(e) => return Err(e.into()),
};
}
}
}
Ok(())
}
}

View File

@ -1,12 +1,12 @@
//! Test helpers for actix http client to use during testing. //! Test helpers for actix http client to use during testing.
use std::fmt::Write as FmtWrite; use std::fmt::Write as FmtWrite;
use actix_http::cookie::{Cookie, CookieJar}; use actix_http::cookie::{Cookie, CookieJar, USERINFO};
use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue}; use actix_http::http::header::{self, Header, HeaderValue, IntoHeaderValue};
use actix_http::http::{HeaderName, HttpTryFrom, StatusCode, Version}; use actix_http::http::{HeaderName, HttpTryFrom, StatusCode, Version};
use actix_http::{h1, Payload, ResponseHead}; use actix_http::{h1, Payload, ResponseHead};
use bytes::Bytes; use bytes::Bytes;
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use percent_encoding::percent_encode;
use crate::ClientResponse; use crate::ClientResponse;
@ -87,8 +87,8 @@ impl TestResponse {
let mut cookie = String::new(); let mut cookie = String::new();
for c in self.cookies.delta() { for c in self.cookies.delta() {
let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); let name = percent_encode(c.name().as_bytes(), USERINFO);
let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(c.value().as_bytes(), USERINFO);
let _ = write!(&mut cookie, "; {}={}", name, value); let _ = write!(&mut cookie, "; {}={}", name, value);
} }
if !cookie.is_empty() { if !cookie.is_empty() {

View File

@ -8,9 +8,10 @@ use actix_codec::Framed;
use actix_http::cookie::{Cookie, CookieJar}; use actix_http::cookie::{Cookie, CookieJar};
use actix_http::{ws, Payload, RequestHead}; use actix_http::{ws, Payload, RequestHead};
use futures::future::{err, Either, Future}; use futures::future::{err, Either, Future};
use percent_encoding::{percent_encode, USERINFO_ENCODE_SET}; use percent_encoding::percent_encode;
use tokio_timer::Timeout; use tokio_timer::Timeout;
use actix_http::cookie::USERINFO;
pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message}; pub use actix_http::ws::{CloseCode, CloseReason, Codec, Frame, Message};
use crate::connect::BoxedSocket; use crate::connect::BoxedSocket;
@ -232,12 +233,19 @@ impl WebsocketsRequest {
return Either::A(err(InvalidUrl::UnknownScheme.into())); return Either::A(err(InvalidUrl::UnknownScheme.into()));
} }
if !self.head.headers.contains_key(header::HOST) {
self.head.headers.insert(
header::HOST,
HeaderValue::from_str(uri.host().unwrap()).unwrap(),
);
}
// set cookies // set cookies
if let Some(ref mut jar) = self.cookies { if let Some(ref mut jar) = self.cookies {
let mut cookie = String::new(); let mut cookie = String::new();
for c in jar.delta() { for c in jar.delta() {
let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET); let name = percent_encode(c.name().as_bytes(), USERINFO);
let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET); let value = percent_encode(c.value().as_bytes(), USERINFO);
let _ = write!(&mut cookie, "; {}={}", name, value); let _ = write!(&mut cookie, "; {}={}", name, value);
} }
self.head.headers.insert( self.head.headers.insert(

View File

@ -12,11 +12,10 @@ use flate2::Compression;
use futures::Future; use futures::Future;
use rand::Rng; use rand::Rng;
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http::HttpService; use actix_http::HttpService;
use actix_http_test::TestServer; use actix_http_test::TestServer;
use actix_service::{service_fn, NewService}; use actix_service::{service_fn, NewService};
use actix_web::http::{Cookie, Version}; use actix_web::http::Cookie;
use actix_web::middleware::{BodyEncoding, Compress}; use actix_web::middleware::{BodyEncoding, Compress};
use actix_web::{http::header, web, App, Error, HttpMessage, HttpRequest, HttpResponse}; use actix_web::{http::header, web, App, Error, HttpMessage, HttpRequest, HttpResponse};
use awc::error::SendRequestError; use awc::error::SendRequestError;
@ -43,30 +42,6 @@ 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 \
Hello World Hello World Hello World Hello World Hello World"; Hello World Hello World Hello World Hello World Hello World";
#[cfg(feature = "ssl")]
fn ssl_acceptor<T: AsyncRead + AsyncWrite>(
) -> std::io::Result<actix_server::ssl::OpensslAcceptor<T, ()>> {
use openssl::ssl::{SslAcceptor, SslFiletype, SslMethod};
// load ssl keys
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder
.set_private_key_file("../tests/key.pem", SslFiletype::PEM)
.unwrap();
builder
.set_certificate_chain_file("../tests/cert.pem")
.unwrap();
builder.set_alpn_select_callback(|_, protos| {
const H2: &[u8] = b"\x02h2";
if protos.windows(3).any(|window| window == H2) {
Ok(b"h2")
} else {
Err(openssl::ssl::AlpnError::NOACK)
}
});
builder.set_alpn_protos(b"\x02h2")?;
Ok(actix_server::ssl::OpensslAcceptor::new(builder.build()))
}
#[test] #[test]
fn test_simple() { fn test_simple() {
let mut srv = let mut srv =
@ -207,60 +182,6 @@ fn test_connection_reuse() {
assert_eq!(num.load(Ordering::Relaxed), 1); assert_eq!(num.load(Ordering::Relaxed), 1);
} }
#[cfg(feature = "ssl")]
#[test]
fn test_connection_reuse_h2() {
let openssl = ssl_acceptor().unwrap();
let num = Arc::new(AtomicUsize::new(0));
let num2 = num.clone();
let mut srv = TestServer::new(move || {
let num2 = num2.clone();
service_fn(move |io| {
num2.fetch_add(1, Ordering::Relaxed);
Ok(io)
})
.and_then(
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e)),
)
.and_then(
HttpService::build()
.h2(App::new()
.service(web::resource("/").route(web::to(|| HttpResponse::Ok()))))
.map_err(|_| ()),
)
});
// disable ssl verification
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));
let client = awc::Client::build()
.connector(awc::Connector::new().ssl(builder.build()).finish())
.finish();
// req 1
let request = client.get(srv.surl("/")).send();
let response = srv.block_on(request).unwrap();
assert!(response.status().is_success());
// req 2
let req = client.post(srv.surl("/"));
let response = srv.block_on_fn(move || req.send()).unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
// one connection
assert_eq!(num.load(Ordering::Relaxed), 1);
}
#[test] #[test]
fn test_connection_force_close() { fn test_connection_force_close() {
let num = Arc::new(AtomicUsize::new(0)); let num = Arc::new(AtomicUsize::new(0));

View File

@ -0,0 +1,96 @@
#![cfg(feature = "rust-tls")]
use rustls::{
internal::pemfile::{certs, pkcs8_private_keys},
ClientConfig, NoClientAuth,
};
use std::fs::File;
use std::io::{BufReader, Result};
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http::HttpService;
use actix_http_test::TestServer;
use actix_server::ssl::RustlsAcceptor;
use actix_service::{service_fn, NewService};
use actix_web::http::Version;
use actix_web::{web, App, HttpResponse};
fn ssl_acceptor<T: AsyncRead + AsyncWrite>() -> Result<RustlsAcceptor<T, ()>> {
use rustls::ServerConfig;
// load ssl keys
let mut config = ServerConfig::new(NoClientAuth::new());
let cert_file = &mut BufReader::new(File::open("../tests/cert.pem").unwrap());
let key_file = &mut BufReader::new(File::open("../tests/key.pem").unwrap());
let cert_chain = certs(cert_file).unwrap();
let mut keys = pkcs8_private_keys(key_file).unwrap();
config.set_single_cert(cert_chain, keys.remove(0)).unwrap();
let protos = vec![b"h2".to_vec()];
config.set_protocols(&protos);
Ok(RustlsAcceptor::new(config))
}
mod danger {
pub struct NoCertificateVerification {}
impl rustls::ServerCertVerifier for NoCertificateVerification {
fn verify_server_cert(
&self,
_roots: &rustls::RootCertStore,
_presented_certs: &[rustls::Certificate],
_dns_name: webpki::DNSNameRef<'_>,
_ocsp: &[u8],
) -> Result<rustls::ServerCertVerified, rustls::TLSError> {
Ok(rustls::ServerCertVerified::assertion())
}
}
}
#[test]
fn test_connection_reuse_h2() {
let rustls = ssl_acceptor().unwrap();
let num = Arc::new(AtomicUsize::new(0));
let num2 = num.clone();
let mut srv = TestServer::new(move || {
let num2 = num2.clone();
service_fn(move |io| {
num2.fetch_add(1, Ordering::Relaxed);
Ok(io)
})
.and_then(rustls.clone().map_err(|e| println!("Rustls error: {}", e)))
.and_then(
HttpService::build()
.h2(App::new()
.service(web::resource("/").route(web::to(|| HttpResponse::Ok()))))
.map_err(|_| ()),
)
});
// disable ssl verification
let mut config = ClientConfig::new();
let protos = vec![b"h2".to_vec(), b"http/1.1".to_vec()];
config.set_protocols(&protos);
config
.dangerous()
.set_certificate_verifier(Arc::new(danger::NoCertificateVerification {}));
let client = awc::Client::build()
.connector(awc::Connector::new().rustls(Arc::new(config)).finish())
.finish();
// req 1
let request = client.get(srv.surl("/")).send();
let response = srv.block_on(request).unwrap();
assert!(response.status().is_success());
// req 2
let req = client.post(srv.surl("/"));
let response = srv.block_on_fn(move || req.send()).unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
// one connection
assert_eq!(num.load(Ordering::Relaxed), 1);
}

View File

@ -0,0 +1,86 @@
#![cfg(feature = "ssl")]
use openssl::ssl::{SslAcceptor, SslConnector, SslFiletype, SslMethod, SslVerifyMode};
use std::io::Result;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use actix_codec::{AsyncRead, AsyncWrite};
use actix_http::HttpService;
use actix_http_test::TestServer;
use actix_server::ssl::OpensslAcceptor;
use actix_service::{service_fn, NewService};
use actix_web::http::Version;
use actix_web::{web, App, HttpResponse};
fn ssl_acceptor<T: AsyncRead + AsyncWrite>() -> Result<OpensslAcceptor<T, ()>> {
// load ssl keys
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
builder
.set_private_key_file("../tests/key.pem", SslFiletype::PEM)
.unwrap();
builder
.set_certificate_chain_file("../tests/cert.pem")
.unwrap();
builder.set_alpn_select_callback(|_, protos| {
const H2: &[u8] = b"\x02h2";
if protos.windows(3).any(|window| window == H2) {
Ok(b"h2")
} else {
Err(openssl::ssl::AlpnError::NOACK)
}
});
builder.set_alpn_protos(b"\x02h2")?;
Ok(actix_server::ssl::OpensslAcceptor::new(builder.build()))
}
#[test]
fn test_connection_reuse_h2() {
let openssl = ssl_acceptor().unwrap();
let num = Arc::new(AtomicUsize::new(0));
let num2 = num.clone();
let mut srv = TestServer::new(move || {
let num2 = num2.clone();
service_fn(move |io| {
num2.fetch_add(1, Ordering::Relaxed);
Ok(io)
})
.and_then(
openssl
.clone()
.map_err(|e| println!("Openssl error: {}", e)),
)
.and_then(
HttpService::build()
.h2(App::new()
.service(web::resource("/").route(web::to(|| HttpResponse::Ok()))))
.map_err(|_| ()),
)
});
// disable ssl verification
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));
let client = awc::Client::build()
.connector(awc::Connector::new().ssl(builder.build()).finish())
.finish();
// req 1
let request = client.get(srv.surl("/")).send();
let response = srv.block_on(request).unwrap();
assert!(response.status().is_success());
// req 2
let req = client.post(srv.surl("/"));
let response = srv.block_on_fn(move || req.send()).unwrap();
assert!(response.status().is_success());
assert_eq!(response.version(), Version::HTTP_2);
// one connection
assert_eq!(num.load(Ordering::Relaxed), 1);
}

View File

@ -20,6 +20,7 @@ fn no_params() -> &'static str {
"Hello world!\r\n" "Hello world!\r\n"
} }
#[cfg(feature = "uds")]
fn main() -> std::io::Result<()> { fn main() -> std::io::Result<()> {
std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info"); std::env::set_var("RUST_LOG", "actix_server=info,actix_web=info");
env_logger::init(); env_logger::init();
@ -47,3 +48,6 @@ fn main() -> std::io::Result<()> {
.workers(1) .workers(1)
.run() .run()
} }
#[cfg(not(feature = "uds"))]
fn main() {}

View File

@ -169,7 +169,7 @@ where
match self.data_factories_fut[idx].poll()? { match self.data_factories_fut[idx].poll()? {
Async::Ready(f) => { Async::Ready(f) => {
self.data_factories.push(f); self.data_factories.push(f);
self.data_factories_fut.remove(idx); let _ = self.data_factories_fut.remove(idx);
} }
Async::NotReady => idx += 1, Async::NotReady => idx += 1,
} }

View File

@ -133,7 +133,7 @@ impl AppConfig {
/// Set server host name. /// Set server host name.
/// ///
/// Host name is used by application router aa a hostname for url /// Host name is used by application router as a hostname for url
/// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo.
/// html#method.host) documentation for more information. /// html#method.host) documentation for more information.
/// ///

View File

@ -77,6 +77,11 @@ impl<T> Data<T> {
pub fn get_ref(&self) -> &T { pub fn get_ref(&self) -> &T {
self.0.as_ref() self.0.as_ref()
} }
/// Convert to the internal Arc<T>
pub fn into_inner(self) -> Arc<T> {
self.0
}
} }
impl<T> Deref for Data<T> { impl<T> Deref for Data<T> {

View File

@ -32,8 +32,12 @@ pub enum UrlencodedError {
#[display(fmt = "Can not decode chunked transfer encoding")] #[display(fmt = "Can not decode chunked transfer encoding")]
Chunked, Chunked,
/// Payload size is bigger than allowed. (default: 256kB) /// Payload size is bigger than allowed. (default: 256kB)
#[display(fmt = "Urlencoded payload size is bigger than allowed (default: 256kB)")] #[display(
Overflow, fmt = "Urlencoded payload size is bigger ({} bytes) than allowed (default: {} bytes)",
size,
limit
)]
Overflow { size: usize, limit: usize },
/// Payload size is now known /// Payload size is now known
#[display(fmt = "Payload size is now known")] #[display(fmt = "Payload size is now known")]
UnknownLength, UnknownLength,
@ -52,7 +56,7 @@ pub enum UrlencodedError {
impl ResponseError for UrlencodedError { impl ResponseError for UrlencodedError {
fn error_response(&self) -> HttpResponse { fn error_response(&self) -> HttpResponse {
match *self { match *self {
UrlencodedError::Overflow => { UrlencodedError::Overflow { .. } => {
HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE) HttpResponse::new(StatusCode::PAYLOAD_TOO_LARGE)
} }
UrlencodedError::UnknownLength => { UrlencodedError::UnknownLength => {
@ -164,7 +168,8 @@ mod tests {
#[test] #[test]
fn test_urlencoded_error() { fn test_urlencoded_error() {
let resp: HttpResponse = UrlencodedError::Overflow.error_response(); let resp: HttpResponse =
UrlencodedError::Overflow { size: 0, limit: 0 }.error_response();
assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE); assert_eq!(resp.status(), StatusCode::PAYLOAD_TOO_LARGE);
let resp: HttpResponse = UrlencodedError::UnknownLength.error_response(); let resp: HttpResponse = UrlencodedError::UnknownLength.error_response();
assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED); assert_eq!(resp.status(), StatusCode::LENGTH_REQUIRED);

View File

@ -46,9 +46,9 @@ pub trait FromRequest: Sized {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest}; /// use actix_web::{web, dev, App, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest; /// use actix_web::error::ErrorBadRequest;
/// use serde_derive::Deserialize;
/// use rand; /// use rand;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]
@ -119,9 +119,9 @@ where
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest}; /// use actix_web::{web, dev, App, Result, Error, HttpRequest, FromRequest};
/// use actix_web::error::ErrorBadRequest; /// use actix_web::error::ErrorBadRequest;
/// use serde_derive::Deserialize;
/// use rand; /// use rand;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]

View File

@ -1,16 +1,16 @@
//! Route match guards. //! Route match guards.
//! //!
//! Guards are one of the way how actix-web router chooses //! Guards are one of the ways how actix-web router chooses a
//! handler service. In essence it just function that accepts //! handler service. In essence it is just a function that accepts a
//! reference to a `RequestHead` instance and returns boolean. //! reference to a `RequestHead` instance and returns a boolean.
//! It is possible to add guards to *scopes*, *resources* //! It is possible to add guards to *scopes*, *resources*
//! and *routes*. Actix provide several guards by default, like various //! and *routes*. Actix provide several guards by default, like various
//! http methods, header, etc. To become a guard, type must implement `Guard` //! http methods, header, etc. To become a guard, type must implement `Guard`
//! trait. Simple functions coulds guards as well. //! trait. Simple functions coulds guards as well.
//! //!
//! Guard can not modify request object. But it is possible to //! Guards can not modify the request object. But it is possible
//! to store extra attributes on a request by using `Extensions` container. //! to store extra attributes on a request by using the `Extensions` container.
//! Extensions container available via `RequestHead::extensions()` method. //! Extensions containers are available via the `RequestHead::extensions()` method.
//! //!
//! ```rust //! ```rust
//! use actix_web::{web, http, dev, guard, App, HttpResponse}; //! use actix_web::{web, http, dev, guard, App, HttpResponse};
@ -26,14 +26,14 @@
//! ``` //! ```
#![allow(non_snake_case)] #![allow(non_snake_case)]
use actix_http::http::{self, header, HttpTryFrom}; use actix_http::http::{self, header, uri::Uri, HttpTryFrom};
use actix_http::RequestHead; use actix_http::RequestHead;
/// Trait defines resource guards. Guards are used for routes selection. /// Trait defines resource guards. Guards are used for route selection.
/// ///
/// Guard can not modify request object. But it is possible to /// Guards can not modify the request object. But it is possible
/// to store extra attributes on request by using `Extensions` container, /// to store extra attributes on a request by using the `Extensions` container.
/// Extensions container available via `RequestHead::extensions()` method. /// Extensions containers are available via the `RequestHead::extensions()` method.
pub trait Guard { pub trait Guard {
/// Check if request matches predicate /// Check if request matches predicate
fn check(&self, request: &RequestHead) -> bool; fn check(&self, request: &RequestHead) -> bool;
@ -256,45 +256,70 @@ impl Guard for HeaderGuard {
} }
} }
// /// Return predicate that matches if request contains specified Host name. /// Return predicate that matches if request contains specified Host name.
// /// ///
// /// ```rust,ignore /// ```rust,ignore
// /// # extern crate actix_web; /// # extern crate actix_web;
// /// use actix_web::{pred, App, HttpResponse}; /// use actix_web::{guard::Host, App, HttpResponse};
// /// ///
// /// fn main() { /// fn main() {
// /// App::new().resource("/index.html", |r| { /// App::new().resource("/index.html", |r| {
// /// r.route() /// r.route()
// /// .guard(pred::Host("www.rust-lang.org")) /// .guard(Host("www.rust-lang.org"))
// /// .f(|_| HttpResponse::MethodNotAllowed()) /// .f(|_| HttpResponse::MethodNotAllowed())
// /// }); /// });
// /// } /// }
// /// ``` /// ```
// pub fn Host<H: AsRef<str>>(host: H) -> HostGuard { pub fn Host<H: AsRef<str>>(host: H) -> HostGuard {
// HostGuard(host.as_ref().to_string(), None) HostGuard(host.as_ref().to_string(), None)
// } }
// #[doc(hidden)] fn get_host_uri(req: &RequestHead) -> Option<Uri> {
// pub struct HostGuard(String, Option<String>); use core::str::FromStr;
req.headers
.get(header::HOST)
.and_then(|host_value| host_value.to_str().ok())
.or_else(|| req.uri.host())
.map(|host: &str| Uri::from_str(host).ok())
.and_then(|host_success| host_success)
}
// impl HostGuard { #[doc(hidden)]
// /// Set reuest scheme to match pub struct HostGuard(String, Option<String>);
// pub fn scheme<H: AsRef<str>>(&mut self, scheme: H) {
// self.1 = Some(scheme.as_ref().to_string())
// }
// }
// impl Guard for HostGuard { impl HostGuard {
// fn check(&self, _req: &RequestHead) -> bool { /// Set request scheme to match
// // let info = req.connection_info(); pub fn scheme<H: AsRef<str>>(mut self, scheme: H) -> HostGuard {
// // if let Some(ref scheme) = self.1 { self.1 = Some(scheme.as_ref().to_string());
// // self.0 == info.host() && scheme == info.scheme() self
// // } else { }
// // self.0 == info.host() }
// // }
// false impl Guard for HostGuard {
// } fn check(&self, req: &RequestHead) -> bool {
// } let req_host_uri = if let Some(uri) = get_host_uri(req) {
uri
} else {
return false;
};
if let Some(uri_host) = req_host_uri.host() {
if self.0 != uri_host {
return false;
}
} else {
return false;
}
if let Some(ref scheme) = self.1 {
if let Some(ref req_host_uri_scheme) = req_host_uri.scheme_str() {
return scheme == req_host_uri_scheme;
}
}
true
}
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
@ -318,21 +343,89 @@ mod tests {
assert!(!pred.check(req.head())); assert!(!pred.check(req.head()));
} }
// #[test] #[test]
// fn test_host() { fn test_host() {
// let req = TestServiceRequest::default() let req = TestRequest::default()
// .header( .header(
// header::HOST, header::HOST,
// header::HeaderValue::from_static("www.rust-lang.org"), header::HeaderValue::from_static("www.rust-lang.org"),
// ) )
// .request(); .to_http_request();
// let pred = Host("www.rust-lang.org"); let pred = Host("www.rust-lang.org");
// assert!(pred.check(&req)); assert!(pred.check(req.head()));
// let pred = Host("localhost"); let pred = Host("www.rust-lang.org").scheme("https");
// assert!(!pred.check(&req)); assert!(pred.check(req.head()));
// }
let pred = Host("blog.rust-lang.org");
assert!(!pred.check(req.head()));
let pred = Host("blog.rust-lang.org").scheme("https");
assert!(!pred.check(req.head()));
let pred = Host("crates.io");
assert!(!pred.check(req.head()));
let pred = Host("localhost");
assert!(!pred.check(req.head()));
}
#[test]
fn test_host_scheme() {
let req = TestRequest::default()
.header(
header::HOST,
header::HeaderValue::from_static("https://www.rust-lang.org"),
)
.to_http_request();
let pred = Host("www.rust-lang.org").scheme("https");
assert!(pred.check(req.head()));
let pred = Host("www.rust-lang.org");
assert!(pred.check(req.head()));
let pred = Host("www.rust-lang.org").scheme("http");
assert!(!pred.check(req.head()));
let pred = Host("blog.rust-lang.org");
assert!(!pred.check(req.head()));
let pred = Host("blog.rust-lang.org").scheme("https");
assert!(!pred.check(req.head()));
let pred = Host("crates.io").scheme("https");
assert!(!pred.check(req.head()));
let pred = Host("localhost");
assert!(!pred.check(req.head()));
}
#[test]
fn test_host_without_header() {
let req = TestRequest::default()
.uri("www.rust-lang.org")
.to_http_request();
let pred = Host("www.rust-lang.org");
assert!(pred.check(req.head()));
let pred = Host("www.rust-lang.org").scheme("https");
assert!(pred.check(req.head()));
let pred = Host("blog.rust-lang.org");
assert!(!pred.check(req.head()));
let pred = Host("blog.rust-lang.org").scheme("https");
assert!(!pred.check(req.head()));
let pred = Host("crates.io");
assert!(!pred.check(req.head()));
let pred = Host("localhost");
assert!(!pred.check(req.head()));
}
#[test] #[test]
fn test_methods() { fn test_methods() {

View File

@ -155,9 +155,9 @@ impl ConnectionInfo {
&self.host &self.host
} }
/// Remote IP of client initiated HTTP request. /// Remote socket addr of client initiated HTTP request.
/// ///
/// The IP is resolved through the following headers, in this order: /// The addr is resolved through the following headers, in this order:
/// ///
/// - Forwarded /// - Forwarded
/// - X-Forwarded-For /// - X-Forwarded-For

View File

@ -61,9 +61,9 @@
//! * Configurable request routing //! * Configurable request routing
//! * Multipart streams //! * Multipart streams
//! * SSL support with OpenSSL or `native-tls` //! * SSL support with OpenSSL or `native-tls`
//! * Middlewares (`Logger`, `Session`, `CORS`, `CSRF`, `DefaultHeaders`) //! * Middlewares (`Logger`, `Session`, `CORS`, `DefaultHeaders`)
//! * Supports [Actix actor framework](https://github.com/actix/actix) //! * Supports [Actix actor framework](https://github.com/actix/actix)
//! * Supported Rust version: 1.31 or later //! * Supported Rust version: 1.36 or later
//! //!
//! ## Package feature //! ## Package feature
//! //!

143
src/middleware/condition.rs Normal file
View File

@ -0,0 +1,143 @@
//! `Middleware` for conditionally enables another middleware.
use actix_service::{Service, Transform};
use futures::future::{ok, Either, FutureResult, Map};
use futures::{Future, Poll};
/// `Middleware` for conditionally enables another middleware.
/// The controled middleware must not change the `Service` interfaces.
/// This means you cannot control such middlewares like `Logger` or `Compress`.
///
/// ## Usage
///
/// ```rust
/// use actix_web::middleware::{Condition, NormalizePath};
/// use actix_web::App;
///
/// fn main() {
/// let enable_normalize = std::env::var("NORMALIZE_PATH") == Ok("true".into());
/// let app = App::new()
/// .wrap(Condition::new(enable_normalize, NormalizePath));
/// }
/// ```
pub struct Condition<T> {
trans: T,
enable: bool,
}
impl<T> Condition<T> {
pub fn new(enable: bool, trans: T) -> Self {
Self { trans, enable }
}
}
impl<S, T> Transform<S> for Condition<T>
where
S: Service,
T: Transform<S, Request = S::Request, Response = S::Response, Error = S::Error>,
{
type Request = S::Request;
type Response = S::Response;
type Error = S::Error;
type InitError = T::InitError;
type Transform = ConditionMiddleware<T::Transform, S>;
type Future = Either<
Map<T::Future, fn(T::Transform) -> Self::Transform>,
FutureResult<Self::Transform, Self::InitError>,
>;
fn new_transform(&self, service: S) -> Self::Future {
if self.enable {
let f = self
.trans
.new_transform(service)
.map(ConditionMiddleware::Enable as fn(T::Transform) -> Self::Transform);
Either::A(f)
} else {
Either::B(ok(ConditionMiddleware::Disable(service)))
}
}
}
pub enum ConditionMiddleware<E, D> {
Enable(E),
Disable(D),
}
impl<E, D> Service for ConditionMiddleware<E, D>
where
E: Service,
D: Service<Request = E::Request, Response = E::Response, Error = E::Error>,
{
type Request = E::Request;
type Response = E::Response;
type Error = E::Error;
type Future = Either<E::Future, D::Future>;
fn poll_ready(&mut self) -> Poll<(), Self::Error> {
use ConditionMiddleware::*;
match self {
Enable(service) => service.poll_ready(),
Disable(service) => service.poll_ready(),
}
}
fn call(&mut self, req: E::Request) -> Self::Future {
use ConditionMiddleware::*;
match self {
Enable(service) => Either::A(service.call(req)),
Disable(service) => Either::B(service.call(req)),
}
}
}
#[cfg(test)]
mod tests {
use actix_service::IntoService;
use super::*;
use crate::dev::{ServiceRequest, ServiceResponse};
use crate::error::Result;
use crate::http::{header::CONTENT_TYPE, HeaderValue, StatusCode};
use crate::middleware::errhandlers::*;
use crate::test::{self, TestRequest};
use crate::HttpResponse;
fn render_500<B>(mut res: ServiceResponse<B>) -> Result<ErrorHandlerResponse<B>> {
res.response_mut()
.headers_mut()
.insert(CONTENT_TYPE, HeaderValue::from_static("0001"));
Ok(ErrorHandlerResponse::Response(res))
}
#[test]
fn test_handler_enabled() {
let srv = |req: ServiceRequest| {
req.into_response(HttpResponse::InternalServerError().finish())
};
let mw =
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
let mut mw =
test::block_on(Condition::new(true, mw).new_transform(srv.into_service()))
.unwrap();
let resp = test::call_service(&mut mw, TestRequest::default().to_srv_request());
assert_eq!(resp.headers().get(CONTENT_TYPE).unwrap(), "0001");
}
#[test]
fn test_handler_disabled() {
let srv = |req: ServiceRequest| {
req.into_response(HttpResponse::InternalServerError().finish())
};
let mw =
ErrorHandlers::new().handler(StatusCode::INTERNAL_SERVER_ERROR, render_500);
let mut mw =
test::block_on(Condition::new(false, mw).new_transform(srv.into_service()))
.unwrap();
let resp = test::call_service(&mut mw, TestRequest::default().to_srv_request());
assert_eq!(resp.headers().get(CONTENT_TYPE), None);
}
}

View File

@ -2,11 +2,13 @@
mod compress; mod compress;
pub use self::compress::{BodyEncoding, Compress}; pub use self::compress::{BodyEncoding, Compress};
mod condition;
mod defaultheaders; mod defaultheaders;
pub mod errhandlers; pub mod errhandlers;
mod logger; mod logger;
mod normalize; mod normalize;
pub use self::condition::Condition;
pub use self::defaultheaders::DefaultHeaders; pub use self::defaultheaders::DefaultHeaders;
pub use self::logger::Logger; pub use self::logger::Logger;
pub use self::normalize::NormalizePath; pub use self::normalize::NormalizePath;

View File

@ -151,5 +151,4 @@ mod tests {
let res = block_on(normalize.call(req)).unwrap(); let res = block_on(normalize.call(req)).unwrap();
assert!(res.status().is_success()); assert!(res.status().is_success());
} }
} }

View File

@ -259,6 +259,7 @@ impl Drop for HttpRequest {
if Rc::strong_count(&self.0) == 1 { if Rc::strong_count(&self.0) == 1 {
let v = &mut self.0.pool.0.borrow_mut(); let v = &mut self.0.pool.0.borrow_mut();
if v.len() < 128 { if v.len() < 128 {
self.extensions_mut().clear();
v.push(self.0.clone()); v.push(self.0.clone());
} }
} }
@ -270,8 +271,8 @@ impl Drop for HttpRequest {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, HttpRequest}; /// use actix_web::{web, App, HttpRequest};
/// use serde_derive::Deserialize;
/// ///
/// /// extract `Thing` from request /// /// extract `Thing` from request
/// fn index(req: HttpRequest) -> String { /// fn index(req: HttpRequest) -> String {
@ -494,4 +495,38 @@ mod tests {
let resp = call_service(&mut srv, req); let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::BAD_REQUEST); assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
} }
#[test]
fn test_extensions_dropped() {
struct Tracker {
pub dropped: bool,
}
struct Foo {
tracker: Rc<RefCell<Tracker>>,
}
impl Drop for Foo {
fn drop(&mut self) {
self.tracker.borrow_mut().dropped = true;
}
}
let tracker = Rc::new(RefCell::new(Tracker { dropped: false }));
{
let tracker2 = Rc::clone(&tracker);
let mut srv = init_service(App::new().data(10u32).service(
web::resource("/").to(move |req: HttpRequest| {
req.extensions_mut().insert(Foo {
tracker: Rc::clone(&tracker2),
});
HttpResponse::Ok()
}),
));
let req = TestRequest::default().to_request();
let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
assert!(tracker.borrow().dropped);
}
} }

View File

@ -189,11 +189,21 @@ where
/// )); /// ));
/// } /// }
/// ``` /// ```
pub fn data<U: 'static>(mut self, data: U) -> Self { pub fn data<U: 'static>(self, data: U) -> Self {
self.register_data(Data::new(data))
}
/// Set or override application data.
///
/// This method has the same effect as [`Resource::data`](#method.data),
/// except that instead of taking a value of some type `T`, it expects a
/// value of type `Data<T>`. Use a `Data<T>` extractor to retrieve its
/// value.
pub fn register_data<U: 'static>(mut self, data: Data<U>) -> Self {
if self.data.is_none() { if self.data.is_none() {
self.data = Some(Extensions::new()); self.data = Some(Extensions::new());
} }
self.data.as_mut().unwrap().insert(Data::new(data)); self.data.as_mut().unwrap().insert(data);
self self
} }
@ -764,4 +774,33 @@ mod tests {
assert_eq!(resp.status(), StatusCode::NO_CONTENT); assert_eq!(resp.status(), StatusCode::NO_CONTENT);
} }
#[test]
fn test_data() {
let mut srv = init_service(
App::new()
.data(1.0f64)
.data(1usize)
.register_data(web::Data::new('-'))
.service(
web::resource("/test")
.data(10usize)
.register_data(web::Data::new('*'))
.guard(guard::Get())
.to(
|data1: web::Data<usize>,
data2: web::Data<char>,
data3: web::Data<f64>| {
assert_eq!(*data1, 10);
assert_eq!(*data2, '*');
assert_eq!(*data3, 1.0);
HttpResponse::Ok()
},
),
),
);
let req = TestRequest::get().uri("/test").to_request();
let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
} }

View File

@ -178,8 +178,8 @@ impl Route {
/// Set handler function, use request extractors for parameters. /// Set handler function, use request extractors for parameters.
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, http, App}; /// use actix_web::{web, http, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -239,9 +239,9 @@ impl Route {
/// ///
/// ```rust /// ```rust
/// # use futures::future::ok; /// # use futures::future::ok;
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, Error}; /// use actix_web::{web, App, Error};
/// use futures::Future; /// use futures::Future;
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {

View File

@ -148,11 +148,20 @@ where
/// ); /// );
/// } /// }
/// ``` /// ```
pub fn data<U: 'static>(mut self, data: U) -> Self { pub fn data<U: 'static>(self, data: U) -> Self {
self.register_data(Data::new(data))
}
/// Set or override application data.
///
/// This method has the same effect as [`Scope::data`](#method.data), except
/// that instead of taking a value of some type `T`, it expects a value of
/// type `Data<T>`. Use a `Data<T>` extractor to retrieve its value.
pub fn register_data<U: 'static>(mut self, data: Data<U>) -> Self {
if self.data.is_none() { if self.data.is_none() {
self.data = Some(Extensions::new()); self.data = Some(Extensions::new());
} }
self.data.as_mut().unwrap().insert(Data::new(data)); self.data.as_mut().unwrap().insert(data);
self self
} }
@ -1082,6 +1091,28 @@ mod tests {
assert_eq!(resp.status(), StatusCode::OK); assert_eq!(resp.status(), StatusCode::OK);
} }
#[test]
fn test_override_register_data() {
let mut srv = init_service(
App::new().register_data(web::Data::new(1usize)).service(
web::scope("app")
.register_data(web::Data::new(10usize))
.route(
"/t",
web::get().to(|data: web::Data<usize>| {
assert_eq!(*data, 10);
let _ = data.clone();
HttpResponse::Ok()
}),
),
),
);
let req = TestRequest::with_uri("/app/t").to_request();
let resp = call_service(&mut srv, req);
assert_eq!(resp.status(), StatusCode::OK);
}
#[test] #[test]
fn test_scope_config() { fn test_scope_config() {
let mut srv = let mut srv =

View File

@ -180,7 +180,7 @@ where
/// Set server host name. /// Set server host name.
/// ///
/// Host name is used by application router aa a hostname for url /// Host name is used by application router as a hostname for url
/// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo. /// generation. Check [ConnectionInfo](./dev/struct.ConnectionInfo.
/// html#method.host) documentation for more information. /// html#method.host) documentation for more information.
pub fn server_hostname<T: AsRef<str>>(mut self, val: T) -> Self { pub fn server_hostname<T: AsRef<str>>(mut self, val: T) -> Self {
@ -435,6 +435,37 @@ where
Ok(self) Ok(self)
} }
#[cfg(feature = "uds")]
/// Start listening for unix domain connections on existing listener.
///
/// This method is available with `uds` feature.
pub fn listen_uds(
mut self,
lst: std::os::unix::net::UnixListener,
) -> io::Result<Self> {
let cfg = self.config.clone();
let factory = self.factory.clone();
// todo duplicated:
self.sockets.push(Socket {
scheme: "http",
addr: net::SocketAddr::new(
net::IpAddr::V4(net::Ipv4Addr::new(127, 0, 0, 1)),
8080,
),
});
let addr = format!("actix-web-service-{:?}", lst.local_addr()?);
self.builder = self.builder.listen_uds(addr, lst, move || {
let c = cfg.lock();
HttpService::build()
.keep_alive(c.keep_alive)
.client_timeout(c.client_timeout)
.finish(factory())
})?;
Ok(self)
}
#[cfg(feature = "uds")] #[cfg(feature = "uds")]
/// Start listening for incoming unix domain connections. /// Start listening for incoming unix domain connections.
/// ///

View File

@ -68,6 +68,34 @@ impl ServiceRequest {
(self.0, pl) (self.0, pl)
} }
/// Construct request from parts.
///
/// `ServiceRequest` can be re-constructed only if `req` hasnt been cloned.
pub fn from_parts(
mut req: HttpRequest,
pl: Payload,
) -> Result<Self, (HttpRequest, Payload)> {
if Rc::strong_count(&req.0) == 1 && Rc::weak_count(&req.0) == 0 {
Rc::get_mut(&mut req.0).unwrap().payload = pl;
Ok(ServiceRequest(req))
} else {
Err((req, pl))
}
}
/// Construct request from request.
///
/// `HttpRequest` implements `Clone` trait via `Rc` type. `ServiceRequest`
/// can be re-constructed only if rc's strong pointers count eq 1 and
/// weak pointers count is 0.
pub fn from_request(req: HttpRequest) -> Result<Self, HttpRequest> {
if Rc::strong_count(&req.0) == 1 && Rc::weak_count(&req.0) == 0 {
Ok(ServiceRequest(req))
} else {
Err(req)
}
}
/// Create service response /// Create service response
#[inline] #[inline]
pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> { pub fn into_response<B, R: Into<Response<B>>>(self, res: R) -> ServiceResponse<B> {
@ -514,6 +542,27 @@ mod tests {
use crate::test::{call_service, init_service, TestRequest}; use crate::test::{call_service, init_service, TestRequest};
use crate::{guard, http, web, App, HttpResponse}; use crate::{guard, http, web, App, HttpResponse};
#[test]
fn test_service_request() {
let req = TestRequest::default().to_srv_request();
let (r, pl) = req.into_parts();
assert!(ServiceRequest::from_parts(r, pl).is_ok());
let req = TestRequest::default().to_srv_request();
let (r, pl) = req.into_parts();
let _r2 = r.clone();
assert!(ServiceRequest::from_parts(r, pl).is_err());
let req = TestRequest::default().to_srv_request();
let (r, _pl) = req.into_parts();
assert!(ServiceRequest::from_request(r).is_ok());
let req = TestRequest::default().to_srv_request();
let (r, _pl) = req.into_parts();
let _r2 = r.clone();
assert!(ServiceRequest::from_request(r).is_err());
}
#[test] #[test]
fn test_service() { fn test_service() {
let mut srv = init_service( let mut srv = init_service(

View File

@ -1,5 +1,4 @@
//! Various helpers for Actix applications to use during testing. //! Various helpers for Actix applications to use during testing.
use std::cell::RefCell;
use std::rc::Rc; use std::rc::Rc;
use actix_http::http::header::{ContentType, Header, HeaderName, IntoHeaderValue}; use actix_http::http::header::{ContentType, Header, HeaderName, IntoHeaderValue};
@ -7,17 +6,17 @@ use actix_http::http::{HttpTryFrom, Method, StatusCode, Uri, Version};
use actix_http::test::TestRequest as HttpTestRequest; use actix_http::test::TestRequest as HttpTestRequest;
use actix_http::{cookie::Cookie, Extensions, Request}; use actix_http::{cookie::Cookie, Extensions, Request};
use actix_router::{Path, ResourceDef, Url}; use actix_router::{Path, ResourceDef, Url};
use actix_rt::{System, SystemRunner};
use actix_server_config::ServerConfig; use actix_server_config::ServerConfig;
use actix_service::{IntoNewService, IntoService, NewService, Service}; use actix_service::{IntoNewService, IntoService, NewService, Service};
use bytes::{Bytes, BytesMut}; use bytes::{Bytes, BytesMut};
use futures::future::{lazy, ok, Future, IntoFuture}; use futures::future::{ok, Future};
use futures::Stream; use futures::Stream;
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize; use serde::Serialize;
use serde_json; use serde_json;
pub use actix_http::test::TestBuffer; pub use actix_http::test::TestBuffer;
pub use actix_testing::{block_fn, block_on, run_on};
use crate::config::{AppConfig, AppConfigInner}; use crate::config::{AppConfig, AppConfigInner};
use crate::data::Data; use crate::data::Data;
@ -27,78 +26,6 @@ use crate::rmap::ResourceMap;
use crate::service::{ServiceRequest, ServiceResponse}; use crate::service::{ServiceRequest, ServiceResponse};
use crate::{Error, HttpRequest, HttpResponse}; use crate::{Error, HttpRequest, HttpResponse};
thread_local! {
static RT: RefCell<Inner> = {
RefCell::new(Inner(Some(System::builder().build())))
};
}
struct Inner(Option<SystemRunner>);
impl Inner {
fn get_mut(&mut self) -> &mut SystemRunner {
self.0.as_mut().unwrap()
}
}
impl Drop for Inner {
fn drop(&mut self) {
std::mem::forget(self.0.take().unwrap())
}
}
/// Runs the provided future, blocking the current thread until the future
/// completes.
///
/// This function can be used to synchronously block the current thread
/// until the provided `future` has resolved either successfully or with an
/// error. The result of the future is then returned from this function
/// call.
///
/// Note that this function is intended to be used only for testing purpose.
/// This function panics on nested call.
pub fn block_on<F>(f: F) -> Result<F::Item, F::Error>
where
F: IntoFuture,
{
RT.with(move |rt| rt.borrow_mut().get_mut().block_on(f.into_future()))
}
/// Runs the provided function, blocking the current thread until the result
/// future completes.
///
/// This function can be used to synchronously block the current thread
/// until the provided `future` has resolved either successfully or with an
/// error. The result of the future is then returned from this function
/// call.
///
/// Note that this function is intended to be used only for testing purpose.
/// This function panics on nested call.
pub fn block_fn<F, R>(f: F) -> Result<R::Item, R::Error>
where
F: FnOnce() -> R,
R: IntoFuture,
{
RT.with(move |rt| rt.borrow_mut().get_mut().block_on(lazy(f)))
}
#[doc(hidden)]
/// Runs the provided function, with runtime enabled.
///
/// Note that this function is intended to be used only for testing purpose.
/// This function panics on nested call.
pub fn run_on<F, R>(f: F) -> R
where
F: FnOnce() -> R,
{
RT.with(move |rt| {
rt.borrow_mut()
.get_mut()
.block_on(lazy(|| Ok::<_, ()>(f())))
})
.unwrap()
}
/// Create service that always responds with `HttpResponse::Ok()` /// Create service that always responds with `HttpResponse::Ok()`
pub fn ok_service( pub fn ok_service(
) -> impl Service<Request = ServiceRequest, Response = ServiceResponse<Body>, Error = Error> ) -> impl Service<Request = ServiceRequest, Response = ServiceResponse<Body>, Error = Error>
@ -478,6 +405,16 @@ impl TestRequest {
self self
} }
/// Serialize `data` to a URL encoded form and set it as the request payload. The `Content-Type`
/// header is set to `application/x-www-form-urlencoded`.
pub fn set_form<T: Serialize>(mut self, data: &T) -> Self {
let bytes = serde_urlencoded::to_string(data)
.expect("Failed to serialize test data as a urlencoded form");
self.req.set_payload(bytes);
self.req.set(ContentType::form_url_encoded());
self
}
/// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is /// Serialize `data` to JSON and set it as the request payload. The `Content-Type` header is
/// set to `application/json`. /// set to `application/json`.
pub fn set_json<T: Serialize>(mut self, data: &T) -> Self { pub fn set_json<T: Serialize>(mut self, data: &T) -> Self {
@ -670,6 +607,31 @@ mod tests {
assert_eq!(&result.id, "12345"); assert_eq!(&result.id, "12345");
} }
#[test]
fn test_request_response_form() {
let mut app = init_service(App::new().service(web::resource("/people").route(
web::post().to(|person: web::Form<Person>| {
HttpResponse::Ok().json(person.into_inner())
}),
)));
let payload = Person {
id: "12345".to_string(),
name: "User name".to_string(),
};
let req = TestRequest::post()
.uri("/people")
.set_form(&payload)
.to_request();
assert_eq!(req.content_type(), "application/x-www-form-urlencoded");
let result: Person = read_response_json(&mut app, req);
assert_eq!(&result.id, "12345");
assert_eq!(&result.name, "User name");
}
#[test] #[test]
fn test_request_response_json() { fn test_request_response_json() {
let mut app = init_service(App::new().service(web::resource("/people").route( let mut app = init_service(App::new().service(web::resource("/people").route(

View File

@ -3,20 +3,29 @@
use std::rc::Rc; use std::rc::Rc;
use std::{fmt, ops}; use std::{fmt, ops};
use actix_http::{Error, HttpMessage, Payload}; use actix_http::{Error, HttpMessage, Payload, Response};
use bytes::BytesMut; use bytes::BytesMut;
use encoding_rs::{Encoding, UTF_8}; use encoding_rs::{Encoding, UTF_8};
use futures::{Future, Poll, Stream}; use futures::{Future, Poll, Stream};
use serde::de::DeserializeOwned; use serde::de::DeserializeOwned;
use serde::Serialize;
use crate::dev::Decompress; use crate::dev::Decompress;
use crate::error::UrlencodedError; use crate::error::UrlencodedError;
use crate::extract::FromRequest; use crate::extract::FromRequest;
use crate::http::header::CONTENT_LENGTH; use crate::http::{
header::{ContentType, CONTENT_LENGTH},
StatusCode,
};
use crate::request::HttpRequest; use crate::request::HttpRequest;
use crate::responder::Responder;
#[derive(PartialEq, Eq, PartialOrd, Ord)] /// Form data helper (`application/x-www-form-urlencoded`)
/// Extract typed information from the request's body. ///
/// Can be use to extract url-encoded data from the request body,
/// or send url-encoded data as the response.
///
/// ## Extract
/// ///
/// To extract typed information from request's body, the type `T` must /// To extract typed information from request's body, the type `T` must
/// implement the `Deserialize` trait from *serde*. /// implement the `Deserialize` trait from *serde*.
@ -24,12 +33,10 @@ use crate::request::HttpRequest;
/// [**FormConfig**](struct.FormConfig.html) allows to configure extraction /// [**FormConfig**](struct.FormConfig.html) allows to configure extraction
/// process. /// process.
/// ///
/// ## Example /// ### Example
///
/// ```rust /// ```rust
/// # extern crate actix_web; /// use actix_web::web;
/// #[macro_use] extern crate serde_derive; /// use serde_derive::Deserialize;
/// use actix_web::{web, App};
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct FormData { /// struct FormData {
@ -44,6 +51,36 @@ use crate::request::HttpRequest;
/// } /// }
/// # fn main() {} /// # fn main() {}
/// ``` /// ```
///
/// ## Respond
///
/// The `Form` type also allows you to respond with well-formed url-encoded data:
/// simply return a value of type Form<T> where T is the type to be url-encoded.
/// The type must implement `serde::Serialize`;
///
/// ### Example
/// ```rust
/// use actix_web::*;
/// use serde_derive::Serialize;
///
/// #[derive(Serialize)]
/// struct SomeForm {
/// name: String,
/// age: u8
/// }
///
/// // Will return a 200 response with header
/// // `Content-Type: application/x-www-form-urlencoded`
/// // and body "name=actix&age=123"
/// fn index() -> web::Form<SomeForm> {
/// web::Form(SomeForm {
/// name: "actix".into(),
/// age: 123
/// })
/// }
/// # fn main() {}
/// ```
#[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct Form<T>(pub T); pub struct Form<T>(pub T);
impl<T> Form<T> { impl<T> Form<T> {
@ -110,11 +147,27 @@ impl<T: fmt::Display> fmt::Display for Form<T> {
} }
} }
impl<T: Serialize> Responder for Form<T> {
type Error = Error;
type Future = Result<Response, Error>;
fn respond_to(self, _: &HttpRequest) -> Self::Future {
let body = match serde_urlencoded::to_string(&self.0) {
Ok(body) => body,
Err(e) => return Err(e.into()),
};
Ok(Response::build(StatusCode::OK)
.set(ContentType::form_url_encoded())
.body(body))
}
}
/// Form extractor configuration /// Form extractor configuration
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, FromRequest, Result}; /// use actix_web::{web, App, FromRequest, Result};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct FormData { /// struct FormData {
@ -264,7 +317,7 @@ where
let limit = self.limit; let limit = self.limit;
if let Some(len) = self.length.take() { if let Some(len) = self.length.take() {
if len > limit { if len > limit {
return Err(UrlencodedError::Overflow); return Err(UrlencodedError::Overflow { size: len, limit });
} }
} }
@ -277,7 +330,10 @@ where
.from_err() .from_err()
.fold(BytesMut::with_capacity(8192), move |mut body, chunk| { .fold(BytesMut::with_capacity(8192), move |mut body, chunk| {
if (body.len() + chunk.len()) > limit { if (body.len() + chunk.len()) > limit {
Err(UrlencodedError::Overflow) Err(UrlencodedError::Overflow {
size: body.len() + chunk.len(),
limit,
})
} else { } else {
body.extend_from_slice(&chunk); body.extend_from_slice(&chunk);
Ok(body) Ok(body)
@ -304,15 +360,16 @@ where
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use bytes::Bytes; use bytes::Bytes;
use serde::Deserialize; use serde::{Deserialize, Serialize};
use super::*; use super::*;
use crate::http::header::CONTENT_TYPE; use crate::http::header::{HeaderValue, CONTENT_TYPE};
use crate::test::{block_on, TestRequest}; use crate::test::{block_on, TestRequest};
#[derive(Deserialize, Debug, PartialEq)] #[derive(Deserialize, Serialize, Debug, PartialEq)]
struct Info { struct Info {
hello: String, hello: String,
counter: i64,
} }
#[test] #[test]
@ -320,17 +377,23 @@ mod tests {
let (req, mut pl) = let (req, mut pl) =
TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(CONTENT_LENGTH, "11") .header(CONTENT_LENGTH, "11")
.set_payload(Bytes::from_static(b"hello=world")) .set_payload(Bytes::from_static(b"hello=world&counter=123"))
.to_http_parts(); .to_http_parts();
let s = block_on(Form::<Info>::from_request(&req, &mut pl)).unwrap(); let Form(s) = block_on(Form::<Info>::from_request(&req, &mut pl)).unwrap();
assert_eq!(s.hello, "world"); assert_eq!(
s,
Info {
hello: "world".into(),
counter: 123
}
);
} }
fn eq(err: UrlencodedError, other: UrlencodedError) -> bool { fn eq(err: UrlencodedError, other: UrlencodedError) -> bool {
match err { match err {
UrlencodedError::Overflow => match other { UrlencodedError::Overflow { .. } => match other {
UrlencodedError::Overflow => true, UrlencodedError::Overflow { .. } => true,
_ => false, _ => false,
}, },
UrlencodedError::UnknownLength => match other { UrlencodedError::UnknownLength => match other {
@ -359,7 +422,10 @@ mod tests {
.header(CONTENT_LENGTH, "1000000") .header(CONTENT_LENGTH, "1000000")
.to_http_parts(); .to_http_parts();
let info = block_on(UrlEncoded::<Info>::new(&req, &mut pl)); let info = block_on(UrlEncoded::<Info>::new(&req, &mut pl));
assert!(eq(info.err().unwrap(), UrlencodedError::Overflow)); assert!(eq(
info.err().unwrap(),
UrlencodedError::Overflow { size: 0, limit: 0 }
));
let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "text/plain") let (req, mut pl) = TestRequest::with_header(CONTENT_TYPE, "text/plain")
.header(CONTENT_LENGTH, "10") .header(CONTENT_LENGTH, "10")
@ -373,14 +439,15 @@ mod tests {
let (req, mut pl) = let (req, mut pl) =
TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded") TestRequest::with_header(CONTENT_TYPE, "application/x-www-form-urlencoded")
.header(CONTENT_LENGTH, "11") .header(CONTENT_LENGTH, "11")
.set_payload(Bytes::from_static(b"hello=world")) .set_payload(Bytes::from_static(b"hello=world&counter=123"))
.to_http_parts(); .to_http_parts();
let info = block_on(UrlEncoded::<Info>::new(&req, &mut pl)).unwrap(); let info = block_on(UrlEncoded::<Info>::new(&req, &mut pl)).unwrap();
assert_eq!( assert_eq!(
info, info,
Info { Info {
hello: "world".to_owned() hello: "world".to_owned(),
counter: 123
} }
); );
@ -389,15 +456,35 @@ mod tests {
"application/x-www-form-urlencoded; charset=utf-8", "application/x-www-form-urlencoded; charset=utf-8",
) )
.header(CONTENT_LENGTH, "11") .header(CONTENT_LENGTH, "11")
.set_payload(Bytes::from_static(b"hello=world")) .set_payload(Bytes::from_static(b"hello=world&counter=123"))
.to_http_parts(); .to_http_parts();
let info = block_on(UrlEncoded::<Info>::new(&req, &mut pl)).unwrap(); let info = block_on(UrlEncoded::<Info>::new(&req, &mut pl)).unwrap();
assert_eq!( assert_eq!(
info, info,
Info { Info {
hello: "world".to_owned() hello: "world".to_owned(),
counter: 123
} }
); );
} }
#[test]
fn test_responder() {
let req = TestRequest::default().to_http_request();
let form = Form(Info {
hello: "world".to_string(),
counter: 123,
});
let resp = form.respond_to(&req).unwrap();
assert_eq!(resp.status(), StatusCode::OK);
assert_eq!(
resp.headers().get(CONTENT_TYPE).unwrap(),
HeaderValue::from_static("application/x-www-form-urlencoded")
);
use crate::responder::tests::BodyTest;
assert_eq!(resp.body().bin_ref(), b"hello=world&counter=123");
}
} }

View File

@ -33,8 +33,8 @@ use crate::responder::Responder;
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -60,9 +60,9 @@ use crate::responder::Responder;
/// trait from *serde*. /// trait from *serde*.
/// ///
/// ```rust /// ```rust
/// # #[macro_use] extern crate serde_derive; /// use actix_web::*;
/// # use actix_web::*; /// use serde_derive::Serialize;
/// # ///
/// #[derive(Serialize)] /// #[derive(Serialize)]
/// struct MyObj { /// struct MyObj {
/// name: String, /// name: String,
@ -144,8 +144,8 @@ impl<T: Serialize> Responder for Json<T> {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -203,8 +203,8 @@ where
/// Json extractor configuration /// Json extractor configuration
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {

View File

@ -39,8 +39,8 @@ use crate::FromRequest;
/// implements `Deserialize` trait from *serde*. /// implements `Deserialize` trait from *serde*.
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, Error}; /// use actix_web::{web, App, Error};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -134,8 +134,8 @@ impl<T: fmt::Display> fmt::Display for Path<T> {
/// implements `Deserialize` trait from *serde*. /// implements `Deserialize` trait from *serde*.
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App, Error}; /// use actix_web::{web, App, Error};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {
@ -190,10 +190,9 @@ where
/// Path extractor configuration /// Path extractor configuration
/// ///
/// ```rust /// ```rust
/// # #[macro_use]
/// # extern crate serde_derive;
/// use actix_web::web::PathConfig; /// use actix_web::web::PathConfig;
/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize, Debug)] /// #[derive(Deserialize, Debug)]
/// enum Folder { /// enum Folder {

View File

@ -43,7 +43,14 @@ use crate::request::HttpRequest;
/// ); /// );
/// } /// }
/// ``` /// ```
pub struct Payload(crate::dev::Payload); pub struct Payload(pub crate::dev::Payload);
impl Payload {
/// Deconstruct to a inner value
pub fn into_inner(self) -> crate::dev::Payload {
self.0
}
}
impl Stream for Payload { impl Stream for Payload {
type Item = Bytes; type Item = Bytes;

View File

@ -12,14 +12,17 @@ use crate::error::QueryPayloadError;
use crate::extract::FromRequest; use crate::extract::FromRequest;
use crate::request::HttpRequest; use crate::request::HttpRequest;
#[derive(PartialEq, Eq, PartialOrd, Ord)]
/// Extract typed information from the request's query. /// Extract typed information from the request's query.
/// ///
/// **Note**: A query string consists of unordered `key=value` pairs, therefore it cannot
/// be decoded into any type which depends upon data ordering e.g. tuples or tuple-structs.
/// Attempts to do so will *fail at runtime*.
///
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]
/// pub enum ResponseType { /// pub enum ResponseType {
@ -33,10 +36,10 @@ use crate::request::HttpRequest;
/// response_type: ResponseType, /// response_type: ResponseType,
/// } /// }
/// ///
/// // Use `Query` extractor for query information. /// // Use `Query` extractor for query information (and destructure it within the signature).
/// // This handler get called only if request's query contains `username` field /// // This handler gets called only if the request's query string contains a `username` field.
/// // The correct request for this handler would be `/index.html?id=64&response_type=Code"` /// // The correct request for this handler would be `/index.html?id=64&response_type=Code"`.
/// fn index(info: web::Query<AuthRequest>) -> String { /// fn index(web::Query(info): web::Query<AuthRequest>) -> String {
/// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type) /// format!("Authorization request for client with id={} and type={:?}!", info.id, info.response_type)
/// } /// }
/// ///
@ -45,7 +48,8 @@ use crate::request::HttpRequest;
/// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor /// web::resource("/index.html").route(web::get().to(index))); // <- use `Query` extractor
/// } /// }
/// ``` /// ```
pub struct Query<T>(T); #[derive(PartialEq, Eq, PartialOrd, Ord)]
pub struct Query<T>(pub T);
impl<T> Query<T> { impl<T> Query<T> {
/// Deconstruct to a inner value /// Deconstruct to a inner value
@ -95,8 +99,8 @@ impl<T: fmt::Display> fmt::Display for Query<T> {
/// ## Example /// ## Example
/// ///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{web, App}; /// use actix_web::{web, App};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Debug, Deserialize)] /// #[derive(Debug, Deserialize)]
/// pub enum ResponseType { /// pub enum ResponseType {
@ -162,9 +166,11 @@ where
/// Query extractor configuration /// Query extractor configuration
/// ///
/// ## Example
///
/// ```rust /// ```rust
/// #[macro_use] extern crate serde_derive;
/// use actix_web::{error, web, App, FromRequest, HttpResponse}; /// use actix_web::{error, web, App, FromRequest, HttpResponse};
/// use serde_derive::Deserialize;
/// ///
/// #[derive(Deserialize)] /// #[derive(Deserialize)]
/// struct Info { /// struct Info {

View File

@ -1,5 +1,17 @@
# Changes # Changes
## [0.2.5] - 2019-0917
### Changed
* Update serde_urlencoded to "0.6.1"
* Increase TestServerRuntime timeouts from 500ms to 3000ms
### Fixed
* Do not override current `System`
## [0.2.4] - 2019-07-18 ## [0.2.4] - 2019-07-18
* Update actix-server to 0.6 * Update actix-server to 0.6

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-http-test" name = "actix-http-test"
version = "0.2.4" version = "0.2.5"
authors = ["Nikolay Kim <fafhrd91@gmail.com>"] authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
description = "Actix http test server" description = "Actix http test server"
readme = "README.md" readme = "README.md"
@ -35,7 +35,8 @@ actix-rt = "0.2.2"
actix-service = "0.4.1" actix-service = "0.4.1"
actix-server = "0.6.0" actix-server = "0.6.0"
actix-utils = "0.4.1" actix-utils = "0.4.1"
awc = "0.2.2" awc = "0.2.6"
actix-connect = "0.2.2"
base64 = "0.10" base64 = "0.10"
bytes = "0.4" bytes = "0.4"
@ -48,12 +49,12 @@ serde = "1.0"
serde_json = "1.0" serde_json = "1.0"
sha1 = "0.6" sha1 = "0.6"
slab = "0.4" slab = "0.4"
serde_urlencoded = "0.5.3" serde_urlencoded = "0.6.1"
time = "0.1" time = "0.1"
tokio-tcp = "0.1" tokio-tcp = "0.1"
tokio-timer = "0.2" tokio-timer = "0.2"
openssl = { version="0.10", optional = true } openssl = { version="0.10", optional = true }
[dev-dependencies] [dev-dependencies]
actix-web = "1.0.0" actix-web = "1.0.7"
actix-http = "0.2.4" actix-http = "0.2.9"

View File

@ -103,8 +103,8 @@ pub struct TestServer;
/// Test server controller /// Test server controller
pub struct TestServerRuntime { pub struct TestServerRuntime {
addr: net::SocketAddr, addr: net::SocketAddr,
rt: Runtime,
client: Client, client: Client,
system: System,
} }
impl TestServer { impl TestServer {
@ -130,41 +130,47 @@ impl TestServer {
}); });
let (system, addr) = rx.recv().unwrap(); let (system, addr) = rx.recv().unwrap();
let mut rt = Runtime::new().unwrap();
let client = rt let client = block_on(lazy(move || {
.block_on(lazy(move || { let connector = {
let connector = { #[cfg(feature = "ssl")]
#[cfg(feature = "ssl")] {
{ use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
use openssl::ssl::{SslConnector, SslMethod, SslVerifyMode};
let mut builder = let mut builder = SslConnector::builder(SslMethod::tls()).unwrap();
SslConnector::builder(SslMethod::tls()).unwrap(); builder.set_verify(SslVerifyMode::NONE);
builder.set_verify(SslVerifyMode::NONE); let _ = builder
let _ = builder.set_alpn_protos(b"\x02h2\x08http/1.1").map_err( .set_alpn_protos(b"\x02h2\x08http/1.1")
|e| log::error!("Can not set alpn protocol: {:?}", e), .map_err(|e| log::error!("Can not set alpn protocol: {:?}", e));
); Connector::new()
Connector::new() .conn_lifetime(time::Duration::from_secs(0))
.conn_lifetime(time::Duration::from_secs(0)) .timeout(time::Duration::from_millis(3000))
.timeout(time::Duration::from_millis(500)) .ssl(builder.build())
.ssl(builder.build()) .finish()
.finish() }
} #[cfg(not(feature = "ssl"))]
#[cfg(not(feature = "ssl"))] {
{ Connector::new()
Connector::new() .conn_lifetime(time::Duration::from_secs(0))
.conn_lifetime(time::Duration::from_secs(0)) .timeout(time::Duration::from_millis(3000))
.timeout(time::Duration::from_millis(500)) .finish()
.finish() }
} };
};
Ok::<Client, ()>(Client::build().connector(connector).finish()) Ok::<Client, ()>(Client::build().connector(connector).finish())
})) }))
.unwrap(); .unwrap();
System::set_current(system);
TestServerRuntime { addr, rt, client } block_on(lazy(
|| Ok::<_, ()>(actix_connect::start_default_resolver()),
))
.unwrap();
TestServerRuntime {
addr,
client,
system,
}
} }
/// Get first available unused address /// Get first available unused address
@ -184,7 +190,7 @@ impl TestServerRuntime {
where where
F: Future<Item = I, Error = E>, F: Future<Item = I, Error = E>,
{ {
self.rt.block_on(fut) block_on(fut)
} }
/// Execute future on current core /// Execute future on current core
@ -193,7 +199,7 @@ impl TestServerRuntime {
F: FnOnce() -> R, F: FnOnce() -> R,
R: Future, R: Future,
{ {
self.rt.block_on(lazy(f)) block_on(lazy(f))
} }
/// Execute function on current core /// Execute function on current core
@ -201,7 +207,7 @@ impl TestServerRuntime {
where where
F: FnOnce() -> R, F: FnOnce() -> R,
{ {
self.rt.block_on(lazy(|| Ok::<_, ()>(fut()))).unwrap() block_on(lazy(|| Ok::<_, ()>(fut()))).unwrap()
} }
/// Construct test server url /// Construct test server url
@ -212,18 +218,18 @@ impl TestServerRuntime {
/// Construct test server url /// Construct test server url
pub fn url(&self, uri: &str) -> String { pub fn url(&self, uri: &str) -> String {
if uri.starts_with('/') { if uri.starts_with('/') {
format!("http://127.0.0.1:{}{}", self.addr.port(), uri) format!("http://localhost:{}{}", self.addr.port(), uri)
} else { } else {
format!("http://127.0.0.1:{}/{}", self.addr.port(), uri) format!("http://localhost:{}/{}", self.addr.port(), uri)
} }
} }
/// Construct test https server url /// Construct test https server url
pub fn surl(&self, uri: &str) -> String { pub fn surl(&self, uri: &str) -> String {
if uri.starts_with('/') { if uri.starts_with('/') {
format!("https://127.0.0.1:{}{}", self.addr.port(), uri) format!("https://localhost:{}{}", self.addr.port(), uri)
} else { } else {
format!("https://127.0.0.1:{}/{}", self.addr.port(), uri) format!("https://localhost:{}/{}", self.addr.port(), uri)
} }
} }
@ -320,8 +326,7 @@ impl TestServerRuntime {
{ {
let url = self.url(path); let url = self.url(path);
let connect = self.client.ws(url).connect(); let connect = self.client.ws(url).connect();
self.rt block_on(lazy(move || connect.map(|(_, framed)| framed)))
.block_on(lazy(move || connect.map(|(_, framed)| framed)))
} }
/// Connect to a websocket server /// Connect to a websocket server
@ -334,7 +339,7 @@ impl TestServerRuntime {
/// Stop http server /// Stop http server
fn stop(&mut self) { fn stop(&mut self) {
System::current().stop(); self.system.stop();
} }
} }

View File

@ -1,20 +1,32 @@
-----BEGIN CERTIFICATE----- -----BEGIN CERTIFICATE-----
MIIDPjCCAiYCCQCmkoCBehOyYTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV MIIFfjCCA2agAwIBAgIJAOIBvp/w68KrMA0GCSqGSIb3DQEBCwUAMGsxCzAJBgNV
UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMRAwDgYDVQQKDAdDb21wYW55MQww BAYTAlJVMRkwFwYDVQQIDBBTYWludC1QZXRlcnNidXJnMRkwFwYDVQQHDBBTYWlu
CgYDVQQLDANPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xOTAzMjky dC1QZXRlcnNidXJnMRIwEAYDVQQKDAlLdXBpYmlsZXQxEjAQBgNVBAMMCWxvY2Fs
MzE5MDlaFw0yMDAzMjgyMzE5MDlaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD aG9zdDAgFw0xOTA3MjcxODIzMTJaGA8zMDE5MDcyNzE4MjMxMlowazELMAkGA1UE
QTELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBhbnkxDDAKBgNVBAsMA09yZzEY BhMCUlUxGTAXBgNVBAgMEFNhaW50LVBldGVyc2J1cmcxGTAXBgNVBAcMEFNhaW50
MBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A LVBldGVyc2J1cmcxEjAQBgNVBAoMCUt1cGliaWxldDESMBAGA1UEAwwJbG9jYWxo
MIIBCgKCAQEA2uFoWm74qumqIIsBBf/rgP3ZtZw6dRQhVoYjIwYk00T1RLmmbt8r b3N0MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEAuiQZzTO3gRRPr6ZH
YNh3lehmnrQlM/YC3dzcspucGqIfvs5FEReh/vgvsqY3lfy47Q1zzdtBrKq2ZBro wcmKqkoXig9taCCqx72Qvb9tvCLhQLE1dDPZV8I/r8bx+mM4Yz3r0Hm5LxTIhCM9
AuJUe4ayMYz/L/2jAtPtGDQqWyzhKv6x/oz6N/tKqlzoGbjSGSJUqKAV+Tuo4YI4 p3/abuiJAZENC/VkxgFzBGg7KGLSFmzU+A8Ft+2mrKmj5MpIPBCxDeVg80TCQOJy
xw3r/RJg3I3+ruXOgM65GBdja7usI/BhseEOp9VXotoTEItGmvG2RFZ4A7cN124x hj+NU3PpBo9nxTgxWNWO6X+ZovZohdp78fYLLtns8rxjug3FVzdPrrLnBvihkGlq
giFl2IeYuC60jteZ+bnhPiqxcdzf3K4dnZlzrYma+FxwWbaow4wlpQcZVFdZ+K/Y gfImkh+vZxMTj1OgtxyCOhdbO4Ol4jCbn7a5yIw+iixHOEgBQfTQopRP7z1PEUV2
p/Bbm/FDKoUHnEdn/QAanTruRxSGdai0owIDAQABMA0GCSqGSIb3DQEBCwUAA4IB WIy2VEGzvQDlj2OyzH86T1IOFV5rz5MjdZuW0qNzeS0w3Jzgp/olSbIZLhGAaIk0
AQAEWn3WAwAbd64f5jo2w4076s2qFiCJjPWoxO6bO75FgFFtw/NNev8pxGVw1ehg gN7y9XvSHqs7rO0wW+467ico7+uP1ScGgPgJA5fGu7ahp7F7G3ZSoAqAcS60wYsX
HiTO6VRYolL5S/RKOchjA83AcDEBjgf8fKtvTmE9kxZSUIo4kIvv8V9ZM72gJhDN kStoA3RWAuqste6aChv1tlgTt+Rhk8qjGhuh0ng2qVnTGyo2T3OCHB/c47Bcsp6L
8D/lXduTZ9JMwLOa1NUB8/I6CbaU3VzWkfodArKKpQF3M+LLgK03i12PD0KPQ5zv xiyTCnQIPH3fh2iO/SC7gPw3jihPMCAQZYlkC3MhMk974rn2zs9cKtJ8ubnG2m5F
bwaNoQo6cTmPNIdsVZETRvPqONiCUaQV57G74dGtjeirCh/DO5EYRtb1thgS7TGm VFVYmduRqS/YQS/O802jVCFdc8KDmoeAYNuHzgRZjQv9018UUeW3jtWKnopJnWs5
+Xg8OC5vZ6g0+xsrSqDBmWNtlI7S3bsL5C3LIEOOAL1ZJHRy2KvIGQ9ipb3XjnKS ae9pbtmYeOtc7OovOxT7J2AaVfUkHRhmlqWZMzEJTcZws0fRPTZDifFJ5LFWbZsC
N7/wlQduRyPH7oaD/o4xf5Gt zW4tCKBKvYM9eAnbb+abiHXlY1MCAwEAAaMjMCEwHwYDVR0RBBgwFoIJbG9jYWxo
b3N0ggkxMjcuMC4wLjEwDQYJKoZIhvcNAQELBQADggIBAC1EU4SVCfUKc7JbhYRf
P87F+8e13bBTLxevJdnTCH3Xw2AN8UPmwQ2vv9Mv2FMulMBQ7yLnQLGtgGUua2OE
XO+EdBBEKnglo9cwXGzU6qHhaiCeXZDM8s53qOOrD42XsDsY0nOoFYqDLW3WixP9
f1fWbcEf6+ktlvqi/1/3R6QtQR+6LS43enbsYHq8aAP60NrpXxdXxEoUwW6Z/sje
XAQluH8jzledwJcY8bXRskAHZlE4kGlOVuGgnyI3BXyLiwB4g9smFzYIs98iAGmV
7ZBaR5IIiRCtoKBG+SngM7Log0bHphvFPjDDvgqWYiWaOHboYM60Y2Z/gRbcjuMU
WZX64jw29fa8UPFdtGTupt+iuO7iXnHnm0lBBK36rVdOvsZup76p6L4BXmFsRmFK
qJ2Zd8uWNPDq80Am0mYaAqENuIANHHJXX38SesC+QO+G2JZt6vCwkGk/Qune4GIg
1GwhvsDRfTQopSxg1rdPwPM7HWeTfUGHZ34B5p/iILA3o6PfYQU8fNAWIsCDkRX2
MrgDgCnLZxKb6pjR4DYNAdPwkxyMFACZ2T46z6WvLWFlnkK5nbZoqsOsp+GJHole
llafhrelXEzt3zFR0q4zGcqheJDI+Wy+fBy3XawgAc4eN0T2UCzL/jKxKgzlzSU3
+xh1SDNjFLRd6sGzZHPMgXN0
-----END CERTIFICATE----- -----END CERTIFICATE-----

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