mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-03 09:36:36 +02:00
Compare commits
369 Commits
Author | SHA1 | Date | |
---|---|---|---|
67f383f346 | |||
49f5c335f6 | |||
692e11a584 | |||
208117ca6f | |||
3e276ac921 | |||
4af115a19c | |||
051703eb2c | |||
31fbbd3168 | |||
fee1e255ac | |||
a4c933e56e | |||
9ddf5a3550 | |||
9ab0fa604d | |||
6c709b33cc | |||
71b4c07ea4 | |||
ac9eba8261 | |||
cad55f9c80 | |||
4263574a58 | |||
84ef5ee410 | |||
598fb9190d | |||
9a404a0c03 | |||
3dd8fdf450 | |||
05f5ba0084 | |||
8169149554 | |||
8d1de6c497 | |||
caaace82e3 | |||
02dd5375a9 | |||
717602472a | |||
b56be8e571 | |||
2853086463 | |||
e2107ec6f4 | |||
c33caddf57 | |||
db1e04e418 | |||
f8b8fe3865 | |||
1c6ddfd34c | |||
49e007ff2a | |||
2068eee669 | |||
f3c63e631a | |||
3f0803a7d3 | |||
f12b613211 | |||
695c052c58 | |||
63634be542 | |||
f88f1c65b6 | |||
a0b589eb96 | |||
ebdc983dfe | |||
395243a539 | |||
1ab676d7eb | |||
47f01e5b7e | |||
ffb89935b6 | |||
77a111b95c | |||
6c0fb3a7d2 | |||
824244622f | |||
42d2a29b1d | |||
af8875f6ab | |||
1db1ce1ca3 | |||
f55ef3a059 | |||
67bf0ae79f | |||
b06cf32329 | |||
7cce29b633 | |||
c26d9545a5 | |||
b950d6997d | |||
0bf29a522b | |||
24342fb745 | |||
0278e364ec | |||
b9d6bbd357 | |||
5816ecd1bc | |||
2ff55ee1c5 | |||
b42de6c41f | |||
9e0e081c90 | |||
178f5a104e | |||
1e42f9575a | |||
24dfcf1303 | |||
6cc3aaef1b | |||
436a16a2c8 | |||
9afad5885b | |||
04d0abb3c7 | |||
1e5daa1de8 | |||
d3c859f9f3 | |||
c1419413aa | |||
acd33cccbb | |||
57a1d68f89 | |||
5c88441cd7 | |||
6a3c5c4ce0 | |||
14a511bdad | |||
e4ed53d691 | |||
5bf4f3be8b | |||
6b9e51740b | |||
be7e8d159b | |||
ceb97cd6b9 | |||
85b650048d | |||
a0e6313d56 | |||
9cc6f6b1e4 | |||
526753ee88 | |||
779e773185 | |||
7eb310f8ce | |||
d573cf2d97 | |||
32b5544ad9 | |||
e182ed33b1 | |||
6f1836f80e | |||
5b530f11b5 | |||
0c30057c8c | |||
6078344ecc | |||
67f5a949a4 | |||
05e49e893e | |||
c8844425ad | |||
b282ec106e | |||
ea2a8f6908 | |||
2b942ec5f2 | |||
bf77be0337 | |||
c2741054bb | |||
e708f51156 | |||
cbb821148b | |||
d6b021e185 | |||
0adb7e8553 | |||
dbfa1f0ac8 | |||
11347e3c7d | |||
631fe72a46 | |||
f673dba759 | |||
ab978a18ff | |||
327df159c6 | |||
2ccbd5fa18 | |||
058630d041 | |||
f456be0309 | |||
9bd6cb03ac | |||
16afeda79c | |||
83fcdfd91f | |||
8f94ae41cc | |||
4e41347de8 | |||
6acb6dd4e7 | |||
791a980e2d | |||
c2d8abcee7 | |||
16c05f07ba | |||
2158ad29ee | |||
feba5aeffd | |||
343888017e | |||
3a5d445b2f | |||
e60acb7607 | |||
bebfc6c9b5 | |||
3b2928a391 | |||
10f57dac31 | |||
b640b49b05 | |||
1fea4bd9a6 | |||
206c4e581a | |||
4e13505b92 | |||
5b6d7cddbf | |||
4aaf9f08f8 | |||
b0ba23ff55 | |||
42b19b1819 | |||
0335fde3f9 | |||
f27edbff89 | |||
d62d6e68e0 | |||
1284264511 | |||
d977fe563b | |||
bb68f9dd90 | |||
313396d9b5 | |||
171a23561e | |||
67f33a4760 | |||
764421fe44 | |||
b339ea0a3a | |||
8994732227 | |||
7591592279 | |||
4a48b43927 | |||
b1ad4763a2 | |||
2f3a2115c0 | |||
1283c00583 | |||
9f81eae215 | |||
ccb6ebb259 | |||
da76de76f0 | |||
c316a99746 | |||
1e2aa4fc90 | |||
e2c8f17c2c | |||
9b06eac720 | |||
4f99cd1580 | |||
f56fa49a9b | |||
1f063e4136 | |||
33c935dccc | |||
a7bf635158 | |||
aac9b5a97c | |||
6c480fae90 | |||
5dcb558f50 | |||
a344c3a02e | |||
0ab8bc11f3 | |||
abae65a49e | |||
d6fd4a3524 | |||
72aa2d9eae | |||
644f1a9518 | |||
56ae565688 | |||
0a3b776aa7 | |||
6ef9c60361 | |||
a2b98b31e8 | |||
ab5ed27bf1 | |||
141b992450 | |||
4e41e13baf | |||
ea8e8e75a2 | |||
a855c8b2c9 | |||
fd31eb74c5 | |||
3b22b1b168 | |||
7a7df7f8fb | |||
25aabfb3e2 | |||
3a3657cfaf | |||
aff43cc8b8 | |||
4a9c1ae894 | |||
4a07430e8e | |||
9a076c69d1 | |||
8f2d3a0a76 | |||
d4611f8bb9 | |||
fd56e5dc82 | |||
7c74259453 | |||
6a01af32bc | |||
5634e5794f | |||
187644e178 | |||
7198dde465 | |||
2374aa42ed | |||
03912d2089 | |||
3f95cce9e8 | |||
979cea03ac | |||
6424defee6 | |||
6ee14efbe2 | |||
4d81186059 | |||
ddc82395e8 | |||
360ffbba68 | |||
f2f1798215 | |||
548f4e4d62 | |||
cb70d5ec3d | |||
edd114f6e4 | |||
816c6fb0e0 | |||
0da382a7a4 | |||
3e3d3279b8 | |||
3c95823e53 | |||
8607c51bcf | |||
080bb3e5ae | |||
d31e71a169 | |||
7b0e1642b6 | |||
096dee519c | |||
8bce3b9d10 | |||
b28ecbcf0c | |||
8f9ec5c23c | |||
96b87761d1 | |||
b1eec3131f | |||
a544034c06 | |||
4b8181476c | |||
eb041de36d | |||
80285f2a32 | |||
de869ed879 | |||
7ccacb92ce | |||
57655d8153 | |||
335ca8ff33 | |||
720d8c36c1 | |||
8c1b5fa945 | |||
232aba2080 | |||
30bdf9cb5e | |||
285c66e7d8 | |||
856055c6ca | |||
e3081306da | |||
94c4053cb5 | |||
762961b0f4 | |||
3109f9be62 | |||
2d049e4a9f | |||
0c98775b51 | |||
b4b5c78b51 | |||
78da98a16d | |||
74377ef73d | |||
728377a447 | |||
73ed1342eb | |||
bc6300be34 | |||
2faf3a5eb6 | |||
6181a84d7b | |||
f8f99ec0c7 | |||
bd03ba1192 | |||
d0cbf7cd25 | |||
93aa220e8d | |||
81e4fb9353 | |||
884ea02f5c | |||
b6d5516e3a | |||
46841cc87e | |||
7ad66956b2 | |||
d568161852 | |||
671ab35cf6 | |||
c63ad4b6f1 | |||
eb713bd60e | |||
2b74fbf586 | |||
58f85658bd | |||
7e9fbfca72 | |||
5115384501 | |||
a1b96b1cf4 | |||
a565e71018 | |||
e41b175e3d | |||
db39f122be | |||
afd2dc4666 | |||
cba7e426a5 | |||
01e7cc9620 | |||
5a5497b745 | |||
b698e3546b | |||
e99a5e8144 | |||
577f91206c | |||
76f9542df7 | |||
9739168d48 | |||
5cbaf3a1b8 | |||
a02e0dfab6 | |||
5cc3bba5cc | |||
6e51573975 | |||
b686f39d0b | |||
6416a796c3 | |||
b6a394a113 | |||
456fd1364a | |||
f3cce6a04c | |||
9835a4537a | |||
715ec4ae2f | |||
55b2fb7f77 | |||
7c7743c145 | |||
826fc62299 | |||
5dd2e7523d | |||
4821d51167 | |||
c446be48e3 | |||
042f8391bb | |||
d4bc3294a3 | |||
04d53d6f57 | |||
881e0e0346 | |||
b9f8a00ba3 | |||
99bed67bec | |||
52a454800f | |||
c09c8e4980 | |||
b931dda1fe | |||
74166b4834 | |||
4abb769ee5 | |||
e8e2ca1526 | |||
78967dea13 | |||
58a5d493b7 | |||
c5341017cd | |||
f4873fcdee | |||
35efd017bb | |||
fb76c490c6 | |||
3653c78e92 | |||
1053c44326 | |||
e6ea177181 | |||
1957469061 | |||
2227120ae0 | |||
21c8c0371d | |||
1914a6a0d8 | |||
1cff4619e7 | |||
7bb7adf89c | |||
f55ff24925 | |||
f5f78d79e6 | |||
9180625dfd | |||
552320bae2 | |||
7cf221f767 | |||
98931a8623 | |||
ae10a89014 | |||
71d534dadb | |||
867bb1d409 | |||
91c44a1cf1 | |||
3bc60a8d5d | |||
58df8fa4b9 | |||
81f92b43e5 | |||
e1d9c3803b | |||
a7c24aace1 | |||
89a89e7b18 | |||
3425f7be40 | |||
09a6f8a34f | |||
7060f298b4 | |||
33dbe15760 | |||
e95c7dfc29 | |||
927a92fcac | |||
2b0f3d2a9a | |||
93fdb596d4 | |||
305666067e | |||
b805d87ee7 | |||
bc6bb9984f | |||
c043fd7912 | |||
781282897a |
@ -4,13 +4,13 @@ environment:
|
||||
matrix:
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: 1.20.0
|
||||
CHANNEL: 1.21.0
|
||||
- TARGET: i686-pc-windows-msvc
|
||||
CHANNEL: 1.20.0
|
||||
CHANNEL: 1.21.0
|
||||
- TARGET: x86_64-pc-windows-gnu
|
||||
CHANNEL: 1.20.0
|
||||
CHANNEL: 1.21.0
|
||||
- TARGET: x86_64-pc-windows-msvc
|
||||
CHANNEL: 1.20.0
|
||||
CHANNEL: 1.21.0
|
||||
# Stable channel
|
||||
- TARGET: i686-pc-windows-gnu
|
||||
CHANNEL: stable
|
||||
@ -59,6 +59,4 @@ build: false
|
||||
|
||||
# Equivalent to Travis' `script` phase
|
||||
test_script:
|
||||
- cargo build --no-default-features
|
||||
- cargo clean
|
||||
- cargo test --no-default-features
|
||||
|
24
.travis.yml
24
.travis.yml
@ -8,7 +8,7 @@ cache:
|
||||
|
||||
matrix:
|
||||
include:
|
||||
- rust: 1.20.0
|
||||
- rust: 1.21.0
|
||||
- rust: stable
|
||||
- rust: beta
|
||||
- rust: nightly
|
||||
@ -17,14 +17,14 @@ matrix:
|
||||
- rust: beta
|
||||
|
||||
#rust:
|
||||
# - 1.20.0
|
||||
# - 1.21.0
|
||||
# - stable
|
||||
# - beta
|
||||
# - nightly-2018-01-03
|
||||
|
||||
env:
|
||||
global:
|
||||
- RUSTFLAGS="-C link-dead-code"
|
||||
# - RUSTFLAGS="-C link-dead-code"
|
||||
- OPENSSL_VERSION=openssl-1.0.2
|
||||
|
||||
before_install:
|
||||
@ -46,20 +46,28 @@ script:
|
||||
cargo clean
|
||||
USE_SKEPTIC=1 cargo test --features=alpn
|
||||
else
|
||||
cargo test --features=alpn
|
||||
cargo clean
|
||||
cargo test -- --nocapture
|
||||
# --features=alpn
|
||||
fi
|
||||
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "1.20.0" ]]; then
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then
|
||||
cd examples/basics && cargo check && cd ../..
|
||||
cd examples/hello-world && cargo check && cd ../..
|
||||
cd examples/http-proxy && cargo check && cd ../..
|
||||
cd examples/multipart && cargo check && cd ../..
|
||||
cd examples/json && cargo check && cd ../..
|
||||
cd examples/juniper && cargo check && cd ../..
|
||||
cd examples/protobuf && cargo check && cd ../..
|
||||
cd examples/state && cargo check && cd ../..
|
||||
cd examples/template_tera && cargo check && cd ../..
|
||||
cd examples/diesel && cargo check && cd ../..
|
||||
cd examples/r2d2 && cargo check && cd ../..
|
||||
cd examples/tls && cargo check && cd ../..
|
||||
cd examples/websocket-chat && cargo check && cd ../..
|
||||
cd examples/websocket && cargo check && cd ../..
|
||||
cd examples/unix-socket && cargo check && cd ../..
|
||||
fi
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "nightly" && $CLIPPY ]]; then
|
||||
@ -69,8 +77,8 @@ script:
|
||||
# Upload docs
|
||||
after_success:
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "1.20.0" ]]; then
|
||||
cargo doc --features alpn --no-deps &&
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_PULL_REQUEST" = "false" && "$TRAVIS_BRANCH" == "master" && "$TRAVIS_RUST_VERSION" == "nightly" ]]; then
|
||||
cargo doc --features "alpn, tls, session" --no-deps &&
|
||||
echo "<meta http-equiv=refresh content=0;url=os_balloon/index.html>" > target/doc/index.html &&
|
||||
cargo install mdbook &&
|
||||
cd guide && mdbook build -d ../target/doc/guide && cd .. &&
|
||||
@ -80,7 +88,7 @@ after_success:
|
||||
fi
|
||||
|
||||
- |
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "stable" ]]; then
|
||||
if [[ "$TRAVIS_OS_NAME" == "linux" && "$TRAVIS_RUST_VERSION" == "1.21.0" ]]; then
|
||||
bash <(curl https://raw.githubusercontent.com/xd009642/tarpaulin/master/travis-install.sh)
|
||||
USE_SKEPTIC=1 cargo tarpaulin --out Xml
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
|
141
CHANGES.md
141
CHANGES.md
@ -1,5 +1,146 @@
|
||||
# Changes
|
||||
|
||||
## 0.4.7 (2018-03-11)
|
||||
|
||||
* Fix panic on unknown content encoding
|
||||
|
||||
* Fix connection get closed too early
|
||||
|
||||
* Fix streaming response handling for http/2
|
||||
|
||||
* Better sleep on error support
|
||||
|
||||
|
||||
## 0.4.6 (2018-03-10)
|
||||
|
||||
* Fix client cookie handling
|
||||
|
||||
* Fix json content type detection
|
||||
|
||||
* Fix CORS middleware #117
|
||||
|
||||
* Optimize websockets stream support
|
||||
|
||||
|
||||
## 0.4.5 (2018-03-07)
|
||||
|
||||
* Fix compression #103 and #104
|
||||
|
||||
* Fix client cookie handling #111
|
||||
|
||||
* Non-blocking processing of a `NamedFile`
|
||||
|
||||
* Enable compression support for `NamedFile`
|
||||
|
||||
* Better support for `NamedFile` type
|
||||
|
||||
* Add `ResponseError` impl for `SendRequestError`. This improves ergonomics of the client.
|
||||
|
||||
* Add native-tls support for client
|
||||
|
||||
* Allow client connection timeout to be set #108
|
||||
|
||||
* Allow to use std::net::TcpListener for HttpServer
|
||||
|
||||
* Handle panics in worker threads
|
||||
|
||||
|
||||
## 0.4.4 (2018-03-04)
|
||||
|
||||
* Allow to use Arc<Vec<u8>> as response/request body
|
||||
|
||||
* Fix handling of requests with an encoded body with a length > 8192 #93
|
||||
|
||||
## 0.4.3 (2018-03-03)
|
||||
|
||||
* Fix request body read bug
|
||||
|
||||
* Fix segmentation fault #79
|
||||
|
||||
* Set reuse address before bind #90
|
||||
|
||||
|
||||
## 0.4.2 (2018-03-02)
|
||||
|
||||
* Better naming for websockets implementation
|
||||
|
||||
* Add `Pattern::with_prefix()`, make it more usable outside of actix
|
||||
|
||||
* Add csrf middleware for filter for cross-site request forgery #89
|
||||
|
||||
* Fix disconnect on idle connections
|
||||
|
||||
|
||||
## 0.4.1 (2018-03-01)
|
||||
|
||||
* Rename `Route::p()` to `Route::filter()`
|
||||
|
||||
* Better naming for http codes
|
||||
|
||||
* Fix payload parse in situation when socket data is not ready.
|
||||
|
||||
* Fix Session mutable borrow lifetime #87
|
||||
|
||||
|
||||
## 0.4.0 (2018-02-28)
|
||||
|
||||
* Actix 0.5 compatibility
|
||||
|
||||
* Fix request json/urlencoded loaders
|
||||
|
||||
* Simplify HttpServer type definition
|
||||
|
||||
* Added HttpRequest::encoding() method
|
||||
|
||||
* Added HttpRequest::mime_type() method
|
||||
|
||||
* Added HttpRequest::uri_mut(), allows to modify request uri
|
||||
|
||||
* Added StaticFiles::index_file()
|
||||
|
||||
* Added http client
|
||||
|
||||
* Added websocket client
|
||||
|
||||
* Added TestServer::ws(), test websockets client
|
||||
|
||||
* Added TestServer http client support
|
||||
|
||||
* Allow to override content encoding on application level
|
||||
|
||||
|
||||
## 0.3.3 (2018-01-25)
|
||||
|
||||
* Stop processing any events after context stop
|
||||
|
||||
* Re-enable write back-pressure for h1 connections
|
||||
|
||||
* Refactor HttpServer::start_ssl() method
|
||||
|
||||
* Upgrade openssl to 0.10
|
||||
|
||||
|
||||
## 0.3.2 (2018-01-21)
|
||||
|
||||
* Fix HEAD requests handling
|
||||
|
||||
* Log request processing errors
|
||||
|
||||
* Always enable content encoding if encoding explicitly selected
|
||||
|
||||
* Allow multiple Applications on a single server with different state #49
|
||||
|
||||
* CORS middleware: allowed_headers is defaulting to None #50
|
||||
|
||||
|
||||
## 0.3.1 (2018-01-13)
|
||||
|
||||
* Fix directory entry path #47
|
||||
|
||||
* Do not enable chunked encoding for HTTP/1.0
|
||||
|
||||
* Allow explicitly disable chunked encoding
|
||||
|
||||
|
||||
## 0.3.0 (2018-01-12)
|
||||
|
||||
|
46
CODE_OF_CONDUCT.md
Normal file
46
CODE_OF_CONDUCT.md
Normal file
@ -0,0 +1,46 @@
|
||||
# Contributor Covenant Code of Conduct
|
||||
|
||||
## Our Pledge
|
||||
|
||||
In the interest of fostering an open and welcoming environment, we as contributors and maintainers pledge to making participation in our project and our community a harassment-free experience for everyone, regardless of age, body size, disability, ethnicity, gender identity and expression, level of experience, nationality, personal appearance, race, religion, or sexual identity and orientation.
|
||||
|
||||
## Our Standards
|
||||
|
||||
Examples of behavior that contributes to creating a positive environment include:
|
||||
|
||||
* Using welcoming and inclusive language
|
||||
* Being respectful of differing viewpoints and experiences
|
||||
* Gracefully accepting constructive criticism
|
||||
* Focusing on what is best for the community
|
||||
* Showing empathy towards other community members
|
||||
|
||||
Examples of unacceptable behavior by participants include:
|
||||
|
||||
* The use of sexualized language or imagery and unwelcome sexual attention or advances
|
||||
* Trolling, insulting/derogatory comments, and personal or political attacks
|
||||
* Public or private harassment
|
||||
* Publishing others' private information, such as a physical or electronic address, without explicit permission
|
||||
* Other conduct which could reasonably be considered inappropriate in a professional setting
|
||||
|
||||
## Our Responsibilities
|
||||
|
||||
Project maintainers are responsible for clarifying the standards of acceptable behavior and are expected to take appropriate and fair corrective action in response to any instances of unacceptable behavior.
|
||||
|
||||
Project maintainers have the right and responsibility to remove, edit, or reject comments, commits, code, wiki edits, issues, and other contributions that are not aligned to this Code of Conduct, or to ban temporarily or permanently any contributor for other behaviors that they deem inappropriate, threatening, offensive, or harmful.
|
||||
|
||||
## Scope
|
||||
|
||||
This Code of Conduct applies both within project spaces and in public spaces when an individual is representing the project or its community. Examples of representing a project or community include using an official project e-mail address, posting via an official social media account, or acting as an appointed representative at an online or offline event. Representation of a project may be further defined and clarified by project maintainers.
|
||||
|
||||
## Enforcement
|
||||
|
||||
Instances of abusive, harassing, or otherwise unacceptable behavior may be reported by contacting the project team at fafhrd91@gmail.com. The project team will review and investigate all complaints, and will respond in a way that it deems appropriate to the circumstances. The project team is obligated to maintain confidentiality with regard to the reporter of an incident. Further details of specific enforcement policies may be posted separately.
|
||||
|
||||
Project maintainers who do not follow or enforce the Code of Conduct in good faith may face temporary or permanent repercussions as determined by other members of the project's leadership.
|
||||
|
||||
## Attribution
|
||||
|
||||
This Code of Conduct is adapted from the [Contributor Covenant][homepage], version 1.4, available at [http://contributor-covenant.org/version/1/4][version]
|
||||
|
||||
[homepage]: http://contributor-covenant.org
|
||||
[version]: http://contributor-covenant.org/version/1/4/
|
84
Cargo.toml
84
Cargo.toml
@ -1,17 +1,20 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "0.3.0"
|
||||
version = "0.4.7"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
description = "Actix web framework"
|
||||
description = "Actix web is a simple, pragmatic, extremely fast, web framework for Rust."
|
||||
readme = "README.md"
|
||||
keywords = ["http", "web", "framework", "async", "futures"]
|
||||
homepage = "https://github.com/actix/actix-web"
|
||||
repository = "https://github.com/actix/actix-web.git"
|
||||
documentation = "https://docs.rs/actix-web/"
|
||||
categories = ["network-programming", "asynchronous",
|
||||
"web-programming::http-server", "web-programming::websocket"]
|
||||
"web-programming::http-server",
|
||||
"web-programming::http-client",
|
||||
"web-programming::websocket"]
|
||||
license = "MIT/Apache-2.0"
|
||||
exclude = [".gitignore", ".travis.yml", ".cargo/config", "appveyor.yml"]
|
||||
exclude = [".gitignore", ".travis.yml", ".cargo/config",
|
||||
"appveyor.yml", "/examples/**"]
|
||||
build = "build.rs"
|
||||
|
||||
[badges]
|
||||
@ -24,7 +27,7 @@ name = "actix_web"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["session"]
|
||||
|
||||
# tls
|
||||
tls = ["native-tls", "tokio-tls"]
|
||||
@ -32,59 +35,60 @@ tls = ["native-tls", "tokio-tls"]
|
||||
# openssl
|
||||
alpn = ["openssl", "openssl/v102", "openssl/v110", "tokio-openssl"]
|
||||
|
||||
# sessions
|
||||
session = ["cookie/secure"]
|
||||
|
||||
[dependencies]
|
||||
log = "0.4"
|
||||
failure = "0.1"
|
||||
failure_derive = "0.1"
|
||||
actix = "^0.5.2"
|
||||
|
||||
base64 = "0.9"
|
||||
bitflags = "1.0"
|
||||
brotli2 = "^0.3.2"
|
||||
failure = "0.1.1"
|
||||
flate2 = "1.0"
|
||||
h2 = "0.1"
|
||||
http = "^0.1.2"
|
||||
http = "^0.1.5"
|
||||
httparse = "1.2"
|
||||
http-range = "0.1"
|
||||
time = "0.1"
|
||||
mime = "0.3"
|
||||
mime_guess = "1.8"
|
||||
regex = "0.2"
|
||||
sha1 = "0.4"
|
||||
url = "1.6"
|
||||
libc = "0.2"
|
||||
log = "0.4"
|
||||
mime = "0.3"
|
||||
mime_guess = "2.0.0-alpha"
|
||||
num_cpus = "1.0"
|
||||
percent-encoding = "1.0"
|
||||
rand = "0.4"
|
||||
regex = "0.2"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
brotli2 = "^0.3.2"
|
||||
percent-encoding = "1.0"
|
||||
sha1 = "0.6"
|
||||
smallvec = "0.6"
|
||||
bitflags = "1.0"
|
||||
num_cpus = "1.0"
|
||||
flate2 = "1.0"
|
||||
cookie = { version="0.10", features=["percent-encode", "secure"] }
|
||||
|
||||
# ring nightly compilation bug
|
||||
# cookie = { git="https://github.com/alexcrichton/cookie-rs.git", features=["percent-encode", "secure"] }
|
||||
time = "0.1"
|
||||
encoding = "0.2"
|
||||
language-tags = "0.2"
|
||||
url = { version="1.7", features=["query_encoding"] }
|
||||
cookie = { version="0.10", features=["percent-encode"] }
|
||||
|
||||
# io
|
||||
mio = "0.6"
|
||||
mio = "^0.6.13"
|
||||
net2 = "0.2"
|
||||
bytes = "0.4"
|
||||
byteorder = "1"
|
||||
futures = "0.1"
|
||||
futures-cpupool = "0.1"
|
||||
tokio-io = "0.1"
|
||||
tokio-core = "0.1"
|
||||
trust-dns-resolver = "0.8"
|
||||
|
||||
# native-tls
|
||||
native-tls = { version="0.1", optional = true }
|
||||
tokio-tls = { version="0.1", optional = true }
|
||||
|
||||
# openssl
|
||||
tokio-openssl = { version="0.1", optional = true }
|
||||
|
||||
[dependencies.actix]
|
||||
version = "^0.4.2"
|
||||
|
||||
[dependencies.openssl]
|
||||
version = "0.9"
|
||||
optional = true
|
||||
openssl = { version="0.10", optional = true }
|
||||
tokio-openssl = { version="0.2", optional = true }
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.4"
|
||||
reqwest = "0.8"
|
||||
env_logger = "0.5"
|
||||
skeptic = "0.13"
|
||||
serde_derive = "1.0"
|
||||
|
||||
@ -95,19 +99,27 @@ version_check = "0.1"
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 3
|
||||
# debug = true
|
||||
codegen-units = 1
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./",
|
||||
"examples/basics",
|
||||
"examples/juniper",
|
||||
"examples/diesel",
|
||||
"examples/r2d2",
|
||||
"examples/json",
|
||||
"examples/protobuf",
|
||||
"examples/hello-world",
|
||||
"examples/http-proxy",
|
||||
"examples/multipart",
|
||||
"examples/state",
|
||||
"examples/redis-session",
|
||||
"examples/template_tera",
|
||||
"examples/tls",
|
||||
"examples/websocket",
|
||||
"examples/websocket-chat",
|
||||
"examples/web-cors/backend",
|
||||
"examples/unix-socket",
|
||||
"tools/wsload/",
|
||||
]
|
||||
|
81
README.md
81
README.md
@ -1,6 +1,35 @@
|
||||
# Actix web [](https://travis-ci.org/actix/actix-web) [](https://ci.appveyor.com/project/fafhrd91/actix-web-hdy9d/branch/master) [](https://codecov.io/gh/actix/actix-web) [](https://crates.io/crates/actix-web) [](https://gitter.im/actix/actix?utm_source=badge&utm_medium=badge&utm_campaign=pr-badge&utm_content=badge)
|
||||
|
||||
Actix web is a small, fast, pragmatic, open source rust web framework.
|
||||
Actix web is a simple, pragmatic, extremely fast, web framework for Rust.
|
||||
|
||||
* Supported *HTTP/1.x* and [*HTTP/2.0*](https://actix.github.io/actix-web/guide/qs_13.html) protocols
|
||||
* Streaming and pipelining
|
||||
* Keep-alive and slow requests handling
|
||||
* Client/server [WebSockets](https://actix.github.io/actix-web/guide/qs_9.html) support
|
||||
* Transparent content compression/decompression (br, gzip, deflate)
|
||||
* Configurable [request routing](https://actix.github.io/actix-web/guide/qs_5.html)
|
||||
* Graceful server shutdown
|
||||
* Multipart streams
|
||||
* Static assets
|
||||
* SSL support with openssl or native-tls
|
||||
* Middlewares ([Logger](https://actix.github.io/actix-web/guide/qs_10.html#logging),
|
||||
[Session](https://actix.github.io/actix-web/guide/qs_10.html#user-sessions),
|
||||
[Redis sessions](https://github.com/actix/actix-redis),
|
||||
[DefaultHeaders](https://actix.github.io/actix-web/guide/qs_10.html#default-headers),
|
||||
[CORS](https://actix.github.io/actix-web/actix_web/middleware/cors/index.html),
|
||||
[CSRF](https://actix.github.io/actix-web/actix_web/middleware/csrf/index.html))
|
||||
* Built on top of [Actix actor framework](https://github.com/actix/actix).
|
||||
|
||||
## Documentation
|
||||
|
||||
* [User Guide](http://actix.github.io/actix-web/guide/)
|
||||
* [API Documentation (Development)](http://actix.github.io/actix-web/actix_web/)
|
||||
* [API Documentation (Releases)](https://docs.rs/actix-web/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
|
||||
* Minimum supported Rust version: 1.21 or later
|
||||
|
||||
## Example
|
||||
|
||||
```rust,ignore
|
||||
extern crate actix_web;
|
||||
@ -19,48 +48,28 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
## Documentation
|
||||
|
||||
* [User Guide](http://actix.github.io/actix-web/guide/)
|
||||
* [API Documentation (Development)](http://actix.github.io/actix-web/actix_web/)
|
||||
* [API Documentation (Releases)](https://docs.rs/actix-web/)
|
||||
* [Chat on gitter](https://gitter.im/actix/actix)
|
||||
* Cargo package: [actix-web](https://crates.io/crates/actix-web)
|
||||
* Minimum supported Rust version: 1.20 or later
|
||||
|
||||
## Features
|
||||
|
||||
* Supported *HTTP/1.x* and *HTTP/2.0* protocols
|
||||
* Streaming and pipelining
|
||||
* Keep-alive and slow requests handling
|
||||
* [WebSockets](https://actix.github.io/actix-web/actix_web/ws/index.html)
|
||||
* Transparent content compression/decompression (br, gzip, deflate)
|
||||
* Configurable request routing
|
||||
* Graceful server shutdown
|
||||
* Multipart streams
|
||||
* Middlewares ([Logger](https://actix.github.io/actix-web/guide/qs_10.html#logging),
|
||||
[Session](https://actix.github.io/actix-web/guide/qs_10.html#user-sessions),
|
||||
[DefaultHeaders](https://actix.github.io/actix-web/guide/qs_10.html#default-headers),
|
||||
[CORS](https://actix.github.io/actix-web/actix_web/middleware/cors/index.html))
|
||||
* Built on top of [Actix](https://github.com/actix/actix).
|
||||
|
||||
## Benchmarks
|
||||
|
||||
Some basic benchmarks could be found in this [respository](https://github.com/fafhrd91/benchmarks).
|
||||
|
||||
## Examples
|
||||
### More examples
|
||||
|
||||
* [Basics](https://github.com/actix/actix-web/tree/master/examples/basics/)
|
||||
* [Stateful](https://github.com/actix/actix-web/tree/master/examples/state/)
|
||||
* [Mulitpart streams](https://github.com/actix/actix-web/tree/master/examples/multipart/)
|
||||
* [Protobuf support](https://github.com/actix/actix-web/tree/master/examples/protobuf/)
|
||||
* [Multipart streams](https://github.com/actix/actix-web/tree/master/examples/multipart/)
|
||||
* [Simple websocket session](https://github.com/actix/actix-web/tree/master/examples/websocket/)
|
||||
* [Tera templates](https://github.com/actix/actix-web/tree/master/examples/template_tera/)
|
||||
* [Diesel integration](https://github.com/actix/actix-web/tree/master/examples/diesel/)
|
||||
* [SSL / HTTP/2.0](https://github.com/actix/actix-web/tree/master/examples/tls/)
|
||||
* [Tcp/Websocket chat](https://github.com/actix/actix-web/tree/master/examples/websocket-chat/)
|
||||
* [SockJS Server](https://github.com/actix/actix-sockjs)
|
||||
* [Json](https://github.com/actix/actix-web/tree/master/examples/json/)
|
||||
|
||||
You may consider checking out
|
||||
[this directory](https://github.com/actix/actix-web/tree/master/examples) for more examples.
|
||||
|
||||
## Benchmarks
|
||||
|
||||
* [TechEmpower Framework Benchmark](https://www.techempower.com/benchmarks/#section=data-r15&hw=ph&test=plaintext)
|
||||
|
||||
* Some basic benchmarks could be found in this [repository](https://github.com/fafhrd91/benchmarks).
|
||||
|
||||
## License
|
||||
|
||||
This project is licensed under either of
|
||||
@ -69,3 +78,9 @@ This project is licensed under either of
|
||||
* MIT license ([LICENSE-MIT](LICENSE-MIT) or [http://opensource.org/licenses/MIT](http://opensource.org/licenses/MIT))
|
||||
|
||||
at your option.
|
||||
|
||||
## Code of Conduct
|
||||
|
||||
Contribution to the actix-web crate is organized under the terms of the
|
||||
Contributor Covenant, the maintainer of actix-web, @fafhrd91, promises to
|
||||
intervene to uphold that code of conduct.
|
||||
|
2
build.rs
2
build.rs
@ -6,6 +6,7 @@ use std::{env, fs};
|
||||
|
||||
#[cfg(unix)]
|
||||
fn main() {
|
||||
println!("cargo:rerun-if-env-changed=USE_SKEPTIC");
|
||||
let f = env::var("OUT_DIR").unwrap() + "/skeptic-tests.rs";
|
||||
if env::var("USE_SKEPTIC").is_ok() {
|
||||
let _ = fs::remove_file(f);
|
||||
@ -25,6 +26,7 @@ fn main() {
|
||||
"guide/src/qs_10.md",
|
||||
"guide/src/qs_12.md",
|
||||
"guide/src/qs_13.md",
|
||||
"guide/src/qs_14.md",
|
||||
]);
|
||||
} else {
|
||||
let _ = fs::File::create(f);
|
||||
|
@ -6,6 +6,6 @@ workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
futures = "*"
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { path = "../../" }
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path="../.." }
|
||||
|
@ -7,6 +7,7 @@ extern crate env_logger;
|
||||
extern crate futures;
|
||||
use futures::Stream;
|
||||
|
||||
use std::{io, env};
|
||||
use actix_web::*;
|
||||
use actix_web::middleware::RequestSession;
|
||||
use futures::future::{FutureResult, result};
|
||||
@ -21,7 +22,7 @@ fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
println!("{:?}", req);
|
||||
|
||||
// example of ...
|
||||
if let Ok(ch) = req.payload_mut().readany().poll() {
|
||||
if let Ok(ch) = req.poll() {
|
||||
if let futures::Async::Ready(Some(d)) = ch {
|
||||
println!("{}", String::from_utf8_lossy(d.as_ref()));
|
||||
}
|
||||
@ -56,17 +57,17 @@ fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
fn p404(req: HttpRequest) -> Result<HttpResponse> {
|
||||
|
||||
// html
|
||||
let html = format!(r#"<!DOCTYPE html><html><head><title>actix - basics</title><link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" /></head>
|
||||
let html = r#"<!DOCTYPE html><html><head><title>actix - basics</title><link rel="shortcut icon" type="image/x-icon" href="/favicon.ico" /></head>
|
||||
<body>
|
||||
<a href="index.html">back to home</a>
|
||||
<h1>404</h1>
|
||||
</body>
|
||||
</html>"#);
|
||||
</html>"#;
|
||||
|
||||
// response
|
||||
Ok(HttpResponse::build(StatusCode::NOT_FOUND)
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(&html).unwrap())
|
||||
.body(html).unwrap())
|
||||
}
|
||||
|
||||
|
||||
@ -92,8 +93,9 @@ fn with_param(req: HttpRequest) -> Result<HttpResponse>
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
env::set_var("RUST_LOG", "actix_web=debug");
|
||||
env::set_var("RUST_BACKTRACE", "1");
|
||||
env_logger::init();
|
||||
let sys = actix::System::new("basic-example");
|
||||
|
||||
let addr = HttpServer::new(
|
||||
@ -121,6 +123,9 @@ fn main() {
|
||||
_ => httpcodes::HTTPNotFound,
|
||||
}
|
||||
}))
|
||||
.resource("/error.html", |r| r.f(|req| {
|
||||
error::ErrorBadRequest(io::Error::new(io::ErrorKind::Other, "test"))
|
||||
}))
|
||||
// static files
|
||||
.handler("/static/", fs::StaticFiles::new("../static/", true))
|
||||
// redirect
|
||||
@ -134,7 +139,7 @@ fn main() {
|
||||
// default
|
||||
.default_resource(|r| {
|
||||
r.method(Method::GET).f(p404);
|
||||
r.route().p(pred::Not(pred::Get())).f(|req| httpcodes::HTTPMethodNotAllowed);
|
||||
r.route().filter(pred::Not(pred::Get())).f(|req| httpcodes::HTTPMethodNotAllowed);
|
||||
}))
|
||||
|
||||
.bind("127.0.0.1:8080").expect("Can not bind to 127.0.0.1:8080")
|
||||
|
@ -5,9 +5,9 @@ authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
||||
futures = "0.1"
|
||||
uuid = { version = "0.5", features = ["serde", "v4"] }
|
||||
|
@ -8,7 +8,7 @@ use diesel::prelude::*;
|
||||
use models;
|
||||
use schema;
|
||||
|
||||
/// This is db executor actor. We are going to run 3 of them in parallele.
|
||||
/// This is db executor actor. We are going to run 3 of them in parallel.
|
||||
pub struct DbExecutor(pub SqliteConnection);
|
||||
|
||||
/// This is only message that this actor can handle, but it is easy to extend number of
|
||||
@ -17,9 +17,8 @@ pub struct CreateUser {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl ResponseType for CreateUser {
|
||||
type Item = models::User;
|
||||
type Error = Error;
|
||||
impl Message for CreateUser {
|
||||
type Result = Result<models::User, Error>;
|
||||
}
|
||||
|
||||
impl Actor for DbExecutor {
|
||||
@ -27,7 +26,7 @@ impl Actor for DbExecutor {
|
||||
}
|
||||
|
||||
impl Handler<CreateUser> for DbExecutor {
|
||||
type Result = MessageResult<CreateUser>;
|
||||
type Result = Result<models::User, Error>;
|
||||
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Self::Result {
|
||||
use self::schema::users::dsl::*;
|
||||
|
@ -1,8 +1,8 @@
|
||||
//! Actix web diesel example
|
||||
//!
|
||||
//! Diesel does not support tokio, so we have to run it in separate threads.
|
||||
//! Actix supports sync actors by default, so we going to create sync actor that will
|
||||
//! use diesel. Technically sync actors are worker style actors, multiple of them
|
||||
//! Actix supports sync actors by default, so we going to create sync actor that use diesel.
|
||||
//! Technically sync actors are worker style actors, multiple of them
|
||||
//! can run in parallel and process messages from same queue.
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
@ -31,14 +31,15 @@ use db::{CreateUser, DbExecutor};
|
||||
|
||||
/// State with DbExecutor address
|
||||
struct State {
|
||||
db: SyncAddress<DbExecutor>,
|
||||
db: Addr<Syn, DbExecutor>,
|
||||
}
|
||||
|
||||
/// Async request handler
|
||||
fn index(req: HttpRequest<State>) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
let name = &req.match_info()["name"];
|
||||
|
||||
req.state().db.call_fut(CreateUser{name: name.to_owned()})
|
||||
// send async `CreateUser` message to a `DbExecutor`
|
||||
req.state().db.send(CreateUser{name: name.to_owned()})
|
||||
.from_err()
|
||||
.and_then(|res| {
|
||||
match res {
|
||||
@ -54,7 +55,7 @@ fn main() {
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("diesel-example");
|
||||
|
||||
// Start db executor actors
|
||||
// Start 3 db executor actors
|
||||
let addr = SyncArbiter::start(3, || {
|
||||
DbExecutor(SqliteConnection::establish("test.db").unwrap())
|
||||
});
|
||||
|
@ -5,6 +5,6 @@ authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
11
examples/http-proxy/Cargo.toml
Normal file
11
examples/http-proxy/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "http-proxy"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
futures = "0.1"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../", features=["alpn"] }
|
59
examples/http-proxy/src/main.rs
Normal file
59
examples/http-proxy/src/main.rs
Normal file
@ -0,0 +1,59 @@
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate futures;
|
||||
extern crate env_logger;
|
||||
|
||||
use actix_web::*;
|
||||
use futures::{Future, Stream};
|
||||
|
||||
|
||||
/// Stream client request response and then send body to a server response
|
||||
fn index(_req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
client::ClientRequest::get("https://www.rust-lang.org/en-US/")
|
||||
.finish().unwrap()
|
||||
.send()
|
||||
.map_err(error::Error::from) // <- convert SendRequestError to an Error
|
||||
.and_then(
|
||||
|resp| resp.body() // <- this is MessageBody type, resolves to complete body
|
||||
.from_err() // <- convet PayloadError to a Error
|
||||
.and_then(|body| { // <- we got complete body, now send as server response
|
||||
httpcodes::HttpOk.build()
|
||||
.body(body)
|
||||
.map_err(error::Error::from)
|
||||
}))
|
||||
.responder()
|
||||
}
|
||||
|
||||
/// streaming client request to a streaming server response
|
||||
fn streaming(_req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
// send client request
|
||||
client::ClientRequest::get("https://www.rust-lang.org/en-US/")
|
||||
.finish().unwrap()
|
||||
.send() // <- connect to host and send request
|
||||
.map_err(error::Error::from) // <- convert SendRequestError to an Error
|
||||
.and_then(|resp| { // <- we received client response
|
||||
httpcodes::HttpOk.build()
|
||||
// read one chunk from client response and send this chunk to a server response
|
||||
// .from_err() converts PayloadError to a Error
|
||||
.body(Body::Streaming(Box::new(resp.from_err())))
|
||||
.map_err(|e| e.into()) // HttpOk::build() mayb return HttpError, we need to convert it to a Error
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
env_logger::init();
|
||||
let sys = actix::System::new("http-proxy");
|
||||
|
||||
let _addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/streaming", |r| r.f(streaming))
|
||||
.resource("/", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
@ -14,5 +14,5 @@ serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
json = "*"
|
||||
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
actix = "0.5"
|
||||
actix-web = { path="../../" }
|
||||
|
@ -34,9 +34,9 @@ fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
const MAX_SIZE: usize = 262_144; // max payload size is 256k
|
||||
|
||||
/// This handler manually load request payload and parse serde json
|
||||
fn index_manual(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
// readany() returns asynchronous stream of Bytes objects
|
||||
req.payload_mut().readany()
|
||||
fn index_manual(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
// HttpRequest is stream of Bytes objects
|
||||
req
|
||||
// `Future::from_err` acts like `?` in that it coerces the error type from
|
||||
// the future into the final error type
|
||||
.from_err()
|
||||
@ -63,8 +63,8 @@ fn index_manual(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Err
|
||||
}
|
||||
|
||||
/// This handler manually load request payload and parse json-rust
|
||||
fn index_mjsonrust(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.payload_mut().readany().concat2()
|
||||
fn index_mjsonrust(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.concat2()
|
||||
.from_err()
|
||||
.and_then(|body| {
|
||||
// body is loaded, now we can deserialize json-rust
|
||||
|
17
examples/juniper/Cargo.toml
Normal file
17
examples/juniper/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "juniper-example"
|
||||
version = "0.1.0"
|
||||
authors = ["pyros2097 <pyros2097@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
||||
futures = "0.1"
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
juniper = "0.9.2"
|
15
examples/juniper/README.md
Normal file
15
examples/juniper/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# juniper
|
||||
|
||||
Juniper integration for Actix web
|
||||
|
||||
### server
|
||||
|
||||
```bash
|
||||
cd actix-web/examples/juniper
|
||||
cargo run (or ``cargo watch -x run``)
|
||||
# Started http server: 127.0.0.1:8080
|
||||
```
|
||||
|
||||
### web client
|
||||
|
||||
[http://127.0.0.1:8080/graphiql](http://127.0.0.1:8080/graphiql)
|
110
examples/juniper/src/main.rs
Normal file
110
examples/juniper/src/main.rs
Normal file
@ -0,0 +1,110 @@
|
||||
//! Actix web juniper example
|
||||
//!
|
||||
//! A simple example integrating juniper in actix-web
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
#[macro_use]
|
||||
extern crate serde_derive;
|
||||
#[macro_use]
|
||||
extern crate juniper;
|
||||
extern crate futures;
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
use juniper::http::graphiql::graphiql_source;
|
||||
use juniper::http::GraphQLRequest;
|
||||
|
||||
use futures::future::Future;
|
||||
|
||||
mod schema;
|
||||
|
||||
use schema::Schema;
|
||||
use schema::create_schema;
|
||||
|
||||
struct State {
|
||||
executor: Addr<Syn, GraphQLExecutor>,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub struct GraphQLData(GraphQLRequest);
|
||||
|
||||
impl Message for GraphQLData {
|
||||
type Result = Result<String, Error>;
|
||||
}
|
||||
|
||||
pub struct GraphQLExecutor {
|
||||
schema: std::sync::Arc<Schema>
|
||||
}
|
||||
|
||||
impl GraphQLExecutor {
|
||||
fn new(schema: std::sync::Arc<Schema>) -> GraphQLExecutor {
|
||||
GraphQLExecutor {
|
||||
schema: schema,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Actor for GraphQLExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Handler<GraphQLData> for GraphQLExecutor {
|
||||
type Result = Result<String, Error>;
|
||||
|
||||
fn handle(&mut self, msg: GraphQLData, _: &mut Self::Context) -> Self::Result {
|
||||
let res = msg.0.execute(&self.schema, &());
|
||||
let res_text = serde_json::to_string(&res)?;
|
||||
Ok(res_text)
|
||||
}
|
||||
}
|
||||
|
||||
fn graphiql(_req: HttpRequest<State>) -> Result<HttpResponse> {
|
||||
let html = graphiql_source("http://127.0.0.1:8080/graphql");
|
||||
Ok(HttpResponse::build(StatusCode::OK)
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html).unwrap())
|
||||
}
|
||||
|
||||
fn graphql(req: HttpRequest<State>) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
let executor = req.state().executor.clone();
|
||||
req.json()
|
||||
.from_err()
|
||||
.and_then(move |val: GraphQLData| {
|
||||
executor.send(val)
|
||||
.from_err()
|
||||
.and_then(|res| {
|
||||
match res {
|
||||
Ok(user) => Ok(httpcodes::HTTPOk.build().body(user)?),
|
||||
Err(_) => Ok(httpcodes::HTTPInternalServerError.into())
|
||||
}
|
||||
})
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("juniper-example");
|
||||
|
||||
let schema = std::sync::Arc::new(create_schema());
|
||||
let addr = SyncArbiter::start(3, move || {
|
||||
GraphQLExecutor::new(schema.clone())
|
||||
});
|
||||
|
||||
// Start http server
|
||||
let _addr = HttpServer::new(move || {
|
||||
Application::with_state(State{executor: addr.clone()})
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/graphql", |r| r.method(Method::POST).a(graphql))
|
||||
.resource("/graphiql", |r| r.method(Method::GET).f(graphiql))})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
58
examples/juniper/src/schema.rs
Normal file
58
examples/juniper/src/schema.rs
Normal file
@ -0,0 +1,58 @@
|
||||
use juniper::FieldResult;
|
||||
use juniper::RootNode;
|
||||
|
||||
#[derive(GraphQLEnum)]
|
||||
enum Episode {
|
||||
NewHope,
|
||||
Empire,
|
||||
Jedi,
|
||||
}
|
||||
|
||||
#[derive(GraphQLObject)]
|
||||
#[graphql(description = "A humanoid creature in the Star Wars universe")]
|
||||
struct Human {
|
||||
id: String,
|
||||
name: String,
|
||||
appears_in: Vec<Episode>,
|
||||
home_planet: String,
|
||||
}
|
||||
|
||||
#[derive(GraphQLInputObject)]
|
||||
#[graphql(description = "A humanoid creature in the Star Wars universe")]
|
||||
struct NewHuman {
|
||||
name: String,
|
||||
appears_in: Vec<Episode>,
|
||||
home_planet: String,
|
||||
}
|
||||
|
||||
pub struct QueryRoot;
|
||||
|
||||
graphql_object!(QueryRoot: () |&self| {
|
||||
field human(&executor, id: String) -> FieldResult<Human> {
|
||||
Ok(Human{
|
||||
id: "1234".to_owned(),
|
||||
name: "Luke".to_owned(),
|
||||
appears_in: vec![Episode::NewHope],
|
||||
home_planet: "Mars".to_owned(),
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
pub struct MutationRoot;
|
||||
|
||||
graphql_object!(MutationRoot: () |&self| {
|
||||
field createHuman(&executor, new_human: NewHuman) -> FieldResult<Human> {
|
||||
Ok(Human{
|
||||
id: "1234".to_owned(),
|
||||
name: new_human.name,
|
||||
appears_in: new_human.appears_in,
|
||||
home_planet: new_human.home_planet,
|
||||
})
|
||||
}
|
||||
});
|
||||
|
||||
pub type Schema = RootNode<'static, QueryRoot, MutationRoot>;
|
||||
|
||||
pub fn create_schema() -> Schema {
|
||||
Schema::new(QueryRoot {}, MutationRoot {})
|
||||
}
|
@ -11,5 +11,5 @@ path = "src/main.rs"
|
||||
[dependencies]
|
||||
env_logger = "*"
|
||||
futures = "0.1"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
actix = "0.5"
|
||||
actix-web = { path="../../" }
|
||||
|
@ -11,7 +11,7 @@ use futures::{Future, Stream};
|
||||
use futures::future::{result, Either};
|
||||
|
||||
|
||||
fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>>
|
||||
fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>>
|
||||
{
|
||||
println!("{:?}", req);
|
||||
|
||||
|
16
examples/protobuf/Cargo.toml
Normal file
16
examples/protobuf/Cargo.toml
Normal file
@ -0,0 +1,16 @@
|
||||
[package]
|
||||
name = "protobuf-example"
|
||||
version = "0.1.0"
|
||||
authors = ["kingxsp <jin_hb_zh@126.com>"]
|
||||
|
||||
[dependencies]
|
||||
bytes = "0.4"
|
||||
futures = "0.1"
|
||||
failure = "0.1"
|
||||
env_logger = "*"
|
||||
|
||||
prost = "0.2.0"
|
||||
prost-derive = "0.2.0"
|
||||
|
||||
actix = "0.5"
|
||||
actix-web = { path="../../" }
|
66
examples/protobuf/client.py
Normal file
66
examples/protobuf/client.py
Normal file
@ -0,0 +1,66 @@
|
||||
# just start server and run client.py
|
||||
|
||||
# wget https://github.com/google/protobuf/releases/download/v3.5.1/protobuf-python-3.5.1.zip
|
||||
# unzip protobuf-python-3.5.1.zip.1
|
||||
# cd protobuf-3.5.1/python/
|
||||
# python3.6 setup.py install
|
||||
|
||||
# pip3.6 install --upgrade pip
|
||||
# pip3.6 install aiohttp
|
||||
|
||||
#!/usr/bin/env python
|
||||
import test_pb2
|
||||
import traceback
|
||||
import sys
|
||||
|
||||
import asyncio
|
||||
import aiohttp
|
||||
|
||||
def op():
|
||||
try:
|
||||
obj = test_pb2.MyObj()
|
||||
obj.number = 9
|
||||
obj.name = 'USB'
|
||||
|
||||
#Serialize
|
||||
sendDataStr = obj.SerializeToString()
|
||||
#print serialized string value
|
||||
print('serialized string:', sendDataStr)
|
||||
#------------------------#
|
||||
# message transmission #
|
||||
#------------------------#
|
||||
receiveDataStr = sendDataStr
|
||||
receiveData = test_pb2.MyObj()
|
||||
|
||||
#Deserialize
|
||||
receiveData.ParseFromString(receiveDataStr)
|
||||
print('pares serialize string, return: devId = ', receiveData.number, ', name = ', receiveData.name)
|
||||
except(Exception, e):
|
||||
print(Exception, ':', e)
|
||||
print(traceback.print_exc())
|
||||
errInfo = sys.exc_info()
|
||||
print(errInfo[0], ':', errInfo[1])
|
||||
|
||||
|
||||
async def fetch(session):
|
||||
obj = test_pb2.MyObj()
|
||||
obj.number = 9
|
||||
obj.name = 'USB'
|
||||
async with session.post('http://localhost:8080/', data=obj.SerializeToString(),
|
||||
headers={"content-type": "application/protobuf"}) as resp:
|
||||
print(resp.status)
|
||||
data = await resp.read()
|
||||
receiveObj = test_pb2.MyObj()
|
||||
receiveObj.ParseFromString(data)
|
||||
print(receiveObj)
|
||||
|
||||
async def go(loop):
|
||||
obj = test_pb2.MyObj()
|
||||
obj.number = 9
|
||||
obj.name = 'USB'
|
||||
async with aiohttp.ClientSession(loop=loop) as session:
|
||||
await fetch(session)
|
||||
|
||||
loop = asyncio.get_event_loop()
|
||||
loop.run_until_complete(go(loop))
|
||||
loop.close()
|
55
examples/protobuf/src/main.rs
Normal file
55
examples/protobuf/src/main.rs
Normal file
@ -0,0 +1,55 @@
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate bytes;
|
||||
extern crate futures;
|
||||
#[macro_use]
|
||||
extern crate failure;
|
||||
extern crate env_logger;
|
||||
extern crate prost;
|
||||
#[macro_use]
|
||||
extern crate prost_derive;
|
||||
|
||||
use actix_web::*;
|
||||
use futures::Future;
|
||||
|
||||
mod protobuf;
|
||||
use protobuf::ProtoBufResponseBuilder;
|
||||
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Message)]
|
||||
pub struct MyObj {
|
||||
#[prost(int32, tag="1")]
|
||||
pub number: i32,
|
||||
#[prost(string, tag="2")]
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
|
||||
/// This handler uses `ProtoBufMessage` for loading protobuf object.
|
||||
fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
protobuf::ProtoBufMessage::new(req)
|
||||
.from_err() // convert all errors into `Error`
|
||||
.and_then(|val: MyObj| {
|
||||
println!("model: {:?}", val);
|
||||
Ok(httpcodes::HTTPOk.build().protobuf(val)?) // <- send response
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("protobuf-example");
|
||||
|
||||
let addr = HttpServer::new(|| {
|
||||
Application::new()
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/", |r| r.method(Method::POST).f(index))})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.shutdown_timeout(1)
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8080");
|
||||
let _ = sys.run();
|
||||
}
|
169
examples/protobuf/src/protobuf.rs
Normal file
169
examples/protobuf/src/protobuf.rs
Normal file
@ -0,0 +1,169 @@
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{Poll, Future, Stream};
|
||||
|
||||
use bytes::IntoBuf;
|
||||
use prost::Message;
|
||||
use prost::DecodeError as ProtoBufDecodeError;
|
||||
use prost::EncodeError as ProtoBufEncodeError;
|
||||
|
||||
use actix_web::header::http::{CONTENT_TYPE, CONTENT_LENGTH};
|
||||
use actix_web::{Responder, HttpMessage, HttpRequest, HttpResponse};
|
||||
use actix_web::dev::HttpResponseBuilder;
|
||||
use actix_web::error::{Error, PayloadError, ResponseError};
|
||||
use actix_web::httpcodes::{HttpBadRequest, HttpPayloadTooLarge};
|
||||
|
||||
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum ProtoBufPayloadError {
|
||||
/// Payload size is bigger than 256k
|
||||
#[fail(display="Payload size is bigger than 256k")]
|
||||
Overflow,
|
||||
/// Content type error
|
||||
#[fail(display="Content type error")]
|
||||
ContentType,
|
||||
/// Serialize error
|
||||
#[fail(display="ProtoBud serialize error: {}", _0)]
|
||||
Serialize(#[cause] ProtoBufEncodeError),
|
||||
/// Deserialize error
|
||||
#[fail(display="ProtoBud deserialize error: {}", _0)]
|
||||
Deserialize(#[cause] ProtoBufDecodeError),
|
||||
/// Payload error
|
||||
#[fail(display="Error that occur during reading payload: {}", _0)]
|
||||
Payload(#[cause] PayloadError),
|
||||
}
|
||||
|
||||
impl ResponseError for ProtoBufPayloadError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
ProtoBufPayloadError::Overflow => HttpPayloadTooLarge.into(),
|
||||
_ => HttpBadRequest.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PayloadError> for ProtoBufPayloadError {
|
||||
fn from(err: PayloadError) -> ProtoBufPayloadError {
|
||||
ProtoBufPayloadError::Payload(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ProtoBufDecodeError> for ProtoBufPayloadError {
|
||||
fn from(err: ProtoBufDecodeError) -> ProtoBufPayloadError {
|
||||
ProtoBufPayloadError::Deserialize(err)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProtoBuf<T: Message>(pub T);
|
||||
|
||||
impl<T: Message> Responder for ProtoBuf<T> {
|
||||
type Item = HttpResponse;
|
||||
type Error = Error;
|
||||
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
let mut buf = Vec::new();
|
||||
self.0.encode(&mut buf)
|
||||
.map_err(|e| Error::from(ProtoBufPayloadError::Serialize(e)))
|
||||
.and_then(|()| {
|
||||
Ok(HttpResponse::Ok()
|
||||
.content_type("application/protobuf")
|
||||
.body(buf)
|
||||
.into())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProtoBufMessage<T, U: Message + Default>{
|
||||
limit: usize,
|
||||
ct: &'static str,
|
||||
req: Option<T>,
|
||||
fut: Option<Box<Future<Item=U, Error=ProtoBufPayloadError>>>,
|
||||
}
|
||||
|
||||
impl<T, U: Message + Default> ProtoBufMessage<T, U> {
|
||||
|
||||
/// Create `ProtoBufMessage` for request.
|
||||
pub fn new(req: T) -> Self {
|
||||
ProtoBufMessage{
|
||||
limit: 262_144,
|
||||
req: Some(req),
|
||||
fut: None,
|
||||
ct: "application/protobuf",
|
||||
}
|
||||
}
|
||||
|
||||
/// Change max size of payload. By default max size is 256Kb
|
||||
pub fn limit(mut self, limit: usize) -> Self {
|
||||
self.limit = limit;
|
||||
self
|
||||
}
|
||||
|
||||
/// Set allowed content type.
|
||||
///
|
||||
/// By default *application/protobuf* content type is used. Set content type
|
||||
/// to empty string if you want to disable content type check.
|
||||
pub fn content_type(mut self, ct: &'static str) -> Self {
|
||||
self.ct = ct;
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<T, U: Message + Default + 'static> Future for ProtoBufMessage<T, U>
|
||||
where T: HttpMessage + Stream<Item=Bytes, Error=PayloadError> + 'static
|
||||
{
|
||||
type Item = U;
|
||||
type Error = ProtoBufPayloadError;
|
||||
|
||||
fn poll(&mut self) -> Poll<U, ProtoBufPayloadError> {
|
||||
if let Some(req) = self.req.take() {
|
||||
if let Some(len) = req.headers().get(CONTENT_LENGTH) {
|
||||
if let Ok(s) = len.to_str() {
|
||||
if let Ok(len) = s.parse::<usize>() {
|
||||
if len > self.limit {
|
||||
return Err(ProtoBufPayloadError::Overflow);
|
||||
}
|
||||
} else {
|
||||
return Err(ProtoBufPayloadError::Overflow);
|
||||
}
|
||||
}
|
||||
}
|
||||
// check content-type
|
||||
if !self.ct.is_empty() && req.content_type() != self.ct {
|
||||
return Err(ProtoBufPayloadError::ContentType)
|
||||
}
|
||||
|
||||
let limit = self.limit;
|
||||
let fut = req.from_err()
|
||||
.fold(BytesMut::new(), move |mut body, chunk| {
|
||||
if (body.len() + chunk.len()) > limit {
|
||||
Err(ProtoBufPayloadError::Overflow)
|
||||
} else {
|
||||
body.extend_from_slice(&chunk);
|
||||
Ok(body)
|
||||
}
|
||||
})
|
||||
.and_then(|body| Ok(<U>::decode(&mut body.into_buf())?));
|
||||
self.fut = Some(Box::new(fut));
|
||||
}
|
||||
|
||||
self.fut.as_mut().expect("ProtoBufBody could not be used second time").poll()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub trait ProtoBufResponseBuilder {
|
||||
|
||||
fn protobuf<T: Message>(&mut self, value: T) -> Result<HttpResponse, Error>;
|
||||
}
|
||||
|
||||
impl ProtoBufResponseBuilder for HttpResponseBuilder {
|
||||
|
||||
fn protobuf<T: Message>(&mut self, value: T) -> Result<HttpResponse, Error> {
|
||||
self.header(CONTENT_TYPE, "application/protobuf");
|
||||
|
||||
let mut body = Vec::new();
|
||||
value.encode(&mut body).map_err(|e| ProtoBufPayloadError::Serialize(e))?;
|
||||
Ok(self.body(body)?)
|
||||
}
|
||||
}
|
6
examples/protobuf/test.proto
Normal file
6
examples/protobuf/test.proto
Normal file
@ -0,0 +1,6 @@
|
||||
syntax = "proto3";
|
||||
|
||||
message MyObj {
|
||||
int32 number = 1;
|
||||
string name = 2;
|
||||
}
|
76
examples/protobuf/test_pb2.py
Normal file
76
examples/protobuf/test_pb2.py
Normal file
@ -0,0 +1,76 @@
|
||||
# Generated by the protocol buffer compiler. DO NOT EDIT!
|
||||
# source: test.proto
|
||||
|
||||
import sys
|
||||
_b=sys.version_info[0]<3 and (lambda x:x) or (lambda x:x.encode('latin1'))
|
||||
from google.protobuf import descriptor as _descriptor
|
||||
from google.protobuf import message as _message
|
||||
from google.protobuf import reflection as _reflection
|
||||
from google.protobuf import symbol_database as _symbol_database
|
||||
from google.protobuf import descriptor_pb2
|
||||
# @@protoc_insertion_point(imports)
|
||||
|
||||
_sym_db = _symbol_database.Default()
|
||||
|
||||
|
||||
|
||||
|
||||
DESCRIPTOR = _descriptor.FileDescriptor(
|
||||
name='test.proto',
|
||||
package='',
|
||||
syntax='proto3',
|
||||
serialized_pb=_b('\n\ntest.proto\"%\n\x05MyObj\x12\x0e\n\x06number\x18\x01 \x01(\x05\x12\x0c\n\x04name\x18\x02 \x01(\tb\x06proto3')
|
||||
)
|
||||
_sym_db.RegisterFileDescriptor(DESCRIPTOR)
|
||||
|
||||
|
||||
|
||||
|
||||
_MYOBJ = _descriptor.Descriptor(
|
||||
name='MyObj',
|
||||
full_name='MyObj',
|
||||
filename=None,
|
||||
file=DESCRIPTOR,
|
||||
containing_type=None,
|
||||
fields=[
|
||||
_descriptor.FieldDescriptor(
|
||||
name='number', full_name='MyObj.number', index=0,
|
||||
number=1, type=5, cpp_type=1, label=1,
|
||||
has_default_value=False, default_value=0,
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
_descriptor.FieldDescriptor(
|
||||
name='name', full_name='MyObj.name', index=1,
|
||||
number=2, type=9, cpp_type=9, label=1,
|
||||
has_default_value=False, default_value=_b("").decode('utf-8'),
|
||||
message_type=None, enum_type=None, containing_type=None,
|
||||
is_extension=False, extension_scope=None,
|
||||
options=None),
|
||||
],
|
||||
extensions=[
|
||||
],
|
||||
nested_types=[],
|
||||
enum_types=[
|
||||
],
|
||||
options=None,
|
||||
is_extendable=False,
|
||||
syntax='proto3',
|
||||
extension_ranges=[],
|
||||
oneofs=[
|
||||
],
|
||||
serialized_start=14,
|
||||
serialized_end=51,
|
||||
)
|
||||
|
||||
DESCRIPTOR.message_types_by_name['MyObj'] = _MYOBJ
|
||||
|
||||
MyObj = _reflection.GeneratedProtocolMessageType('MyObj', (_message.Message,), dict(
|
||||
DESCRIPTOR = _MYOBJ,
|
||||
__module__ = 'test_pb2'
|
||||
# @@protoc_insertion_point(class_scope:MyObj)
|
||||
))
|
||||
_sym_db.RegisterMessage(MyObj)
|
||||
|
||||
|
||||
# @@protoc_insertion_point(module_scope)
|
20
examples/r2d2/Cargo.toml
Normal file
20
examples/r2d2/Cargo.toml
Normal file
@ -0,0 +1,20 @@
|
||||
[package]
|
||||
name = "r2d2-example"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
||||
futures = "0.1"
|
||||
uuid = { version = "0.5", features = ["serde", "v4"] }
|
||||
serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
r2d2 = "*"
|
||||
r2d2_sqlite = "*"
|
||||
rusqlite = "*"
|
41
examples/r2d2/src/db.rs
Normal file
41
examples/r2d2/src/db.rs
Normal file
@ -0,0 +1,41 @@
|
||||
//! Db executor actor
|
||||
use std::io;
|
||||
use uuid;
|
||||
use actix_web::*;
|
||||
use actix::prelude::*;
|
||||
use r2d2::Pool;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
|
||||
/// This is db executor actor. We are going to run 3 of them in parallel.
|
||||
pub struct DbExecutor(pub Pool<SqliteConnectionManager>);
|
||||
|
||||
/// This is only message that this actor can handle, but it is easy to extend number of
|
||||
/// messages.
|
||||
pub struct CreateUser {
|
||||
pub name: String,
|
||||
}
|
||||
|
||||
impl Message for CreateUser {
|
||||
type Result = Result<String, io::Error>;
|
||||
}
|
||||
|
||||
impl Actor for DbExecutor {
|
||||
type Context = SyncContext<Self>;
|
||||
}
|
||||
|
||||
impl Handler<CreateUser> for DbExecutor {
|
||||
type Result = Result<String, io::Error>;
|
||||
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Self::Result {
|
||||
let conn = self.0.get().unwrap();
|
||||
|
||||
let uuid = format!("{}", uuid::Uuid::new_v4());
|
||||
conn.execute("INSERT INTO users (id, name) VALUES ($1, $2)",
|
||||
&[&uuid, &msg.name]).unwrap();
|
||||
|
||||
Ok(conn.query_row("SELECT name FROM users WHERE id=$1", &[&uuid], |row| {
|
||||
row.get(0)
|
||||
}).map_err(|_| io::Error::new(io::ErrorKind::Other, "db error"))?)
|
||||
}
|
||||
}
|
64
examples/r2d2/src/main.rs
Normal file
64
examples/r2d2/src/main.rs
Normal file
@ -0,0 +1,64 @@
|
||||
//! Actix web r2d2 example
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate uuid;
|
||||
extern crate futures;
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate r2d2;
|
||||
extern crate r2d2_sqlite;
|
||||
extern crate rusqlite;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
use futures::future::Future;
|
||||
use r2d2_sqlite::SqliteConnectionManager;
|
||||
|
||||
mod db;
|
||||
use db::{CreateUser, DbExecutor};
|
||||
|
||||
|
||||
/// State with DbExecutor address
|
||||
struct State {
|
||||
db: Addr<Syn, DbExecutor>,
|
||||
}
|
||||
|
||||
/// Async request handler
|
||||
fn index(req: HttpRequest<State>) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
let name = &req.match_info()["name"];
|
||||
|
||||
req.state().db.send(CreateUser{name: name.to_owned()})
|
||||
.from_err()
|
||||
.and_then(|res| {
|
||||
match res {
|
||||
Ok(user) => Ok(httpcodes::HTTPOk.build().json(user)?),
|
||||
Err(_) => Ok(httpcodes::HTTPInternalServerError.into())
|
||||
}
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=debug");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("r2d2-example");
|
||||
|
||||
// r2d2 pool
|
||||
let manager = SqliteConnectionManager::file("test.db");
|
||||
let pool = r2d2::Pool::new(manager).unwrap();
|
||||
|
||||
// Start db executor actors
|
||||
let addr = SyncArbiter::start(3, move || DbExecutor(pool.clone()));
|
||||
|
||||
// Start http server
|
||||
let _addr = HttpServer::new(move || {
|
||||
Application::with_state(State{db: addr.clone()})
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/{name}", |r| r.method(Method::GET).a(index))})
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
||||
let _ = sys.run();
|
||||
}
|
BIN
examples/r2d2/test.db
Normal file
BIN
examples/r2d2/test.db
Normal file
Binary file not shown.
11
examples/redis-session/Cargo.toml
Normal file
11
examples/redis-session/Cargo.toml
Normal file
@ -0,0 +1,11 @@
|
||||
[package]
|
||||
name = "redis-session"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = "0.4"
|
||||
actix-redis = { version = "0.2", features = ["web"] }
|
48
examples/redis-session/src/main.rs
Normal file
48
examples/redis-session/src/main.rs
Normal file
@ -0,0 +1,48 @@
|
||||
#![allow(unused_variables)]
|
||||
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate actix_redis;
|
||||
extern crate env_logger;
|
||||
|
||||
use actix_web::*;
|
||||
use actix_web::middleware::RequestSession;
|
||||
use actix_redis::RedisSessionBackend;
|
||||
|
||||
|
||||
/// simple handler
|
||||
fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
println!("{:?}", req);
|
||||
|
||||
// session
|
||||
if let Some(count) = req.session().get::<i32>("counter")? {
|
||||
println!("SESSION value: {}", count);
|
||||
req.session().set("counter", count+1)?;
|
||||
} else {
|
||||
req.session().set("counter", 1)?;
|
||||
}
|
||||
|
||||
Ok("Welcome!".into())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info,actix_redis=info");
|
||||
env_logger::init();
|
||||
let sys = actix::System::new("basic-example");
|
||||
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
// cookie session middleware
|
||||
.middleware(middleware::SessionStorage::new(
|
||||
RedisSessionBackend::new("127.0.0.1:6379", &[0; 32])
|
||||
))
|
||||
// register simple route, handle all methods
|
||||
.resource("/", |r| r.f(index)))
|
||||
.bind("0.0.0.0:8080").unwrap()
|
||||
.threads(1)
|
||||
.start();
|
||||
|
||||
let _ = sys.run();
|
||||
}
|
@ -6,6 +6,6 @@ workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
futures = "*"
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
@ -1,5 +1,5 @@
|
||||
#![cfg_attr(feature="cargo-clippy", allow(needless_pass_by_value))]
|
||||
//! There are two level of statfulness in actix-web. Application has state
|
||||
//! There are two level of statefulness in actix-web. Application has state
|
||||
//! that is shared across all handlers within same Application.
|
||||
//! And individual handler can have state.
|
||||
|
||||
@ -33,20 +33,19 @@ struct MyWebSocket {
|
||||
}
|
||||
|
||||
impl Actor for MyWebSocket {
|
||||
type Context = HttpContext<Self, AppState>;
|
||||
type Context = ws::WebsocketContext<Self, AppState>;
|
||||
}
|
||||
|
||||
impl Handler<ws::Message> for MyWebSocket {
|
||||
type Result = ();
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for MyWebSocket {
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
self.counter += 1;
|
||||
println!("WS({}): {:?}", self.counter, msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => ws::WsWriter::pong(ctx, &msg),
|
||||
ws::Message::Text(text) => ws::WsWriter::text(ctx, &text),
|
||||
ws::Message::Binary(bin) => ws::WsWriter::binary(ctx, bin),
|
||||
ws::Message::Closed | ws::Message::Error => {
|
||||
ws::Message::Ping(msg) => ctx.pong(&msg),
|
||||
ws::Message::Text(text) => ctx.text(text),
|
||||
ws::Message::Binary(bin) => ctx.binary(bin),
|
||||
ws::Message::Close(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
_ => (),
|
||||
|
@ -5,7 +5,7 @@ authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "0.4"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
tera = "*"
|
||||
|
@ -9,6 +9,7 @@ name = "server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.4"
|
||||
actix = "^0.4.2"
|
||||
actix-web = { git = "https://github.com/actix/actix-web", features=["alpn"] }
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../", features=["alpn"] }
|
||||
openssl = { version="0.10" }
|
||||
|
31
examples/tls/cert.pem
Normal file
31
examples/tls/cert.pem
Normal file
@ -0,0 +1,31 @@
|
||||
-----BEGIN CERTIFICATE-----
|
||||
MIIFPjCCAyYCCQDvLYiYD+jqeTANBgkqhkiG9w0BAQsFADBhMQswCQYDVQQGEwJV
|
||||
UzELMAkGA1UECAwCQ0ExCzAJBgNVBAcMAlNGMRAwDgYDVQQKDAdDb21wYW55MQww
|
||||
CgYDVQQLDANPcmcxGDAWBgNVBAMMD3d3dy5leGFtcGxlLmNvbTAeFw0xODAxMjUx
|
||||
NzQ2MDFaFw0xOTAxMjUxNzQ2MDFaMGExCzAJBgNVBAYTAlVTMQswCQYDVQQIDAJD
|
||||
QTELMAkGA1UEBwwCU0YxEDAOBgNVBAoMB0NvbXBhbnkxDDAKBgNVBAsMA09yZzEY
|
||||
MBYGA1UEAwwPd3d3LmV4YW1wbGUuY29tMIICIjANBgkqhkiG9w0BAQEFAAOCAg8A
|
||||
MIICCgKCAgEA2WzIA2IpVR9Tb9EFhITlxuhE5rY2a3S6qzYNzQVgSFggxXEPn8k1
|
||||
sQEcer5BfAP986Sck3H0FvB4Bt/I8PwOtUCmhwcc8KtB5TcGPR4fjXnrpC+MIK5U
|
||||
NLkwuyBDKziYzTdBj8kUFX1WxmvEHEgqToPOZfBgsS71cJAR/zOWraDLSRM54jXy
|
||||
voLZN4Ti9rQagQrvTQ44Vz5ycDQy7UxtbUGh1CVv69vNVr7/SOOh/Nw5FNOZWLWr
|
||||
odGyoec5wh9iqRZgRqiTUc6Lt7V2RWc2X2gjwST2UfI+U46Ip3oaQ7ZD4eAkoqND
|
||||
xdniBZAykVG3c/99ux4BAESTF8fsNch6UticBxYMuTu+ouvP0psfI9wwwNliJDmA
|
||||
CRUTB9AgRynbL1AzhqQoDfsb98IZfjfNOpwnwuLwpMAPhbgd5KNdZaIJ4Hb6/stI
|
||||
yFElOExxd3TAxF2Gshd/lq1JcNHAZ1DSXV5MvOWT/NWgXwbIzUgQ8eIi+HuDYX2U
|
||||
UuaB6R8tbd52H7rbUv6HrfinuSlKWqjSYLkiKHkwUpoMw8y9UycRSzs1E9nPwPTO
|
||||
vRXb0mNCQeBCV9FvStNVXdCUTT8LGPv87xSD2pmt7LijlE6mHLG8McfcWkzA69un
|
||||
CEHIFAFDimTuN7EBljc119xWFTcHMyoZAfFF+oTqwSbBGImruCxnaJECAwEAATAN
|
||||
BgkqhkiG9w0BAQsFAAOCAgEApavsgsn7SpPHfhDSN5iZs1ILZQRewJg0Bty0xPfk
|
||||
3tynSW6bNH3nSaKbpsdmxxomthNSQgD2heOq1By9YzeOoNR+7Pk3s4FkASnf3ToI
|
||||
JNTUasBFFfaCG96s4Yvs8KiWS/k84yaWuU8c3Wb1jXs5Rv1qE1Uvuwat1DSGXSoD
|
||||
JNluuIkCsC4kWkyq5pWCGQrabWPRTWsHwC3PTcwSRBaFgYLJaR72SloHB1ot02zL
|
||||
d2age9dmFRFLLCBzP+D7RojBvL37qS/HR+rQ4SoQwiVc/JzaeqSe7ZbvEH9sZYEu
|
||||
ALowJzgbwro7oZflwTWunSeSGDSltkqKjvWvZI61pwfHKDahUTmZ5h2y67FuGEaC
|
||||
CIOUI8dSVSPKITxaq3JL4ze2e9/0Lt7hj19YK2uUmtMAW5Tirz4Yx5lyGH9U8Wur
|
||||
y/X8VPxTc4A9TMlJgkyz0hqvhbPOT/zSWB10zXh0glKAsSBryAOEDxV1UygmSir7
|
||||
YV8Qaq+oyKUTMc1MFq5vZ07M51EPaietn85t8V2Y+k/8XYltRp32NxsypxAJuyxh
|
||||
g/ko6RVTrWa1sMvz/F9LFqAdKiK5eM96lh9IU4xiLg4ob8aS/GRAA8oIFkZFhLrt
|
||||
tOwjIUPmEPyHWFi8dLpNuQKYalLYhuwZftG/9xV+wqhKGZO9iPrpHSYBRTap8w2y
|
||||
1QU=
|
||||
-----END CERTIFICATE-----
|
Binary file not shown.
51
examples/tls/key.pem
Normal file
51
examples/tls/key.pem
Normal file
@ -0,0 +1,51 @@
|
||||
-----BEGIN RSA PRIVATE KEY-----
|
||||
MIIJKAIBAAKCAgEA2WzIA2IpVR9Tb9EFhITlxuhE5rY2a3S6qzYNzQVgSFggxXEP
|
||||
n8k1sQEcer5BfAP986Sck3H0FvB4Bt/I8PwOtUCmhwcc8KtB5TcGPR4fjXnrpC+M
|
||||
IK5UNLkwuyBDKziYzTdBj8kUFX1WxmvEHEgqToPOZfBgsS71cJAR/zOWraDLSRM5
|
||||
4jXyvoLZN4Ti9rQagQrvTQ44Vz5ycDQy7UxtbUGh1CVv69vNVr7/SOOh/Nw5FNOZ
|
||||
WLWrodGyoec5wh9iqRZgRqiTUc6Lt7V2RWc2X2gjwST2UfI+U46Ip3oaQ7ZD4eAk
|
||||
oqNDxdniBZAykVG3c/99ux4BAESTF8fsNch6UticBxYMuTu+ouvP0psfI9wwwNli
|
||||
JDmACRUTB9AgRynbL1AzhqQoDfsb98IZfjfNOpwnwuLwpMAPhbgd5KNdZaIJ4Hb6
|
||||
/stIyFElOExxd3TAxF2Gshd/lq1JcNHAZ1DSXV5MvOWT/NWgXwbIzUgQ8eIi+HuD
|
||||
YX2UUuaB6R8tbd52H7rbUv6HrfinuSlKWqjSYLkiKHkwUpoMw8y9UycRSzs1E9nP
|
||||
wPTOvRXb0mNCQeBCV9FvStNVXdCUTT8LGPv87xSD2pmt7LijlE6mHLG8McfcWkzA
|
||||
69unCEHIFAFDimTuN7EBljc119xWFTcHMyoZAfFF+oTqwSbBGImruCxnaJECAwEA
|
||||
AQKCAgAME3aoeXNCPxMrSri7u4Xnnk71YXl0Tm9vwvjRQlMusXZggP8VKN/KjP0/
|
||||
9AE/GhmoxqPLrLCZ9ZE1EIjgmZ9Xgde9+C8rTtfCG2RFUL7/5J2p6NonlocmxoJm
|
||||
YkxYwjP6ce86RTjQWL3RF3s09u0inz9/efJk5O7M6bOWMQ9VZXDlBiRY5BYvbqUR
|
||||
6FeSzD4MnMbdyMRoVBeXE88gTvZk8xhB6DJnLzYgc0tKiRoeKT0iYv5JZw25VyRM
|
||||
ycLzfTrFmXCPfB1ylb483d9Ly4fBlM8nkx37PzEnAuukIawDxsPOb9yZC+hfvNJI
|
||||
7NFiMN+3maEqG2iC00w4Lep4skHY7eHUEUMl+Wjr+koAy2YGLWAwHZQTm7iXn9Ab
|
||||
L6adL53zyCKelRuEQOzbeosJAqS+5fpMK0ekXyoFIuskj7bWuIoCX7K/kg6q5IW+
|
||||
vC2FrlsrbQ79GztWLVmHFO1I4J9M5r666YS0qdh8c+2yyRl4FmSiHfGxb3eOKpxQ
|
||||
b6uI97iZlkxPF9LYUCSc7wq0V2gGz+6LnGvTHlHrOfVXqw/5pLAKhXqxvnroDTwz
|
||||
0Ay/xFF6ei/NSxBY5t8ztGCBm45wCU3l8pW0X6dXqwUipw5b4MRy1VFRu6rqlmbL
|
||||
OPSCuLxqyqsigiEYsBgS/icvXz9DWmCQMPd2XM9YhsHvUq+R4QKCAQEA98EuMMXI
|
||||
6UKIt1kK2t/3OeJRyDd4iv/fCMUAnuPjLBvFE4cXD/SbqCxcQYqb+pue3PYkiTIC
|
||||
71rN8OQAc5yKhzmmnCE5N26br/0pG4pwEjIr6mt8kZHmemOCNEzvhhT83nfKmV0g
|
||||
9lNtuGEQMiwmZrpUOF51JOMC39bzcVjYX2Cmvb7cFbIq3lR0zwM+aZpQ4P8LHCIu
|
||||
bgHmwbdlkLyIULJcQmHIbo6nPFB3ZZE4mqmjwY+rA6Fh9rgBa8OFCfTtrgeYXrNb
|
||||
IgZQ5U8GoYRPNC2ot0vpTinraboa/cgm6oG4M7FW1POCJTl+/ktHEnKuO5oroSga
|
||||
/BSg7hCNFVaOhwKCAQEA4Kkys0HtwEbV5mY/NnvUD5KwfXX7BxoXc9lZ6seVoLEc
|
||||
KjgPYxqYRVrC7dB2YDwwp3qcRTi/uBAgFNm3iYlDzI4xS5SeaudUWjglj7BSgXE2
|
||||
iOEa7EwcvVPluLaTgiWjlzUKeUCNNHWSeQOt+paBOT+IgwRVemGVpAgkqQzNh/nP
|
||||
tl3p9aNtgzEm1qVlPclY/XUCtf3bcOR+z1f1b4jBdn0leu5OhnxkC+Htik+2fTXD
|
||||
jt6JGrMkanN25YzsjnD3Sn+v6SO26H99wnYx5oMSdmb8SlWRrKtfJHnihphjG/YY
|
||||
l1cyorV6M/asSgXNQfGJm4OuJi0I4/FL2wLUHnU+JwKCAQEAzh4WipcRthYXXcoj
|
||||
gMKRkMOb3GFh1OpYqJgVExtudNTJmZxq8GhFU51MR27Eo7LycMwKy2UjEfTOnplh
|
||||
Us2qZiPtW7k8O8S2m6yXlYUQBeNdq9IuuYDTaYD94vsazscJNSAeGodjE+uGvb1q
|
||||
1wLqE87yoE7dUInYa1cOA3+xy2/CaNuviBFJHtzOrSb6tqqenQEyQf6h9/12+DTW
|
||||
t5pSIiixHrzxHiFqOoCLRKGToQB+71rSINwTf0nITNpGBWmSj5VcC3VV3TG5/XxI
|
||||
fPlxV2yhD5WFDPVNGBGvwPDSh4jSMZdZMSNBZCy4XWFNSKjGEWoK4DFYed3DoSt9
|
||||
5IG1YwKCAQA63ntHl64KJUWlkwNbboU583FF3uWBjee5VqoGKHhf3CkKMxhtGqnt
|
||||
+oN7t5VdUEhbinhqdx1dyPPvIsHCS3K1pkjqii4cyzNCVNYa2dQ00Qq+QWZBpwwc
|
||||
3GAkz8rFXsGIPMDa1vxpU6mnBjzPniKMcsZ9tmQDppCEpBGfLpio2eAA5IkK8eEf
|
||||
cIDB3CM0Vo94EvI76CJZabaE9IJ+0HIJb2+jz9BJ00yQBIqvJIYoNy9gP5Xjpi+T
|
||||
qV/tdMkD5jwWjHD3AYHLWKUGkNwwkAYFeqT/gX6jpWBP+ZRPOp011X3KInJFSpKU
|
||||
DT5GQ1Dux7EMTCwVGtXqjO8Ym5wjwwsfAoIBAEcxlhIW1G6BiNfnWbNPWBdh3v/K
|
||||
5Ln98Rcrz8UIbWyl7qNPjYb13C1KmifVG1Rym9vWMO3KuG5atK3Mz2yLVRtmWAVc
|
||||
fxzR57zz9MZFDun66xo+Z1wN3fVxQB4CYpOEI4Lb9ioX4v85hm3D6RpFukNtRQEc
|
||||
Gfr4scTjJX4jFWDp0h6ffMb8mY+quvZoJ0TJqV9L9Yj6Ksdvqez/bdSraev97bHQ
|
||||
4gbQxaTZ6WjaD4HjpPQefMdWp97Metg0ZQSS8b8EzmNFgyJ3XcjirzwliKTAQtn6
|
||||
I2sd0NCIooelrKRD8EJoDUwxoOctY7R97wpZ7/wEHU45cBCbRV3H4JILS5c=
|
||||
-----END RSA PRIVATE KEY-----
|
@ -2,14 +2,13 @@
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
|
||||
use std::fs::File;
|
||||
use std::io::Read;
|
||||
extern crate openssl;
|
||||
|
||||
use actix_web::*;
|
||||
use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype};
|
||||
|
||||
|
||||
/// somple handle
|
||||
/// simple handle
|
||||
fn index(req: HttpRequest) -> Result<HttpResponse> {
|
||||
println!("{:?}", req);
|
||||
Ok(httpcodes::HTTPOk
|
||||
@ -20,15 +19,15 @@ fn index(req: HttpRequest) -> Result<HttpResponse> {
|
||||
|
||||
fn main() {
|
||||
if ::std::env::var("RUST_LOG").is_err() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=trace");
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
}
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
let mut file = File::open("identity.pfx").unwrap();
|
||||
let mut pkcs12 = vec![];
|
||||
file.read_to_end(&mut pkcs12).unwrap();
|
||||
let pkcs12 = Pkcs12::from_der(&pkcs12).unwrap().parse("12345").unwrap();
|
||||
// load ssl keys
|
||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||
builder.set_private_key_file("key.pem", SslFiletype::PEM).unwrap();
|
||||
builder.set_certificate_chain_file("cert.pem").unwrap();
|
||||
|
||||
let addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
@ -44,7 +43,7 @@ fn main() {
|
||||
.body(Body::Empty)
|
||||
})))
|
||||
.bind("127.0.0.1:8443").unwrap()
|
||||
.start_ssl(&pkcs12).unwrap();
|
||||
.start_ssl(builder).unwrap();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8443");
|
||||
let _ = sys.run();
|
||||
|
10
examples/unix-socket/Cargo.toml
Normal file
10
examples/unix-socket/Cargo.toml
Normal file
@ -0,0 +1,10 @@
|
||||
[package]
|
||||
name = "unix-socket"
|
||||
version = "0.1.0"
|
||||
authors = ["Messense Lv <messense@icloud.com>"]
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
tokio-uds = "0.1"
|
14
examples/unix-socket/README.md
Normal file
14
examples/unix-socket/README.md
Normal file
@ -0,0 +1,14 @@
|
||||
## Unix domain socket example
|
||||
|
||||
```bash
|
||||
$ curl --unix-socket /tmp/actix-uds.socket http://localhost/
|
||||
Hello world!
|
||||
```
|
||||
|
||||
Although this will only one thread for handling incoming connections
|
||||
according to the
|
||||
[documentation](https://actix.github.io/actix-web/actix_web/struct.HttpServer.html#method.start_incoming).
|
||||
|
||||
And it does not delete the socket file (`/tmp/actix-uds.socket`) when stopping
|
||||
the server so it will fail to start next time you run it unless you delete
|
||||
the socket file manually.
|
31
examples/unix-socket/src/main.rs
Normal file
31
examples/unix-socket/src/main.rs
Normal file
@ -0,0 +1,31 @@
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate tokio_uds;
|
||||
|
||||
use actix::*;
|
||||
use actix_web::*;
|
||||
use tokio_uds::UnixListener;
|
||||
|
||||
|
||||
fn index(_req: HttpRequest) -> &'static str {
|
||||
"Hello world!"
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("unix-socket");
|
||||
|
||||
let listener = UnixListener::bind("/tmp/actix-uds.socket", Arbiter::handle()).expect("bind failed");
|
||||
let _addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
// enable logger
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/index.html", |r| r.f(|_| "Hello world!"))
|
||||
.resource("/", |r| r.f(index)))
|
||||
.start_incoming(listener.incoming(), false);
|
||||
|
||||
println!("Started http server: /tmp/actix-uds.socket");
|
||||
let _ = sys.run();
|
||||
}
|
15
examples/web-cors/README.md
Normal file
15
examples/web-cors/README.md
Normal file
@ -0,0 +1,15 @@
|
||||
# Actix Web CORS example
|
||||
|
||||
## start
|
||||
1 - backend server
|
||||
```bash
|
||||
$ cd web-cors/backend
|
||||
$ cargo run
|
||||
```
|
||||
2 - frontend server
|
||||
```bash
|
||||
$ cd web-cors/frontend
|
||||
$ npm install
|
||||
$ npm run dev
|
||||
```
|
||||
then open browser 'http://localhost:1234/'
|
4
examples/web-cors/backend/.gitignore
vendored
Normal file
4
examples/web-cors/backend/.gitignore
vendored
Normal file
@ -0,0 +1,4 @@
|
||||
|
||||
/target/
|
||||
**/*.rs.bk
|
||||
Cargo.lock
|
17
examples/web-cors/backend/Cargo.toml
Normal file
17
examples/web-cors/backend/Cargo.toml
Normal file
@ -0,0 +1,17 @@
|
||||
[package]
|
||||
name = "actix-web-cors"
|
||||
version = "0.1.0"
|
||||
authors = ["krircc <krircc@aliyun.com>"]
|
||||
workspace = "../../../"
|
||||
|
||||
[dependencies]
|
||||
serde = "1.0"
|
||||
serde_derive = "1.0"
|
||||
serde_json = "1.0"
|
||||
http = "0.1"
|
||||
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../../" }
|
||||
dotenv = "0.10"
|
||||
env_logger = "0.5"
|
||||
futures = "0.1"
|
45
examples/web-cors/backend/src/main.rs
Normal file
45
examples/web-cors/backend/src/main.rs
Normal file
@ -0,0 +1,45 @@
|
||||
#[macro_use] extern crate serde_derive;
|
||||
extern crate serde;
|
||||
extern crate serde_json;
|
||||
extern crate futures;
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate http;
|
||||
|
||||
use std::env;
|
||||
use http::header;
|
||||
use actix_web::*;
|
||||
use actix_web::middleware::cors;
|
||||
|
||||
mod user;
|
||||
use user::info;
|
||||
|
||||
|
||||
fn main() {
|
||||
env::set_var("RUST_LOG", "actix_web=info");
|
||||
env_logger::init();
|
||||
|
||||
let sys = actix::System::new("Actix-web-CORS");
|
||||
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.middleware(middleware::Logger::default())
|
||||
.resource("/user/info", |r| {
|
||||
cors::Cors::build()
|
||||
.allowed_origin("http://localhost:1234")
|
||||
.allowed_methods(vec!["GET", "POST"])
|
||||
.allowed_headers(
|
||||
vec![header::AUTHORIZATION,
|
||||
header::ACCEPT, header::CONTENT_TYPE])
|
||||
.max_age(3600)
|
||||
.finish().expect("Can not create CORS middleware")
|
||||
.register(r);
|
||||
r.method(Method::POST).a(info);
|
||||
}))
|
||||
.bind("127.0.0.1:8000").unwrap()
|
||||
.shutdown_timeout(200)
|
||||
.start();
|
||||
|
||||
let _ = sys.run();
|
||||
}
|
19
examples/web-cors/backend/src/user.rs
Normal file
19
examples/web-cors/backend/src/user.rs
Normal file
@ -0,0 +1,19 @@
|
||||
use actix_web::*;
|
||||
use futures::Future;
|
||||
|
||||
|
||||
#[derive(Deserialize,Serialize, Debug)]
|
||||
struct Info {
|
||||
username: String,
|
||||
email: String,
|
||||
password: String,
|
||||
confirm_password: String,
|
||||
}
|
||||
|
||||
pub fn info(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.json()
|
||||
.from_err()
|
||||
.and_then(|res: Info| {
|
||||
Ok(httpcodes::HTTPOk.build().json(res)?)
|
||||
}).responder()
|
||||
}
|
3
examples/web-cors/frontend/.babelrc
Normal file
3
examples/web-cors/frontend/.babelrc
Normal file
@ -0,0 +1,3 @@
|
||||
{
|
||||
"presets": ["env"]
|
||||
}
|
14
examples/web-cors/frontend/.gitignore
vendored
Normal file
14
examples/web-cors/frontend/.gitignore
vendored
Normal file
@ -0,0 +1,14 @@
|
||||
.DS_Store
|
||||
node_modules/
|
||||
/dist/
|
||||
.cache
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
|
||||
# Editor directories and files
|
||||
.idea
|
||||
*.suo
|
||||
*.ntvs*
|
||||
*.njsproj
|
||||
*.sln
|
13
examples/web-cors/frontend/index.html
Normal file
13
examples/web-cors/frontend/index.html
Normal file
@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1.0">
|
||||
<title>webapp</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
<script src="./src/main.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
22
examples/web-cors/frontend/package.json
Normal file
22
examples/web-cors/frontend/package.json
Normal file
@ -0,0 +1,22 @@
|
||||
{
|
||||
"name": "actix-web-cors",
|
||||
"version": "0.1.0",
|
||||
"description": "webapp",
|
||||
"main": "main.js",
|
||||
"scripts": {
|
||||
"dev": "rm -rf dist/ && NODE_ENV=development parcel index.html",
|
||||
"build": "NODE_ENV=production parcel build index.html",
|
||||
"test": "echo \"Error: no test specified\" && exit 1"
|
||||
},
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"vue": "^2.5.13",
|
||||
"vue-router": "^3.0.1",
|
||||
"axios": "^0.17.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"babel-preset-env": "^1.6.1",
|
||||
"parcel-bundler": "^1.4.1",
|
||||
"parcel-plugin-vue": "^1.5.0"
|
||||
}
|
||||
}
|
145
examples/web-cors/frontend/src/app.vue
Normal file
145
examples/web-cors/frontend/src/app.vue
Normal file
@ -0,0 +1,145 @@
|
||||
<template>
|
||||
<div id="app">
|
||||
<div id="content">
|
||||
<div id="title">
|
||||
<a to="#">SignUp</a>
|
||||
</div>
|
||||
<input type="text" name="username" placeholder="Username" v-model="Username" required />
|
||||
<input type="text" name="email" placeholder="E-mail" v-model="Email" required />
|
||||
<input type="password" name="password" placeholder="Password" v-model="Password" required/>
|
||||
<input type="password" name="confirm_password" placeholder="Confirm password" v-model="ConfirmPassword" required/><br/>
|
||||
|
||||
<button id="submit" @click="signup">Sign up</button>
|
||||
|
||||
<div id="user-info">
|
||||
<p>Click Above 'Sign up' Button <br> Then Get Your Signup Info!</p>
|
||||
<p>email : {{ email }}</p>
|
||||
<p>username :{{ username }}</p>
|
||||
<p>password : {{ password }}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import axios from 'axios'
|
||||
export default {
|
||||
name: 'app',
|
||||
data () {
|
||||
return {
|
||||
Username: '',
|
||||
Email: '',
|
||||
Password: '',
|
||||
ConfirmPassword: '',
|
||||
|
||||
email: '',
|
||||
username: '',
|
||||
password: ''
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
signup () {
|
||||
var username = this.Username
|
||||
var email = this.Email
|
||||
var password = this.Password
|
||||
var confirm_password = this.ConfirmPassword
|
||||
console.log(email)
|
||||
axios.post('http://localhost:8000/user/info', {
|
||||
username: username,
|
||||
email: email,
|
||||
password: password,
|
||||
confirm_password: confirm_password
|
||||
})
|
||||
.then(response => {
|
||||
console.log(response.data)
|
||||
this.email = response.data.email
|
||||
this.username = response.data.username
|
||||
this.password = response.data.password
|
||||
})
|
||||
.catch(e => {
|
||||
console.log(e)
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
#content {
|
||||
width: 250px;
|
||||
margin: 0 auto;
|
||||
padding-top: 33px;
|
||||
}
|
||||
#title {
|
||||
padding: 0.5rem 0;
|
||||
font-size: 22px;
|
||||
font-weight: bold;
|
||||
background-color:bisque;
|
||||
text-align: center;
|
||||
}
|
||||
input[type="text"],
|
||||
input[type="password"] {
|
||||
margin: 6px auto auto;
|
||||
width: 250px;
|
||||
height: 36px;
|
||||
border: none;
|
||||
border-bottom: 1px solid #AAA;
|
||||
font-size: 16px;
|
||||
}
|
||||
#submit {
|
||||
margin: 10px 0 20px 0;
|
||||
width: 250px;
|
||||
height: 33px;
|
||||
background-color:bisque;
|
||||
border: none;
|
||||
border-radius: 2px;
|
||||
font-family: 'Roboto', sans-serif;
|
||||
font-weight: bold;
|
||||
text-transform: uppercase;
|
||||
transition: 0.1s ease;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
margin-top: 11px;
|
||||
}
|
||||
dialog {
|
||||
top: 50%;
|
||||
width: 80%;
|
||||
border: 5px solid rgba(0, 0, 0, 0.3);
|
||||
}
|
||||
dialog::backdrop{
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
}
|
||||
#closeDialog {
|
||||
display: inline-block;
|
||||
border-radius: 3px;
|
||||
border: none;
|
||||
font-size: 1rem;
|
||||
padding: 0.4rem 0.8em;
|
||||
background: #eb9816;
|
||||
border-bottom: 1px solid #f1b75c;
|
||||
color: white;
|
||||
font-weight: bold;
|
||||
text-align: center;
|
||||
}
|
||||
#closeDialog:hover, #closeDialog:focus {
|
||||
opacity: 0.92;
|
||||
cursor: pointer;
|
||||
}
|
||||
#user-info {
|
||||
width: 250px;
|
||||
margin: 0 auto;
|
||||
padding-top: 44px;
|
||||
}
|
||||
@media only screen and (min-width: 600px) {
|
||||
#content {
|
||||
margin: 0 auto;
|
||||
padding-top: 100px;
|
||||
}
|
||||
}
|
||||
</style>
|
11
examples/web-cors/frontend/src/main.js
Normal file
11
examples/web-cors/frontend/src/main.js
Normal file
@ -0,0 +1,11 @@
|
||||
import Vue from 'vue'
|
||||
import App from './app'
|
||||
|
||||
new Vue({
|
||||
el: '#app',
|
||||
render: h => h(App)
|
||||
})
|
||||
|
||||
if (module.hot) {
|
||||
module.hot.accept();
|
||||
}
|
@ -25,5 +25,5 @@ serde = "1.0"
|
||||
serde_json = "1.0"
|
||||
serde_derive = "1.0"
|
||||
|
||||
actix = "^0.4.2"
|
||||
actix-web = { git = "https://github.com/actix/actix-web" }
|
||||
actix = "0.5"
|
||||
actix-web = { path="../../" }
|
||||
|
@ -16,8 +16,8 @@ Chat server listens for incoming tcp connections. Server can access several type
|
||||
* `\list` - list all available rooms
|
||||
* `\join name` - join room, if room does not exist, create new one
|
||||
* `\name name` - set session name
|
||||
* `some message` - just string, send messsage to all peers in same room
|
||||
* client has to send heartbeat `Ping` messages, if server does not receive a heartbeat message for 10 seconds connection gets droppped
|
||||
* `some message` - just string, send message to all peers in same room
|
||||
* client has to send heartbeat `Ping` messages, if server does not receive a heartbeat message for 10 seconds connection gets dropped
|
||||
|
||||
To start server use command: `cargo run --bin server`
|
||||
|
||||
|
@ -12,6 +12,9 @@ use std::{io, net, process, thread};
|
||||
use std::str::FromStr;
|
||||
use std::time::Duration;
|
||||
use futures::Future;
|
||||
use tokio_io::AsyncRead;
|
||||
use tokio_io::io::WriteHalf;
|
||||
use tokio_io::codec::FramedRead;
|
||||
use tokio_core::net::TcpStream;
|
||||
use actix::prelude::*;
|
||||
|
||||
@ -26,7 +29,12 @@ fn main() {
|
||||
Arbiter::handle().spawn(
|
||||
TcpStream::connect(&addr, Arbiter::handle())
|
||||
.and_then(|stream| {
|
||||
let addr: SyncAddress<_> = ChatClient.framed(stream, codec::ClientChatCodec);
|
||||
let addr: Addr<Syn, _> = ChatClient::create(|ctx| {
|
||||
let (r, w) = stream.split();
|
||||
ChatClient::add_stream(FramedRead::new(r, codec::ClientChatCodec), ctx);
|
||||
ChatClient{
|
||||
framed: actix::io::FramedWrite::new(
|
||||
w, codec::ClientChatCodec, ctx)}});
|
||||
|
||||
// start console loop
|
||||
thread::spawn(move|| {
|
||||
@ -37,7 +45,7 @@ fn main() {
|
||||
return
|
||||
}
|
||||
|
||||
addr.send(ClientCommand(cmd));
|
||||
addr.do_send(ClientCommand(cmd));
|
||||
}
|
||||
});
|
||||
|
||||
@ -54,44 +62,45 @@ fn main() {
|
||||
}
|
||||
|
||||
|
||||
struct ChatClient;
|
||||
struct ChatClient {
|
||||
framed: actix::io::FramedWrite<WriteHalf<TcpStream>, codec::ClientChatCodec>,
|
||||
}
|
||||
|
||||
#[derive(Message)]
|
||||
struct ClientCommand(String);
|
||||
|
||||
impl Actor for ChatClient {
|
||||
type Context = FramedContext<Self>;
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut FramedContext<Self>) {
|
||||
fn started(&mut self, ctx: &mut Context<Self>) {
|
||||
// start heartbeats otherwise server will disconnect after 10 seconds
|
||||
self.hb(ctx)
|
||||
}
|
||||
|
||||
fn stopping(&mut self, _: &mut FramedContext<Self>) -> bool {
|
||||
fn stopped(&mut self, _: &mut Context<Self>) {
|
||||
println!("Disconnected");
|
||||
|
||||
// Stop application on disconnect
|
||||
Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
|
||||
true
|
||||
Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatClient {
|
||||
fn hb(&self, ctx: &mut FramedContext<Self>) {
|
||||
fn hb(&self, ctx: &mut Context<Self>) {
|
||||
ctx.run_later(Duration::new(1, 0), |act, ctx| {
|
||||
if ctx.send(codec::ChatRequest::Ping).is_ok() {
|
||||
act.hb(ctx);
|
||||
}
|
||||
act.framed.write(codec::ChatRequest::Ping);
|
||||
act.hb(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl actix::io::WriteHandler<io::Error> for ChatClient {}
|
||||
|
||||
/// Handle stdin commands
|
||||
impl Handler<ClientCommand> for ChatClient {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ClientCommand, ctx: &mut FramedContext<Self>) {
|
||||
fn handle(&mut self, msg: ClientCommand, _: &mut Context<Self>) {
|
||||
let m = msg.0.trim();
|
||||
if m.is_empty() {
|
||||
return
|
||||
@ -102,11 +111,11 @@ impl Handler<ClientCommand> for ChatClient {
|
||||
let v: Vec<&str> = m.splitn(2, ' ').collect();
|
||||
match v[0] {
|
||||
"/list" => {
|
||||
let _ = ctx.send(codec::ChatRequest::List);
|
||||
self.framed.write(codec::ChatRequest::List);
|
||||
},
|
||||
"/join" => {
|
||||
if v.len() == 2 {
|
||||
let _ = ctx.send(codec::ChatRequest::Join(v[1].to_owned()));
|
||||
self.framed.write(codec::ChatRequest::Join(v[1].to_owned()));
|
||||
} else {
|
||||
println!("!!! room name is required");
|
||||
}
|
||||
@ -114,36 +123,31 @@ impl Handler<ClientCommand> for ChatClient {
|
||||
_ => println!("!!! unknown command"),
|
||||
}
|
||||
} else {
|
||||
let _ = ctx.send(codec::ChatRequest::Message(m.to_owned()));
|
||||
self.framed.write(codec::ChatRequest::Message(m.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Server communication
|
||||
|
||||
impl FramedActor for ChatClient {
|
||||
type Io = TcpStream;
|
||||
type Codec = codec::ClientChatCodec;
|
||||
impl StreamHandler<codec::ChatResponse, io::Error> for ChatClient {
|
||||
|
||||
fn handle(&mut self, msg: io::Result<codec::ChatResponse>, ctx: &mut FramedContext<Self>) {
|
||||
fn handle(&mut self, msg: codec::ChatResponse, _: &mut Context<Self>) {
|
||||
match msg {
|
||||
Err(_) => ctx.stop(),
|
||||
Ok(msg) => match msg {
|
||||
codec::ChatResponse::Message(ref msg) => {
|
||||
println!("message: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Joined(ref msg) => {
|
||||
println!("!!! joined: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Rooms(rooms) => {
|
||||
println!("\n!!! Available rooms:");
|
||||
for room in rooms {
|
||||
println!("{}", room);
|
||||
}
|
||||
println!("");
|
||||
}
|
||||
_ => (),
|
||||
codec::ChatResponse::Message(ref msg) => {
|
||||
println!("message: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Joined(ref msg) => {
|
||||
println!("!!! joined: {}", msg);
|
||||
}
|
||||
codec::ChatResponse::Rooms(rooms) => {
|
||||
println!("\n!!! Available rooms:");
|
||||
for room in rooms {
|
||||
println!("{}", room);
|
||||
}
|
||||
println!("");
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -4,10 +4,9 @@ use serde_json as json;
|
||||
use byteorder::{BigEndian , ByteOrder};
|
||||
use bytes::{BytesMut, BufMut};
|
||||
use tokio_io::codec::{Encoder, Decoder};
|
||||
use actix::ResponseType;
|
||||
|
||||
/// Client request
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Message)]
|
||||
#[serde(tag="cmd", content="data")]
|
||||
pub enum ChatRequest {
|
||||
/// List rooms
|
||||
@ -20,13 +19,8 @@ pub enum ChatRequest {
|
||||
Ping
|
||||
}
|
||||
|
||||
impl ResponseType for ChatRequest {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// Server response
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
#[derive(Serialize, Deserialize, Debug, Message)]
|
||||
#[serde(tag="cmd", content="data")]
|
||||
pub enum ChatResponse {
|
||||
Ping,
|
||||
@ -41,11 +35,6 @@ pub enum ChatResponse {
|
||||
Message(String),
|
||||
}
|
||||
|
||||
impl ResponseType for ChatResponse {
|
||||
type Item = ();
|
||||
type Error = ();
|
||||
}
|
||||
|
||||
/// Codec for Client -> Server transport
|
||||
pub struct ChatCodec;
|
||||
|
||||
|
@ -26,7 +26,7 @@ mod session;
|
||||
/// This is our websocket route state, this state is shared with all route instances
|
||||
/// via `HttpContext::state()`
|
||||
struct WsChatSessionState {
|
||||
addr: SyncAddress<server::ChatServer>,
|
||||
addr: Addr<Syn, server::ChatServer>,
|
||||
}
|
||||
|
||||
/// Entry point for our route
|
||||
@ -62,12 +62,12 @@ impl Actor for WsChatSession {
|
||||
// before processing any other events.
|
||||
// HttpContext::state() is instance of WsChatSessionState, state is shared across all
|
||||
// routes within application
|
||||
let subs = ctx.sync_subscriber();
|
||||
ctx.state().addr.call(
|
||||
self, server::Connect{addr: subs}).then(
|
||||
|res, act, ctx| {
|
||||
let addr: Addr<Syn, _> = ctx.address();
|
||||
ctx.state().addr.send(server::Connect{addr: addr.recipient()})
|
||||
.into_actor(self)
|
||||
.then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(Ok(res)) => act.id = res,
|
||||
Ok(res) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
@ -75,10 +75,10 @@ impl Actor for WsChatSession {
|
||||
}).wait(ctx);
|
||||
}
|
||||
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> bool {
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||
// notify chat server
|
||||
ctx.state().addr.send(server::Disconnect{id: self.id});
|
||||
true
|
||||
ctx.state().addr.do_send(server::Disconnect{id: self.id});
|
||||
Running::Stop
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,13 +87,12 @@ impl Handler<session::Message> for WsChatSession {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: session::Message, ctx: &mut Self::Context) {
|
||||
ctx.text(&msg.0);
|
||||
ctx.text(msg.0);
|
||||
}
|
||||
}
|
||||
|
||||
/// WebSocket message handler
|
||||
impl Handler<ws::Message> for WsChatSession {
|
||||
type Result = ();
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for WsChatSession {
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
println!("WEBSOCKET MESSAGE: {:?}", msg);
|
||||
@ -109,17 +108,19 @@ impl Handler<ws::Message> for WsChatSession {
|
||||
"/list" => {
|
||||
// Send ListRooms message to chat server and wait for response
|
||||
println!("List rooms");
|
||||
ctx.state().addr.call(self, server::ListRooms).then(|res, _, ctx| {
|
||||
match res {
|
||||
Ok(Ok(rooms)) => {
|
||||
for room in rooms {
|
||||
ctx.text(&room);
|
||||
}
|
||||
},
|
||||
_ => println!("Something is wrong"),
|
||||
}
|
||||
fut::ok(())
|
||||
}).wait(ctx)
|
||||
ctx.state().addr.send(server::ListRooms)
|
||||
.into_actor(self)
|
||||
.then(|res, _, ctx| {
|
||||
match res {
|
||||
Ok(rooms) => {
|
||||
for room in rooms {
|
||||
ctx.text(room);
|
||||
}
|
||||
},
|
||||
_ => println!("Something is wrong"),
|
||||
}
|
||||
fut::ok(())
|
||||
}).wait(ctx)
|
||||
// .wait(ctx) pauses all events in context,
|
||||
// so actor wont receive any new messages until it get list
|
||||
// of rooms back
|
||||
@ -127,7 +128,7 @@ impl Handler<ws::Message> for WsChatSession {
|
||||
"/join" => {
|
||||
if v.len() == 2 {
|
||||
self.room = v[1].to_owned();
|
||||
ctx.state().addr.send(
|
||||
ctx.state().addr.do_send(
|
||||
server::Join{id: self.id, name: self.room.clone()});
|
||||
|
||||
ctx.text("joined");
|
||||
@ -142,7 +143,7 @@ impl Handler<ws::Message> for WsChatSession {
|
||||
ctx.text("!!! name is required");
|
||||
}
|
||||
},
|
||||
_ => ctx.text(&format!("!!! unknown command: {:?}", m)),
|
||||
_ => ctx.text(format!("!!! unknown command: {:?}", m)),
|
||||
}
|
||||
} else {
|
||||
let msg = if let Some(ref name) = self.name {
|
||||
@ -151,7 +152,7 @@ impl Handler<ws::Message> for WsChatSession {
|
||||
m.to_owned()
|
||||
};
|
||||
// send message to chat server
|
||||
ctx.state().addr.send(
|
||||
ctx.state().addr.do_send(
|
||||
server::Message{id: self.id,
|
||||
msg: msg,
|
||||
room: self.room.clone()})
|
||||
@ -159,10 +160,9 @@ impl Handler<ws::Message> for WsChatSession {
|
||||
},
|
||||
ws::Message::Binary(bin) =>
|
||||
println!("Unexpected binary"),
|
||||
ws::Message::Closed | ws::Message::Error => {
|
||||
ws::Message::Close(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -172,12 +172,11 @@ fn main() {
|
||||
let sys = actix::System::new("websocket-example");
|
||||
|
||||
// Start chat server actor in separate thread
|
||||
let server: SyncAddress<_> =
|
||||
Arbiter::start(|_| server::ChatServer::default());
|
||||
let server: Addr<Syn, _> = Arbiter::start(|_| server::ChatServer::default());
|
||||
|
||||
// Start tcp server in separate thread
|
||||
let srv = server.clone();
|
||||
Arbiter::new("tcp-server").send::<msgs::Execute>(
|
||||
Arbiter::new("tcp-server").do_send::<msgs::Execute>(
|
||||
msgs::Execute::new(move || {
|
||||
session::TcpServer::new("127.0.0.1:12345", srv);
|
||||
Ok(())
|
||||
|
@ -12,16 +12,10 @@ use session;
|
||||
/// Message for chat server communications
|
||||
|
||||
/// New chat session is created
|
||||
#[derive(Message)]
|
||||
#[rtype(usize)]
|
||||
pub struct Connect {
|
||||
pub addr: Box<actix::Subscriber<session::Message> + Send>,
|
||||
}
|
||||
|
||||
/// Response type for Connect message
|
||||
///
|
||||
/// Chat server returns unique session id
|
||||
impl ResponseType for Connect {
|
||||
type Item = usize;
|
||||
type Error = ();
|
||||
pub addr: Recipient<Syn, session::Message>,
|
||||
}
|
||||
|
||||
/// Session is disconnected
|
||||
@ -44,9 +38,8 @@ pub struct Message {
|
||||
/// List of available rooms
|
||||
pub struct ListRooms;
|
||||
|
||||
impl ResponseType for ListRooms {
|
||||
type Item = Vec<String>;
|
||||
type Error = ();
|
||||
impl actix::Message for ListRooms {
|
||||
type Result = Vec<String>;
|
||||
}
|
||||
|
||||
/// Join room, if room does not exists create new one.
|
||||
@ -61,7 +54,7 @@ pub struct Join {
|
||||
/// `ChatServer` manages chat rooms and responsible for coordinating chat session.
|
||||
/// implementation is super primitive
|
||||
pub struct ChatServer {
|
||||
sessions: HashMap<usize, Box<actix::Subscriber<session::Message> + Send>>,
|
||||
sessions: HashMap<usize, Recipient<Syn, session::Message>>,
|
||||
rooms: HashMap<String, HashSet<usize>>,
|
||||
rng: RefCell<ThreadRng>,
|
||||
}
|
||||
@ -87,7 +80,7 @@ impl ChatServer {
|
||||
for id in sessions {
|
||||
if *id != skip_id {
|
||||
if let Some(addr) = self.sessions.get(id) {
|
||||
let _ = addr.send(session::Message(message.to_owned()));
|
||||
let _ = addr.do_send(session::Message(message.to_owned()));
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -106,7 +99,7 @@ impl Actor for ChatServer {
|
||||
///
|
||||
/// Register new session and assign unique id to this session
|
||||
impl Handler<Connect> for ChatServer {
|
||||
type Result = MessageResult<Connect>;
|
||||
type Result = usize;
|
||||
|
||||
fn handle(&mut self, msg: Connect, _: &mut Context<Self>) -> Self::Result {
|
||||
println!("Someone joined");
|
||||
@ -122,7 +115,7 @@ impl Handler<Connect> for ChatServer {
|
||||
self.rooms.get_mut(&"Main".to_owned()).unwrap().insert(id);
|
||||
|
||||
// send id back
|
||||
Ok(id)
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
@ -171,7 +164,7 @@ impl Handler<ListRooms> for ChatServer {
|
||||
rooms.push(key.to_owned())
|
||||
}
|
||||
|
||||
Ok(rooms)
|
||||
MessageResult(rooms)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -4,6 +4,9 @@ use std::{io, net};
|
||||
use std::str::FromStr;
|
||||
use std::time::{Instant, Duration};
|
||||
use futures::Stream;
|
||||
use tokio_io::AsyncRead;
|
||||
use tokio_io::io::WriteHalf;
|
||||
use tokio_io::codec::FramedRead;
|
||||
use tokio_core::net::{TcpStream, TcpListener};
|
||||
|
||||
use actix::prelude::*;
|
||||
@ -16,22 +19,24 @@ use codec::{ChatRequest, ChatResponse, ChatCodec};
|
||||
#[derive(Message)]
|
||||
pub struct Message(pub String);
|
||||
|
||||
/// `ChatSession` actor is responsible for tcp peer communitions.
|
||||
/// `ChatSession` actor is responsible for tcp peer communications.
|
||||
pub struct ChatSession {
|
||||
/// unique session id
|
||||
id: usize,
|
||||
/// this is address of chat server
|
||||
addr: SyncAddress<ChatServer>,
|
||||
addr: Addr<Syn, ChatServer>,
|
||||
/// Client must send ping at least once per 10 seconds, otherwise we drop connection.
|
||||
hb: Instant,
|
||||
/// joined room
|
||||
room: String,
|
||||
/// Framed wrapper
|
||||
framed: actix::io::FramedWrite<WriteHalf<TcpStream>, ChatCodec>,
|
||||
}
|
||||
|
||||
impl Actor for ChatSession {
|
||||
/// For tcp communication we are going to use `FramedContext`.
|
||||
/// It is convinient wrapper around `Framed` object from `tokio_io`
|
||||
type Context = FramedContext<Self>;
|
||||
/// It is convenient wrapper around `Framed` object from `tokio_io`
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Self::Context) {
|
||||
// we'll start heartbeat process on session start.
|
||||
@ -40,11 +45,12 @@ impl Actor for ChatSession {
|
||||
// register self in chat server. `AsyncContext::wait` register
|
||||
// future within context, but context waits until this future resolves
|
||||
// before processing any other events.
|
||||
let addr: SyncAddress<_> = ctx.address();
|
||||
self.addr.call(self, server::Connect{addr: addr.subscriber()})
|
||||
let addr: Addr<Syn, _> = ctx.address();
|
||||
self.addr.send(server::Connect{addr: addr.recipient()})
|
||||
.into_actor(self)
|
||||
.then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(Ok(res)) => act.id = res,
|
||||
Ok(res) => act.id = res,
|
||||
// something is wrong with chat server
|
||||
_ => ctx.stop(),
|
||||
}
|
||||
@ -52,56 +58,55 @@ impl Actor for ChatSession {
|
||||
}).wait(ctx);
|
||||
}
|
||||
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> bool {
|
||||
fn stopping(&mut self, ctx: &mut Self::Context) -> Running {
|
||||
// notify chat server
|
||||
self.addr.send(server::Disconnect{id: self.id});
|
||||
true
|
||||
self.addr.do_send(server::Disconnect{id: self.id});
|
||||
Running::Stop
|
||||
}
|
||||
}
|
||||
|
||||
/// To use `FramedContext` we have to define Io type and Codec
|
||||
impl FramedActor for ChatSession {
|
||||
type Io = TcpStream;
|
||||
type Codec= ChatCodec;
|
||||
impl actix::io::WriteHandler<io::Error> for ChatSession {}
|
||||
|
||||
/// To use `Framed` we have to define Io type and Codec
|
||||
impl StreamHandler<ChatRequest, io::Error> for ChatSession {
|
||||
|
||||
/// This is main event loop for client requests
|
||||
fn handle(&mut self, msg: io::Result<ChatRequest>, ctx: &mut FramedContext<Self>) {
|
||||
fn handle(&mut self, msg: ChatRequest, ctx: &mut Context<Self>) {
|
||||
match msg {
|
||||
Err(_) => ctx.stop(),
|
||||
Ok(msg) => match msg {
|
||||
ChatRequest::List => {
|
||||
// Send ListRooms message to chat server and wait for response
|
||||
println!("List rooms");
|
||||
self.addr.call(self, server::ListRooms).then(|res, _, ctx| {
|
||||
ChatRequest::List => {
|
||||
// Send ListRooms message to chat server and wait for response
|
||||
println!("List rooms");
|
||||
self.addr.send(server::ListRooms)
|
||||
.into_actor(self)
|
||||
.then(|res, act, ctx| {
|
||||
match res {
|
||||
Ok(Ok(rooms)) => {
|
||||
let _ = ctx.send(ChatResponse::Rooms(rooms));
|
||||
Ok(rooms) => {
|
||||
act.framed.write(ChatResponse::Rooms(rooms));
|
||||
},
|
||||
_ => println!("Something is wrong"),
|
||||
_ => println!("Something is wrong"),
|
||||
}
|
||||
actix::fut::ok(())
|
||||
}).wait(ctx)
|
||||
// .wait(ctx) pauses all events in context,
|
||||
// so actor wont receive any new messages until it get list of rooms back
|
||||
},
|
||||
ChatRequest::Join(name) => {
|
||||
println!("Join to room: {}", name);
|
||||
self.room = name.clone();
|
||||
self.addr.send(server::Join{id: self.id, name: name.clone()});
|
||||
let _ = ctx.send(ChatResponse::Joined(name));
|
||||
},
|
||||
ChatRequest::Message(message) => {
|
||||
// send message to chat server
|
||||
println!("Peer message: {}", message);
|
||||
self.addr.send(
|
||||
server::Message{id: self.id,
|
||||
msg: message, room:
|
||||
self.room.clone()})
|
||||
}
|
||||
// we update heartbeat time on ping from peer
|
||||
ChatRequest::Ping =>
|
||||
self.hb = Instant::now(),
|
||||
// .wait(ctx) pauses all events in context,
|
||||
// so actor wont receive any new messages until it get list of rooms back
|
||||
},
|
||||
ChatRequest::Join(name) => {
|
||||
println!("Join to room: {}", name);
|
||||
self.room = name.clone();
|
||||
self.addr.do_send(server::Join{id: self.id, name: name.clone()});
|
||||
self.framed.write(ChatResponse::Joined(name));
|
||||
},
|
||||
ChatRequest::Message(message) => {
|
||||
// send message to chat server
|
||||
println!("Peer message: {}", message);
|
||||
self.addr.do_send(
|
||||
server::Message{id: self.id,
|
||||
msg: message, room:
|
||||
self.room.clone()})
|
||||
}
|
||||
// we update heartbeat time on ping from peer
|
||||
ChatRequest::Ping =>
|
||||
self.hb = Instant::now(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -110,23 +115,25 @@ impl FramedActor for ChatSession {
|
||||
impl Handler<Message> for ChatSession {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: Message, ctx: &mut FramedContext<Self>) {
|
||||
fn handle(&mut self, msg: Message, ctx: &mut Context<Self>) {
|
||||
// send message to peer
|
||||
let _ = ctx.send(ChatResponse::Message(msg.0));
|
||||
self.framed.write(ChatResponse::Message(msg.0));
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper methods
|
||||
impl ChatSession {
|
||||
|
||||
pub fn new(addr: SyncAddress<ChatServer>) -> ChatSession {
|
||||
ChatSession {id: 0, addr: addr, hb: Instant::now(), room: "Main".to_owned()}
|
||||
pub fn new(addr: Addr<Syn,ChatServer>,
|
||||
framed: actix::io::FramedWrite<WriteHalf<TcpStream>, ChatCodec>) -> ChatSession {
|
||||
ChatSession {id: 0, addr: addr, hb: Instant::now(),
|
||||
room: "Main".to_owned(), framed: framed}
|
||||
}
|
||||
|
||||
/// helper method that sends ping to client every second.
|
||||
///
|
||||
/// also this method check heartbeats from client
|
||||
fn hb(&self, ctx: &mut FramedContext<Self>) {
|
||||
fn hb(&self, ctx: &mut Context<Self>) {
|
||||
ctx.run_later(Duration::new(1, 0), |act, ctx| {
|
||||
// check client heartbeats
|
||||
if Instant::now().duration_since(act.hb) > Duration::new(10, 0) {
|
||||
@ -134,29 +141,28 @@ impl ChatSession {
|
||||
println!("Client heartbeat failed, disconnecting!");
|
||||
|
||||
// notify chat server
|
||||
act.addr.send(server::Disconnect{id: act.id});
|
||||
act.addr.do_send(server::Disconnect{id: act.id});
|
||||
|
||||
// stop actor
|
||||
ctx.stop();
|
||||
}
|
||||
|
||||
if ctx.send(ChatResponse::Ping).is_ok() {
|
||||
// if we can not send message to sink, sink is closed (disconnected)
|
||||
act.hb(ctx);
|
||||
}
|
||||
act.framed.write(ChatResponse::Ping);
|
||||
// if we can not send message to sink, sink is closed (disconnected)
|
||||
act.hb(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Define tcp server that will accept incomint tcp connection and create
|
||||
/// Define tcp server that will accept incoming tcp connection and create
|
||||
/// chat actors.
|
||||
pub struct TcpServer {
|
||||
chat: SyncAddress<ChatServer>,
|
||||
chat: Addr<Syn, ChatServer>,
|
||||
}
|
||||
|
||||
impl TcpServer {
|
||||
pub fn new(s: &str, chat: SyncAddress<ChatServer>) {
|
||||
pub fn new(s: &str, chat: Addr<Syn, ChatServer>) {
|
||||
// Create server listener
|
||||
let addr = net::SocketAddr::from_str("127.0.0.1:12345").unwrap();
|
||||
let listener = TcpListener::bind(&addr, Arbiter::handle()).unwrap();
|
||||
@ -192,6 +198,10 @@ impl Handler<TcpConnect> for TcpServer {
|
||||
// For each incoming connection we create `ChatSession` actor
|
||||
// with out chat server address.
|
||||
let server = self.chat.clone();
|
||||
let _: () = ChatSession::new(server).framed(msg.0, ChatCodec);
|
||||
let _: () = ChatSession::create(|ctx| {
|
||||
let (r, w) = msg.0.split();
|
||||
ChatSession::add_stream(FramedRead::new(r, ChatCodec), ctx);
|
||||
ChatSession::new(server, actix::io::FramedWrite::new(w, ChatCodec, ctx))
|
||||
});
|
||||
}
|
||||
}
|
||||
|
@ -2,13 +2,19 @@
|
||||
name = "websocket"
|
||||
version = "0.1.0"
|
||||
authors = ["Nikolay Kim <fafhrd91@gmail.com>"]
|
||||
workspace = "../.."
|
||||
|
||||
[[bin]]
|
||||
name = "server"
|
||||
path = "src/main.rs"
|
||||
|
||||
[[bin]]
|
||||
name = "client"
|
||||
path = "src/client.rs"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "*"
|
||||
futures = "0.1"
|
||||
actix = "^0.4.2"
|
||||
actix-web = { git = "https://github.com/actix/actix-web.git" }
|
||||
tokio-core = "0.1"
|
||||
actix = "0.5"
|
||||
actix-web = { path="../../" }
|
||||
|
113
examples/websocket/src/client.rs
Normal file
113
examples/websocket/src/client.rs
Normal file
@ -0,0 +1,113 @@
|
||||
//! Simple websocket client.
|
||||
|
||||
#![allow(unused_variables)]
|
||||
extern crate actix;
|
||||
extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
extern crate futures;
|
||||
extern crate tokio_core;
|
||||
|
||||
use std::{io, thread};
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::*;
|
||||
use futures::Future;
|
||||
use actix_web::ws::{Message, ProtocolError, Client, ClientWriter};
|
||||
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
Arbiter::handle().spawn(
|
||||
Client::new("http://127.0.0.1:8080/ws/")
|
||||
.connect()
|
||||
.map_err(|e| {
|
||||
println!("Error: {}", e);
|
||||
()
|
||||
})
|
||||
.map(|(reader, writer)| {
|
||||
let addr: Addr<Syn, _> = ChatClient::create(|ctx| {
|
||||
ChatClient::add_stream(reader, ctx);
|
||||
ChatClient(writer)
|
||||
});
|
||||
|
||||
// start console loop
|
||||
thread::spawn(move|| {
|
||||
loop {
|
||||
let mut cmd = String::new();
|
||||
if io::stdin().read_line(&mut cmd).is_err() {
|
||||
println!("error");
|
||||
return
|
||||
}
|
||||
addr.do_send(ClientCommand(cmd));
|
||||
}
|
||||
});
|
||||
|
||||
()
|
||||
})
|
||||
);
|
||||
|
||||
let _ = sys.run();
|
||||
}
|
||||
|
||||
|
||||
struct ChatClient(ClientWriter);
|
||||
|
||||
#[derive(Message)]
|
||||
struct ClientCommand(String);
|
||||
|
||||
impl Actor for ChatClient {
|
||||
type Context = Context<Self>;
|
||||
|
||||
fn started(&mut self, ctx: &mut Context<Self>) {
|
||||
// start heartbeats otherwise server will disconnect after 10 seconds
|
||||
self.hb(ctx)
|
||||
}
|
||||
|
||||
fn stopped(&mut self, _: &mut Context<Self>) {
|
||||
println!("Disconnected");
|
||||
|
||||
// Stop application on disconnect
|
||||
Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
}
|
||||
}
|
||||
|
||||
impl ChatClient {
|
||||
fn hb(&self, ctx: &mut Context<Self>) {
|
||||
ctx.run_later(Duration::new(1, 0), |act, ctx| {
|
||||
act.0.ping("");
|
||||
act.hb(ctx);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle stdin commands
|
||||
impl Handler<ClientCommand> for ChatClient {
|
||||
type Result = ();
|
||||
|
||||
fn handle(&mut self, msg: ClientCommand, ctx: &mut Context<Self>) {
|
||||
self.0.text(msg.0)
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle server websocket messages
|
||||
impl StreamHandler<Message, ProtocolError> for ChatClient {
|
||||
|
||||
fn handle(&mut self, msg: Message, ctx: &mut Context<Self>) {
|
||||
match msg {
|
||||
Message::Text(txt) => println!("Server: {:?}", txt),
|
||||
_ => ()
|
||||
}
|
||||
}
|
||||
|
||||
fn started(&mut self, ctx: &mut Context<Self>) {
|
||||
println!("Connected");
|
||||
}
|
||||
|
||||
fn finished(&mut self, ctx: &mut Context<Self>) {
|
||||
println!("Server disconnected");
|
||||
ctx.stop()
|
||||
}
|
||||
}
|
@ -25,17 +25,16 @@ impl Actor for MyWebSocket {
|
||||
}
|
||||
|
||||
/// Handler for `ws::Message`
|
||||
impl Handler<ws::Message> for MyWebSocket {
|
||||
type Result = ();
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for MyWebSocket {
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
// process websocket messages
|
||||
println!("WS: {:?}", msg);
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => ctx.pong(&msg),
|
||||
ws::Message::Text(text) => ctx.text(&text),
|
||||
ws::Message::Text(text) => ctx.text(text),
|
||||
ws::Message::Binary(bin) => ctx.binary(bin),
|
||||
ws::Message::Closed | ws::Message::Error => {
|
||||
ws::Message::Close(_) => {
|
||||
ctx.stop();
|
||||
}
|
||||
_ => (),
|
||||
@ -44,7 +43,7 @@ impl Handler<ws::Message> for MyWebSocket {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
::std::env::set_var("RUST_LOG", "actix_web=trace");
|
||||
::std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
let _ = env_logger::init();
|
||||
let sys = actix::System::new("ws-example");
|
||||
|
||||
@ -55,7 +54,8 @@ fn main() {
|
||||
// websocket route
|
||||
.resource("/ws/", |r| r.method(Method::GET).f(ws_index))
|
||||
// static files
|
||||
.handler("/", fs::StaticFiles::new("../static/", true)))
|
||||
.handler("/", fs::StaticFiles::new("../static/", true)
|
||||
.index_file("index.html")))
|
||||
// start http server on 127.0.0.1:8080
|
||||
.bind("127.0.0.1:8080").unwrap()
|
||||
.start();
|
||||
|
@ -1,3 +1,4 @@
|
||||
[book]
|
||||
title = "Actix web"
|
||||
description = "Actix web framework guide"
|
||||
author = "Actix Project and Contributors"
|
||||
|
@ -17,7 +17,7 @@ If you already have rustup installed, run this command to ensure you have the la
|
||||
rustup update
|
||||
```
|
||||
|
||||
Actix web framework requies rust version 1.20 and up.
|
||||
Actix web framework requires rust version 1.21 and up.
|
||||
|
||||
## Running Examples
|
||||
|
||||
|
@ -1,7 +1,7 @@
|
||||
# Middlewares
|
||||
|
||||
Actix middlewares system allows to add additional behaviour to request/response processing.
|
||||
Middleware can hook into incomnig request process and modify request or halt request
|
||||
Actix middlewares system allows to add additional behavior to request/response processing.
|
||||
Middleware can hook into incoming request process and modify request or halt request
|
||||
processing and return response early. Also it can hook into response processing.
|
||||
|
||||
Typically middlewares involves in following actions:
|
||||
@ -12,9 +12,9 @@ Typically middlewares involves in following actions:
|
||||
* Access external services (redis, logging, sessions)
|
||||
|
||||
Middlewares are registered for each application and get executed in same order as
|
||||
registraton order. In general, *middleware* is a type that implements
|
||||
registration order. In general, *middleware* is a type that implements
|
||||
[*Middleware trait*](../actix_web/middlewares/trait.Middleware.html). Each method
|
||||
in this trait has default implementation. Each method can return result immidietly
|
||||
in this trait has default implementation. Each method can return result immediately
|
||||
or *future* object.
|
||||
|
||||
Here is example of simple middleware that adds request and response headers:
|
||||
@ -53,7 +53,7 @@ impl<S> Middleware<S> for Headers {
|
||||
fn main() {
|
||||
Application::new()
|
||||
.middleware(Headers) // <- Register middleware, this method could be called multiple times
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk));
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk));
|
||||
}
|
||||
```
|
||||
|
||||
@ -64,7 +64,9 @@ Active provides several useful middlewares, like *logging*, *user sessions*, etc
|
||||
|
||||
Logging is implemented as middleware.
|
||||
It is common to register logging middleware as first middleware for application.
|
||||
Logging middleware has to be registered for each application.
|
||||
Logging middleware has to be registered for each application. *Logger* middleware
|
||||
uses standard log crate to log information. You should enable logger for *actix_web*
|
||||
package to see access log. ([env_logger](https://docs.rs/env_logger/*/env_logger/) or similar)
|
||||
|
||||
### Usage
|
||||
|
||||
@ -76,10 +78,14 @@ Default `Logger` could be created with `default` method, it uses the default for
|
||||
```
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
extern crate env_logger;
|
||||
use actix_web::Application;
|
||||
use actix_web::middleware::Logger;
|
||||
|
||||
fn main() {
|
||||
std::env::set_var("RUST_LOG", "actix_web=info");
|
||||
env_logger::init();
|
||||
|
||||
Application::new()
|
||||
.middleware(Logger::default())
|
||||
.middleware(Logger::new("%a %{User-Agent}i"))
|
||||
@ -138,8 +144,8 @@ fn main() {
|
||||
.header("X-Version", "0.2")
|
||||
.finish())
|
||||
.resource("/test", |r| {
|
||||
r.method(Method::GET).f(|req| httpcodes::HTTPOk);
|
||||
r.method(Method::HEAD).f(|req| httpcodes::HTTPMethodNotAllowed);
|
||||
r.method(Method::GET).f(|req| httpcodes::HttpOk);
|
||||
r.method(Method::HEAD).f(|req| httpcodes::HttpMethodNotAllowed);
|
||||
})
|
||||
.finish();
|
||||
}
|
||||
@ -148,7 +154,7 @@ fn main() {
|
||||
## User sessions
|
||||
|
||||
Actix provides general solution for session management.
|
||||
[*Session storage*](../actix_web/middleware/struct.SessionStorage.html) middleare can be
|
||||
[*Session storage*](../actix_web/middleware/struct.SessionStorage.html) middleware can be
|
||||
use with different backend types to store session data in different backends.
|
||||
By default only cookie session backend is implemented. Other backend implementations
|
||||
could be added later.
|
||||
@ -162,7 +168,7 @@ You need to pass a random value to the constructor of *CookieSessionBackend*.
|
||||
This is private key for cookie session. When this value is changed, all session data is lost.
|
||||
Note that whatever you write into your session is visible by the user (but not modifiable).
|
||||
|
||||
In general case, you cretate
|
||||
In general case, you create
|
||||
[*Session storage*](../actix_web/middleware/struct.SessionStorage.html) middleware
|
||||
and initializes it with specific backend implementation, like *CookieSessionBackend*.
|
||||
To access session data
|
||||
@ -200,7 +206,7 @@ fn main() {
|
||||
)))
|
||||
.bind("127.0.0.1:59880").unwrap()
|
||||
.start();
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
# actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
# let _ = sys.run();
|
||||
}
|
||||
```
|
||||
|
@ -42,3 +42,8 @@ fn main() {
|
||||
First parameter is a base directory. Second parameter is *show_index*, if it is set to *true*
|
||||
directory listing would be returned for directories, if it is set to *false*
|
||||
then *404 Not Found* would be returned instead of directory listing.
|
||||
|
||||
Instead of showing files listing for directory, it is possible to redirect to specific
|
||||
index file. Use
|
||||
[*StaticFiles::index_file()*](../actix_web/s/struct.StaticFiles.html#method.index_file)
|
||||
method to configure this redirect.
|
||||
|
@ -4,7 +4,7 @@ Actix web automatically upgrades connection to *HTTP/2.0* if possible.
|
||||
|
||||
## Negotiation
|
||||
|
||||
*HTTP/2.0* protocol over tls without prior knowlage requires
|
||||
*HTTP/2.0* protocol over tls without prior knowledge requires
|
||||
[tls alpn](https://tools.ietf.org/html/rfc7301). At the moment only
|
||||
`rust-openssl` has support. Turn on `alpn` feature to enable `alpn` negotiation.
|
||||
With enable `alpn` feature `HttpServer` provides
|
||||
@ -12,24 +12,26 @@ With enable `alpn` feature `HttpServer` provides
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix-web = { git = "https://github.com/actix/actix-web", features=["alpn"] }
|
||||
actix-web = { version = "0.3.3", features=["alpn"] }
|
||||
openssl = { version="0.10", features = ["v110"] }
|
||||
```
|
||||
|
||||
```rust,ignore
|
||||
use std::fs::File;
|
||||
use actix_web::*;
|
||||
use openssl::ssl::{SslMethod, SslAcceptor, SslFiletype};
|
||||
|
||||
fn main() {
|
||||
let mut file = File::open("identity.pfx").unwrap();
|
||||
let mut pkcs12 = vec![];
|
||||
file.read_to_end(&mut pkcs12).unwrap();
|
||||
let pkcs12 = Pkcs12::from_der(&pkcs12).unwrap().parse("12345").unwrap();
|
||||
// load ssl keys
|
||||
let mut builder = SslAcceptor::mozilla_intermediate(SslMethod::tls()).unwrap();
|
||||
builder.set_private_key_file("key.pem", SslFiletype::PEM).unwrap();
|
||||
builder.set_certificate_chain_file("cert.pem").unwrap();
|
||||
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/index.html", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8080").unwrap();
|
||||
.serve_ssl(pkcs12).unwrap();
|
||||
.serve_ssl(builder).unwrap();
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -9,10 +9,10 @@ can be run in parallel and process messages from same queue (sync actors work in
|
||||
|
||||
Let's create simple db api that can insert new user row into sqlite table.
|
||||
We have to define sync actor and connection that this actor will use. Same approach
|
||||
could used for other databases.
|
||||
could be used for other databases.
|
||||
|
||||
```rust,ignore
|
||||
use actix::prelude::*;*
|
||||
use actix::prelude::*;
|
||||
|
||||
struct DbExecutor(SqliteConnection);
|
||||
|
||||
@ -24,11 +24,13 @@ impl Actor for DbExecutor {
|
||||
This is definition of our actor. Now we need to define *create user* message and response.
|
||||
|
||||
```rust,ignore
|
||||
#[derive(Message)]
|
||||
#[rtype(User, Error)]
|
||||
struct CreateUser {
|
||||
name: String,
|
||||
}
|
||||
|
||||
impl Message for CreateUser {
|
||||
type Result = Result<User, Error>;
|
||||
}
|
||||
```
|
||||
|
||||
We can send `CreateUser` message to `DbExecutor` actor, and as result we get
|
||||
@ -36,8 +38,9 @@ We can send `CreateUser` message to `DbExecutor` actor, and as result we get
|
||||
|
||||
```rust,ignore
|
||||
impl Handler<CreateUser> for DbExecutor {
|
||||
type Result = Result<User, Error>;
|
||||
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Response<Self, CreateUser>
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Self::Result
|
||||
{
|
||||
use self::schema::users::dsl::*;
|
||||
|
||||
@ -59,7 +62,7 @@ impl Handler<CreateUser> for DbExecutor {
|
||||
.load::<models::User>(&self.0)
|
||||
.expect("Error loading person");
|
||||
|
||||
Self::reply(items.pop().unwrap())
|
||||
Ok(items.pop().unwrap())
|
||||
}
|
||||
}
|
||||
```
|
||||
@ -71,13 +74,13 @@ can access it.
|
||||
```rust,ignore
|
||||
/// This is state where we will store *DbExecutor* address.
|
||||
struct State {
|
||||
db: SyncAddress<DbExecutor>,
|
||||
db: Addr<Syn, DbExecutor>,
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let sys = actix::System::new("diesel-example");
|
||||
|
||||
// Start 3 parallele db executors
|
||||
// Start 3 parallel db executors
|
||||
let addr = SyncArbiter::start(3, || {
|
||||
DbExecutor(SqliteConnection::establish("test.db").unwrap())
|
||||
});
|
||||
@ -94,7 +97,7 @@ fn main() {
|
||||
}
|
||||
```
|
||||
|
||||
And finally we can use address in a requst handler. We get message response
|
||||
And finally we can use address in a request handler. We get message response
|
||||
asynchronously, so handler needs to return future object, also `Route::a()` needs to be
|
||||
used for async handler registration.
|
||||
|
||||
@ -105,12 +108,12 @@ fn index(req: HttpRequest<State>) -> Box<Future<Item=HttpResponse, Error=Error>>
|
||||
let name = &req.match_info()["name"];
|
||||
|
||||
// Send message to `DbExecutor` actor
|
||||
req.state().db.call_fut(CreateUser{name: name.to_owned()})
|
||||
req.state().db.send(CreateUser{name: name.to_owned()})
|
||||
.from_err()
|
||||
.and_then(|res| {
|
||||
match res {
|
||||
Ok(user) => Ok(httpcodes::HTTPOk.build().json(user)?),
|
||||
Err(_) => Ok(httpcodes::HTTPInternalServerError.into())
|
||||
Ok(user) => Ok(httpcodes::HttpOk.build().json(user)?),
|
||||
Err(_) => Ok(httpcodes::HttpInternalServerError.into())
|
||||
}
|
||||
})
|
||||
.responder()
|
||||
@ -121,4 +124,4 @@ Full example is available in
|
||||
[examples directory](https://github.com/actix/actix-web/tree/master/examples/diesel/).
|
||||
|
||||
More information on sync actors could be found in
|
||||
[actix documentation](https://docs.rs/actix/0.3.3/actix/sync/index.html).
|
||||
[actix documentation](https://docs.rs/actix/0.5.0/actix/sync/index.html).
|
||||
|
@ -20,8 +20,8 @@ contains the following:
|
||||
|
||||
```toml
|
||||
[dependencies]
|
||||
actix = "0.4"
|
||||
actix-web = "0.3"
|
||||
actix = "0.5"
|
||||
actix-web = "0.4"
|
||||
```
|
||||
|
||||
In order to implement a web server, first we need to create a request handler.
|
||||
@ -71,7 +71,7 @@ Here is full source of main.rs file:
|
||||
|
||||
```rust
|
||||
# use std::thread;
|
||||
# extern crate actix_web;
|
||||
extern crate actix_web;
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> &'static str {
|
||||
@ -79,13 +79,16 @@ fn index(req: HttpRequest) -> &'static str {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
# thread::spawn(|| {
|
||||
# // In the doctest suite we can't run blocking code - deliberately leak a thread
|
||||
# // If copying this example in show-all mode make sure you skip the thread spawn
|
||||
# // call.
|
||||
# thread::spawn(|| {
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.f(index)))
|
||||
.bind("127.0.0.1:8088").expect("Can not bind to 127.0.0.1:8088")
|
||||
.run();
|
||||
# });
|
||||
# });
|
||||
}
|
||||
```
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
Actix web provides some primitives to build web servers and applications with Rust.
|
||||
It provides routing, middlewares, pre-processing of requests, and post-processing of responses,
|
||||
websocket protcol handling, multipart streams, etc.
|
||||
websocket protocol handling, multipart streams, etc.
|
||||
|
||||
All actix web server is built around `Application` instance.
|
||||
It is used for registering routes for resources, middlewares.
|
||||
@ -10,9 +10,9 @@ Also it stores application specific state that is shared across all handlers
|
||||
within same application.
|
||||
|
||||
Application acts as namespace for all routes, i.e all routes for specific application
|
||||
has same url path prefix. Application prefix always contains laading "/" slash.
|
||||
has same url path prefix. Application prefix always contains leading "/" slash.
|
||||
If supplied prefix does not contain leading slash, it get inserted.
|
||||
Prefix should consists of valud path segments. i.e for application with prefix `/app`
|
||||
Prefix should consists of value path segments. i.e for application with prefix `/app`
|
||||
any request with following paths `/app`, `/app/` or `/app/test` would match,
|
||||
but path `/application` would not match.
|
||||
|
||||
@ -46,15 +46,15 @@ Multiple applications could be served with one server:
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
HttpServer::<TcpStream, SocketAddr, _, _>::new(|| vec![
|
||||
HttpServer::new(|| vec![
|
||||
Application::new()
|
||||
.prefix("/app1")
|
||||
.resource("/", |r| r.f(|r| httpcodes::HTTPOk)),
|
||||
.resource("/", |r| r.f(|r| httpcodes::HttpOk)),
|
||||
Application::new()
|
||||
.prefix("/app2")
|
||||
.resource("/", |r| r.f(|r| httpcodes::HTTPOk)),
|
||||
.resource("/", |r| r.f(|r| httpcodes::HttpOk)),
|
||||
Application::new()
|
||||
.resource("/", |r| r.f(|r| httpcodes::HTTPOk)),
|
||||
.resource("/", |r| r.f(|r| httpcodes::HttpOk)),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
[*HttpServer*](../actix_web/struct.HttpServer.html) type is responsible for
|
||||
serving http requests. *HttpServer* accept application factory as a parameter,
|
||||
Application factory must have `Send` + `Sync` bounderies. More about that in
|
||||
Application factory must have `Send` + `Sync` boundaries. More about that in
|
||||
*multi-threading* section. To bind to specific socket address `bind()` must be used.
|
||||
This method could be called multiple times. To start http server one of the *start*
|
||||
methods could be used. `start()` method start simple server, `start_tls()` or `start_ssl()`
|
||||
@ -20,11 +20,11 @@ fn main() {
|
||||
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk)))
|
||||
.bind("127.0.0.1:59080").unwrap()
|
||||
.start();
|
||||
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
# actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
@ -52,12 +52,12 @@ use std::sync::mpsc;
|
||||
|
||||
fn main() {
|
||||
let (tx, rx) = mpsc::channel();
|
||||
|
||||
|
||||
thread::spawn(move || {
|
||||
let sys = actix::System::new("http-server");
|
||||
let addr = HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk)))
|
||||
.bind("127.0.0.1:0").expect("Can not bind to 127.0.0.1:0")
|
||||
.shutdown_timeout(60) // <- Set shutdown timeout to 60 seconds
|
||||
.start();
|
||||
@ -66,7 +66,7 @@ fn main() {
|
||||
});
|
||||
|
||||
let addr = rx.recv().unwrap();
|
||||
let _ = addr.call_fut(
|
||||
let _ = addr.send(
|
||||
server::StopServer{graceful:true}).wait(); // <- Send `StopServer` message to server.
|
||||
}
|
||||
```
|
||||
@ -80,14 +80,12 @@ could be overridden with `HttpServer::threads()` method.
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate tokio_core;
|
||||
# use tokio_core::net::TcpStream;
|
||||
# use std::net::SocketAddr;
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
HttpServer::<TcpStream, SocketAddr, _, _>::new(
|
||||
HttpServer::new(
|
||||
|| Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk)))
|
||||
.threads(4); // <- Start 4 workers
|
||||
}
|
||||
```
|
||||
@ -136,22 +134,31 @@ for full example.
|
||||
Actix can wait for requests on a keep-alive connection. *Keep alive*
|
||||
connection behavior is defined by server settings.
|
||||
|
||||
* `Some(75)` - enable 75 sec *keep alive* timer according request and response settings.
|
||||
* `Some(0)` - disable *keep alive*.
|
||||
* `None` - Use `SO_KEEPALIVE` socket option.
|
||||
* `75` or `Some(75)` or `KeepAlive::Timeout(75)` - enable 75 sec *keep alive* timer according
|
||||
request and response settings.
|
||||
* `None` or `KeepAlive::Disabled` - disable *keep alive*.
|
||||
* `KeepAlive::Tcp(75)` - Use `SO_KEEPALIVE` socket option.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate tokio_core;
|
||||
# use tokio_core::net::TcpStream;
|
||||
# use std::net::SocketAddr;
|
||||
use actix_web::*;
|
||||
|
||||
fn main() {
|
||||
HttpServer::<TcpStream, SocketAddr, _, _>::new(||
|
||||
HttpServer::new(||
|
||||
Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk)))
|
||||
.keep_alive(None); // <- Use `SO_KEEPALIVE` socket option.
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk)))
|
||||
.keep_alive(75); // <- Set keep-alive to 75 seconds
|
||||
|
||||
HttpServer::new(||
|
||||
Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk)))
|
||||
.keep_alive(server::KeepAlive::Tcp(75)); // <- Use `SO_KEEPALIVE` socket option.
|
||||
|
||||
HttpServer::new(||
|
||||
Application::new()
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk)))
|
||||
.keep_alive(None); // <- Disable keep-alive
|
||||
}
|
||||
```
|
||||
|
||||
@ -159,7 +166,7 @@ If first option is selected then *keep alive* state
|
||||
calculated based on response's *connection-type*. By default
|
||||
`HttpResponse::connection_type` is not defined in that case *keep alive*
|
||||
defined by request's http version. Keep alive is off for *HTTP/1.0*
|
||||
and is on for *HTTP/1.1* and "HTTP/2.0".
|
||||
and is on for *HTTP/1.1* and *HTTP/2.0*.
|
||||
|
||||
*Connection type* could be change with `HttpResponseBuilder::connection_type()` method.
|
||||
|
||||
@ -169,7 +176,7 @@ and is on for *HTTP/1.1* and "HTTP/2.0".
|
||||
use actix_web::*;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
HTTPOk.build()
|
||||
HttpOk.build()
|
||||
.connection_type(headers::ConnectionType::Close) // <- Close connection
|
||||
.force_close() // <- Alternative method
|
||||
.finish().unwrap()
|
||||
|
@ -65,7 +65,7 @@ impl<S> Handler<S> for MyHandler {
|
||||
/// Handle request
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result {
|
||||
self.0 += 1;
|
||||
httpcodes::HTTPOk.into()
|
||||
httpcodes::HttpOk.into()
|
||||
}
|
||||
}
|
||||
# fn main() {}
|
||||
@ -89,9 +89,8 @@ impl<S> Handler<S> for MyHandler {
|
||||
|
||||
/// Handle request
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Self::Result {
|
||||
let num = self.0.load(Ordering::Relaxed) + 1;
|
||||
self.0.store(num, Ordering::Relaxed);
|
||||
httpcodes::HTTPOk.into()
|
||||
self.0.fetch_add(1, Ordering::Relaxed);
|
||||
httpcodes::HttpOk.into()
|
||||
}
|
||||
}
|
||||
|
||||
@ -110,7 +109,7 @@ fn main() {
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8088");
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
# actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
@ -168,7 +167,7 @@ fn main() {
|
||||
.start();
|
||||
|
||||
println!("Started http server: 127.0.0.1:8088");
|
||||
# actix::Arbiter::system().send(actix::msgs::SystemExit(0));
|
||||
# actix::Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
let _ = sys.run();
|
||||
}
|
||||
```
|
||||
@ -235,3 +234,50 @@ fn main() {
|
||||
```
|
||||
|
||||
Both methods could be combined. (i.e Async response with streaming body)
|
||||
|
||||
## Different return types (Either)
|
||||
|
||||
Sometimes you need to return different types of responses. For example
|
||||
you can do error check and return error and return async response otherwise.
|
||||
Or any result that requires two different types.
|
||||
For this case [*Either*](../actix_web/enum.Either.html) type can be used.
|
||||
*Either* allows to combine two different responder types into a single type.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# use actix_web::*;
|
||||
# use futures::future::Future;
|
||||
use futures::future::result;
|
||||
use actix_web::{Either, Error, HttpResponse, httpcodes};
|
||||
|
||||
type RegisterResult = Either<HttpResponse, Box<Future<Item=HttpResponse, Error=Error>>>;
|
||||
|
||||
fn index(req: HttpRequest) -> RegisterResult {
|
||||
if is_a_variant() { // <- choose variant A
|
||||
Either::A(
|
||||
httpcodes::HttpBadRequest.with_body("Bad data"))
|
||||
} else {
|
||||
Either::B( // <- variant B
|
||||
result(HttpResponse::Ok()
|
||||
.content_type("text/html")
|
||||
.body(format!("Hello!"))
|
||||
.map_err(|e| e.into())).responder())
|
||||
}
|
||||
}
|
||||
# fn is_a_variant() -> bool { true }
|
||||
# fn main() {
|
||||
# Application::new()
|
||||
# .resource("/register", |r| r.f(index))
|
||||
# .finish();
|
||||
# }
|
||||
```
|
||||
|
||||
## Tokio core handle
|
||||
|
||||
Any actix web handler runs within properly configured
|
||||
[actix system](https://actix.github.io/actix/actix/struct.System.html)
|
||||
and [arbiter](https://actix.github.io/actix/actix/struct.Arbiter.html).
|
||||
You can always get access to tokio handle via
|
||||
[Arbiter::handle()](https://actix.github.io/actix/actix/struct.Arbiter.html#method.handle)
|
||||
method.
|
||||
|
@ -5,7 +5,7 @@ and [`ResponseError` trait](../actix_web/error/trait.ResponseError.html)
|
||||
for handling handler's errors.
|
||||
Any error that implements `ResponseError` trait can be returned as error value.
|
||||
*Handler* can return *Result* object, actix by default provides
|
||||
`Responder` implemenation for compatible result object. Here is implementation
|
||||
`Responder` implementation for compatible result object. Here is implementation
|
||||
definition:
|
||||
|
||||
```rust,ignore
|
||||
@ -14,7 +14,7 @@ impl<T: Responder, E: Into<Error>> Responder for Result<T, E>
|
||||
|
||||
And any error that implements `ResponseError` can be converted into `Error` object.
|
||||
For example if *handler* function returns `io::Error`, it would be converted
|
||||
into `HTTPInternalServerError` response. Implementation for `io::Error` is provided
|
||||
into `HttpInternalServerError` response. Implementation for `io::Error` is provided
|
||||
by default.
|
||||
|
||||
```rust
|
||||
@ -134,3 +134,18 @@ fn index(req: HttpRequest) -> Result<&'static str> {
|
||||
```
|
||||
|
||||
In this example *BAD REQUEST* response get generated for `MyError` error.
|
||||
|
||||
## Error logging
|
||||
|
||||
Actix logs all errors with `WARN` log level. If log level set to `DEBUG`
|
||||
and `RUST_BACKTRACE` is enabled, backtrace get logged. The Error type uses
|
||||
cause's error backtrace if available, if the underlying failure does not provide
|
||||
a backtrace, a new backtrace is constructed pointing to that conversion point
|
||||
(rather than the origin of the error). This construction only happens if there
|
||||
is no underlying backtrace; if it does have a backtrace no new backtrace is constructed.
|
||||
|
||||
You can enable backtrace and debug logging with following command:
|
||||
|
||||
```
|
||||
>> RUST_BACKTRACE=1 RUST_LOG=actix_web=debug cargo run
|
||||
```
|
||||
|
@ -1,16 +1,14 @@
|
||||
# URL Dispatch
|
||||
|
||||
URL dispatch provides a simple way to map URLs to `Handler` code using a simple pattern matching
|
||||
language. *Regex* crate and it's
|
||||
[*RegexSet*](https://doc.rust-lang.org/regex/regex/struct.RegexSet.html) is beeing used for
|
||||
pattern matching. If one of the patterns matches the path information associated with a request,
|
||||
language. If one of the patterns matches the path information associated with a request,
|
||||
a particular handler object is invoked. A handler is a specific object that implements
|
||||
`Handler` trait, defined in your application, that receives the request and returns
|
||||
a response object. More informatin is available in [handler section](../qs_4.html).
|
||||
a response object. More information is available in [handler section](../qs_4.html).
|
||||
|
||||
## Resource configuration
|
||||
|
||||
Resource configuraiton is the act of adding a new resource to an application.
|
||||
Resource configuration is the act of adding a new resource to an application.
|
||||
A resource has a name, which acts as an identifier to be used for URL generation.
|
||||
The name also allows developers to add routes to existing resources.
|
||||
A resource also has a pattern, meant to match against the *PATH* portion of a *URL*,
|
||||
@ -19,7 +17,7 @@ port, e.g., */foo/bar* in the *URL* *http://localhost:8080/foo/bar?q=value*).
|
||||
|
||||
The [Application::resource](../actix_web/struct.Application.html#method.resource) methods
|
||||
add a single resource to application routing table. This method accepts *path pattern*
|
||||
and resource configuration funnction.
|
||||
and resource configuration function.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
@ -34,27 +32,27 @@ fn main() {
|
||||
Application::new()
|
||||
.resource("/prefix", |r| r.f(index))
|
||||
.resource("/user/{name}",
|
||||
|r| r.method(Method::GET).f(|req| HTTPOk))
|
||||
|r| r.method(Method::GET).f(|req| HttpOk))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
|
||||
*Configuraiton function* has following type:
|
||||
*Configuration function* has following type:
|
||||
|
||||
```rust,ignore
|
||||
FnOnce(&mut Resource<_>) -> ()
|
||||
```
|
||||
|
||||
*Configration function* can set name and register specific routes.
|
||||
*Configuration function* can set name and register specific routes.
|
||||
If resource does not contain any route or does not have any matching routes it
|
||||
returns *NOT FOUND* http resources.
|
||||
|
||||
## Configuring a Route
|
||||
|
||||
Resource contains set of routes. Each route in turn has set of predicates and handler.
|
||||
New route could be crearted with `Resource::route()` method which returns reference
|
||||
New route could be created with `Resource::route()` method which returns reference
|
||||
to new *Route* instance. By default *route* does not contain any predicates, so matches
|
||||
all requests and default handler is `HTTPNotFound`.
|
||||
all requests and default handler is `HttpNotFound`.
|
||||
|
||||
Application routes incoming requests based on route criteria which is defined during
|
||||
resource registration and route registration. Resource matches all routes it contains in
|
||||
@ -70,9 +68,9 @@ fn main() {
|
||||
Application::new()
|
||||
.resource("/path", |resource|
|
||||
resource.route()
|
||||
.p(pred::Get())
|
||||
.p(pred::Header("content-type", "text/plain"))
|
||||
.f(|req| HTTPOk)
|
||||
.filter(pred::Get())
|
||||
.filter(pred::Header("content-type", "text/plain"))
|
||||
.f(|req| HttpOk)
|
||||
)
|
||||
.finish();
|
||||
}
|
||||
@ -87,21 +85,21 @@ If resource can not match any route "NOT FOUND" response get returned.
|
||||
[*Route*](../actix_web/struct.Route.html) object. Route can be configured with
|
||||
builder-like pattern. Following configuration methods are available:
|
||||
|
||||
* [*Route::p()*](../actix_web/struct.Route.html#method.p) method registers new predicate,
|
||||
* [*Route::filter()*](../actix_web/struct.Route.html#method.filter) method registers new predicate,
|
||||
any number of predicates could be registered for each route.
|
||||
|
||||
* [*Route::f()*](../actix_web/struct.Route.html#method.f) method registers handler function
|
||||
for this route. Only one handler could be registered. Usually handler registeration
|
||||
is the last config operation. Handler fanction could be function or closure and has type
|
||||
for this route. Only one handler could be registered. Usually handler registration
|
||||
is the last config operation. Handler function could be function or closure and has type
|
||||
`Fn(HttpRequest<S>) -> R + 'static`
|
||||
|
||||
* [*Route::h()*](../actix_web/struct.Route.html#method.h) method registers handler object
|
||||
that implements `Handler` trait. This is similar to `f()` method, only one handler could
|
||||
be registered. Handler registeration is the last config operation.
|
||||
be registered. Handler registration is the last config operation.
|
||||
|
||||
* [*Route::a()*](../actix_web/struct.Route.html#method.a) method registers asynchandler
|
||||
function for this route. Only one handler could be registered. Handler registeration
|
||||
is the last config operation. Handler fanction could be function or closure and has type
|
||||
* [*Route::a()*](../actix_web/struct.Route.html#method.a) method registers async handler
|
||||
function for this route. Only one handler could be registered. Handler registration
|
||||
is the last config operation. Handler function could be function or closure and has type
|
||||
`Fn(HttpRequest<S>) -> Future<Item = HttpResponse, Error = Error> + 'static`
|
||||
|
||||
## Route matching
|
||||
@ -110,11 +108,10 @@ The main purpose of route configuration is to match (or not match) the request's
|
||||
against a URL path pattern. `path` represents the path portion of the URL that was requested.
|
||||
|
||||
The way that *actix* does this is very simple. When a request enters the system,
|
||||
for each resource configuration registration present in the system, actix checks
|
||||
the request's path against the pattern declared. *Regex* crate and it's
|
||||
[*RegexSet*](https://doc.rust-lang.org/regex/regex/struct.RegexSet.html) is beeing used for
|
||||
pattern matching. If resource could not be found, *default resource* get used as matched
|
||||
resource.
|
||||
for each resource configuration declaration present in the system, actix checks
|
||||
the request's path against the pattern declared. This checking happens in the order that
|
||||
the routes were declared via `Application::resource()` method. If resource could not be found,
|
||||
*default resource* get used as matched resource.
|
||||
|
||||
When a route configuration is declared, it may contain route predicate arguments. All route
|
||||
predicates associated with a route declaration must be `true` for the route configuration to
|
||||
@ -339,14 +336,14 @@ resource with the name "foo" and the pattern "{a}/{b}/{c}", you might do this.
|
||||
#
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
let url = req.url_for("foo", &["1", "2", "3"]); // <- generate url for "foo" resource
|
||||
HTTPOk.into()
|
||||
HttpOk.into()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let app = Application::new()
|
||||
.resource("/test/{a}/{b}/{c}", |r| {
|
||||
r.name("foo"); // <- set resource name, then it could be used in `url_for`
|
||||
r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
r.method(Method::GET).f(|_| httpcodes::HttpOk);
|
||||
})
|
||||
.finish();
|
||||
}
|
||||
@ -370,7 +367,7 @@ use actix_web::*;
|
||||
fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
let url = req.url_for("youtube", &["oHg5SJYRHA0"])?;
|
||||
assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0");
|
||||
Ok(httpcodes::HTTPOk.into())
|
||||
Ok(httpcodes::HttpOk.into())
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@ -407,7 +404,7 @@ This handler designed to be use as a handler for application's *default resource
|
||||
# use actix_web::*;
|
||||
#
|
||||
# fn index(req: HttpRequest) -> httpcodes::StaticResponse {
|
||||
# httpcodes::HTTPOk
|
||||
# httpcodes::HttpOk
|
||||
# }
|
||||
fn main() {
|
||||
let app = Application::new()
|
||||
@ -432,7 +429,7 @@ It is possible to register path normalization only for *GET* requests only
|
||||
# use actix_web::*;
|
||||
#
|
||||
# fn index(req: HttpRequest) -> httpcodes::StaticResponse {
|
||||
# httpcodes::HTTPOk
|
||||
# httpcodes::HttpOk
|
||||
# }
|
||||
fn main() {
|
||||
let app = Application::new()
|
||||
@ -505,8 +502,8 @@ fn main() {
|
||||
Application::new()
|
||||
.resource("/index.html", |r|
|
||||
r.route()
|
||||
.p(ContentTypeHeader)
|
||||
.h(HTTPOk));
|
||||
.filter(ContentTypeHeader)
|
||||
.h(HttpOk));
|
||||
}
|
||||
```
|
||||
|
||||
@ -516,7 +513,7 @@ Predicates can have access to application's state via `HttpRequest::state()` met
|
||||
Also predicates can store extra information in
|
||||
[requests`s extensions](../actix_web/struct.HttpRequest.html#method.extensions).
|
||||
|
||||
### Modifing predicate values
|
||||
### Modifying predicate values
|
||||
|
||||
You can invert the meaning of any predicate value by wrapping it in a `Not` predicate.
|
||||
For example if you want to return "METHOD NOT ALLOWED" response for all methods
|
||||
@ -533,8 +530,8 @@ fn main() {
|
||||
Application::new()
|
||||
.resource("/index.html", |r|
|
||||
r.route()
|
||||
.p(pred::Not(pred::Get()))
|
||||
.f(|req| HTTPMethodNotAllowed))
|
||||
.filter(pred::Not(pred::Get()))
|
||||
.f(|req| HttpMethodNotAllowed))
|
||||
.finish();
|
||||
}
|
||||
```
|
||||
@ -570,8 +567,8 @@ use actix_web::httpcodes::*;
|
||||
fn main() {
|
||||
Application::new()
|
||||
.default_resource(|r| {
|
||||
r.method(Method::GET).f(|req| HTTPNotFound);
|
||||
r.route().p(pred::Not(pred::Get())).f(|req| HTTPMethodNotAllowed);
|
||||
r.method(Method::GET).f(|req| HttpNotFound);
|
||||
r.route().filter(pred::Not(pred::Get())).f(|req| HttpMethodNotAllowed);
|
||||
})
|
||||
# .finish();
|
||||
}
|
||||
|
@ -4,7 +4,7 @@
|
||||
|
||||
Builder-like patter is used to construct an instance of `HttpResponse`.
|
||||
`HttpResponse` provides several method that returns `HttpResponseBuilder` instance,
|
||||
which is implements various convinience methods that helps build response.
|
||||
which is implements various convenience methods that helps build response.
|
||||
Check [documentation](../actix_web/dev/struct.HttpResponseBuilder.html)
|
||||
for type description. Methods `.body`, `.finish`, `.json` finalizes response creation and
|
||||
returns constructed *HttpResponse* instance. if this methods get called for the same
|
||||
@ -84,14 +84,14 @@ fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.json().from_err()
|
||||
.and_then(|val: MyObj| {
|
||||
println!("model: {:?}", val);
|
||||
Ok(httpcodes::HTTPOk.build().json(val)?) // <- send response
|
||||
Ok(httpcodes::HttpOk.build().json(val)?) // <- send response
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
|
||||
Or you can manually load payload into memory and ther deserialize it.
|
||||
Or you can manually load payload into memory and then deserialize it.
|
||||
Here is simple example. We will deserialize *MyObj* struct. We need to load request
|
||||
body first and then deserialize json into object.
|
||||
|
||||
@ -106,10 +106,10 @@ use futures::{Future, Stream};
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct MyObj {name: String, number: i32}
|
||||
|
||||
fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
fn index(req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
// `concat2` will asynchronously read each chunk of the request body and
|
||||
// return a single, concatenated, chunk
|
||||
req.payload_mut().readany().concat2()
|
||||
req.concat2()
|
||||
// `Future::from_err` acts like `?` in that it coerces the error type from
|
||||
// the future into the final error type
|
||||
.from_err()
|
||||
@ -117,7 +117,7 @@ fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
// synchronous workflow
|
||||
.and_then(|body| { // <- body is loaded, now we can deserialize json
|
||||
let obj = serde_json::from_slice::<MyObj>(&body)?;
|
||||
Ok(httpcodes::HTTPOk.build().json(obj)?) // <- send response
|
||||
Ok(httpcodes::HttpOk.build().json(obj)?) // <- send response
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
@ -169,13 +169,18 @@ get enabled automatically.
|
||||
Enabling chunked encoding for *HTTP/2.0* responses is forbidden.
|
||||
|
||||
```rust
|
||||
# extern crate bytes;
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# use futures::Stream;
|
||||
use actix_web::*;
|
||||
use bytes::Bytes;
|
||||
use futures::stream::once;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
HttpResponse::Ok()
|
||||
.chunked()
|
||||
.body(Body::Streaming(payload::Payload::empty().stream())).unwrap()
|
||||
.body(Body::Streaming(Box::new(once(Ok(Bytes::from_static(b"data")))))).unwrap()
|
||||
}
|
||||
# fn main() {}
|
||||
```
|
||||
@ -200,7 +205,7 @@ fn index(req: HttpRequest) -> Box<Future<...>> {
|
||||
match item {
|
||||
// Handle multipart Field
|
||||
multipart::MultipartItem::Field(field) => {
|
||||
println!("==== FIELD ==== {:?} {:?}", field.heders(), field.content_type());
|
||||
println!("==== FIELD ==== {:?} {:?}", field.headers(), field.content_type());
|
||||
|
||||
Either::A(
|
||||
// Field in turn is a stream of *Bytes* objects
|
||||
@ -246,7 +251,7 @@ fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
.from_err()
|
||||
.and_then(|params| { // <- url encoded parameters
|
||||
println!("==== BODY ==== {:?}", params);
|
||||
ok(httpcodes::HTTPOk.into())
|
||||
ok(httpcodes::HttpOk.into())
|
||||
})
|
||||
.responder()
|
||||
}
|
||||
@ -256,21 +261,8 @@ fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
|
||||
## Streaming request
|
||||
|
||||
Actix uses [*Payload*](../actix_web/payload/struct.Payload.html) object as request payload stream.
|
||||
*HttpRequest* provides several methods, which can be used for payload access.
|
||||
At the same time *Payload* implements *Stream* trait, so it could be used with various
|
||||
stream combinators. Also *Payload* provides serveral convinience methods that return
|
||||
future object that resolve to Bytes object.
|
||||
|
||||
* *readany()* method returns *Stream* of *Bytes* objects.
|
||||
|
||||
* *readexactly()* method returns *Future* that resolves when specified number of bytes
|
||||
get received.
|
||||
|
||||
* *readline()* method returns *Future* that resolves when `\n` get received.
|
||||
|
||||
* *readuntil()* method returns *Future* that resolves when specified bytes string
|
||||
matches in input bytes stream
|
||||
*HttpRequest* is a stream of `Bytes` objects. It could be used to read request
|
||||
body payload.
|
||||
|
||||
In this example handle reads request payload chunk by chunk and prints every chunk.
|
||||
|
||||
@ -283,9 +275,7 @@ use futures::{Future, Stream};
|
||||
|
||||
|
||||
fn index(mut req: HttpRequest) -> Box<Future<Item=HttpResponse, Error=Error>> {
|
||||
req.payload_mut()
|
||||
.readany()
|
||||
.from_err()
|
||||
req.from_err()
|
||||
.fold((), |_, chunk| {
|
||||
println!("Chunk: {:?}", chunk);
|
||||
result::<_, error::PayloadError>(Ok(()))
|
||||
|
@ -20,10 +20,10 @@ use actix_web::test::TestRequest;
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
if let Some(hdr) = req.headers().get(header::CONTENT_TYPE) {
|
||||
if let Ok(s) = hdr.to_str() {
|
||||
return httpcodes::HTTPOk.into()
|
||||
return httpcodes::HttpOk.into()
|
||||
}
|
||||
}
|
||||
httpcodes::HTTPBadRequest.into()
|
||||
httpcodes::HttpBadRequest.into()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
@ -45,8 +45,8 @@ fn main() {
|
||||
There are several methods how you can test your application. Actix provides
|
||||
[*TestServer*](../actix_web/test/struct.TestServer.html)
|
||||
server that could be used to run whole application of just specific handlers
|
||||
in real http server. At the moment it is required to use third-party libraries
|
||||
to make actual requests, libraries like [reqwest](https://crates.io/crates/reqwest).
|
||||
in real http server. *TrstServer::get()*, *TrstServer::post()* or *TrstServer::client()*
|
||||
methods could be used to send request to test server.
|
||||
|
||||
In simple form *TestServer* could be configured to use handler. *TestServer::new* method
|
||||
accepts configuration function, only argument for this function is *test application*
|
||||
@ -55,18 +55,21 @@ for more information.
|
||||
|
||||
```rust
|
||||
# extern crate actix_web;
|
||||
extern crate reqwest;
|
||||
use actix_web::*;
|
||||
use actix_web::test::TestServer;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
httpcodes::HTTPOk.into()
|
||||
httpcodes::HttpOk.into()
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let srv = TestServer::new(|app| app.handler(index)); // <- Start new test server
|
||||
let url = srv.url("/"); // <- get handler url
|
||||
assert!(reqwest::get(&url).unwrap().status().is_success()); // <- make request
|
||||
let mut srv = TestServer::new(|app| app.handler(index)); // <- Start new test server
|
||||
|
||||
let request = srv.get().finish().unwrap(); // <- create client request
|
||||
let response = srv.execute(request.send()).unwrap(); // <- send request to the server
|
||||
assert!(response.status().is_success()); // <- check response
|
||||
|
||||
let bytes = srv.execute(response.body()).unwrap(); // <- read response body
|
||||
}
|
||||
```
|
||||
|
||||
@ -74,13 +77,14 @@ Other option is to use application factory. In this case you need to pass factor
|
||||
same as you use for real http server configuration.
|
||||
|
||||
```rust
|
||||
# extern crate http;
|
||||
# extern crate actix_web;
|
||||
extern crate reqwest;
|
||||
use http::Method;
|
||||
use actix_web::*;
|
||||
use actix_web::test::TestServer;
|
||||
|
||||
fn index(req: HttpRequest) -> HttpResponse {
|
||||
httpcodes::HTTPOk.into()
|
||||
httpcodes::HttpOk.into()
|
||||
}
|
||||
|
||||
/// This function get called by http server.
|
||||
@ -90,8 +94,61 @@ fn create_app() -> Application {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let srv = TestServer::with_factory(create_app); // <- Start new test server
|
||||
let url = srv.url("/test"); // <- get handler url
|
||||
assert!(reqwest::get(&url).unwrap().status().is_success()); // <- make request
|
||||
let mut srv = TestServer::with_factory(create_app); // <- Start new test server
|
||||
|
||||
let request = srv.client(Method::GET, "/test").finish().unwrap(); // <- create client request
|
||||
let response = srv.execute(request.send()).unwrap(); // <- send request to the server
|
||||
|
||||
assert!(response.status().is_success()); // <- check response
|
||||
}
|
||||
```
|
||||
|
||||
## WebSocket server tests
|
||||
|
||||
It is possible to register *handler* with `TestApp::handler()` method that
|
||||
initiate web socket connection. *TestServer* provides `ws()` which connects to
|
||||
websocket server and returns ws reader and writer objects. *TestServer* also
|
||||
provides `execute()` method which runs future object to completion and returns
|
||||
result of the future computation.
|
||||
|
||||
Here is simple example, that shows how to test server websocket handler.
|
||||
|
||||
```rust
|
||||
# extern crate actix;
|
||||
# extern crate actix_web;
|
||||
# extern crate futures;
|
||||
# extern crate http;
|
||||
# extern crate bytes;
|
||||
|
||||
use actix_web::*;
|
||||
use futures::Stream;
|
||||
# use actix::prelude::*;
|
||||
|
||||
struct Ws; // <- WebSocket actor
|
||||
|
||||
impl Actor for Ws {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
}
|
||||
|
||||
impl StreamHandler<ws::Message, ws::WsError> for Ws {
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
match msg {
|
||||
ws::Message::Text(text) => ctx.text(text),
|
||||
_ => (),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
let mut srv = test::TestServer::new( // <- start our server with ws handler
|
||||
|app| app.handler(|req| ws::start(req, Ws)));
|
||||
|
||||
let (reader, mut writer) = srv.ws().unwrap(); // <- connect to ws server
|
||||
|
||||
writer.text("text"); // <- send message to server
|
||||
|
||||
let (item, reader) = srv.execute(reader.into_future()).unwrap(); // <- wait for one message
|
||||
assert_eq!(item, Some(ws::Message::Text("text".to_owned())));
|
||||
}
|
||||
```
|
||||
|
@ -3,7 +3,7 @@
|
||||
Actix supports WebSockets out-of-the-box. It is possible to convert request's `Payload`
|
||||
to a stream of [*ws::Message*](../actix_web/ws/enum.Message.html) with
|
||||
a [*ws::WsStream*](../actix_web/ws/struct.WsStream.html) and then use stream
|
||||
combinators to handle actual messages. But it is simplier to handle websocket communications
|
||||
combinators to handle actual messages. But it is simpler to handle websocket communications
|
||||
with http actor.
|
||||
|
||||
This is example of simple websocket echo server:
|
||||
@ -21,14 +21,13 @@ impl Actor for Ws {
|
||||
type Context = ws::WebsocketContext<Self>;
|
||||
}
|
||||
|
||||
/// Define Handler for ws::Message message
|
||||
impl Handler<ws::Message> for Ws {
|
||||
type Result=();
|
||||
/// Handler for ws::Message message
|
||||
impl StreamHandler<ws::Message, ws::ProtocolError> for Ws {
|
||||
|
||||
fn handle(&mut self, msg: ws::Message, ctx: &mut Self::Context) {
|
||||
match msg {
|
||||
ws::Message::Ping(msg) => ctx.pong(&msg),
|
||||
ws::Message::Text(text) => ctx.text(&text),
|
||||
ws::Message::Text(text) => ctx.text(text),
|
||||
ws::Message::Binary(bin) => ctx.binary(bin),
|
||||
_ => (),
|
||||
}
|
||||
@ -43,7 +42,7 @@ fn main() {
|
||||
```
|
||||
|
||||
Simple websocket echo server example is available in
|
||||
[examples directory](https://github.com/actix/actix-web/blob/master/examples/websocket.rs).
|
||||
[examples directory](https://github.com/actix/actix-web/blob/master/examples/websocket).
|
||||
|
||||
Example chat server with ability to chat over websocket connection or tcp connection
|
||||
is available in [websocket-chat directory](https://github.com/actix/actix-web/tree/master/examples/websocket-chat/)
|
||||
|
@ -6,6 +6,7 @@ use std::collections::HashMap;
|
||||
use handler::Reply;
|
||||
use router::{Router, Pattern};
|
||||
use resource::Resource;
|
||||
use headers::ContentEncoding;
|
||||
use handler::{Handler, RouteHandler, WrapHandler};
|
||||
use httprequest::HttpRequest;
|
||||
use pipeline::{Pipeline, PipelineHandler};
|
||||
@ -24,6 +25,7 @@ pub struct HttpApplication<S=()> {
|
||||
pub(crate) struct Inner<S> {
|
||||
prefix: usize,
|
||||
default: Resource<S>,
|
||||
encoding: ContentEncoding,
|
||||
router: Router,
|
||||
resources: Vec<Resource<S>>,
|
||||
handlers: Vec<(String, Box<RouteHandler<S>>)>,
|
||||
@ -31,6 +33,10 @@ pub(crate) struct Inner<S> {
|
||||
|
||||
impl<S: 'static> PipelineHandler<S> for Inner<S> {
|
||||
|
||||
fn encoding(&self) -> ContentEncoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
fn handle(&mut self, mut req: HttpRequest<S>) -> Reply {
|
||||
if let Some(idx) = self.router.recognize(&mut req) {
|
||||
self.resources[idx].handle(req.clone(), Some(&mut self.default))
|
||||
@ -38,8 +44,9 @@ impl<S: 'static> PipelineHandler<S> for Inner<S> {
|
||||
for &mut (ref prefix, ref mut handler) in &mut self.handlers {
|
||||
let m = {
|
||||
let path = &req.path()[self.prefix..];
|
||||
path.starts_with(prefix) && (path.len() == prefix.len() ||
|
||||
path.split_at(prefix.len()).1.starts_with('/'))
|
||||
path.starts_with(prefix) && (
|
||||
path.len() == prefix.len() ||
|
||||
path.split_at(prefix.len()).1.starts_with('/'))
|
||||
};
|
||||
if m {
|
||||
let path: &'static str = unsafe {
|
||||
@ -59,9 +66,11 @@ impl<S: 'static> PipelineHandler<S> for Inner<S> {
|
||||
|
||||
#[cfg(test)]
|
||||
impl<S: 'static> HttpApplication<S> {
|
||||
#[cfg(test)]
|
||||
pub(crate) fn run(&mut self, req: HttpRequest<S>) -> Reply {
|
||||
self.inner.borrow_mut().handle(req)
|
||||
}
|
||||
#[cfg(test)]
|
||||
pub(crate) fn prepare_request(&self, req: HttpRequest) -> HttpRequest<S> {
|
||||
req.with_state(Rc::clone(&self.state), self.router.clone())
|
||||
}
|
||||
@ -92,9 +101,10 @@ struct ApplicationParts<S> {
|
||||
prefix: String,
|
||||
settings: ServerSettings,
|
||||
default: Resource<S>,
|
||||
resources: HashMap<Pattern, Option<Resource<S>>>,
|
||||
resources: Vec<(Pattern, Option<Resource<S>>)>,
|
||||
handlers: Vec<(String, Box<RouteHandler<S>>)>,
|
||||
external: HashMap<String, Pattern>,
|
||||
encoding: ContentEncoding,
|
||||
middlewares: Vec<Box<Middleware<S>>>,
|
||||
}
|
||||
|
||||
@ -114,9 +124,10 @@ impl Application<()> {
|
||||
prefix: "/".to_owned(),
|
||||
settings: ServerSettings::default(),
|
||||
default: Resource::default_not_found(),
|
||||
resources: HashMap::new(),
|
||||
resources: Vec::new(),
|
||||
handlers: Vec::new(),
|
||||
external: HashMap::new(),
|
||||
encoding: ContentEncoding::Auto,
|
||||
middlewares: Vec::new(),
|
||||
})
|
||||
}
|
||||
@ -134,19 +145,20 @@ impl<S> Application<S> where S: 'static {
|
||||
/// Create application with specific state. Application can be
|
||||
/// configured with builder-like pattern.
|
||||
///
|
||||
/// State is shared with all reousrces within same application and could be
|
||||
/// State is shared with all resources within same application and could be
|
||||
/// accessed with `HttpRequest::state()` method.
|
||||
pub fn with_state(state: S) -> Application<S> {
|
||||
Application {
|
||||
parts: Some(ApplicationParts {
|
||||
state: state,
|
||||
state,
|
||||
prefix: "/".to_owned(),
|
||||
settings: ServerSettings::default(),
|
||||
default: Resource::default_not_found(),
|
||||
resources: HashMap::new(),
|
||||
resources: Vec::new(),
|
||||
handlers: Vec::new(),
|
||||
external: HashMap::new(),
|
||||
middlewares: Vec::new(),
|
||||
encoding: ContentEncoding::Auto,
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -154,7 +166,7 @@ impl<S> Application<S> where S: 'static {
|
||||
/// Set application prefix
|
||||
///
|
||||
/// Only requests that matches application's prefix get processed by this application.
|
||||
/// Application prefix always contains laading "/" slash. If supplied prefix
|
||||
/// Application prefix always contains leading "/" slash. If supplied prefix
|
||||
/// does not contain leading slash, it get inserted. Prefix should
|
||||
/// consists valid path segments. i.e for application with
|
||||
/// prefix `/app` any request with following paths `/app`, `/app/` or `/app/test`
|
||||
@ -172,8 +184,8 @@ impl<S> Application<S> where S: 'static {
|
||||
/// let app = Application::new()
|
||||
/// .prefix("/app")
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HTTPMethodNotAllowed);
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HttpOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HttpMethodNotAllowed);
|
||||
/// })
|
||||
/// .finish();
|
||||
/// }
|
||||
@ -215,8 +227,8 @@ impl<S> Application<S> where S: 'static {
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
/// .resource("/test", |r| {
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HTTPOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HTTPMethodNotAllowed);
|
||||
/// r.method(Method::GET).f(|_| httpcodes::HttpOk);
|
||||
/// r.method(Method::HEAD).f(|_| httpcodes::HttpMethodNotAllowed);
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
@ -230,12 +242,8 @@ impl<S> Application<S> where S: 'static {
|
||||
let mut resource = Resource::default();
|
||||
f(&mut resource);
|
||||
|
||||
let pattern = Pattern::new(resource.get_name(), path, "^/");
|
||||
if parts.resources.contains_key(&pattern) {
|
||||
panic!("Resource {:?} is registered.", path);
|
||||
}
|
||||
|
||||
parts.resources.insert(pattern, Some(resource));
|
||||
let pattern = Pattern::new(resource.get_name(), path);
|
||||
parts.resources.push((pattern, Some(resource)));
|
||||
}
|
||||
self
|
||||
}
|
||||
@ -251,6 +259,16 @@ impl<S> Application<S> where S: 'static {
|
||||
self
|
||||
}
|
||||
|
||||
/// Set default content encoding. `ContentEncoding::Auto` is set by default.
|
||||
pub fn default_encoding<F>(mut self, encoding: ContentEncoding) -> Application<S>
|
||||
{
|
||||
{
|
||||
let parts = self.parts.as_mut().expect("Use after finish");
|
||||
parts.encoding = encoding;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Register external resource.
|
||||
///
|
||||
/// External resources are useful for URL generation purposes only and
|
||||
@ -264,7 +282,7 @@ impl<S> Application<S> where S: 'static {
|
||||
/// fn index(mut req: HttpRequest) -> Result<HttpResponse> {
|
||||
/// let url = req.url_for("youtube", &["oHg5SJYRHA0"])?;
|
||||
/// assert_eq!(url.as_str(), "https://youtube.com/watch/oHg5SJYRHA0");
|
||||
/// Ok(httpcodes::HTTPOk.into())
|
||||
/// Ok(httpcodes::HttpOk.into())
|
||||
/// }
|
||||
///
|
||||
/// fn main() {
|
||||
@ -284,7 +302,7 @@ impl<S> Application<S> where S: 'static {
|
||||
panic!("External resource {:?} is registered.", name.as_ref());
|
||||
}
|
||||
parts.external.insert(
|
||||
String::from(name.as_ref()), Pattern::new(name.as_ref(), url.as_ref(), "^/"));
|
||||
String::from(name.as_ref()), Pattern::new(name.as_ref(), url.as_ref()));
|
||||
}
|
||||
self
|
||||
}
|
||||
@ -303,9 +321,9 @@ impl<S> Application<S> where S: 'static {
|
||||
/// let app = Application::new()
|
||||
/// .handler("/app", |req: HttpRequest| {
|
||||
/// match *req.method() {
|
||||
/// Method::GET => httpcodes::HTTPOk,
|
||||
/// Method::POST => httpcodes::HTTPMethodNotAllowed,
|
||||
/// _ => httpcodes::HTTPNotFound,
|
||||
/// Method::GET => httpcodes::HttpOk,
|
||||
/// Method::POST => httpcodes::HttpMethodNotAllowed,
|
||||
/// _ => httpcodes::HttpNotFound,
|
||||
/// }});
|
||||
/// }
|
||||
/// ```
|
||||
@ -333,7 +351,7 @@ impl<S> Application<S> where S: 'static {
|
||||
|
||||
let mut resources = parts.resources;
|
||||
for (_, pattern) in parts.external {
|
||||
resources.insert(pattern, None);
|
||||
resources.push((pattern, None));
|
||||
}
|
||||
|
||||
let (router, resources) = Router::new(prefix, parts.settings, resources);
|
||||
@ -342,20 +360,55 @@ impl<S> Application<S> where S: 'static {
|
||||
Inner {
|
||||
prefix: prefix.len(),
|
||||
default: parts.default,
|
||||
encoding: parts.encoding,
|
||||
router: router.clone(),
|
||||
resources: resources,
|
||||
handlers: parts.handlers,
|
||||
resources,
|
||||
}
|
||||
));
|
||||
|
||||
HttpApplication {
|
||||
state: Rc::new(parts.state),
|
||||
prefix: prefix.to_owned(),
|
||||
inner: inner,
|
||||
router: router.clone(),
|
||||
middlewares: Rc::new(parts.middlewares),
|
||||
inner,
|
||||
}
|
||||
}
|
||||
|
||||
/// Convenience method for creating `Box<HttpHandler>` instance.
|
||||
///
|
||||
/// This method is useful if you need to register several application instances
|
||||
/// with different state.
|
||||
///
|
||||
/// ```rust
|
||||
/// # use std::thread;
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::*;
|
||||
///
|
||||
/// struct State1;
|
||||
///
|
||||
/// struct State2;
|
||||
///
|
||||
/// fn main() {
|
||||
/// # thread::spawn(|| {
|
||||
/// HttpServer::new(|| { vec![
|
||||
/// Application::with_state(State1)
|
||||
/// .prefix("/app1")
|
||||
/// .resource("/", |r| r.h(httpcodes::HttpOk))
|
||||
/// .boxed(),
|
||||
/// Application::with_state(State2)
|
||||
/// .prefix("/app2")
|
||||
/// .resource("/", |r| r.h(httpcodes::HttpOk))
|
||||
/// .boxed() ]})
|
||||
/// .bind("127.0.0.1:8080").unwrap()
|
||||
/// .run()
|
||||
/// # });
|
||||
/// }
|
||||
/// ```
|
||||
pub fn boxed(mut self) -> Box<HttpHandler> {
|
||||
Box::new(self.finish())
|
||||
}
|
||||
}
|
||||
|
||||
impl<S: 'static> IntoHttpHandler for Application<S> {
|
||||
@ -407,7 +460,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_default_resource() {
|
||||
let mut app = Application::new()
|
||||
.resource("/test", |r| r.h(httpcodes::HTTPOk))
|
||||
.resource("/test", |r| r.h(httpcodes::HttpOk))
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
@ -419,7 +472,7 @@ mod tests {
|
||||
assert_eq!(resp.as_response().unwrap().status(), StatusCode::NOT_FOUND);
|
||||
|
||||
let mut app = Application::new()
|
||||
.default_resource(|r| r.h(httpcodes::HTTPMethodNotAllowed))
|
||||
.default_resource(|r| r.h(httpcodes::HttpMethodNotAllowed))
|
||||
.finish();
|
||||
let req = TestRequest::with_uri("/blah").finish();
|
||||
let resp = app.run(req);
|
||||
@ -430,7 +483,7 @@ mod tests {
|
||||
fn test_unhandled_prefix() {
|
||||
let mut app = Application::new()
|
||||
.prefix("/test")
|
||||
.resource("/test", |r| r.h(httpcodes::HTTPOk))
|
||||
.resource("/test", |r| r.h(httpcodes::HttpOk))
|
||||
.finish();
|
||||
assert!(app.handle(HttpRequest::default()).is_err());
|
||||
}
|
||||
@ -438,7 +491,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_state() {
|
||||
let mut app = Application::with_state(10)
|
||||
.resource("/", |r| r.h(httpcodes::HTTPOk))
|
||||
.resource("/", |r| r.h(httpcodes::HttpOk))
|
||||
.finish();
|
||||
let req = HttpRequest::default().with_state(Rc::clone(&app.state), app.router.clone());
|
||||
let resp = app.run(req);
|
||||
@ -449,7 +502,7 @@ mod tests {
|
||||
fn test_prefix() {
|
||||
let mut app = Application::new()
|
||||
.prefix("/test")
|
||||
.resource("/blah", |r| r.h(httpcodes::HTTPOk))
|
||||
.resource("/blah", |r| r.h(httpcodes::HttpOk))
|
||||
.finish();
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
let resp = app.handle(req);
|
||||
@ -471,7 +524,7 @@ mod tests {
|
||||
#[test]
|
||||
fn test_handler() {
|
||||
let mut app = Application::new()
|
||||
.handler("/test", httpcodes::HTTPOk)
|
||||
.handler("/test", httpcodes::HttpOk)
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
@ -499,7 +552,7 @@ mod tests {
|
||||
fn test_handler_prefix() {
|
||||
let mut app = Application::new()
|
||||
.prefix("/app")
|
||||
.handler("/test", httpcodes::HTTPOk)
|
||||
.handler("/test", httpcodes::HttpOk)
|
||||
.finish();
|
||||
|
||||
let req = TestRequest::with_uri("/test").finish();
|
||||
|
47
src/body.rs
47
src/body.rs
@ -1,4 +1,4 @@
|
||||
use std::fmt;
|
||||
use std::{fmt, mem};
|
||||
use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
@ -31,11 +31,13 @@ pub enum Binary {
|
||||
Bytes(Bytes),
|
||||
/// Static slice
|
||||
Slice(&'static [u8]),
|
||||
/// Shared stirng body
|
||||
/// Shared string body
|
||||
SharedString(Rc<String>),
|
||||
/// Shared string body
|
||||
#[doc(hidden)]
|
||||
ArcSharedString(Arc<String>),
|
||||
/// Shared vec body
|
||||
SharedVec(Arc<Vec<u8>>),
|
||||
}
|
||||
|
||||
impl Body {
|
||||
@ -115,6 +117,7 @@ impl Binary {
|
||||
Binary::Slice(slice) => slice.len(),
|
||||
Binary::SharedString(ref s) => s.len(),
|
||||
Binary::ArcSharedString(ref s) => s.len(),
|
||||
Binary::SharedVec(ref s) => s.len(),
|
||||
}
|
||||
}
|
||||
|
||||
@ -122,6 +125,23 @@ impl Binary {
|
||||
pub fn from_slice(s: &[u8]) -> Binary {
|
||||
Binary::Bytes(Bytes::from(s))
|
||||
}
|
||||
|
||||
/// Convert Binary to a Bytes instance
|
||||
pub fn take(&mut self) -> Bytes {
|
||||
mem::replace(self, Binary::Slice(b"")).into()
|
||||
}
|
||||
}
|
||||
|
||||
impl Clone for Binary {
|
||||
fn clone(&self) -> Binary {
|
||||
match *self {
|
||||
Binary::Bytes(ref bytes) => Binary::Bytes(bytes.clone()),
|
||||
Binary::Slice(slice) => Binary::Bytes(Bytes::from(slice)),
|
||||
Binary::SharedString(ref s) => Binary::SharedString(s.clone()),
|
||||
Binary::ArcSharedString(ref s) => Binary::ArcSharedString(s.clone()),
|
||||
Binary::SharedVec(ref s) => Binary::SharedVec(s.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Into<Bytes> for Binary {
|
||||
@ -131,6 +151,7 @@ impl Into<Bytes> for Binary {
|
||||
Binary::Slice(slice) => Bytes::from(slice),
|
||||
Binary::SharedString(s) => Bytes::from(s.as_str()),
|
||||
Binary::ArcSharedString(s) => Bytes::from(s.as_str()),
|
||||
Binary::SharedVec(s) => Bytes::from(AsRef::<[u8]>::as_ref(s.as_ref())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -201,6 +222,18 @@ impl<'a> From<&'a Arc<String>> for Binary {
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Arc<Vec<u8>>> for Binary {
|
||||
fn from(body: Arc<Vec<u8>>) -> Binary {
|
||||
Binary::SharedVec(body)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Arc<Vec<u8>>> for Binary {
|
||||
fn from(body: &'a Arc<Vec<u8>>) -> Binary {
|
||||
Binary::SharedVec(Arc::clone(body))
|
||||
}
|
||||
}
|
||||
|
||||
impl AsRef<[u8]> for Binary {
|
||||
fn as_ref(&self) -> &[u8] {
|
||||
match *self {
|
||||
@ -208,6 +241,7 @@ impl AsRef<[u8]> for Binary {
|
||||
Binary::Slice(slice) => slice,
|
||||
Binary::SharedString(ref s) => s.as_bytes(),
|
||||
Binary::ArcSharedString(ref s) => s.as_bytes(),
|
||||
Binary::SharedVec(ref s) => s.as_ref().as_ref(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -288,6 +322,15 @@ mod tests {
|
||||
assert_eq!(Binary::from(&b).as_ref(), "test".as_bytes());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_shared_vec() {
|
||||
let b = Arc::new(Vec::from(&b"test"[..]));
|
||||
assert_eq!(Binary::from(b.clone()).len(), 4);
|
||||
assert_eq!(Binary::from(b.clone()).as_ref(), &b"test"[..]);
|
||||
assert_eq!(Binary::from(&b).len(), 4);
|
||||
assert_eq!(Binary::from(&b).as_ref(), &b"test"[..]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_bytes_mut() {
|
||||
let b = BytesMut::from("test");
|
||||
|
359
src/client/connector.rs
Normal file
359
src/client/connector.rs
Normal file
@ -0,0 +1,359 @@
|
||||
use std::{io, time};
|
||||
use std::net::Shutdown;
|
||||
use std::time::Duration;
|
||||
|
||||
use actix::{fut, Actor, ActorFuture, Context,
|
||||
Handler, Message, ActorResponse, Supervised};
|
||||
use actix::registry::ArbiterService;
|
||||
use actix::fut::WrapFuture;
|
||||
use actix::actors::{Connector, ConnectorError, Connect as ResolveConnect};
|
||||
|
||||
use http::{Uri, HttpTryFrom, Error as HttpError};
|
||||
use futures::Poll;
|
||||
use tokio_io::{AsyncRead, AsyncWrite};
|
||||
|
||||
#[cfg(feature="alpn")]
|
||||
use openssl::ssl::{SslMethod, SslConnector, Error as OpensslError};
|
||||
#[cfg(feature="alpn")]
|
||||
use tokio_openssl::SslConnectorExt;
|
||||
#[cfg(feature="alpn")]
|
||||
use futures::Future;
|
||||
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
use native_tls::{TlsConnector, Error as TlsError};
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
use tokio_tls::TlsConnectorExt;
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
use futures::Future;
|
||||
|
||||
use {HAS_OPENSSL, HAS_TLS};
|
||||
use server::IoStream;
|
||||
|
||||
|
||||
#[derive(Debug)]
|
||||
/// `Connect` type represents message that can be send to `ClientConnector`
|
||||
/// with connection request.
|
||||
pub struct Connect {
|
||||
pub uri: Uri,
|
||||
pub conn_timeout: Duration,
|
||||
}
|
||||
|
||||
impl Connect {
|
||||
/// Create `Connect` message for specified `Uri`
|
||||
pub fn new<U>(uri: U) -> Result<Connect, HttpError> where Uri: HttpTryFrom<U> {
|
||||
Ok(Connect {
|
||||
uri: Uri::try_from(uri).map_err(|e| e.into())?,
|
||||
conn_timeout: Duration::from_secs(1)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Message for Connect {
|
||||
type Result = Result<Connection, ClientConnectorError>;
|
||||
}
|
||||
|
||||
/// A set of errors that can occur during connecting to a http host
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum ClientConnectorError {
|
||||
/// Invalid url
|
||||
#[fail(display="Invalid url")]
|
||||
InvalidUrl,
|
||||
|
||||
/// SSL feature is not enabled
|
||||
#[fail(display="SSL is not supported")]
|
||||
SslIsNotSupported,
|
||||
|
||||
/// SSL error
|
||||
#[cfg(feature="alpn")]
|
||||
#[fail(display="{}", _0)]
|
||||
SslError(#[cause] OpensslError),
|
||||
|
||||
/// SSL error
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
#[fail(display="{}", _0)]
|
||||
SslError(#[cause] TlsError),
|
||||
|
||||
/// Connection error
|
||||
#[fail(display = "{}", _0)]
|
||||
Connector(#[cause] ConnectorError),
|
||||
|
||||
/// Connection took too long
|
||||
#[fail(display = "Timeout out while establishing connection")]
|
||||
Timeout,
|
||||
|
||||
/// Connector has been disconnected
|
||||
#[fail(display = "Internal error: connector has been disconnected")]
|
||||
Disconnected,
|
||||
|
||||
/// Connection io error
|
||||
#[fail(display = "{}", _0)]
|
||||
IoError(#[cause] io::Error),
|
||||
}
|
||||
|
||||
impl From<ConnectorError> for ClientConnectorError {
|
||||
fn from(err: ConnectorError) -> ClientConnectorError {
|
||||
match err {
|
||||
ConnectorError::Timeout => ClientConnectorError::Timeout,
|
||||
_ => ClientConnectorError::Connector(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ClientConnector {
|
||||
#[cfg(all(feature="alpn"))]
|
||||
connector: SslConnector,
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
connector: TlsConnector,
|
||||
}
|
||||
|
||||
impl Actor for ClientConnector {
|
||||
type Context = Context<ClientConnector>;
|
||||
}
|
||||
|
||||
impl Supervised for ClientConnector {}
|
||||
|
||||
impl ArbiterService for ClientConnector {}
|
||||
|
||||
impl Default for ClientConnector {
|
||||
fn default() -> ClientConnector {
|
||||
#[cfg(all(feature="alpn"))]
|
||||
{
|
||||
let builder = SslConnector::builder(SslMethod::tls()).unwrap();
|
||||
ClientConnector {
|
||||
connector: builder.build()
|
||||
}
|
||||
}
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
{
|
||||
let builder = TlsConnector::builder().unwrap();
|
||||
ClientConnector {
|
||||
connector: builder.build().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature="alpn", feature="tls")))]
|
||||
ClientConnector {}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientConnector {
|
||||
|
||||
#[cfg(feature="alpn")]
|
||||
/// Create `ClientConnector` actor with custom `SslConnector` instance.
|
||||
///
|
||||
/// By default `ClientConnector` uses very simple ssl configuration.
|
||||
/// With `with_connector` method it is possible to use custom `SslConnector`
|
||||
/// object.
|
||||
///
|
||||
/// ```rust
|
||||
/// # #![cfg(feature="alpn")]
|
||||
/// # extern crate actix;
|
||||
/// # extern crate actix_web;
|
||||
/// # extern crate futures;
|
||||
/// # use futures::Future;
|
||||
/// # use std::io::Write;
|
||||
/// extern crate openssl;
|
||||
/// use actix::prelude::*;
|
||||
/// use actix_web::client::{Connect, ClientConnector};
|
||||
///
|
||||
/// use openssl::ssl::{SslMethod, SslConnector};
|
||||
///
|
||||
/// fn main() {
|
||||
/// let sys = System::new("test");
|
||||
///
|
||||
/// // Start `ClientConnector` with custom `SslConnector`
|
||||
/// let ssl_conn = SslConnector::builder(SslMethod::tls()).unwrap().build();
|
||||
/// let conn: Address<_> = ClientConnector::with_connector(ssl_conn).start();
|
||||
///
|
||||
/// Arbiter::handle().spawn({
|
||||
/// conn.send(
|
||||
/// Connect::new("https://www.rust-lang.org").unwrap()) // <- connect to host
|
||||
/// .map_err(|_| ())
|
||||
/// .and_then(|res| {
|
||||
/// if let Ok(mut stream) = res {
|
||||
/// stream.write_all(b"GET / HTTP/1.0\r\n\r\n").unwrap();
|
||||
/// }
|
||||
/// # Arbiter::system().do_send(actix::msgs::SystemExit(0));
|
||||
/// Ok(())
|
||||
/// })
|
||||
/// });
|
||||
///
|
||||
/// sys.run();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn with_connector(connector: SslConnector) -> ClientConnector {
|
||||
ClientConnector { connector }
|
||||
}
|
||||
}
|
||||
|
||||
impl Handler<Connect> for ClientConnector {
|
||||
type Result = ActorResponse<ClientConnector, Connection, ClientConnectorError>;
|
||||
|
||||
fn handle(&mut self, msg: Connect, _: &mut Self::Context) -> Self::Result {
|
||||
let uri = &msg.uri;
|
||||
let conn_timeout = msg.conn_timeout;
|
||||
|
||||
// host name is required
|
||||
if uri.host().is_none() {
|
||||
return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl))
|
||||
}
|
||||
|
||||
// supported protocols
|
||||
let proto = match uri.scheme_part() {
|
||||
Some(scheme) => match Protocol::from(scheme.as_str()) {
|
||||
Some(proto) => proto,
|
||||
None => return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)),
|
||||
},
|
||||
None => return ActorResponse::reply(Err(ClientConnectorError::InvalidUrl)),
|
||||
};
|
||||
|
||||
// check ssl availability
|
||||
if proto.is_secure() && !HAS_OPENSSL && !HAS_TLS {
|
||||
return ActorResponse::reply(Err(ClientConnectorError::SslIsNotSupported))
|
||||
}
|
||||
|
||||
let host = uri.host().unwrap().to_owned();
|
||||
let port = uri.port().unwrap_or_else(|| proto.port());
|
||||
|
||||
ActorResponse::async(
|
||||
Connector::from_registry()
|
||||
.send(ResolveConnect::host_and_port(&host, port)
|
||||
.timeout(conn_timeout))
|
||||
.into_actor(self)
|
||||
.map_err(|_, _, _| ClientConnectorError::Disconnected)
|
||||
.and_then(move |res, _act, _| {
|
||||
#[cfg(feature="alpn")]
|
||||
match res {
|
||||
Err(err) => fut::Either::B(fut::err(err.into())),
|
||||
Ok(stream) => {
|
||||
if proto.is_secure() {
|
||||
fut::Either::A(
|
||||
_act.connector.connect_async(&host, stream)
|
||||
.map_err(ClientConnectorError::SslError)
|
||||
.map(|stream| Connection{stream: Box::new(stream)})
|
||||
.into_actor(_act))
|
||||
} else {
|
||||
fut::Either::B(fut::ok(Connection{stream: Box::new(stream)}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(all(feature="tls", not(feature="alpn")))]
|
||||
match res {
|
||||
Err(err) => fut::Either::B(fut::err(err.into())),
|
||||
Ok(stream) => {
|
||||
if proto.is_secure() {
|
||||
fut::Either::A(
|
||||
_act.connector.connect_async(&host, stream)
|
||||
.map_err(ClientConnectorError::SslError)
|
||||
.map(|stream| Connection{stream: Box::new(stream)})
|
||||
.into_actor(_act))
|
||||
} else {
|
||||
fut::Either::B(fut::ok(Connection{stream: Box::new(stream)}))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(any(feature="alpn", feature="tls")))]
|
||||
match res {
|
||||
Err(err) => fut::err(err.into()),
|
||||
Ok(stream) => {
|
||||
if proto.is_secure() {
|
||||
fut::err(ClientConnectorError::SslIsNotSupported)
|
||||
} else {
|
||||
fut::ok(Connection{stream: Box::new(stream)})
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Hash, Debug, Clone, Copy)]
|
||||
enum Protocol {
|
||||
Http,
|
||||
Https,
|
||||
Ws,
|
||||
Wss,
|
||||
}
|
||||
|
||||
impl Protocol {
|
||||
fn from(s: &str) -> Option<Protocol> {
|
||||
match s {
|
||||
"http" => Some(Protocol::Http),
|
||||
"https" => Some(Protocol::Https),
|
||||
"ws" => Some(Protocol::Ws),
|
||||
"wss" => Some(Protocol::Wss),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn is_secure(&self) -> bool {
|
||||
match *self {
|
||||
Protocol::Https | Protocol::Wss => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn port(&self) -> u16 {
|
||||
match *self {
|
||||
Protocol::Http | Protocol::Ws => 80,
|
||||
Protocol::Https | Protocol::Wss => 443
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub struct Connection {
|
||||
stream: Box<IoStream>,
|
||||
}
|
||||
|
||||
impl Connection {
|
||||
pub fn stream(&mut self) -> &mut IoStream {
|
||||
&mut *self.stream
|
||||
}
|
||||
|
||||
pub fn from_stream<T: IoStream>(io: T) -> Connection {
|
||||
Connection{stream: Box::new(io)}
|
||||
}
|
||||
}
|
||||
|
||||
impl IoStream for Connection {
|
||||
fn shutdown(&mut self, how: Shutdown) -> io::Result<()> {
|
||||
IoStream::shutdown(&mut *self.stream, how)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_nodelay(&mut self, nodelay: bool) -> io::Result<()> {
|
||||
IoStream::set_nodelay(&mut *self.stream, nodelay)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn set_linger(&mut self, dur: Option<time::Duration>) -> io::Result<()> {
|
||||
IoStream::set_linger(&mut *self.stream, dur)
|
||||
}
|
||||
}
|
||||
|
||||
impl io::Read for Connection {
|
||||
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
||||
self.stream.read(buf)
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncRead for Connection {}
|
||||
|
||||
impl io::Write for Connection {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
self.stream.write(buf)
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
self.stream.flush()
|
||||
}
|
||||
}
|
||||
|
||||
impl AsyncWrite for Connection {
|
||||
fn shutdown(&mut self) -> Poll<(), io::Error> {
|
||||
self.stream.shutdown()
|
||||
}
|
||||
}
|
31
src/client/mod.rs
Normal file
31
src/client/mod.rs
Normal file
@ -0,0 +1,31 @@
|
||||
//! Http client
|
||||
mod connector;
|
||||
mod parser;
|
||||
mod request;
|
||||
mod response;
|
||||
mod pipeline;
|
||||
mod writer;
|
||||
|
||||
pub use self::pipeline::{SendRequest, SendRequestError};
|
||||
pub use self::request::{ClientRequest, ClientRequestBuilder};
|
||||
pub use self::response::ClientResponse;
|
||||
pub use self::connector::{Connect, Connection, ClientConnector, ClientConnectorError};
|
||||
pub(crate) use self::writer::HttpClientWriter;
|
||||
pub(crate) use self::parser::{HttpResponseParser, HttpResponseParserError};
|
||||
|
||||
|
||||
use httpcodes;
|
||||
use httpresponse::HttpResponse;
|
||||
use error::ResponseError;
|
||||
|
||||
|
||||
/// Convert `SendRequestError` to a `HttpResponse`
|
||||
impl ResponseError for SendRequestError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
SendRequestError::Connector(_) => httpcodes::HttpBadGateway.into(),
|
||||
_ => httpcodes::HttpInternalServerError.into(),
|
||||
}
|
||||
}
|
||||
}
|
193
src/client/parser.rs
Normal file
193
src/client/parser.rs
Normal file
@ -0,0 +1,193 @@
|
||||
use std::mem;
|
||||
use httparse;
|
||||
use http::{Version, HttpTryFrom, HeaderMap, StatusCode};
|
||||
use http::header::{self, HeaderName, HeaderValue};
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures::{Poll, Async};
|
||||
|
||||
use error::{ParseError, PayloadError};
|
||||
|
||||
use server::{utils, IoStream};
|
||||
use server::h1::{Decoder, chunked};
|
||||
|
||||
use super::ClientResponse;
|
||||
use super::response::ClientMessage;
|
||||
|
||||
const MAX_BUFFER_SIZE: usize = 131_072;
|
||||
const MAX_HEADERS: usize = 96;
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct HttpResponseParser {
|
||||
decoder: Option<Decoder>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Fail)]
|
||||
pub enum HttpResponseParserError {
|
||||
/// Server disconnected
|
||||
#[fail(display="Server disconnected")]
|
||||
Disconnect,
|
||||
#[fail(display="{}", _0)]
|
||||
Error(#[cause] ParseError),
|
||||
}
|
||||
|
||||
impl HttpResponseParser {
|
||||
|
||||
pub fn parse<T>(&mut self, io: &mut T, buf: &mut BytesMut)
|
||||
-> Poll<ClientResponse, HttpResponseParserError>
|
||||
where T: IoStream
|
||||
{
|
||||
// if buf is empty parse_message will always return NotReady, let's avoid that
|
||||
if buf.is_empty() {
|
||||
match utils::read_from_io(io, buf) {
|
||||
Ok(Async::Ready(0)) =>
|
||||
return Err(HttpResponseParserError::Disconnect),
|
||||
Ok(Async::Ready(_)) => (),
|
||||
Ok(Async::NotReady) =>
|
||||
return Ok(Async::NotReady),
|
||||
Err(err) =>
|
||||
return Err(HttpResponseParserError::Error(err.into()))
|
||||
}
|
||||
}
|
||||
|
||||
loop {
|
||||
match HttpResponseParser::parse_message(buf)
|
||||
.map_err(HttpResponseParserError::Error)?
|
||||
{
|
||||
Async::Ready((msg, decoder)) => {
|
||||
self.decoder = decoder;
|
||||
return Ok(Async::Ready(msg));
|
||||
},
|
||||
Async::NotReady => {
|
||||
if buf.capacity() >= MAX_BUFFER_SIZE {
|
||||
return Err(HttpResponseParserError::Error(ParseError::TooLarge));
|
||||
}
|
||||
match utils::read_from_io(io, buf) {
|
||||
Ok(Async::Ready(0)) =>
|
||||
return Err(HttpResponseParserError::Disconnect),
|
||||
Ok(Async::Ready(_)) => (),
|
||||
Ok(Async::NotReady) => return Ok(Async::NotReady),
|
||||
Err(err) =>
|
||||
return Err(HttpResponseParserError::Error(err.into())),
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn parse_payload<T>(&mut self, io: &mut T, buf: &mut BytesMut)
|
||||
-> Poll<Option<Bytes>, PayloadError>
|
||||
where T: IoStream
|
||||
{
|
||||
if self.decoder.is_some() {
|
||||
loop {
|
||||
// read payload
|
||||
let not_ready = match utils::read_from_io(io, buf) {
|
||||
Ok(Async::Ready(0)) => {
|
||||
if buf.is_empty() {
|
||||
return Err(PayloadError::Incomplete)
|
||||
}
|
||||
true
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
Ok(Async::NotReady) => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
match self.decoder.as_mut().unwrap().decode(buf) {
|
||||
Ok(Async::Ready(Some(b))) =>
|
||||
return Ok(Async::Ready(Some(b))),
|
||||
Ok(Async::Ready(None)) => {
|
||||
self.decoder.take();
|
||||
return Ok(Async::Ready(None))
|
||||
}
|
||||
Ok(Async::NotReady) => {
|
||||
if not_ready {
|
||||
return Ok(Async::NotReady)
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Ok(Async::Ready(None))
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_message(buf: &mut BytesMut)
|
||||
-> Poll<(ClientResponse, Option<Decoder>), ParseError>
|
||||
{
|
||||
// Parse http message
|
||||
let bytes_ptr = buf.as_ref().as_ptr() as usize;
|
||||
let mut headers: [httparse::Header; MAX_HEADERS] =
|
||||
unsafe{mem::uninitialized()};
|
||||
|
||||
let (len, version, status, headers_len) = {
|
||||
let b = unsafe{ let b: &[u8] = buf; mem::transmute(b) };
|
||||
let mut resp = httparse::Response::new(&mut headers);
|
||||
match resp.parse(b)? {
|
||||
httparse::Status::Complete(len) => {
|
||||
let version = if resp.version.unwrap() == 1 {
|
||||
Version::HTTP_11
|
||||
} else {
|
||||
Version::HTTP_10
|
||||
};
|
||||
let status = StatusCode::from_u16(resp.code.unwrap())
|
||||
.map_err(|_| ParseError::Status)?;
|
||||
|
||||
(len, version, status, resp.headers.len())
|
||||
}
|
||||
httparse::Status::Partial => return Ok(Async::NotReady),
|
||||
}
|
||||
};
|
||||
|
||||
let slice = buf.split_to(len).freeze();
|
||||
|
||||
// convert headers
|
||||
let mut hdrs = HeaderMap::new();
|
||||
for header in headers[..headers_len].iter() {
|
||||
if let Ok(name) = HeaderName::try_from(header.name) {
|
||||
let v_start = header.value.as_ptr() as usize - bytes_ptr;
|
||||
let v_end = v_start + header.value.len();
|
||||
let value = unsafe {
|
||||
HeaderValue::from_shared_unchecked(slice.slice(v_start, v_end)) };
|
||||
hdrs.append(name, value);
|
||||
} else {
|
||||
return Err(ParseError::Header)
|
||||
}
|
||||
}
|
||||
|
||||
let decoder = if status == StatusCode::SWITCHING_PROTOCOLS {
|
||||
Some(Decoder::eof())
|
||||
} else if let Some(len) = hdrs.get(header::CONTENT_LENGTH) {
|
||||
// Content-Length
|
||||
if let Ok(s) = len.to_str() {
|
||||
if let Ok(len) = s.parse::<u64>() {
|
||||
Some(Decoder::length(len))
|
||||
} else {
|
||||
debug!("illegal Content-Length: {:?}", len);
|
||||
return Err(ParseError::Header)
|
||||
}
|
||||
} else {
|
||||
debug!("illegal Content-Length: {:?}", len);
|
||||
return Err(ParseError::Header)
|
||||
}
|
||||
} else if chunked(&hdrs)? {
|
||||
// Chunked encoding
|
||||
Some(Decoder::chunked())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if let Some(decoder) = decoder {
|
||||
Ok(Async::Ready(
|
||||
(ClientResponse::new(
|
||||
ClientMessage{status, version,
|
||||
headers: hdrs, cookies: None}), Some(decoder))))
|
||||
} else {
|
||||
Ok(Async::Ready(
|
||||
(ClientResponse::new(
|
||||
ClientMessage{status, version,
|
||||
headers: hdrs, cookies: None}), None)))
|
||||
}
|
||||
}
|
||||
}
|
442
src/client/pipeline.rs
Normal file
442
src/client/pipeline.rs
Normal file
@ -0,0 +1,442 @@
|
||||
use std::{io, mem};
|
||||
use std::time::Duration;
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use http::header::CONTENT_ENCODING;
|
||||
use futures::{Async, Future, Poll};
|
||||
use futures::unsync::oneshot;
|
||||
use tokio_core::reactor::Timeout;
|
||||
|
||||
use actix::prelude::*;
|
||||
|
||||
use error::Error;
|
||||
use body::{Body, BodyStream};
|
||||
use context::{Frame, ActorHttpContext};
|
||||
use headers::ContentEncoding;
|
||||
use httpmessage::HttpMessage;
|
||||
use error::PayloadError;
|
||||
use server::WriterState;
|
||||
use server::shared::SharedBytes;
|
||||
use server::encoding::PayloadStream;
|
||||
use super::{ClientRequest, ClientResponse};
|
||||
use super::{Connect, Connection, ClientConnector, ClientConnectorError};
|
||||
use super::HttpClientWriter;
|
||||
use super::{HttpResponseParser, HttpResponseParserError};
|
||||
|
||||
/// A set of errors that can occur during sending request and reading response
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum SendRequestError {
|
||||
/// Response took too long
|
||||
#[fail(display = "Timeout out while waiting for response")]
|
||||
Timeout,
|
||||
/// Failed to connect to host
|
||||
#[fail(display="Failed to connect to host: {}", _0)]
|
||||
Connector(#[cause] ClientConnectorError),
|
||||
/// Error parsing response
|
||||
#[fail(display="{}", _0)]
|
||||
ParseError(#[cause] HttpResponseParserError),
|
||||
/// Error reading response payload
|
||||
#[fail(display="Error reading response payload: {}", _0)]
|
||||
Io(#[cause] io::Error),
|
||||
}
|
||||
|
||||
impl From<io::Error> for SendRequestError {
|
||||
fn from(err: io::Error) -> SendRequestError {
|
||||
SendRequestError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ClientConnectorError> for SendRequestError {
|
||||
fn from(err: ClientConnectorError) -> SendRequestError {
|
||||
match err {
|
||||
ClientConnectorError::Timeout => SendRequestError::Timeout,
|
||||
_ => SendRequestError::Connector(err),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
enum State {
|
||||
New,
|
||||
Connect(actix::dev::Request<Unsync, ClientConnector, Connect>),
|
||||
Connection(Connection),
|
||||
Send(Box<Pipeline>),
|
||||
None,
|
||||
}
|
||||
|
||||
/// `SendRequest` is a `Future` which represents asynchronous request sending process.
|
||||
#[must_use = "SendRequest does nothing unless polled"]
|
||||
pub struct SendRequest {
|
||||
req: ClientRequest,
|
||||
state: State,
|
||||
conn: Addr<Unsync, ClientConnector>,
|
||||
conn_timeout: Duration,
|
||||
timeout: Option<Timeout>,
|
||||
}
|
||||
|
||||
impl SendRequest {
|
||||
pub(crate) fn new(req: ClientRequest) -> SendRequest {
|
||||
SendRequest::with_connector(req, ClientConnector::from_registry())
|
||||
}
|
||||
|
||||
pub(crate) fn with_connector(req: ClientRequest, conn: Addr<Unsync, ClientConnector>)
|
||||
-> SendRequest
|
||||
{
|
||||
SendRequest{req, conn,
|
||||
state: State::New,
|
||||
timeout: None,
|
||||
conn_timeout: Duration::from_secs(1)
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn with_connection(req: ClientRequest, conn: Connection) -> SendRequest
|
||||
{
|
||||
SendRequest{req,
|
||||
state: State::Connection(conn),
|
||||
conn: ClientConnector::from_registry(),
|
||||
timeout: None,
|
||||
conn_timeout: Duration::from_secs(1),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set request timeout
|
||||
///
|
||||
/// Request timeout is a total time before response should be received.
|
||||
/// Default value is 5 seconds.
|
||||
pub fn timeout(mut self, timeout: Duration) -> Self {
|
||||
self.timeout = Some(Timeout::new(timeout, Arbiter::handle()).unwrap());
|
||||
self
|
||||
}
|
||||
|
||||
/// Set connection timeout
|
||||
///
|
||||
/// Connection timeout includes resolving hostname and actual connection to
|
||||
/// the host.
|
||||
/// Default value is 1 second.
|
||||
pub fn conn_timeout(mut self, timeout: Duration) -> Self {
|
||||
self.conn_timeout = timeout;
|
||||
self
|
||||
}
|
||||
|
||||
fn poll_timeout(&mut self) -> Poll<(), SendRequestError> {
|
||||
if self.timeout.is_none() {
|
||||
self.timeout = Some(Timeout::new(
|
||||
Duration::from_secs(5), Arbiter::handle()).unwrap());
|
||||
}
|
||||
|
||||
match self.timeout.as_mut().unwrap().poll() {
|
||||
Ok(Async::Ready(())) => Err(SendRequestError::Timeout),
|
||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
||||
Err(_) => unreachable!()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Future for SendRequest {
|
||||
type Item = ClientResponse;
|
||||
type Error = SendRequestError;
|
||||
|
||||
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
|
||||
self.poll_timeout()?;
|
||||
|
||||
loop {
|
||||
let state = mem::replace(&mut self.state, State::None);
|
||||
|
||||
match state {
|
||||
State::New =>
|
||||
self.state = State::Connect(self.conn.send(Connect {
|
||||
uri: self.req.uri().clone(),
|
||||
conn_timeout: self.conn_timeout,
|
||||
})),
|
||||
State::Connect(mut conn) => match conn.poll() {
|
||||
Ok(Async::NotReady) => {
|
||||
self.state = State::Connect(conn);
|
||||
return Ok(Async::NotReady);
|
||||
},
|
||||
Ok(Async::Ready(result)) => match result {
|
||||
Ok(stream) => {
|
||||
self.state = State::Connection(stream)
|
||||
},
|
||||
Err(err) => return Err(err.into()),
|
||||
},
|
||||
Err(_) => return Err(SendRequestError::Connector(
|
||||
ClientConnectorError::Disconnected))
|
||||
},
|
||||
State::Connection(conn) => {
|
||||
let mut writer = HttpClientWriter::new(SharedBytes::default());
|
||||
writer.start(&mut self.req)?;
|
||||
|
||||
let body = match self.req.replace_body(Body::Empty) {
|
||||
Body::Streaming(stream) => IoBody::Payload(stream),
|
||||
Body::Actor(ctx) => IoBody::Actor(ctx),
|
||||
_ => IoBody::Done,
|
||||
};
|
||||
|
||||
let pl = Box::new(Pipeline {
|
||||
body, conn, writer,
|
||||
parser: Some(HttpResponseParser::default()),
|
||||
parser_buf: BytesMut::new(),
|
||||
disconnected: false,
|
||||
drain: None,
|
||||
decompress: None,
|
||||
should_decompress: self.req.response_decompress(),
|
||||
write_state: RunningState::Running,
|
||||
});
|
||||
self.state = State::Send(pl);
|
||||
},
|
||||
State::Send(mut pl) => {
|
||||
pl.poll_write()
|
||||
.map_err(|e| io::Error::new(
|
||||
io::ErrorKind::Other, format!("{}", e).as_str()))?;
|
||||
|
||||
match pl.parse() {
|
||||
Ok(Async::Ready(mut resp)) => {
|
||||
resp.set_pipeline(pl);
|
||||
return Ok(Async::Ready(resp))
|
||||
},
|
||||
Ok(Async::NotReady) => {
|
||||
self.state = State::Send(pl);
|
||||
return Ok(Async::NotReady)
|
||||
},
|
||||
Err(err) => return Err(SendRequestError::ParseError(err))
|
||||
}
|
||||
}
|
||||
State::None => unreachable!(),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
pub(crate) struct Pipeline {
|
||||
body: IoBody,
|
||||
conn: Connection,
|
||||
writer: HttpClientWriter,
|
||||
parser: Option<HttpResponseParser>,
|
||||
parser_buf: BytesMut,
|
||||
disconnected: bool,
|
||||
drain: Option<oneshot::Sender<()>>,
|
||||
decompress: Option<PayloadStream>,
|
||||
should_decompress: bool,
|
||||
write_state: RunningState,
|
||||
}
|
||||
|
||||
enum IoBody {
|
||||
Payload(BodyStream),
|
||||
Actor(Box<ActorHttpContext>),
|
||||
Done,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
enum RunningState {
|
||||
Running,
|
||||
Paused,
|
||||
Done,
|
||||
}
|
||||
|
||||
impl RunningState {
|
||||
#[inline]
|
||||
fn pause(&mut self) {
|
||||
if *self != RunningState::Done {
|
||||
*self = RunningState::Paused
|
||||
}
|
||||
}
|
||||
#[inline]
|
||||
fn resume(&mut self) {
|
||||
if *self != RunningState::Done {
|
||||
*self = RunningState::Running
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pipeline {
|
||||
|
||||
#[inline]
|
||||
pub fn parse(&mut self) -> Poll<ClientResponse, HttpResponseParserError> {
|
||||
match self.parser.as_mut().unwrap().parse(&mut self.conn, &mut self.parser_buf) {
|
||||
Ok(Async::Ready(resp)) => {
|
||||
// check content-encoding
|
||||
if self.should_decompress {
|
||||
if let Some(enc) = resp.headers().get(CONTENT_ENCODING) {
|
||||
if let Ok(enc) = enc.to_str() {
|
||||
match ContentEncoding::from(enc) {
|
||||
ContentEncoding::Auto | ContentEncoding::Identity => (),
|
||||
enc => self.decompress = Some(PayloadStream::new(enc)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Async::Ready(resp))
|
||||
}
|
||||
val => val,
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn poll(&mut self) -> Poll<Option<Bytes>, PayloadError> {
|
||||
let mut need_run = false;
|
||||
|
||||
// need write?
|
||||
if let Async::NotReady = self.poll_write()
|
||||
.map_err(|e| io::Error::new(io::ErrorKind::Other, format!("{}", e)))?
|
||||
{
|
||||
need_run = true;
|
||||
}
|
||||
|
||||
// need read?
|
||||
if self.parser.is_some() {
|
||||
loop {
|
||||
match self.parser.as_mut().unwrap()
|
||||
.parse_payload(&mut self.conn, &mut self.parser_buf)?
|
||||
{
|
||||
Async::Ready(Some(b)) => {
|
||||
if let Some(ref mut decompress) = self.decompress {
|
||||
match decompress.feed_data(b) {
|
||||
Ok(Some(b)) => return Ok(Async::Ready(Some(b))),
|
||||
Ok(None) => return Ok(Async::NotReady),
|
||||
Err(ref err) if err.kind() == io::ErrorKind::WouldBlock =>
|
||||
continue,
|
||||
Err(err) => return Err(err.into()),
|
||||
}
|
||||
} else {
|
||||
return Ok(Async::Ready(Some(b)))
|
||||
}
|
||||
},
|
||||
Async::Ready(None) => {
|
||||
let _ = self.parser.take();
|
||||
break
|
||||
}
|
||||
Async::NotReady => return Ok(Async::NotReady),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// eof
|
||||
if let Some(mut decompress) = self.decompress.take() {
|
||||
let res = decompress.feed_eof();
|
||||
if let Some(b) = res? {
|
||||
return Ok(Async::Ready(Some(b)))
|
||||
}
|
||||
}
|
||||
|
||||
if need_run {
|
||||
Ok(Async::NotReady)
|
||||
} else {
|
||||
Ok(Async::Ready(None))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn poll_write(&mut self) -> Poll<(), Error> {
|
||||
if self.write_state == RunningState::Done {
|
||||
return Ok(Async::Ready(()))
|
||||
}
|
||||
|
||||
let mut done = false;
|
||||
|
||||
if self.drain.is_none() && self.write_state != RunningState::Paused {
|
||||
'outter: loop {
|
||||
let result = match mem::replace(&mut self.body, IoBody::Done) {
|
||||
IoBody::Payload(mut body) => {
|
||||
match body.poll()? {
|
||||
Async::Ready(None) => {
|
||||
self.writer.write_eof()?;
|
||||
self.disconnected = true;
|
||||
break
|
||||
},
|
||||
Async::Ready(Some(chunk)) => {
|
||||
self.body = IoBody::Payload(body);
|
||||
self.writer.write(chunk.into())?
|
||||
}
|
||||
Async::NotReady => {
|
||||
done = true;
|
||||
self.body = IoBody::Payload(body);
|
||||
break
|
||||
},
|
||||
}
|
||||
},
|
||||
IoBody::Actor(mut ctx) => {
|
||||
if self.disconnected {
|
||||
ctx.disconnected();
|
||||
}
|
||||
match ctx.poll()? {
|
||||
Async::Ready(Some(vec)) => {
|
||||
if vec.is_empty() {
|
||||
self.body = IoBody::Actor(ctx);
|
||||
break
|
||||
}
|
||||
let mut res = None;
|
||||
for frame in vec {
|
||||
match frame {
|
||||
Frame::Chunk(None) => {
|
||||
// info.context = Some(ctx);
|
||||
self.disconnected = true;
|
||||
self.writer.write_eof()?;
|
||||
break 'outter
|
||||
},
|
||||
Frame::Chunk(Some(chunk)) =>
|
||||
res = Some(self.writer.write(chunk)?),
|
||||
Frame::Drain(fut) => self.drain = Some(fut),
|
||||
}
|
||||
}
|
||||
self.body = IoBody::Actor(ctx);
|
||||
if self.drain.is_some() {
|
||||
self.write_state.resume();
|
||||
break
|
||||
}
|
||||
res.unwrap()
|
||||
},
|
||||
Async::Ready(None) => {
|
||||
done = true;
|
||||
break
|
||||
}
|
||||
Async::NotReady => {
|
||||
done = true;
|
||||
self.body = IoBody::Actor(ctx);
|
||||
break
|
||||
}
|
||||
}
|
||||
},
|
||||
IoBody::Done => {
|
||||
self.disconnected = true;
|
||||
done = true;
|
||||
break
|
||||
}
|
||||
};
|
||||
|
||||
match result {
|
||||
WriterState::Pause => {
|
||||
self.write_state.pause();
|
||||
break
|
||||
}
|
||||
WriterState::Done => {
|
||||
self.write_state.resume()
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// flush io but only if we need to
|
||||
match self.writer.poll_completed(&mut self.conn, false) {
|
||||
Ok(Async::Ready(_)) => {
|
||||
if self.disconnected {
|
||||
self.write_state = RunningState::Done;
|
||||
} else {
|
||||
self.write_state.resume();
|
||||
}
|
||||
|
||||
// resolve drain futures
|
||||
if let Some(tx) = self.drain.take() {
|
||||
let _ = tx.send(());
|
||||
}
|
||||
// restart io processing
|
||||
if !done || self.write_state == RunningState::Done {
|
||||
self.poll_write()
|
||||
} else {
|
||||
Ok(Async::NotReady)
|
||||
}
|
||||
},
|
||||
Ok(Async::NotReady) => Ok(Async::NotReady),
|
||||
Err(err) => Err(err.into()),
|
||||
}
|
||||
}
|
||||
}
|
601
src/client/request.rs
Normal file
601
src/client/request.rs
Normal file
@ -0,0 +1,601 @@
|
||||
use std::{fmt, mem};
|
||||
use std::fmt::Write as FmtWrite;
|
||||
use std::io::Write;
|
||||
|
||||
use actix::{Addr, Unsync};
|
||||
use cookie::{Cookie, CookieJar};
|
||||
use bytes::{BytesMut, BufMut};
|
||||
use http::{uri, HeaderMap, Method, Version, Uri, HttpTryFrom, Error as HttpError};
|
||||
use http::header::{self, HeaderName, HeaderValue};
|
||||
use serde_json;
|
||||
use serde::Serialize;
|
||||
use percent_encoding::{USERINFO_ENCODE_SET, percent_encode};
|
||||
|
||||
use body::Body;
|
||||
use error::Error;
|
||||
use header::{ContentEncoding, Header, IntoHeaderValue};
|
||||
use super::pipeline::SendRequest;
|
||||
use super::connector::{Connection, ClientConnector};
|
||||
|
||||
/// An HTTP Client Request
|
||||
pub struct ClientRequest {
|
||||
uri: Uri,
|
||||
method: Method,
|
||||
version: Version,
|
||||
headers: HeaderMap,
|
||||
body: Body,
|
||||
chunked: bool,
|
||||
upgrade: bool,
|
||||
encoding: ContentEncoding,
|
||||
response_decompress: bool,
|
||||
buffer_capacity: usize,
|
||||
conn: ConnectionType,
|
||||
}
|
||||
|
||||
enum ConnectionType {
|
||||
Default,
|
||||
Connector(Addr<Unsync, ClientConnector>),
|
||||
Connection(Connection),
|
||||
}
|
||||
|
||||
impl Default for ClientRequest {
|
||||
|
||||
fn default() -> ClientRequest {
|
||||
ClientRequest {
|
||||
uri: Uri::default(),
|
||||
method: Method::default(),
|
||||
version: Version::HTTP_11,
|
||||
headers: HeaderMap::with_capacity(16),
|
||||
body: Body::Empty,
|
||||
chunked: false,
|
||||
upgrade: false,
|
||||
encoding: ContentEncoding::Auto,
|
||||
response_decompress: true,
|
||||
buffer_capacity: 32_768,
|
||||
conn: ConnectionType::Default,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientRequest {
|
||||
|
||||
/// Create request builder for `GET` request
|
||||
pub fn get<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||
let mut builder = ClientRequest::build();
|
||||
builder.method(Method::GET).uri(uri);
|
||||
builder
|
||||
}
|
||||
|
||||
/// Create request builder for `HEAD` request
|
||||
pub fn head<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||
let mut builder = ClientRequest::build();
|
||||
builder.method(Method::HEAD).uri(uri);
|
||||
builder
|
||||
}
|
||||
|
||||
/// Create request builder for `POST` request
|
||||
pub fn post<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||
let mut builder = ClientRequest::build();
|
||||
builder.method(Method::POST).uri(uri);
|
||||
builder
|
||||
}
|
||||
|
||||
/// Create request builder for `PUT` request
|
||||
pub fn put<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||
let mut builder = ClientRequest::build();
|
||||
builder.method(Method::PUT).uri(uri);
|
||||
builder
|
||||
}
|
||||
|
||||
/// Create request builder for `DELETE` request
|
||||
pub fn delete<U>(uri: U) -> ClientRequestBuilder where Uri: HttpTryFrom<U> {
|
||||
let mut builder = ClientRequest::build();
|
||||
builder.method(Method::DELETE).uri(uri);
|
||||
builder
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientRequest {
|
||||
|
||||
/// Create client request builder
|
||||
pub fn build() -> ClientRequestBuilder {
|
||||
ClientRequestBuilder {
|
||||
request: Some(ClientRequest::default()),
|
||||
err: None,
|
||||
cookies: None,
|
||||
default_headers: true
|
||||
}
|
||||
}
|
||||
|
||||
/// Get the request uri
|
||||
#[inline]
|
||||
pub fn uri(&self) -> &Uri {
|
||||
&self.uri
|
||||
}
|
||||
|
||||
/// Set client request uri
|
||||
#[inline]
|
||||
pub fn set_uri(&mut self, uri: Uri) {
|
||||
self.uri = uri
|
||||
}
|
||||
|
||||
/// Get the request method
|
||||
#[inline]
|
||||
pub fn method(&self) -> &Method {
|
||||
&self.method
|
||||
}
|
||||
|
||||
/// Set http `Method` for the request
|
||||
#[inline]
|
||||
pub fn set_method(&mut self, method: Method) {
|
||||
self.method = method
|
||||
}
|
||||
|
||||
/// Get http version for the request
|
||||
#[inline]
|
||||
pub fn version(&self) -> Version {
|
||||
self.version
|
||||
}
|
||||
|
||||
/// Set http `Version` for the request
|
||||
#[inline]
|
||||
pub fn set_version(&mut self, version: Version) {
|
||||
self.version = version
|
||||
}
|
||||
|
||||
/// Get the headers from the request
|
||||
#[inline]
|
||||
pub fn headers(&self) -> &HeaderMap {
|
||||
&self.headers
|
||||
}
|
||||
|
||||
/// Get a mutable reference to the headers
|
||||
#[inline]
|
||||
pub fn headers_mut(&mut self) -> &mut HeaderMap {
|
||||
&mut self.headers
|
||||
}
|
||||
|
||||
/// is chunked encoding enabled
|
||||
#[inline]
|
||||
pub fn chunked(&self) -> bool {
|
||||
self.chunked
|
||||
}
|
||||
|
||||
/// is upgrade request
|
||||
#[inline]
|
||||
pub fn upgrade(&self) -> bool {
|
||||
self.upgrade
|
||||
}
|
||||
|
||||
/// Content encoding
|
||||
#[inline]
|
||||
pub fn content_encoding(&self) -> ContentEncoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
/// Decompress response payload
|
||||
#[inline]
|
||||
pub fn response_decompress(&self) -> bool {
|
||||
self.response_decompress
|
||||
}
|
||||
|
||||
/// Requested write buffer capacity
|
||||
pub fn write_buffer_capacity(&self) -> usize {
|
||||
self.buffer_capacity
|
||||
}
|
||||
|
||||
/// Get body os this response
|
||||
#[inline]
|
||||
pub fn body(&self) -> &Body {
|
||||
&self.body
|
||||
}
|
||||
|
||||
/// Set a body
|
||||
pub fn set_body<B: Into<Body>>(&mut self, body: B) {
|
||||
self.body = body.into();
|
||||
}
|
||||
|
||||
/// Extract body, replace it with Empty
|
||||
pub(crate) fn replace_body(&mut self, body: Body) -> Body {
|
||||
mem::replace(&mut self.body, body)
|
||||
}
|
||||
|
||||
/// Send request
|
||||
///
|
||||
/// This method returns future that resolves to a ClientResponse
|
||||
pub fn send(mut self) -> SendRequest {
|
||||
match mem::replace(&mut self.conn, ConnectionType::Default) {
|
||||
ConnectionType::Default => SendRequest::new(self),
|
||||
ConnectionType::Connector(conn) => SendRequest::with_connector(self, conn),
|
||||
ConnectionType::Connection(conn) => SendRequest::with_connection(self, conn),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ClientRequest {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let res = write!(f, "\nClientRequest {:?} {}:{}\n",
|
||||
self.version, self.method, self.uri);
|
||||
let _ = write!(f, " headers:\n");
|
||||
for (key, val) in self.headers.iter() {
|
||||
let _ = write!(f, " {:?}: {:?}\n", key, val);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// An HTTP Client request builder
|
||||
///
|
||||
/// This type can be used to construct an instance of `ClientRequest` through a
|
||||
/// builder-like pattern.
|
||||
pub struct ClientRequestBuilder {
|
||||
request: Option<ClientRequest>,
|
||||
err: Option<HttpError>,
|
||||
cookies: Option<CookieJar>,
|
||||
default_headers: bool
|
||||
}
|
||||
|
||||
impl ClientRequestBuilder {
|
||||
/// Set HTTP uri of request.
|
||||
#[inline]
|
||||
pub fn uri<U>(&mut self, uri: U) -> &mut Self where Uri: HttpTryFrom<U> {
|
||||
match Uri::try_from(uri) {
|
||||
Ok(uri) => {
|
||||
// set request host header
|
||||
if let Some(host) = uri.host() {
|
||||
self.set_header(header::HOST, host);
|
||||
}
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.uri = uri;
|
||||
}
|
||||
},
|
||||
Err(e) => self.err = Some(e.into(),),
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP method of this request.
|
||||
#[inline]
|
||||
pub fn method(&mut self, method: Method) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.method = method;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set HTTP method of this request.
|
||||
#[inline]
|
||||
pub fn get_method(&mut self) -> &Method {
|
||||
let parts = parts(&mut self.request, &self.err)
|
||||
.expect("cannot reuse request builder");
|
||||
&parts.method
|
||||
}
|
||||
|
||||
/// Set HTTP version of this request.
|
||||
///
|
||||
/// By default requests's http version depends on network stream
|
||||
#[inline]
|
||||
pub fn version(&mut self, version: Version) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.version = version;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a header.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate mime;
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::*;
|
||||
/// # use actix_web::httpcodes::*;
|
||||
/// # use actix_web::client::*;
|
||||
/// #
|
||||
/// use actix_web::header;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let req = ClientRequest::build()
|
||||
/// .set(header::Date::now())
|
||||
/// .set(header::ContentType(mime::TEXT_HTML))
|
||||
/// .finish().unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
#[doc(hidden)]
|
||||
pub fn set<H: Header>(&mut self, hdr: H) -> &mut Self
|
||||
{
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
match hdr.try_into() {
|
||||
Ok(value) => { parts.headers.insert(H::name(), value); }
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
}
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Append a header.
|
||||
///
|
||||
/// Header get appended to existing header.
|
||||
/// To override header use `set_header()` method.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate http;
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::client::*;
|
||||
/// #
|
||||
/// use http::header;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let req = ClientRequest::build()
|
||||
/// .header("X-TEST", "value")
|
||||
/// .header(header::CONTENT_TYPE, "application/json")
|
||||
/// .finish().unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn header<K, V>(&mut self, key: K, value: V) -> &mut Self
|
||||
where HeaderName: HttpTryFrom<K>, V: IntoHeaderValue
|
||||
{
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
match HeaderName::try_from(key) {
|
||||
Ok(key) => {
|
||||
match value.try_into() {
|
||||
Ok(value) => { parts.headers.append(key, value); }
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
}
|
||||
},
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
};
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a header.
|
||||
pub fn set_header<K, V>(&mut self, key: K, value: V) -> &mut Self
|
||||
where HeaderName: HttpTryFrom<K>, V: IntoHeaderValue
|
||||
{
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
match HeaderName::try_from(key) {
|
||||
Ok(key) => {
|
||||
match value.try_into() {
|
||||
Ok(value) => { parts.headers.insert(key, value); }
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
}
|
||||
},
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
};
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set content encoding.
|
||||
///
|
||||
/// By default `ContentEncoding::Identity` is used.
|
||||
#[inline]
|
||||
pub fn content_encoding(&mut self, enc: ContentEncoding) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.encoding = enc;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Enables automatic chunked transfer encoding
|
||||
#[inline]
|
||||
pub fn chunked(&mut self) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.chunked = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Enable connection upgrade
|
||||
#[inline]
|
||||
pub fn upgrade(&mut self) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.upgrade = true;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set request's content type
|
||||
#[inline]
|
||||
pub fn content_type<V>(&mut self, value: V) -> &mut Self
|
||||
where HeaderValue: HttpTryFrom<V>
|
||||
{
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
match HeaderValue::try_from(value) {
|
||||
Ok(value) => { parts.headers.insert(header::CONTENT_TYPE, value); },
|
||||
Err(e) => self.err = Some(e.into()),
|
||||
};
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set content length
|
||||
#[inline]
|
||||
pub fn content_length(&mut self, len: u64) -> &mut Self {
|
||||
let mut wrt = BytesMut::new().writer();
|
||||
let _ = write!(wrt, "{}", len);
|
||||
self.header(header::CONTENT_LENGTH, wrt.get_mut().take().freeze())
|
||||
}
|
||||
|
||||
/// Set a cookie
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # use actix_web::*;
|
||||
/// # use actix_web::httpcodes::*;
|
||||
/// #
|
||||
/// use actix_web::headers::Cookie;
|
||||
/// use actix_web::client::ClientRequest;
|
||||
///
|
||||
/// fn main() {
|
||||
/// let req = ClientRequest::build()
|
||||
/// .cookie(
|
||||
/// Cookie::build("name", "value")
|
||||
/// .domain("www.rust-lang.org")
|
||||
/// .path("/")
|
||||
/// .secure(true)
|
||||
/// .http_only(true)
|
||||
/// .finish())
|
||||
/// .finish().unwrap();
|
||||
/// }
|
||||
/// ```
|
||||
pub fn cookie<'c>(&mut self, cookie: Cookie<'c>) -> &mut Self {
|
||||
if self.cookies.is_none() {
|
||||
let mut jar = CookieJar::new();
|
||||
jar.add(cookie.into_owned());
|
||||
self.cookies = Some(jar)
|
||||
} else {
|
||||
self.cookies.as_mut().unwrap().add(cookie.into_owned());
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Do not add default request headers.
|
||||
/// By default `Accept-Encoding` header is set.
|
||||
pub fn no_default_headers(&mut self) -> &mut Self {
|
||||
self.default_headers = false;
|
||||
self
|
||||
}
|
||||
|
||||
/// Disable automatic decompress response body
|
||||
pub fn disable_decompress(&mut self) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.response_decompress = false;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set write buffer capacity
|
||||
///
|
||||
/// Default buffer capacity is 32kb
|
||||
pub fn write_buffer_capacity(&mut self, cap: usize) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.buffer_capacity = cap;
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Send request using custom connector
|
||||
pub fn with_connector(&mut self, conn: Addr<Unsync, ClientConnector>) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.conn = ConnectionType::Connector(conn);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Send request using existing Connection
|
||||
pub fn with_connection(&mut self, conn: Connection) -> &mut Self {
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.conn = ConnectionType::Connection(conn);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// This method calls provided closure with builder reference if value is true.
|
||||
pub fn if_true<F>(&mut self, value: bool, f: F) -> &mut Self
|
||||
where F: FnOnce(&mut ClientRequestBuilder)
|
||||
{
|
||||
if value {
|
||||
f(self);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// This method calls provided closure with builder reference if value is Some.
|
||||
pub fn if_some<T, F>(&mut self, value: Option<T>, f: F) -> &mut Self
|
||||
where F: FnOnce(T, &mut ClientRequestBuilder)
|
||||
{
|
||||
if let Some(val) = value {
|
||||
f(val, self);
|
||||
}
|
||||
self
|
||||
}
|
||||
|
||||
/// Set a body and generate `ClientRequest`.
|
||||
///
|
||||
/// `ClientRequestBuilder` can not be used after this call.
|
||||
pub fn body<B: Into<Body>>(&mut self, body: B) -> Result<ClientRequest, HttpError> {
|
||||
if let Some(e) = self.err.take() {
|
||||
return Err(e)
|
||||
}
|
||||
|
||||
if self.default_headers {
|
||||
// enable br only for https
|
||||
let https =
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.uri.scheme_part()
|
||||
.map(|s| s == &uri::Scheme::HTTPS).unwrap_or(true)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
|
||||
if https {
|
||||
self.header(header::ACCEPT_ENCODING, "br, gzip, deflate");
|
||||
} else {
|
||||
self.header(header::ACCEPT_ENCODING, "gzip, deflate");
|
||||
}
|
||||
}
|
||||
|
||||
let mut request = self.request.take().expect("cannot reuse request builder");
|
||||
|
||||
// set cookies
|
||||
if let Some(ref mut jar) = self.cookies {
|
||||
let mut cookie = String::new();
|
||||
for c in jar.delta() {
|
||||
let name = percent_encode(c.name().as_bytes(), USERINFO_ENCODE_SET);
|
||||
let value = percent_encode(c.value().as_bytes(), USERINFO_ENCODE_SET);
|
||||
let _ = write!(&mut cookie, "; {}={}", name, value);
|
||||
}
|
||||
request.headers.insert(
|
||||
header::COOKIE, HeaderValue::from_str(&cookie.as_str()[2..]).unwrap());
|
||||
}
|
||||
request.body = body.into();
|
||||
Ok(request)
|
||||
}
|
||||
|
||||
/// Set a json body and generate `ClientRequest`
|
||||
///
|
||||
/// `ClientRequestBuilder` can not be used after this call.
|
||||
pub fn json<T: Serialize>(&mut self, value: T) -> Result<ClientRequest, Error> {
|
||||
let body = serde_json::to_string(&value)?;
|
||||
|
||||
let contains = if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.headers.contains_key(header::CONTENT_TYPE)
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if !contains {
|
||||
self.header(header::CONTENT_TYPE, "application/json");
|
||||
}
|
||||
|
||||
Ok(self.body(body)?)
|
||||
}
|
||||
|
||||
/// Set an empty body and generate `ClientRequest`
|
||||
///
|
||||
/// `ClientRequestBuilder` can not be used after this call.
|
||||
pub fn finish(&mut self) -> Result<ClientRequest, HttpError> {
|
||||
self.body(Body::Empty)
|
||||
}
|
||||
|
||||
/// This method construct new `ClientRequestBuilder`
|
||||
pub fn take(&mut self) -> ClientRequestBuilder {
|
||||
ClientRequestBuilder {
|
||||
request: self.request.take(),
|
||||
err: self.err.take(),
|
||||
cookies: self.cookies.take(),
|
||||
default_headers: self.default_headers
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parts<'a>(parts: &'a mut Option<ClientRequest>, err: &Option<HttpError>)
|
||||
-> Option<&'a mut ClientRequest>
|
||||
{
|
||||
if err.is_some() {
|
||||
return None
|
||||
}
|
||||
parts.as_mut()
|
||||
}
|
148
src/client/response.rs
Normal file
148
src/client/response.rs
Normal file
@ -0,0 +1,148 @@
|
||||
use std::{fmt, str};
|
||||
use std::rc::Rc;
|
||||
use std::cell::UnsafeCell;
|
||||
|
||||
use bytes::Bytes;
|
||||
use cookie::Cookie;
|
||||
use futures::{Async, Poll, Stream};
|
||||
use http::{HeaderMap, StatusCode, Version};
|
||||
use http::header::{self, HeaderValue};
|
||||
|
||||
use httpmessage::HttpMessage;
|
||||
use error::{CookieParseError, PayloadError};
|
||||
|
||||
use super::pipeline::Pipeline;
|
||||
|
||||
|
||||
pub(crate) struct ClientMessage {
|
||||
pub status: StatusCode,
|
||||
pub version: Version,
|
||||
pub headers: HeaderMap<HeaderValue>,
|
||||
pub cookies: Option<Vec<Cookie<'static>>>,
|
||||
}
|
||||
|
||||
impl Default for ClientMessage {
|
||||
|
||||
fn default() -> ClientMessage {
|
||||
ClientMessage {
|
||||
status: StatusCode::OK,
|
||||
version: Version::HTTP_11,
|
||||
headers: HeaderMap::with_capacity(16),
|
||||
cookies: None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An HTTP Client response
|
||||
pub struct ClientResponse(Rc<UnsafeCell<ClientMessage>>, Option<Box<Pipeline>>);
|
||||
|
||||
impl HttpMessage for ClientResponse {
|
||||
/// Get the headers from the response.
|
||||
#[inline]
|
||||
fn headers(&self) -> &HeaderMap {
|
||||
&self.as_ref().headers
|
||||
}
|
||||
}
|
||||
|
||||
impl ClientResponse {
|
||||
|
||||
pub(crate) fn new(msg: ClientMessage) -> ClientResponse {
|
||||
ClientResponse(Rc::new(UnsafeCell::new(msg)), None)
|
||||
}
|
||||
|
||||
pub(crate) fn set_pipeline(&mut self, pl: Box<Pipeline>) {
|
||||
self.1 = Some(pl);
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn as_ref(&self) -> &ClientMessage {
|
||||
unsafe{ &*self.0.get() }
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[cfg_attr(feature = "cargo-clippy", allow(mut_from_ref))]
|
||||
fn as_mut(&self) -> &mut ClientMessage {
|
||||
unsafe{ &mut *self.0.get() }
|
||||
}
|
||||
|
||||
/// Get the HTTP version of this response.
|
||||
#[inline]
|
||||
pub fn version(&self) -> Version {
|
||||
self.as_ref().version
|
||||
}
|
||||
|
||||
/// Get the status from the server.
|
||||
#[inline]
|
||||
pub fn status(&self) -> StatusCode {
|
||||
self.as_ref().status
|
||||
}
|
||||
|
||||
/// Load response cookies.
|
||||
pub fn cookies(&self) -> Result<&Vec<Cookie<'static>>, CookieParseError> {
|
||||
if self.as_ref().cookies.is_none() {
|
||||
let msg = self.as_mut();
|
||||
let mut cookies = Vec::new();
|
||||
for val in msg.headers.get_all(header::SET_COOKIE).iter() {
|
||||
let s = str::from_utf8(val.as_bytes()).map_err(CookieParseError::from)?;
|
||||
cookies.push(Cookie::parse_encoded(s)?.into_owned());
|
||||
}
|
||||
msg.cookies = Some(cookies)
|
||||
}
|
||||
Ok(self.as_ref().cookies.as_ref().unwrap())
|
||||
}
|
||||
|
||||
/// Return request cookie.
|
||||
pub fn cookie(&self, name: &str) -> Option<&Cookie> {
|
||||
if let Ok(cookies) = self.cookies() {
|
||||
for cookie in cookies {
|
||||
if cookie.name() == name {
|
||||
return Some(cookie)
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for ClientResponse {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
let res = write!(
|
||||
f, "\nClientResponse {:?} {}\n", self.version(), self.status());
|
||||
let _ = write!(f, " headers:\n");
|
||||
for (key, val) in self.headers().iter() {
|
||||
let _ = write!(f, " {:?}: {:?}\n", key, val);
|
||||
}
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Future that resolves to a complete request body.
|
||||
impl Stream for ClientResponse {
|
||||
type Item = Bytes;
|
||||
type Error = PayloadError;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Self::Item>, Self::Error> {
|
||||
if let Some(ref mut pl) = self.1 {
|
||||
pl.poll()
|
||||
} else {
|
||||
Ok(Async::Ready(None))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_debug() {
|
||||
let resp = ClientResponse::new(ClientMessage::default());
|
||||
resp.as_mut().headers.insert(
|
||||
header::COOKIE, HeaderValue::from_static("cookie1=value1"));
|
||||
resp.as_mut().headers.insert(
|
||||
header::COOKIE, HeaderValue::from_static("cookie2=value2"));
|
||||
|
||||
let dbg = format!("{:?}", resp);
|
||||
assert!(dbg.contains("ClientResponse"));
|
||||
}
|
||||
}
|
362
src/client/writer.rs
Normal file
362
src/client/writer.rs
Normal file
@ -0,0 +1,362 @@
|
||||
#![cfg_attr(feature = "cargo-clippy", allow(redundant_field_names))]
|
||||
|
||||
use std::io::{self, Write};
|
||||
use std::cell::RefCell;
|
||||
use std::fmt::Write as FmtWrite;
|
||||
|
||||
use time::{self, Duration};
|
||||
use bytes::{BytesMut, BufMut};
|
||||
use futures::{Async, Poll};
|
||||
use tokio_io::AsyncWrite;
|
||||
use http::{Version, HttpTryFrom};
|
||||
use http::header::{HeaderValue, DATE,
|
||||
CONNECTION, CONTENT_ENCODING, CONTENT_LENGTH, TRANSFER_ENCODING};
|
||||
use flate2::Compression;
|
||||
use flate2::write::{GzEncoder, DeflateEncoder};
|
||||
use brotli2::write::BrotliEncoder;
|
||||
|
||||
use body::{Body, Binary};
|
||||
use headers::ContentEncoding;
|
||||
use server::WriterState;
|
||||
use server::shared::SharedBytes;
|
||||
use server::encoding::{ContentEncoder, TransferEncoding};
|
||||
|
||||
use client::ClientRequest;
|
||||
|
||||
|
||||
const AVERAGE_HEADER_SIZE: usize = 30;
|
||||
|
||||
bitflags! {
|
||||
struct Flags: u8 {
|
||||
const STARTED = 0b0000_0001;
|
||||
const UPGRADE = 0b0000_0010;
|
||||
const KEEPALIVE = 0b0000_0100;
|
||||
const DISCONNECTED = 0b0000_1000;
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct HttpClientWriter {
|
||||
flags: Flags,
|
||||
written: u64,
|
||||
headers_size: u32,
|
||||
buffer: SharedBytes,
|
||||
buffer_capacity: usize,
|
||||
encoder: ContentEncoder,
|
||||
}
|
||||
|
||||
impl HttpClientWriter {
|
||||
|
||||
pub fn new(buffer: SharedBytes) -> HttpClientWriter {
|
||||
let encoder = ContentEncoder::Identity(TransferEncoding::eof(buffer.clone()));
|
||||
HttpClientWriter {
|
||||
flags: Flags::empty(),
|
||||
written: 0,
|
||||
headers_size: 0,
|
||||
buffer_capacity: 0,
|
||||
buffer,
|
||||
encoder,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnected(&mut self) {
|
||||
self.buffer.take();
|
||||
}
|
||||
|
||||
// pub fn keepalive(&self) -> bool {
|
||||
// self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE)
|
||||
// }
|
||||
|
||||
fn write_to_stream<T: AsyncWrite>(&mut self, stream: &mut T) -> io::Result<WriterState> {
|
||||
while !self.buffer.is_empty() {
|
||||
match stream.write(self.buffer.as_ref()) {
|
||||
Ok(0) => {
|
||||
self.disconnected();
|
||||
return Ok(WriterState::Done);
|
||||
},
|
||||
Ok(n) => {
|
||||
let _ = self.buffer.split_to(n);
|
||||
},
|
||||
Err(ref e) if e.kind() == io::ErrorKind::WouldBlock => {
|
||||
if self.buffer.len() > self.buffer_capacity {
|
||||
return Ok(WriterState::Pause)
|
||||
} else {
|
||||
return Ok(WriterState::Done)
|
||||
}
|
||||
}
|
||||
Err(err) => return Err(err),
|
||||
}
|
||||
}
|
||||
Ok(WriterState::Done)
|
||||
}
|
||||
}
|
||||
|
||||
impl HttpClientWriter {
|
||||
|
||||
pub fn start(&mut self, msg: &mut ClientRequest) -> io::Result<()> {
|
||||
// prepare task
|
||||
self.flags.insert(Flags::STARTED);
|
||||
self.encoder = content_encoder(self.buffer.clone(), msg);
|
||||
|
||||
// render message
|
||||
{
|
||||
let mut buffer = self.buffer.get_mut();
|
||||
if let Body::Binary(ref bytes) = *msg.body() {
|
||||
buffer.reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE + bytes.len());
|
||||
} else {
|
||||
buffer.reserve(256 + msg.headers().len() * AVERAGE_HEADER_SIZE);
|
||||
}
|
||||
|
||||
if msg.upgrade() {
|
||||
self.flags.insert(Flags::UPGRADE);
|
||||
}
|
||||
|
||||
// status line
|
||||
let _ = write!(buffer, "{} {} {:?}\r\n",
|
||||
msg.method(), msg.uri().path(), msg.version());
|
||||
|
||||
// write headers
|
||||
for (key, value) in msg.headers() {
|
||||
let v = value.as_ref();
|
||||
let k = key.as_str().as_bytes();
|
||||
buffer.reserve(k.len() + v.len() + 4);
|
||||
buffer.put_slice(k);
|
||||
buffer.put_slice(b": ");
|
||||
buffer.put_slice(v);
|
||||
buffer.put_slice(b"\r\n");
|
||||
}
|
||||
|
||||
// set date header
|
||||
if !msg.headers().contains_key(DATE) {
|
||||
buffer.extend_from_slice(b"date: ");
|
||||
set_date(&mut buffer);
|
||||
buffer.extend_from_slice(b"\r\n\r\n");
|
||||
} else {
|
||||
buffer.extend_from_slice(b"\r\n");
|
||||
}
|
||||
self.headers_size = buffer.len() as u32;
|
||||
|
||||
if msg.body().is_binary() {
|
||||
if let Body::Binary(bytes) = msg.replace_body(Body::Empty) {
|
||||
self.written += bytes.len() as u64;
|
||||
self.encoder.write(bytes)?;
|
||||
}
|
||||
} else {
|
||||
self.buffer_capacity = msg.write_buffer_capacity();
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn write(&mut self, payload: Binary) -> io::Result<WriterState> {
|
||||
self.written += payload.len() as u64;
|
||||
if !self.flags.contains(Flags::DISCONNECTED) {
|
||||
if self.flags.contains(Flags::UPGRADE) {
|
||||
self.buffer.extend(payload);
|
||||
} else {
|
||||
self.encoder.write(payload)?;
|
||||
}
|
||||
}
|
||||
|
||||
if self.buffer.len() > self.buffer_capacity {
|
||||
Ok(WriterState::Pause)
|
||||
} else {
|
||||
Ok(WriterState::Done)
|
||||
}
|
||||
}
|
||||
|
||||
pub fn write_eof(&mut self) -> io::Result<()> {
|
||||
self.encoder.write_eof()?;
|
||||
|
||||
if self.encoder.is_eof() {
|
||||
Ok(())
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::Other,
|
||||
"Last payload item, but eof is not reached"))
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn poll_completed<T: AsyncWrite>(&mut self, stream: &mut T, shutdown: bool)
|
||||
-> Poll<(), io::Error>
|
||||
{
|
||||
match self.write_to_stream(stream) {
|
||||
Ok(WriterState::Done) => {
|
||||
if shutdown {
|
||||
stream.shutdown()
|
||||
} else {
|
||||
Ok(Async::Ready(()))
|
||||
}
|
||||
},
|
||||
Ok(WriterState::Pause) => Ok(Async::NotReady),
|
||||
Err(err) => Err(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
fn content_encoder(buf: SharedBytes, req: &mut ClientRequest) -> ContentEncoder {
|
||||
let version = req.version();
|
||||
let mut body = req.replace_body(Body::Empty);
|
||||
let mut encoding = req.content_encoding();
|
||||
|
||||
let transfer = match body {
|
||||
Body::Empty => {
|
||||
req.headers_mut().remove(CONTENT_LENGTH);
|
||||
TransferEncoding::length(0, buf)
|
||||
},
|
||||
Body::Binary(ref mut bytes) => {
|
||||
if encoding.is_compression() {
|
||||
let tmp = SharedBytes::default();
|
||||
let transfer = TransferEncoding::eof(tmp.clone());
|
||||
let mut enc = match encoding {
|
||||
ContentEncoding::Deflate => ContentEncoder::Deflate(
|
||||
DeflateEncoder::new(transfer, Compression::default())),
|
||||
ContentEncoding::Gzip => ContentEncoder::Gzip(
|
||||
GzEncoder::new(transfer, Compression::default())),
|
||||
ContentEncoding::Br => ContentEncoder::Br(
|
||||
BrotliEncoder::new(transfer, 5)),
|
||||
ContentEncoding::Identity => ContentEncoder::Identity(transfer),
|
||||
ContentEncoding::Auto => unreachable!()
|
||||
};
|
||||
// TODO return error!
|
||||
let _ = enc.write(bytes.clone());
|
||||
let _ = enc.write_eof();
|
||||
*bytes = Binary::from(tmp.take());
|
||||
|
||||
req.headers_mut().insert(
|
||||
CONTENT_ENCODING, HeaderValue::from_static(encoding.as_str()));
|
||||
encoding = ContentEncoding::Identity;
|
||||
}
|
||||
let mut b = BytesMut::new();
|
||||
let _ = write!(b, "{}", bytes.len());
|
||||
req.headers_mut().insert(
|
||||
CONTENT_LENGTH, HeaderValue::try_from(b.freeze()).unwrap());
|
||||
TransferEncoding::eof(buf)
|
||||
},
|
||||
Body::Streaming(_) | Body::Actor(_) => {
|
||||
if req.upgrade() {
|
||||
if version == Version::HTTP_2 {
|
||||
error!("Connection upgrade is forbidden for HTTP/2");
|
||||
} else {
|
||||
req.headers_mut().insert(CONNECTION, HeaderValue::from_static("upgrade"));
|
||||
}
|
||||
if encoding != ContentEncoding::Identity {
|
||||
encoding = ContentEncoding::Identity;
|
||||
req.headers_mut().remove(CONTENT_ENCODING);
|
||||
}
|
||||
TransferEncoding::eof(buf)
|
||||
} else {
|
||||
streaming_encoding(buf, version, req)
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if encoding.is_compression() {
|
||||
req.headers_mut().insert(
|
||||
CONTENT_ENCODING, HeaderValue::from_static(encoding.as_str()));
|
||||
}
|
||||
|
||||
req.replace_body(body);
|
||||
match encoding {
|
||||
ContentEncoding::Deflate => ContentEncoder::Deflate(
|
||||
DeflateEncoder::new(transfer, Compression::default())),
|
||||
ContentEncoding::Gzip => ContentEncoder::Gzip(
|
||||
GzEncoder::new(transfer, Compression::default())),
|
||||
ContentEncoding::Br => ContentEncoder::Br(
|
||||
BrotliEncoder::new(transfer, 5)),
|
||||
ContentEncoding::Identity | ContentEncoding::Auto => ContentEncoder::Identity(transfer),
|
||||
}
|
||||
}
|
||||
|
||||
fn streaming_encoding(buf: SharedBytes, version: Version, req: &mut ClientRequest)
|
||||
-> TransferEncoding {
|
||||
if req.chunked() {
|
||||
// Enable transfer encoding
|
||||
req.headers_mut().remove(CONTENT_LENGTH);
|
||||
if version == Version::HTTP_2 {
|
||||
req.headers_mut().remove(TRANSFER_ENCODING);
|
||||
TransferEncoding::eof(buf)
|
||||
} else {
|
||||
req.headers_mut().insert(
|
||||
TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
|
||||
TransferEncoding::chunked(buf)
|
||||
}
|
||||
} else {
|
||||
// if Content-Length is specified, then use it as length hint
|
||||
let (len, chunked) =
|
||||
if let Some(len) = req.headers().get(CONTENT_LENGTH) {
|
||||
// Content-Length
|
||||
if let Ok(s) = len.to_str() {
|
||||
if let Ok(len) = s.parse::<u64>() {
|
||||
(Some(len), false)
|
||||
} else {
|
||||
error!("illegal Content-Length: {:?}", len);
|
||||
(None, false)
|
||||
}
|
||||
} else {
|
||||
error!("illegal Content-Length: {:?}", len);
|
||||
(None, false)
|
||||
}
|
||||
} else {
|
||||
(None, true)
|
||||
};
|
||||
|
||||
if !chunked {
|
||||
if let Some(len) = len {
|
||||
TransferEncoding::length(len, buf)
|
||||
} else {
|
||||
TransferEncoding::eof(buf)
|
||||
}
|
||||
} else {
|
||||
// Enable transfer encoding
|
||||
match version {
|
||||
Version::HTTP_11 => {
|
||||
req.headers_mut().insert(
|
||||
TRANSFER_ENCODING, HeaderValue::from_static("chunked"));
|
||||
TransferEncoding::chunked(buf)
|
||||
},
|
||||
_ => {
|
||||
req.headers_mut().remove(TRANSFER_ENCODING);
|
||||
TransferEncoding::eof(buf)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// "Sun, 06 Nov 1994 08:49:37 GMT".len()
|
||||
pub const DATE_VALUE_LENGTH: usize = 29;
|
||||
|
||||
fn set_date(dst: &mut BytesMut) {
|
||||
CACHED.with(|cache| {
|
||||
let mut cache = cache.borrow_mut();
|
||||
let now = time::get_time();
|
||||
if now > cache.next_update {
|
||||
cache.update(now);
|
||||
}
|
||||
dst.extend_from_slice(cache.buffer());
|
||||
})
|
||||
}
|
||||
|
||||
struct CachedDate {
|
||||
bytes: [u8; DATE_VALUE_LENGTH],
|
||||
next_update: time::Timespec,
|
||||
}
|
||||
|
||||
thread_local!(static CACHED: RefCell<CachedDate> = RefCell::new(CachedDate {
|
||||
bytes: [0; DATE_VALUE_LENGTH],
|
||||
next_update: time::Timespec::new(0, 0),
|
||||
}));
|
||||
|
||||
impl CachedDate {
|
||||
fn buffer(&self) -> &[u8] {
|
||||
&self.bytes[..]
|
||||
}
|
||||
|
||||
fn update(&mut self, now: time::Timespec) {
|
||||
write!(&mut self.bytes[..], "{}", time::at_utc(now).rfc822()).unwrap();
|
||||
self.next_update = now + Duration::seconds(1);
|
||||
self.next_update.nsec = 0;
|
||||
}
|
||||
}
|
110
src/context.rs
110
src/context.rs
@ -1,24 +1,23 @@
|
||||
use std;
|
||||
use std::mem;
|
||||
use std::marker::PhantomData;
|
||||
use std::collections::VecDeque;
|
||||
use futures::{Async, Future, Poll};
|
||||
use futures::sync::oneshot::Sender;
|
||||
use futures::unsync::oneshot;
|
||||
use smallvec::SmallVec;
|
||||
|
||||
use actix::{Actor, ActorState, ActorContext, AsyncContext,
|
||||
Address, SyncAddress, Handler, Subscriber, ResponseType, SpawnHandle};
|
||||
Addr, Handler, Message, SpawnHandle, Syn, Unsync};
|
||||
use actix::fut::ActorFuture;
|
||||
use actix::dev::{queue, AsyncContextApi,
|
||||
ContextImpl, ContextProtocol, Envelope, ToEnvelope, RemoteEnvelope};
|
||||
use actix::dev::{ContextImpl, ToEnvelope, SyncEnvelope};
|
||||
|
||||
use body::{Body, Binary};
|
||||
use error::{Error, Result, ErrorInternalServerError};
|
||||
use error::{Error, ErrorInternalServerError};
|
||||
use httprequest::HttpRequest;
|
||||
|
||||
|
||||
pub trait ActorHttpContext: 'static {
|
||||
fn disconnected(&mut self);
|
||||
fn poll(&mut self) -> Poll<Option<Frame>, Error>;
|
||||
fn poll(&mut self) -> Poll<Option<SmallVec<[Frame; 4]>>, Error>;
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -27,11 +26,20 @@ pub enum Frame {
|
||||
Drain(oneshot::Sender<()>),
|
||||
}
|
||||
|
||||
impl Frame {
|
||||
pub fn len(&self) -> usize {
|
||||
match *self {
|
||||
Frame::Chunk(Some(ref bin)) => bin.len(),
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Http actor execution context
|
||||
pub struct HttpContext<A, S=()> where A: Actor<Context=HttpContext<A, S>>,
|
||||
{
|
||||
inner: ContextImpl<A>,
|
||||
stream: VecDeque<Frame>,
|
||||
stream: Option<SmallVec<[Frame; 4]>>,
|
||||
request: HttpRequest<S>,
|
||||
disconnected: bool,
|
||||
}
|
||||
@ -51,33 +59,36 @@ impl<A, S> ActorContext for HttpContext<A, S> where A: Actor<Context=Self>
|
||||
|
||||
impl<A, S> AsyncContext<A> for HttpContext<A, S> where A: Actor<Context=Self>
|
||||
{
|
||||
#[inline]
|
||||
fn spawn<F>(&mut self, fut: F) -> SpawnHandle
|
||||
where F: ActorFuture<Item=(), Error=(), Actor=A> + 'static
|
||||
{
|
||||
self.inner.spawn(fut)
|
||||
}
|
||||
#[inline]
|
||||
fn wait<F>(&mut self, fut: F)
|
||||
where F: ActorFuture<Item=(), Error=(), Actor=A> + 'static
|
||||
{
|
||||
self.inner.wait(fut)
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
fn waiting(&self) -> bool {
|
||||
self.inner.waiting() || self.inner.state() == ActorState::Stopping ||
|
||||
self.inner.state() == ActorState::Stopped
|
||||
}
|
||||
#[inline]
|
||||
fn cancel_future(&mut self, handle: SpawnHandle) -> bool {
|
||||
self.inner.cancel_future(handle)
|
||||
}
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
impl<A, S> AsyncContextApi<A> for HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
fn unsync_sender(&mut self) -> queue::unsync::UnboundedSender<ContextProtocol<A>> {
|
||||
self.inner.unsync_sender()
|
||||
}
|
||||
#[inline]
|
||||
fn unsync_address(&mut self) -> Address<A> {
|
||||
fn unsync_address(&mut self) -> Addr<Unsync, A> {
|
||||
self.inner.unsync_address()
|
||||
}
|
||||
#[doc(hidden)]
|
||||
#[inline]
|
||||
fn sync_address(&mut self) -> SyncAddress<A> {
|
||||
fn sync_address(&mut self) -> Addr<Syn, A> {
|
||||
self.inner.sync_address()
|
||||
}
|
||||
}
|
||||
@ -91,7 +102,7 @@ impl<A, S: 'static> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
pub fn from_request(req: HttpRequest<S>) -> HttpContext<A, S> {
|
||||
HttpContext {
|
||||
inner: ContextImpl::new(None),
|
||||
stream: VecDeque::new(),
|
||||
stream: None,
|
||||
request: req,
|
||||
disconnected: false,
|
||||
}
|
||||
@ -121,23 +132,23 @@ impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
#[inline]
|
||||
pub fn write<B: Into<Binary>>(&mut self, data: B) {
|
||||
if !self.disconnected {
|
||||
self.stream.push_back(Frame::Chunk(Some(data.into())));
|
||||
self.add_frame(Frame::Chunk(Some(data.into())));
|
||||
} else {
|
||||
warn!("Trying to write to disconnected response");
|
||||
}
|
||||
}
|
||||
|
||||
/// Indicate end of streamimng payload. Also this method calls `Self::close`.
|
||||
/// Indicate end of streaming payload. Also this method calls `Self::close`.
|
||||
#[inline]
|
||||
pub fn write_eof(&mut self) {
|
||||
self.stream.push_back(Frame::Chunk(None));
|
||||
self.add_frame(Frame::Chunk(None));
|
||||
}
|
||||
|
||||
/// Returns drain future
|
||||
pub fn drain(&mut self) -> Drain<A> {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.inner.modify();
|
||||
self.stream.push_back(Frame::Drain(tx));
|
||||
self.add_frame(Frame::Drain(tx));
|
||||
Drain::new(rx)
|
||||
}
|
||||
|
||||
@ -146,25 +157,21 @@ impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
pub fn connected(&self) -> bool {
|
||||
!self.disconnected
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
|
||||
#[inline]
|
||||
#[doc(hidden)]
|
||||
pub fn subscriber<M>(&mut self) -> Box<Subscriber<M>>
|
||||
where A: Handler<M>, M: ResponseType + 'static
|
||||
{
|
||||
self.inner.subscriber()
|
||||
fn add_frame(&mut self, frame: Frame) {
|
||||
if self.stream.is_none() {
|
||||
self.stream = Some(SmallVec::new());
|
||||
}
|
||||
self.stream.as_mut().map(|s| s.push(frame));
|
||||
self.inner.modify();
|
||||
}
|
||||
|
||||
#[inline]
|
||||
#[doc(hidden)]
|
||||
pub fn sync_subscriber<M>(&mut self) -> Box<Subscriber<M> + Send>
|
||||
where A: Handler<M>,
|
||||
M: ResponseType + Send + 'static, M::Item: Send, M::Error: Send,
|
||||
{
|
||||
self.inner.sync_subscriber()
|
||||
/// Handle of the running future
|
||||
///
|
||||
/// SpawnHandle is the handle returned by `AsyncContext::spawn()` method.
|
||||
pub fn handle(&self) -> SpawnHandle {
|
||||
self.inner.curr_handle()
|
||||
}
|
||||
}
|
||||
|
||||
@ -176,9 +183,9 @@ impl<A, S> ActorHttpContext for HttpContext<A, S> where A: Actor<Context=Self>,
|
||||
self.stop();
|
||||
}
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Frame>, Error> {
|
||||
fn poll(&mut self) -> Poll<Option<SmallVec<[Frame; 4]>>, Error> {
|
||||
let ctx: &mut HttpContext<A, S> = unsafe {
|
||||
std::mem::transmute(self as &mut HttpContext<A, S>)
|
||||
mem::transmute(self as &mut HttpContext<A, S>)
|
||||
};
|
||||
|
||||
if self.inner.alive() {
|
||||
@ -189,8 +196,8 @@ impl<A, S> ActorHttpContext for HttpContext<A, S> where A: Actor<Context=Self>,
|
||||
}
|
||||
|
||||
// frames
|
||||
if let Some(frame) = self.stream.pop_front() {
|
||||
Ok(Async::Ready(Some(frame)))
|
||||
if let Some(data) = self.stream.take() {
|
||||
Ok(Async::Ready(Some(data)))
|
||||
} else if self.inner.alive() {
|
||||
Ok(Async::NotReady)
|
||||
} else {
|
||||
@ -199,16 +206,12 @@ impl<A, S> ActorHttpContext for HttpContext<A, S> where A: Actor<Context=Self>,
|
||||
}
|
||||
}
|
||||
|
||||
impl<A, S> ToEnvelope<A> for HttpContext<A, S>
|
||||
where A: Actor<Context=HttpContext<A, S>>,
|
||||
impl<A, M, S> ToEnvelope<Syn, A, M> for HttpContext<A, S>
|
||||
where A: Actor<Context=HttpContext<A, S>> + Handler<M>,
|
||||
M: Message + Send + 'static, M::Result: Send,
|
||||
{
|
||||
#[inline]
|
||||
fn pack<M>(msg: M, tx: Option<Sender<Result<M::Item, M::Error>>>,
|
||||
channel_on_drop: bool) -> Envelope<A>
|
||||
where A: Handler<M>,
|
||||
M: ResponseType + Send + 'static, M::Item: Send, M::Error: Send
|
||||
{
|
||||
RemoteEnvelope::new(msg, tx, channel_on_drop).into()
|
||||
fn pack(msg: M, tx: Option<Sender<M::Result>>) -> SyncEnvelope<A> {
|
||||
SyncEnvelope::new(msg, tx)
|
||||
}
|
||||
}
|
||||
|
||||
@ -228,10 +231,7 @@ pub struct Drain<A> {
|
||||
|
||||
impl<A> Drain<A> {
|
||||
pub fn new(fut: oneshot::Receiver<()>) -> Self {
|
||||
Drain {
|
||||
fut: fut,
|
||||
_a: PhantomData
|
||||
}
|
||||
Drain { fut, _a: PhantomData }
|
||||
}
|
||||
}
|
||||
|
||||
|
380
src/error.rs
380
src/error.rs
@ -4,26 +4,27 @@ use std::str::Utf8Error;
|
||||
use std::string::FromUtf8Error;
|
||||
use std::io::Error as IoError;
|
||||
|
||||
#[cfg(actix_nightly)]
|
||||
use std::error::Error as StdError;
|
||||
|
||||
use cookie;
|
||||
use httparse;
|
||||
use failure::Fail;
|
||||
use actix::MailboxError;
|
||||
use futures::Canceled;
|
||||
use failure;
|
||||
use failure::{Fail, Backtrace};
|
||||
use http2::Error as Http2Error;
|
||||
use http::{header, StatusCode, Error as HttpError};
|
||||
use http::uri::InvalidUriBytes;
|
||||
use http_range::HttpRangeParseError;
|
||||
use serde_json::error::Error as JsonError;
|
||||
use url::ParseError as UrlParseError;
|
||||
pub use url::ParseError as UrlParseError;
|
||||
|
||||
// re-exports
|
||||
pub use cookie::{ParseError as CookieParseError};
|
||||
|
||||
use body::Body;
|
||||
use handler::Responder;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed, HTTPExpectationFailed};
|
||||
use httpcodes::{self, HttpExpectationFailed};
|
||||
|
||||
/// A specialized [`Result`](https://doc.rust-lang.org/std/result/enum.Result.html)
|
||||
/// for actix web operations
|
||||
@ -33,9 +34,9 @@ use httpcodes::{HTTPBadRequest, HTTPMethodNotAllowed, HTTPExpectationFailed};
|
||||
pub type Result<T, E=Error> = result::Result<T, E>;
|
||||
|
||||
/// General purpose actix web error
|
||||
#[derive(Fail, Debug)]
|
||||
pub struct Error {
|
||||
cause: Box<ResponseError>,
|
||||
backtrace: Option<Backtrace>,
|
||||
}
|
||||
|
||||
impl Error {
|
||||
@ -64,6 +65,16 @@ impl fmt::Display for Error {
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Debug for Error {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
if let Some(bt) = self.cause.backtrace() {
|
||||
write!(f, "{:?}\n\n{:?}", &self.cause, bt)
|
||||
} else {
|
||||
write!(f, "{:?}\n\n{:?}", &self.cause, self.backtrace.as_ref().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// `HttpResponse` for `Error`
|
||||
impl From<Error> for HttpResponse {
|
||||
fn from(err: Error) -> Self {
|
||||
@ -74,21 +85,31 @@ impl From<Error> for HttpResponse {
|
||||
/// `Error` for any error that implements `ResponseError`
|
||||
impl<T: ResponseError> From<T> for Error {
|
||||
fn from(err: T) -> Error {
|
||||
Error { cause: Box::new(err) }
|
||||
let backtrace = if err.backtrace().is_none() {
|
||||
Some(Backtrace::new())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Error { cause: Box::new(err), backtrace }
|
||||
}
|
||||
}
|
||||
|
||||
/// Default error is `InternalServerError`
|
||||
#[cfg(actix_nightly)]
|
||||
default impl<T: StdError + Sync + Send + 'static> ResponseError for T {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::INTERNAL_SERVER_ERROR, Body::Empty)
|
||||
/// Compatibility for `failure::Error`
|
||||
impl<T> ResponseError for failure::Compat<T>
|
||||
where T: fmt::Display + fmt::Debug + Sync + Send + 'static { }
|
||||
|
||||
impl From<failure::Error> for Error {
|
||||
fn from(err: failure::Error) -> Error {
|
||||
err.compat().into()
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `JsonError`
|
||||
impl ResponseError for JsonError {}
|
||||
|
||||
/// `InternalServerError` for `UrlParseError`
|
||||
impl ResponseError for UrlParseError {}
|
||||
|
||||
/// Return `InternalServerError` for `HttpError`,
|
||||
/// Response generation can return `HttpError`, so it is internal error
|
||||
impl ResponseError for HttpError {}
|
||||
@ -108,12 +129,26 @@ impl ResponseError for io::Error {
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `InvalidHeaderValue`
|
||||
impl ResponseError for header::InvalidHeaderValue {}
|
||||
/// `BadRequest` for `InvalidHeaderValue`
|
||||
impl ResponseError for header::InvalidHeaderValue {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// `BadRequest` for `InvalidHeaderValue`
|
||||
impl ResponseError for header::InvalidHeaderValueBytes {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
/// `InternalServerError` for `futures::Canceled`
|
||||
impl ResponseError for Canceled {}
|
||||
|
||||
/// `InternalServerError` for `actix::MailboxError`
|
||||
impl ResponseError for MailboxError {}
|
||||
|
||||
/// A set of errors that can occur during parsing HTTP streams
|
||||
#[derive(Fail, Debug)]
|
||||
pub enum ParseError {
|
||||
@ -202,9 +237,9 @@ pub enum PayloadError {
|
||||
/// A payload length is unknown.
|
||||
#[fail(display="A payload length is unknown.")]
|
||||
UnknownLength,
|
||||
/// Parse error
|
||||
/// Io error
|
||||
#[fail(display="{}", _0)]
|
||||
ParseError(#[cause] IoError),
|
||||
Io(#[cause] IoError),
|
||||
/// Http2 error
|
||||
#[fail(display="{}", _0)]
|
||||
Http2(#[cause] Http2Error),
|
||||
@ -212,7 +247,7 @@ pub enum PayloadError {
|
||||
|
||||
impl From<IoError> for PayloadError {
|
||||
fn from(err: IoError) -> PayloadError {
|
||||
PayloadError::ParseError(err)
|
||||
PayloadError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
@ -268,6 +303,9 @@ pub enum MultipartError {
|
||||
/// Multipart boundary is not found
|
||||
#[fail(display="Multipart boundary is not found")]
|
||||
Boundary,
|
||||
/// Multipart stream is incomplete
|
||||
#[fail(display="Multipart stream is incomplete")]
|
||||
Incomplete,
|
||||
/// Error during field parsing
|
||||
#[fail(display="{}", _0)]
|
||||
Parse(#[cause] ParseError),
|
||||
@ -308,57 +346,26 @@ pub enum ExpectError {
|
||||
}
|
||||
|
||||
impl ResponseError for ExpectError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HTTPExpectationFailed.with_body("Unknown Expect")
|
||||
HttpExpectationFailed.with_body("Unknown Expect")
|
||||
}
|
||||
}
|
||||
|
||||
/// Websocket handshake errors
|
||||
/// A set of error that can occure during parsing content type
|
||||
#[derive(Fail, PartialEq, Debug)]
|
||||
pub enum WsHandshakeError {
|
||||
/// Only get method is allowed
|
||||
#[fail(display="Method not allowed")]
|
||||
GetMethodRequired,
|
||||
/// Ugrade header if not set to websocket
|
||||
#[fail(display="Websocket upgrade is expected")]
|
||||
NoWebsocketUpgrade,
|
||||
/// Connection header is not set to upgrade
|
||||
#[fail(display="Connection upgrade is expected")]
|
||||
NoConnectionUpgrade,
|
||||
/// Websocket version header is not set
|
||||
#[fail(display="Websocket version header is required")]
|
||||
NoVersionHeader,
|
||||
/// Unsupported websockt version
|
||||
#[fail(display="Unsupported version")]
|
||||
UnsupportedVersion,
|
||||
/// Websocket key is not set or wrong
|
||||
#[fail(display="Unknown websocket key")]
|
||||
BadWebsocketKey,
|
||||
pub enum ContentTypeError {
|
||||
/// Can not parse content type
|
||||
#[fail(display="Can not parse content type")]
|
||||
ParseError,
|
||||
/// Unknown content encoding
|
||||
#[fail(display="Unknown content encoding")]
|
||||
UnknownEncoding,
|
||||
}
|
||||
|
||||
impl ResponseError for WsHandshakeError {
|
||||
|
||||
/// Return `BadRequest` for `ContentTypeError`
|
||||
impl ResponseError for ContentTypeError {
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
match *self {
|
||||
WsHandshakeError::GetMethodRequired => {
|
||||
HTTPMethodNotAllowed
|
||||
.build()
|
||||
.header(header::ALLOW, "GET")
|
||||
.finish()
|
||||
.unwrap()
|
||||
}
|
||||
WsHandshakeError::NoWebsocketUpgrade =>
|
||||
HTTPBadRequest.with_reason("No WebSocket UPGRADE header found"),
|
||||
WsHandshakeError::NoConnectionUpgrade =>
|
||||
HTTPBadRequest.with_reason("No CONNECTION upgrade"),
|
||||
WsHandshakeError::NoVersionHeader =>
|
||||
HTTPBadRequest.with_reason("Websocket version header is required"),
|
||||
WsHandshakeError::UnsupportedVersion =>
|
||||
HTTPBadRequest.with_reason("Unsupported version"),
|
||||
WsHandshakeError::BadWebsocketKey =>
|
||||
HTTPBadRequest.with_reason("Handshake error"),
|
||||
}
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
@ -377,16 +384,23 @@ pub enum UrlencodedError {
|
||||
/// Content type error
|
||||
#[fail(display="Content type error")]
|
||||
ContentType,
|
||||
/// Parse error
|
||||
#[fail(display="Parse error")]
|
||||
Parse,
|
||||
/// Payload error
|
||||
#[fail(display="Error that occur during reading payload")]
|
||||
Payload(PayloadError),
|
||||
#[fail(display="Error that occur during reading payload: {}", _0)]
|
||||
Payload(#[cause] PayloadError),
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UrlencodedError`
|
||||
impl ResponseError for UrlencodedError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
match *self {
|
||||
UrlencodedError::Overflow => httpcodes::HttpPayloadTooLarge.into(),
|
||||
UrlencodedError::UnknownLength => httpcodes::HttpLengthRequired.into(),
|
||||
_ => httpcodes::HttpBadRequest.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -406,18 +420,21 @@ pub enum JsonPayloadError {
|
||||
#[fail(display="Content type error")]
|
||||
ContentType,
|
||||
/// Deserialize error
|
||||
#[fail(display="Json deserialize error")]
|
||||
Deserialize(JsonError),
|
||||
#[fail(display="Json deserialize error: {}", _0)]
|
||||
Deserialize(#[cause] JsonError),
|
||||
/// Payload error
|
||||
#[fail(display="Error that occur during reading payload")]
|
||||
Payload(PayloadError),
|
||||
#[fail(display="Error that occur during reading payload: {}", _0)]
|
||||
Payload(#[cause] PayloadError),
|
||||
}
|
||||
|
||||
/// Return `BadRequest` for `UrlencodedError`
|
||||
impl ResponseError for JsonPayloadError {
|
||||
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(StatusCode::BAD_REQUEST, Body::Empty)
|
||||
match *self {
|
||||
JsonPayloadError::Overflow => httpcodes::HttpPayloadTooLarge.into(),
|
||||
_ => httpcodes::HttpBadRequest.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -478,39 +495,10 @@ impl From<UrlParseError> for UrlGenerationError {
|
||||
}
|
||||
}
|
||||
|
||||
macro_rules! ERROR_WRAP {
|
||||
($type:ty, $status:expr) => {
|
||||
unsafe impl<T> Sync for $type {}
|
||||
unsafe impl<T> Send for $type {}
|
||||
|
||||
impl<T> $type {
|
||||
pub fn cause(&self) -> &T {
|
||||
&self.0
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: fmt::Debug + 'static> Fail for $type {}
|
||||
impl<T: fmt::Debug + 'static> fmt::Display for $type {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ResponseError for $type
|
||||
where T: Send + Sync + fmt::Debug + 'static,
|
||||
{
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new($status, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper type that can wrap any error and generate *BAD REQUEST* response.
|
||||
/// Helper type that can wrap any error and generate custom response.
|
||||
///
|
||||
/// In following example any `io::Error` will be converted into "BAD REQUEST" response
|
||||
/// as oposite to *INNTERNAL SERVER ERROR* which is defined by default.
|
||||
/// as opposite to *INNTERNAL SERVER ERROR* which is defined by default.
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
@ -523,67 +511,143 @@ macro_rules! ERROR_WRAP {
|
||||
/// }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub struct ErrorBadRequest<T>(pub T);
|
||||
ERROR_WRAP!(ErrorBadRequest<T>, StatusCode::BAD_REQUEST);
|
||||
pub struct InternalError<T> {
|
||||
cause: T,
|
||||
status: StatusCode,
|
||||
backtrace: Backtrace,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *UNAUTHORIZED* response.
|
||||
pub struct ErrorUnauthorized<T>(pub T);
|
||||
ERROR_WRAP!(ErrorUnauthorized<T>, StatusCode::UNAUTHORIZED);
|
||||
unsafe impl<T> Sync for InternalError<T> {}
|
||||
unsafe impl<T> Send for InternalError<T> {}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *FORBIDDEN* response.
|
||||
pub struct ErrorForbidden<T>(pub T);
|
||||
ERROR_WRAP!(ErrorForbidden<T>, StatusCode::FORBIDDEN);
|
||||
impl<T> InternalError<T> {
|
||||
pub fn new(cause: T, status: StatusCode) -> Self {
|
||||
InternalError {
|
||||
cause,
|
||||
status,
|
||||
backtrace: Backtrace::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *NOT FOUND* response.
|
||||
pub struct ErrorNotFound<T>(pub T);
|
||||
ERROR_WRAP!(ErrorNotFound<T>, StatusCode::NOT_FOUND);
|
||||
impl<T> Fail for InternalError<T>
|
||||
where T: Send + Sync + fmt::Debug + 'static
|
||||
{
|
||||
fn backtrace(&self) -> Option<&Backtrace> {
|
||||
Some(&self.backtrace)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *METHOD NOT ALLOWED* response.
|
||||
pub struct ErrorMethodNotAllowed<T>(pub T);
|
||||
ERROR_WRAP!(ErrorMethodNotAllowed<T>, StatusCode::METHOD_NOT_ALLOWED);
|
||||
impl<T> fmt::Debug for InternalError<T>
|
||||
where T: Send + Sync + fmt::Debug + 'static
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.cause, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *REQUEST TIMEOUT* response.
|
||||
pub struct ErrorRequestTimeout<T>(pub T);
|
||||
ERROR_WRAP!(ErrorRequestTimeout<T>, StatusCode::REQUEST_TIMEOUT);
|
||||
impl<T> fmt::Display for InternalError<T>
|
||||
where T: Send + Sync + fmt::Debug + 'static
|
||||
{
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt::Debug::fmt(&self.cause, f)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *CONFLICT* response.
|
||||
pub struct ErrorConflict<T>(pub T);
|
||||
ERROR_WRAP!(ErrorConflict<T>, StatusCode::CONFLICT);
|
||||
impl<T> ResponseError for InternalError<T>
|
||||
where T: Send + Sync + fmt::Debug + 'static
|
||||
{
|
||||
fn error_response(&self) -> HttpResponse {
|
||||
HttpResponse::new(self.status, Body::Empty)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *GONE* response.
|
||||
pub struct ErrorGone<T>(pub T);
|
||||
ERROR_WRAP!(ErrorGone<T>, StatusCode::GONE);
|
||||
impl<T> Responder for InternalError<T>
|
||||
where T: Send + Sync + fmt::Debug + 'static
|
||||
{
|
||||
type Item = HttpResponse;
|
||||
type Error = Error;
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *PRECONDITION FAILED* response.
|
||||
pub struct ErrorPreconditionFailed<T>(pub T);
|
||||
ERROR_WRAP!(ErrorPreconditionFailed<T>, StatusCode::PRECONDITION_FAILED);
|
||||
fn respond_to(self, _: HttpRequest) -> Result<HttpResponse, Error> {
|
||||
Err(self.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *EXPECTATION FAILED* response.
|
||||
pub struct ErrorExpectationFailed<T>(pub T);
|
||||
ERROR_WRAP!(ErrorExpectationFailed<T>, StatusCode::EXPECTATION_FAILED);
|
||||
/// Helper function that creates wrapper of any error and generate *BAD REQUEST* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorBadRequest<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::BAD_REQUEST)
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Helper type that can wrap any error and generate *INTERNAL SERVER ERROR* response.
|
||||
pub struct ErrorInternalServerError<T>(pub T);
|
||||
ERROR_WRAP!(ErrorInternalServerError<T>, StatusCode::INTERNAL_SERVER_ERROR);
|
||||
/// Helper function that creates wrapper of any error and generate *UNAUTHORIZED* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorUnauthorized<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::UNAUTHORIZED)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *FORBIDDEN* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorForbidden<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::FORBIDDEN)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *NOT FOUND* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorNotFound<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::NOT_FOUND)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *METHOD NOT ALLOWED* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorMethodNotAllowed<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::METHOD_NOT_ALLOWED)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *REQUEST TIMEOUT* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorRequestTimeout<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::REQUEST_TIMEOUT)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *CONFLICT* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorConflict<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::CONFLICT)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *GONE* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorGone<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::GONE)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *PRECONDITION FAILED* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorPreconditionFailed<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::PRECONDITION_FAILED)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *EXPECTATION FAILED* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorExpectationFailed<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::EXPECTATION_FAILED)
|
||||
}
|
||||
|
||||
/// Helper function that creates wrapper of any error and generate *INTERNAL SERVER ERROR* response.
|
||||
#[allow(non_snake_case)]
|
||||
pub fn ErrorInternalServerError<T>(err: T) -> InternalError<T> {
|
||||
InternalError::new(err, StatusCode::INTERNAL_SERVER_ERROR)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::env;
|
||||
use std::error::Error as StdError;
|
||||
use std::io;
|
||||
use httparse;
|
||||
use http::{StatusCode, Error as HttpError};
|
||||
use cookie::ParseError as CookieParseError;
|
||||
use failure;
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
@ -660,22 +724,6 @@ mod tests {
|
||||
assert_eq!(resp.status(), StatusCode::EXPECTATION_FAILED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_wserror_http_response() {
|
||||
let resp: HttpResponse = WsHandshakeError::GetMethodRequired.error_response();
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
let resp: HttpResponse = WsHandshakeError::NoWebsocketUpgrade.error_response();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: HttpResponse = WsHandshakeError::NoConnectionUpgrade.error_response();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: HttpResponse = WsHandshakeError::NoVersionHeader.error_response();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: HttpResponse = WsHandshakeError::UnsupportedVersion.error_response();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
let resp: HttpResponse = WsHandshakeError::BadWebsocketKey.error_response();
|
||||
assert_eq!(resp.status(), StatusCode::BAD_REQUEST);
|
||||
}
|
||||
|
||||
macro_rules! from {
|
||||
($from:expr => $error:pat) => {
|
||||
match ParseError::from($from) {
|
||||
@ -712,4 +760,18 @@ mod tests {
|
||||
from!(httparse::Error::TooManyHeaders => ParseError::TooLarge);
|
||||
from!(httparse::Error::Version => ParseError::Version);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn failure_error() {
|
||||
const NAME: &str = "RUST_BACKTRACE";
|
||||
let old_tb = env::var(NAME);
|
||||
env::set_var(NAME, "0");
|
||||
let error = failure::err_msg("Hello!");
|
||||
let resp: Error = error.into();
|
||||
assert_eq!(format!("{:?}", resp), "Compat { error: ErrorMessage { msg: \"Hello!\" } }\n\n");
|
||||
match old_tb {
|
||||
Ok(x) => env::set_var(NAME, x),
|
||||
_ => env::remove_var(NAME),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
320
src/fs.rs
320
src/fs.rs
@ -1,24 +1,42 @@
|
||||
//! Static files support.
|
||||
|
||||
// //! TODO: needs to re-implement actual files handling, current impl blocks
|
||||
use std::io;
|
||||
use std::io::Read;
|
||||
use std::{io, cmp};
|
||||
use std::io::{Read, Seek};
|
||||
use std::fmt::Write;
|
||||
use std::fs::{File, DirEntry};
|
||||
use std::fs::{File, DirEntry, Metadata};
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::ops::{Deref, DerefMut};
|
||||
use std::time::{SystemTime, UNIX_EPOCH};
|
||||
|
||||
#[cfg(unix)]
|
||||
use std::os::unix::fs::MetadataExt;
|
||||
|
||||
use bytes::{Bytes, BytesMut, BufMut};
|
||||
use http::{Method, StatusCode};
|
||||
use futures::{Async, Poll, Future, Stream};
|
||||
use futures_cpupool::{CpuPool, CpuFuture};
|
||||
use mime_guess::get_mime_type;
|
||||
|
||||
use header;
|
||||
use error::Error;
|
||||
use param::FromParam;
|
||||
use handler::{Handler, Responder};
|
||||
use httpmessage::HttpMessage;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
use httpcodes::HTTPOk;
|
||||
use httpcodes::{HttpOk, HttpFound, HttpMethodNotAllowed};
|
||||
|
||||
/// A file with an associated name; responds with the Content-Type based on the
|
||||
/// file extension.
|
||||
#[derive(Debug)]
|
||||
pub struct NamedFile(PathBuf, File);
|
||||
pub struct NamedFile {
|
||||
path: PathBuf,
|
||||
file: File,
|
||||
md: Metadata,
|
||||
modified: Option<SystemTime>,
|
||||
cpu_pool: Option<CpuPool>,
|
||||
}
|
||||
|
||||
impl NamedFile {
|
||||
/// Attempts to open a file in read-only mode.
|
||||
@ -28,18 +46,21 @@ impl NamedFile {
|
||||
/// ```rust
|
||||
/// use actix_web::fs::NamedFile;
|
||||
///
|
||||
/// # #[allow(unused_variables)]
|
||||
/// let file = NamedFile::open("foo.txt");
|
||||
/// ```
|
||||
pub fn open<P: AsRef<Path>>(path: P) -> io::Result<NamedFile> {
|
||||
let file = File::open(path.as_ref())?;
|
||||
Ok(NamedFile(path.as_ref().to_path_buf(), file))
|
||||
let md = file.metadata()?;
|
||||
let path = path.as_ref().to_path_buf();
|
||||
let modified = md.modified().ok();
|
||||
let cpu_pool = None;
|
||||
Ok(NamedFile{path, file, md, modified, cpu_pool})
|
||||
}
|
||||
|
||||
/// Returns reference to the underlying `File` object.
|
||||
#[inline]
|
||||
pub fn file(&self) -> &File {
|
||||
&self.1
|
||||
&self.file
|
||||
}
|
||||
|
||||
/// Retrieve the path of this file.
|
||||
@ -50,7 +71,6 @@ impl NamedFile {
|
||||
/// # use std::io;
|
||||
/// use actix_web::fs::NamedFile;
|
||||
///
|
||||
/// # #[allow(dead_code)]
|
||||
/// # fn path() -> io::Result<()> {
|
||||
/// let file = NamedFile::open("test.txt")?;
|
||||
/// assert_eq!(file.path().as_os_str(), "foo.txt");
|
||||
@ -59,7 +79,37 @@ impl NamedFile {
|
||||
/// ```
|
||||
#[inline]
|
||||
pub fn path(&self) -> &Path {
|
||||
self.0.as_path()
|
||||
self.path.as_path()
|
||||
}
|
||||
|
||||
/// Set `CpuPool` to use
|
||||
#[inline]
|
||||
pub fn set_cpu_pool(mut self, cpu_pool: CpuPool) -> Self {
|
||||
self.cpu_pool = Some(cpu_pool);
|
||||
self
|
||||
}
|
||||
|
||||
fn etag(&self) -> Option<header::EntityTag> {
|
||||
// This etag format is similar to Apache's.
|
||||
self.modified.as_ref().map(|mtime| {
|
||||
let ino = {
|
||||
#[cfg(unix)]
|
||||
{ self.md.ino() }
|
||||
#[cfg(not(unix))]
|
||||
{ 0 }
|
||||
};
|
||||
|
||||
let dur = mtime.duration_since(UNIX_EPOCH)
|
||||
.expect("modification time must be after epoch");
|
||||
header::EntityTag::strong(
|
||||
format!("{:x}:{:x}:{:x}:{:x}",
|
||||
ino, self.md.len(), dur.as_secs(),
|
||||
dur.subsec_nanos()))
|
||||
})
|
||||
}
|
||||
|
||||
fn last_modified(&self) -> Option<header::HttpDate> {
|
||||
self.modified.map(|mtime| mtime.into())
|
||||
}
|
||||
}
|
||||
|
||||
@ -67,31 +117,166 @@ impl Deref for NamedFile {
|
||||
type Target = File;
|
||||
|
||||
fn deref(&self) -> &File {
|
||||
&self.1
|
||||
&self.file
|
||||
}
|
||||
}
|
||||
|
||||
impl DerefMut for NamedFile {
|
||||
fn deref_mut(&mut self) -> &mut File {
|
||||
&mut self.1
|
||||
&mut self.file
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` has no `If-Match` header or one which matches `etag`.
|
||||
fn any_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfMatch>() {
|
||||
None | Some(header::IfMatch::Any) => true,
|
||||
Some(header::IfMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.strong_eq(some_etag) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns true if `req` doesn't have an `If-None-Match` header matching `req`.
|
||||
fn none_match(etag: Option<&header::EntityTag>, req: &HttpRequest) -> bool {
|
||||
match req.get_header::<header::IfNoneMatch>() {
|
||||
Some(header::IfNoneMatch::Any) => false,
|
||||
Some(header::IfNoneMatch::Items(ref items)) => {
|
||||
if let Some(some_etag) = etag {
|
||||
for item in items {
|
||||
if item.weak_eq(some_etag) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
None => true,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
impl Responder for NamedFile {
|
||||
type Item = HttpResponse;
|
||||
type Error = io::Error;
|
||||
|
||||
fn respond_to(mut self, _: HttpRequest) -> Result<HttpResponse, io::Error> {
|
||||
let mut resp = HTTPOk.build();
|
||||
use headers::ContentEncoding;
|
||||
resp.content_encoding(ContentEncoding::Identity);
|
||||
if let Some(ext) = self.path().extension() {
|
||||
let mime = get_mime_type(&ext.to_string_lossy());
|
||||
resp.content_type(format!("{}", mime).as_str());
|
||||
fn respond_to(self, req: HttpRequest) -> Result<HttpResponse, io::Error> {
|
||||
if *req.method() != Method::GET && *req.method() != Method::HEAD {
|
||||
return Ok(HttpMethodNotAllowed.build()
|
||||
.header(header::http::CONTENT_TYPE, "text/plain")
|
||||
.header(header::http::ALLOW, "GET, HEAD")
|
||||
.body("This resource only supports GET and HEAD.").unwrap())
|
||||
}
|
||||
|
||||
let etag = self.etag();
|
||||
let last_modified = self.last_modified();
|
||||
|
||||
// check preconditions
|
||||
let precondition_failed = if !any_match(etag.as_ref(), &req) {
|
||||
true
|
||||
} else if let (Some(ref m), Some(header::IfUnmodifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
m > since
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
// check last modified
|
||||
let not_modified = if !none_match(etag.as_ref(), &req) {
|
||||
true
|
||||
} else if let (Some(ref m), Some(header::IfModifiedSince(ref since))) =
|
||||
(last_modified, req.get_header())
|
||||
{
|
||||
m <= since
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut resp = HttpOk.build();
|
||||
|
||||
resp
|
||||
.if_some(self.path().extension(), |ext, resp| {
|
||||
resp.set(header::ContentType(get_mime_type(&ext.to_string_lossy())));
|
||||
})
|
||||
.if_some(last_modified, |lm, resp| {resp.set(header::LastModified(lm));})
|
||||
.if_some(etag, |etag, resp| {resp.set(header::ETag(etag));});
|
||||
|
||||
if precondition_failed {
|
||||
return Ok(resp.status(StatusCode::PRECONDITION_FAILED).finish().unwrap())
|
||||
} else if not_modified {
|
||||
return Ok(resp.status(StatusCode::NOT_MODIFIED).finish().unwrap())
|
||||
}
|
||||
|
||||
if *req.method() == Method::GET {
|
||||
let reader = ChunkedReadFile {
|
||||
size: self.md.len(),
|
||||
offset: 0,
|
||||
cpu_pool: self.cpu_pool.unwrap_or_else(|| req.cpu_pool().clone()),
|
||||
file: Some(self.file),
|
||||
fut: None,
|
||||
};
|
||||
Ok(resp.streaming(reader).unwrap())
|
||||
} else {
|
||||
Ok(resp.finish().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A helper created from a `std::fs::File` which reads the file
|
||||
/// chunk-by-chunk on a `CpuPool`.
|
||||
pub struct ChunkedReadFile {
|
||||
size: u64,
|
||||
offset: u64,
|
||||
cpu_pool: CpuPool,
|
||||
file: Option<File>,
|
||||
fut: Option<CpuFuture<(File, Bytes), io::Error>>,
|
||||
}
|
||||
|
||||
impl Stream for ChunkedReadFile {
|
||||
type Item = Bytes;
|
||||
type Error= Error;
|
||||
|
||||
fn poll(&mut self) -> Poll<Option<Bytes>, Error> {
|
||||
if self.fut.is_some() {
|
||||
return match self.fut.as_mut().unwrap().poll()? {
|
||||
Async::Ready((file, bytes)) => {
|
||||
self.fut.take();
|
||||
self.file = Some(file);
|
||||
self.offset += bytes.len() as u64;
|
||||
Ok(Async::Ready(Some(bytes)))
|
||||
},
|
||||
Async::NotReady => Ok(Async::NotReady),
|
||||
};
|
||||
}
|
||||
|
||||
let size = self.size;
|
||||
let offset = self.offset;
|
||||
|
||||
if size == offset {
|
||||
Ok(Async::Ready(None))
|
||||
} else {
|
||||
let mut file = self.file.take().expect("Use after completion");
|
||||
self.fut = Some(self.cpu_pool.spawn_fn(move || {
|
||||
let max_bytes = cmp::min(size.saturating_sub(offset), 65_536) as usize;
|
||||
let mut buf = BytesMut::with_capacity(max_bytes);
|
||||
file.seek(io::SeekFrom::Start(offset))?;
|
||||
let nbytes = file.read(unsafe{buf.bytes_mut()})?;
|
||||
if nbytes == 0 {
|
||||
return Err(io::ErrorKind::UnexpectedEof.into())
|
||||
}
|
||||
unsafe{buf.advance_mut(nbytes)};
|
||||
Ok((file, buf.freeze()))
|
||||
}));
|
||||
self.poll()
|
||||
}
|
||||
let mut data = Vec::new();
|
||||
let _ = self.1.read_to_end(&mut data);
|
||||
Ok(resp.body(data).unwrap())
|
||||
}
|
||||
}
|
||||
|
||||
@ -104,10 +289,7 @@ pub struct Directory{
|
||||
|
||||
impl Directory {
|
||||
pub fn new(base: PathBuf, path: PathBuf) -> Directory {
|
||||
Directory {
|
||||
base: base,
|
||||
path: path
|
||||
}
|
||||
Directory { base, path }
|
||||
}
|
||||
|
||||
fn can_list(&self, entry: &io::Result<DirEntry>) -> bool {
|
||||
@ -138,7 +320,7 @@ impl Responder for Directory {
|
||||
for entry in self.path.read_dir()? {
|
||||
if self.can_list(&entry) {
|
||||
let entry = entry.unwrap();
|
||||
let p = match entry.path().strip_prefix(&self.base) {
|
||||
let p = match entry.path().strip_prefix(&self.path) {
|
||||
Ok(p) => base.join(p),
|
||||
Err(_) => continue
|
||||
};
|
||||
@ -166,7 +348,7 @@ impl Responder for Directory {
|
||||
<ul>\
|
||||
{}\
|
||||
</ul></body>\n</html>", index_of, index_of, body);
|
||||
Ok(HTTPOk.build()
|
||||
Ok(HttpOk.build()
|
||||
.content_type("text/html; charset=utf-8")
|
||||
.body(html).unwrap())
|
||||
}
|
||||
@ -176,6 +358,7 @@ impl Responder for Directory {
|
||||
pub enum FilesystemElement {
|
||||
File(NamedFile),
|
||||
Directory(Directory),
|
||||
Redirect(HttpResponse),
|
||||
}
|
||||
|
||||
impl Responder for FilesystemElement {
|
||||
@ -186,6 +369,7 @@ impl Responder for FilesystemElement {
|
||||
match self {
|
||||
FilesystemElement::File(file) => file.respond_to(req),
|
||||
FilesystemElement::Directory(dir) => dir.respond_to(req),
|
||||
FilesystemElement::Redirect(resp) => Ok(resp),
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -209,7 +393,9 @@ impl Responder for FilesystemElement {
|
||||
pub struct StaticFiles {
|
||||
directory: PathBuf,
|
||||
accessible: bool,
|
||||
index: Option<String>,
|
||||
show_index: bool,
|
||||
cpu_pool: CpuPool,
|
||||
_chunk_size: usize,
|
||||
_follow_symlinks: bool,
|
||||
}
|
||||
@ -220,7 +406,7 @@ impl StaticFiles {
|
||||
/// `dir` - base directory
|
||||
///
|
||||
/// `index` - show index for directory
|
||||
pub fn new<D: Into<PathBuf>>(dir: D, index: bool) -> StaticFiles {
|
||||
pub fn new<T: Into<PathBuf>>(dir: T, index: bool) -> StaticFiles {
|
||||
let dir = dir.into();
|
||||
|
||||
let (dir, access) = match dir.canonicalize() {
|
||||
@ -241,12 +427,22 @@ impl StaticFiles {
|
||||
StaticFiles {
|
||||
directory: dir,
|
||||
accessible: access,
|
||||
index: None,
|
||||
show_index: index,
|
||||
cpu_pool: CpuPool::new(40),
|
||||
_chunk_size: 0,
|
||||
_follow_symlinks: false,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set index file
|
||||
///
|
||||
/// Redirects to specific index file for directory "/" instead of
|
||||
/// showing files listing.
|
||||
pub fn index_file<T: Into<String>>(mut self, index: T) -> StaticFiles {
|
||||
self.index = Some(index.into());
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<S> Handler<S> for StaticFiles {
|
||||
@ -269,13 +465,29 @@ impl<S> Handler<S> for StaticFiles {
|
||||
let path = self.directory.join(&relpath).canonicalize()?;
|
||||
|
||||
if path.is_dir() {
|
||||
if self.show_index {
|
||||
Ok(FilesystemElement::Directory(Directory::new(self.directory.clone(), path)))
|
||||
if let Some(ref redir_index) = self.index {
|
||||
// TODO: Don't redirect, just return the index content.
|
||||
// TODO: It'd be nice if there were a good usable URL manipulation library
|
||||
let mut new_path: String = req.path().to_owned();
|
||||
for el in relpath.iter() {
|
||||
new_path.push_str(&el.to_string_lossy());
|
||||
new_path.push('/');
|
||||
}
|
||||
new_path.push_str(redir_index);
|
||||
Ok(FilesystemElement::Redirect(
|
||||
HttpFound
|
||||
.build()
|
||||
.header(header::http::LOCATION, new_path.as_str())
|
||||
.finish().unwrap()))
|
||||
} else if self.show_index {
|
||||
Ok(FilesystemElement::Directory(
|
||||
Directory::new(self.directory.clone(), path)))
|
||||
} else {
|
||||
Err(io::Error::new(io::ErrorKind::NotFound, "not found"))
|
||||
}
|
||||
} else {
|
||||
Ok(FilesystemElement::File(NamedFile::open(path)?))
|
||||
Ok(FilesystemElement::File(
|
||||
NamedFile::open(path)?.set_cpu_pool(self.cpu_pool.clone())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -284,12 +496,14 @@ impl<S> Handler<S> for StaticFiles {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use http::header;
|
||||
use test::TestRequest;
|
||||
use http::{header, Method, StatusCode};
|
||||
|
||||
#[test]
|
||||
fn test_named_file() {
|
||||
assert!(NamedFile::open("test--").is_err());
|
||||
let mut file = NamedFile::open("Cargo.toml").unwrap();
|
||||
let mut file = NamedFile::open("Cargo.toml").unwrap()
|
||||
.set_cpu_pool(CpuPool::new(1));
|
||||
{ file.file();
|
||||
let _f: &File = &file; }
|
||||
{ let _f: &mut File = &mut file; }
|
||||
@ -298,6 +512,15 @@ mod tests {
|
||||
assert_eq!(resp.headers().get(header::CONTENT_TYPE).unwrap(), "text/x-toml")
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_named_file_not_allowed() {
|
||||
let req = TestRequest::default().method(Method::POST).finish();
|
||||
let file = NamedFile::open("Cargo.toml").unwrap();
|
||||
|
||||
let resp = file.respond_to(req).unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::METHOD_NOT_ALLOWED);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_static_files() {
|
||||
let mut st = StaticFiles::new(".", true);
|
||||
@ -317,4 +540,33 @@ mod tests {
|
||||
assert!(resp.body().is_binary());
|
||||
assert!(format!("{:?}", resp.body()).contains("README.md"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redirect_to_index() {
|
||||
let mut st = StaticFiles::new(".", false).index_file("index.html");
|
||||
let mut req = HttpRequest::default();
|
||||
req.match_info_mut().add("tail", "guide");
|
||||
|
||||
let resp = st.handle(req).respond_to(HttpRequest::default()).unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/guide/index.html");
|
||||
|
||||
let mut req = HttpRequest::default();
|
||||
req.match_info_mut().add("tail", "guide/");
|
||||
|
||||
let resp = st.handle(req).respond_to(HttpRequest::default()).unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/guide/index.html");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_redirect_to_index_nested() {
|
||||
let mut st = StaticFiles::new(".", false).index_file("Cargo.toml");
|
||||
let mut req = HttpRequest::default();
|
||||
req.match_info_mut().add("tail", "examples/basics");
|
||||
|
||||
let resp = st.handle(req).respond_to(HttpRequest::default()).unwrap();
|
||||
assert_eq!(resp.status(), StatusCode::FOUND);
|
||||
assert_eq!(resp.headers().get(header::LOCATION).unwrap(), "/examples/basics/Cargo.toml");
|
||||
}
|
||||
}
|
||||
|
157
src/handler.rs
157
src/handler.rs
@ -9,7 +9,7 @@ use error::Error;
|
||||
use httprequest::HttpRequest;
|
||||
use httpresponse::HttpResponse;
|
||||
|
||||
/// Trait defines object that could be regestered as route handler
|
||||
/// Trait defines object that could be registered as route handler
|
||||
#[allow(unused_variables)]
|
||||
pub trait Handler<S>: 'static {
|
||||
|
||||
@ -34,8 +34,65 @@ pub trait Responder {
|
||||
fn respond_to(self, req: HttpRequest) -> Result<Self::Item, Self::Error>;
|
||||
}
|
||||
|
||||
/// Combines two different responder types into a single type
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # extern crate futures;
|
||||
/// # use futures::future::Future;
|
||||
/// use actix_web::AsyncResponder;
|
||||
/// use futures::future::result;
|
||||
/// use actix_web::{Either, Error, HttpRequest, HttpResponse, httpcodes};
|
||||
///
|
||||
/// type RegisterResult = Either<HttpResponse, Box<Future<Item=HttpResponse, Error=Error>>>;
|
||||
///
|
||||
///
|
||||
/// fn index(req: HttpRequest) -> RegisterResult {
|
||||
/// if is_a_variant() { // <- choose variant A
|
||||
/// Either::A(
|
||||
/// httpcodes::HttpBadRequest.with_body("Bad data"))
|
||||
/// } else {
|
||||
/// Either::B( // <- variant B
|
||||
/// result(HttpResponse::Ok()
|
||||
/// .content_type("text/html")
|
||||
/// .body(format!("Hello!"))
|
||||
/// .map_err(|e| e.into())).responder())
|
||||
/// }
|
||||
/// }
|
||||
/// # fn is_a_variant() -> bool { true }
|
||||
/// # fn main() {}
|
||||
/// ```
|
||||
#[derive(Debug)]
|
||||
pub enum Either<A, B> {
|
||||
/// First branch of the type
|
||||
A(A),
|
||||
/// Second branch of the type
|
||||
B(B),
|
||||
}
|
||||
|
||||
impl<A, B> Responder for Either<A, B>
|
||||
where A: Responder, B: Responder
|
||||
{
|
||||
type Item = Reply;
|
||||
type Error = Error;
|
||||
|
||||
fn respond_to(self, req: HttpRequest) -> Result<Reply, Error> {
|
||||
match self {
|
||||
Either::A(a) => match a.respond_to(req) {
|
||||
Ok(val) => Ok(val.into()),
|
||||
Err(err) => Err(err.into()),
|
||||
},
|
||||
Either::B(b) => match b.respond_to(req) {
|
||||
Ok(val) => Ok(val.into()),
|
||||
Err(err) => Err(err.into()),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[doc(hidden)]
|
||||
/// Convinience trait that convert `Future` object into `Boxed` future
|
||||
/// Convenience trait that convert `Future` object into `Boxed` future
|
||||
pub trait AsyncResponder<I, E>: Sized {
|
||||
fn responder(self) -> Box<Future<Item=I, Error=E>>;
|
||||
}
|
||||
@ -193,7 +250,7 @@ impl<I, E> Responder for Box<Future<Item=I, Error=E>>
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait defines object that could be regestered as resource route
|
||||
/// Trait defines object that could be registered as resource route
|
||||
pub(crate) trait RouteHandler<S>: 'static {
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Reply;
|
||||
}
|
||||
@ -215,7 +272,7 @@ impl<S, H, R> WrapHandler<S, H, R>
|
||||
S: 'static,
|
||||
{
|
||||
pub fn new(h: H) -> Self {
|
||||
WrapHandler{h: h, s: PhantomData}
|
||||
WrapHandler{h, s: PhantomData}
|
||||
}
|
||||
}
|
||||
|
||||
@ -225,7 +282,7 @@ impl<S, H, R> RouteHandler<S> for WrapHandler<S, H, R>
|
||||
S: 'static,
|
||||
{
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Reply {
|
||||
let req2 = req.clone_without_state();
|
||||
let req2 = req.without_state();
|
||||
match self.h.handle(req).respond_to(req2) {
|
||||
Ok(reply) => reply.into(),
|
||||
Err(err) => Reply::response(err.into()),
|
||||
@ -266,7 +323,7 @@ impl<S, H, F, R, E> RouteHandler<S> for AsyncHandler<S, H, F, R, E>
|
||||
S: 'static,
|
||||
{
|
||||
fn handle(&mut self, req: HttpRequest<S>) -> Reply {
|
||||
let req2 = req.clone_without_state();
|
||||
let req2 = req.without_state();
|
||||
let fut = (self.h)(req)
|
||||
.map_err(|e| e.into())
|
||||
.then(move |r| {
|
||||
@ -287,6 +344,7 @@ impl<S, H, F, R, E> RouteHandler<S> for AsyncHandler<S, H, F, R, E>
|
||||
/// By normalizing it means:
|
||||
///
|
||||
/// - Add a trailing slash to the path.
|
||||
/// - Remove a trailing slash from the path.
|
||||
/// - Double slashes are replaced by one.
|
||||
///
|
||||
/// The handler returns as soon as it finds a path that resolves
|
||||
@ -308,7 +366,7 @@ impl<S, H, F, R, E> RouteHandler<S> for AsyncHandler<S, H, F, R, E>
|
||||
/// # use actix_web::*;
|
||||
/// #
|
||||
/// # fn index(req: HttpRequest) -> httpcodes::StaticResponse {
|
||||
/// # httpcodes::HTTPOk
|
||||
/// # httpcodes::HttpOk
|
||||
/// # }
|
||||
/// fn main() {
|
||||
/// let app = Application::new()
|
||||
@ -341,13 +399,13 @@ impl Default for NormalizePath {
|
||||
}
|
||||
|
||||
impl NormalizePath {
|
||||
/// Create new `NoramlizePath` instance
|
||||
/// Create new `NormalizePath` instance
|
||||
pub fn new(append: bool, merge: bool, redirect: StatusCode) -> NormalizePath {
|
||||
NormalizePath {
|
||||
append: append,
|
||||
merge: merge,
|
||||
append,
|
||||
merge,
|
||||
redirect,
|
||||
re_merge: Regex::new("//+").unwrap(),
|
||||
redirect: redirect,
|
||||
not_found: StatusCode::NOT_FOUND,
|
||||
}
|
||||
}
|
||||
@ -379,6 +437,32 @@ impl<S> Handler<S> for NormalizePath {
|
||||
.body(Body::Empty);
|
||||
}
|
||||
}
|
||||
|
||||
// try to remove trailing slash
|
||||
if p.ends_with('/') {
|
||||
let p = p.as_ref().trim_right_matches('/');
|
||||
if router.has_route(p) {
|
||||
let mut req = HttpResponse::build(self.redirect);
|
||||
return if !query.is_empty() {
|
||||
req.header(header::LOCATION, (p.to_owned() + "?" + query).as_str())
|
||||
} else {
|
||||
req.header(header::LOCATION, p)
|
||||
}
|
||||
.body(Body::Empty);
|
||||
}
|
||||
}
|
||||
} else if p.ends_with('/') {
|
||||
// try to remove trailing slash
|
||||
let p = p.as_ref().trim_right_matches('/');
|
||||
if router.has_route(p) {
|
||||
let mut req = HttpResponse::build(self.redirect);
|
||||
return if !query.is_empty() {
|
||||
req.header(header::LOCATION, (p.to_owned() + "?" + query).as_str())
|
||||
} else {
|
||||
req.header(header::LOCATION, p)
|
||||
}
|
||||
.body(Body::Empty);
|
||||
}
|
||||
}
|
||||
}
|
||||
// append trailing slash
|
||||
@ -416,16 +500,17 @@ mod tests {
|
||||
.finish();
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![("/resource1", "", StatusCode::OK),
|
||||
("/resource1/", "", StatusCode::NOT_FOUND),
|
||||
("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/", "", StatusCode::OK),
|
||||
("/resource1?p1=1&p2=2", "", StatusCode::OK),
|
||||
("/resource1/?p1=1&p2=2", "", StatusCode::NOT_FOUND),
|
||||
("/resource2?p1=1&p2=2", "/resource2/?p1=1&p2=2",
|
||||
StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/?p1=1&p2=2", "", StatusCode::OK)
|
||||
];
|
||||
let params =
|
||||
vec![("/resource1", "", StatusCode::OK),
|
||||
("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2", "/resource2/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/", "", StatusCode::OK),
|
||||
("/resource1?p1=1&p2=2", "", StatusCode::OK),
|
||||
("/resource1/?p1=1&p2=2", "/resource1?p1=1&p2=2", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2?p1=1&p2=2", "/resource2/?p1=1&p2=2",
|
||||
StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2/?p1=1&p2=2", "", StatusCode::OK)
|
||||
];
|
||||
for (path, target, code) in params {
|
||||
let req = app.prepare_request(TestRequest::with_uri(path).finish());
|
||||
let resp = app.run(req);
|
||||
@ -450,11 +535,11 @@ mod tests {
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![("/resource1", StatusCode::OK),
|
||||
("/resource1/", StatusCode::NOT_FOUND),
|
||||
("/resource1/", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2", StatusCode::NOT_FOUND),
|
||||
("/resource2/", StatusCode::OK),
|
||||
("/resource1?p1=1&p2=2", StatusCode::OK),
|
||||
("/resource1/?p1=1&p2=2", StatusCode::NOT_FOUND),
|
||||
("/resource1/?p1=1&p2=2", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource2?p1=1&p2=2", StatusCode::NOT_FOUND),
|
||||
("/resource2/?p1=1&p2=2", StatusCode::OK)
|
||||
];
|
||||
@ -477,17 +562,21 @@ mod tests {
|
||||
// trailing slashes
|
||||
let params = vec![
|
||||
("/resource1/a/b", "", StatusCode::OK),
|
||||
("/resource1/", "/resource1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource1//", "/resource1", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource1//a//b", "/resource1/a/b", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource1//a//b/", "", StatusCode::NOT_FOUND),
|
||||
("//resource1//a//b/", "/resource1/a/b", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource1//a//b//", "/resource1/a/b", StatusCode::MOVED_PERMANENTLY),
|
||||
("///resource1//a//b", "/resource1/a/b", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource1/a///b", "/resource1/a/b", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource1/a//b/", "", StatusCode::NOT_FOUND),
|
||||
("/////resource1/a//b/", "/resource1/a/b", StatusCode::MOVED_PERMANENTLY),
|
||||
("/resource1/a/b?p=1", "", StatusCode::OK),
|
||||
("//resource1//a//b?p=1", "/resource1/a/b?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("//resource1//a//b/?p=1", "", StatusCode::NOT_FOUND),
|
||||
("//resource1//a//b/?p=1", "/resource1/a/b?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("///resource1//a//b?p=1", "/resource1/a/b?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource1/a///b?p=1", "/resource1/a/b?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource1/a//b/?p=1", "", StatusCode::NOT_FOUND),
|
||||
("/////resource1/a//b/?p=1", "/resource1/a/b?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
("/////resource1/a//b//?p=1", "/resource1/a/b?p=1", StatusCode::MOVED_PERMANENTLY),
|
||||
];
|
||||
for (path, target, code) in params {
|
||||
let req = app.prepare_request(TestRequest::with_uri(path).finish());
|
||||
@ -515,13 +604,14 @@ mod tests {
|
||||
// trailing slashes
|
||||
let params = vec
|
||||
///
|
||||
/// The `Accept` header field can be used by user agents to specify
|
||||
/// response media types that are acceptable. Accept header fields can
|
||||
/// be used to indicate that the request is specifically limited to a
|
||||
/// small set of desired types, as in the case of a request for an
|
||||
/// in-line image
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Accept = #( media-range [ accept-params ] )
|
||||
///
|
||||
/// media-range = ( "*/*"
|
||||
/// / ( type "/" "*" )
|
||||
/// / ( type "/" subtype )
|
||||
/// ) *( OWS ";" OWS parameter )
|
||||
/// accept-params = weight *( accept-ext )
|
||||
/// accept-ext = OWS ";" OWS token [ "=" ( token / quoted-string ) ]
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `audio/*; q=0.2, audio/basic`
|
||||
/// * `text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// extern crate mime;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{Accept, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
///
|
||||
/// builder.set(
|
||||
/// Accept(vec![
|
||||
/// qitem(mime::TEXT_HTML),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// extern crate mime;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{Accept, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
///
|
||||
/// builder.set(
|
||||
/// Accept(vec![
|
||||
/// qitem(mime::APPLICATION_JSON),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// extern crate mime;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{Accept, QualityItem, q, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
///
|
||||
/// builder.set(
|
||||
/// Accept(vec![
|
||||
/// qitem(mime::TEXT_HTML),
|
||||
/// qitem("application/xhtml+xml".parse().unwrap()),
|
||||
/// QualityItem::new(
|
||||
/// mime::TEXT_XML,
|
||||
/// q(900)
|
||||
/// ),
|
||||
/// qitem("image/webp".parse().unwrap()),
|
||||
/// QualityItem::new(
|
||||
/// mime::STAR_STAR,
|
||||
/// q(800)
|
||||
/// ),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(Accept, http::ACCEPT) => (QualityItem<Mime>)+
|
||||
|
||||
test_accept {
|
||||
// Tests from the RFC
|
||||
test_header!(
|
||||
test1,
|
||||
vec![b"audio/*; q=0.2, audio/basic"],
|
||||
Some(HeaderField(vec![
|
||||
QualityItem::new("audio/*".parse().unwrap(), q(200)),
|
||||
qitem("audio/basic".parse().unwrap()),
|
||||
])));
|
||||
test_header!(
|
||||
test2,
|
||||
vec![b"text/plain; q=0.5, text/html, text/x-dvi; q=0.8, text/x-c"],
|
||||
Some(HeaderField(vec![
|
||||
QualityItem::new(TEXT_PLAIN, q(500)),
|
||||
qitem(TEXT_HTML),
|
||||
QualityItem::new(
|
||||
"text/x-dvi".parse().unwrap(),
|
||||
q(800)),
|
||||
qitem("text/x-c".parse().unwrap()),
|
||||
])));
|
||||
// Custom tests
|
||||
test_header!(
|
||||
test3,
|
||||
vec![b"text/plain; charset=utf-8"],
|
||||
Some(Accept(vec![
|
||||
qitem(TEXT_PLAIN_UTF_8),
|
||||
])));
|
||||
test_header!(
|
||||
test4,
|
||||
vec![b"text/plain; charset=utf-8; q=0.5"],
|
||||
Some(Accept(vec![
|
||||
QualityItem::new(TEXT_PLAIN_UTF_8,
|
||||
q(500)),
|
||||
])));
|
||||
|
||||
#[test]
|
||||
fn test_fuzzing1() {
|
||||
use test::TestRequest;
|
||||
let req = TestRequest::with_header(http::ACCEPT, "chunk#;e").finish();
|
||||
let header = Accept::parse(&req);
|
||||
assert!(header.is_ok());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Accept {
|
||||
/// A constructor to easily create `Accept: */*`.
|
||||
pub fn star() -> Accept {
|
||||
Accept(vec![qitem(mime::STAR_STAR)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: application/json`.
|
||||
pub fn json() -> Accept {
|
||||
Accept(vec![qitem(mime::APPLICATION_JSON)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: text/*`.
|
||||
pub fn text() -> Accept {
|
||||
Accept(vec![qitem(mime::TEXT_STAR)])
|
||||
}
|
||||
|
||||
/// A constructor to easily create `Accept: image/*`.
|
||||
pub fn image() -> Accept {
|
||||
Accept(vec![qitem(mime::IMAGE_STAR)])
|
||||
}
|
||||
}
|
69
src/header/common/accept_charset.rs
Normal file
69
src/header/common/accept_charset.rs
Normal file
@ -0,0 +1,69 @@
|
||||
use header::{http, Charset, QualityItem};
|
||||
|
||||
header! {
|
||||
/// `Accept-Charset` header, defined in
|
||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.3)
|
||||
///
|
||||
/// The `Accept-Charset` header field can be sent by a user agent to
|
||||
/// indicate what charsets are acceptable in textual response content.
|
||||
/// This field allows user agents capable of understanding more
|
||||
/// comprehensive or special-purpose charsets to signal that capability
|
||||
/// to an origin server that is capable of representing information in
|
||||
/// those charsets.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Accept-Charset = 1#( ( charset / "*" ) [ weight ] )
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `iso-8859-5, unicode-1-1;q=0.8`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{AcceptCharset, Charset, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// AcceptCharset(vec![qitem(Charset::Us_Ascii)])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{AcceptCharset, Charset, q, QualityItem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// AcceptCharset(vec![
|
||||
/// QualityItem::new(Charset::Us_Ascii, q(900)),
|
||||
/// QualityItem::new(Charset::Iso_8859_10, q(200)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{AcceptCharset, Charset, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// AcceptCharset(vec![qitem(Charset::Ext("utf-8".to_owned()))])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(AcceptCharset, http::ACCEPT_CHARSET) => (QualityItem<Charset>)+
|
||||
|
||||
test_accept_charset {
|
||||
/// Testcase from RFC
|
||||
test_header!(test1, vec![b"iso-8859-5, unicode-1-1;q=0.8"]);
|
||||
}
|
||||
}
|
72
src/header/common/accept_encoding.rs
Normal file
72
src/header/common/accept_encoding.rs
Normal file
@ -0,0 +1,72 @@
|
||||
use header::{Encoding, QualityItem};
|
||||
|
||||
header! {
|
||||
/// `Accept-Encoding` header, defined in
|
||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.4)
|
||||
///
|
||||
/// The `Accept-Encoding` header field can be used by user agents to
|
||||
/// indicate what response content-codings are
|
||||
/// acceptable in the response. An `identity` token is used as a synonym
|
||||
/// for "no encoding" in order to communicate when no encoding is
|
||||
/// preferred.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Accept-Encoding = #( codings [ weight ] )
|
||||
/// codings = content-coding / "identity" / "*"
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `compress, gzip`
|
||||
/// * ``
|
||||
/// * `*`
|
||||
/// * `compress;q=0.5, gzip;q=1`
|
||||
/// * `gzip;q=1.0, identity; q=0.5, *;q=0`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```
|
||||
/// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
|
||||
///
|
||||
/// let mut headers = Headers::new();
|
||||
/// headers.set(
|
||||
/// AcceptEncoding(vec![qitem(Encoding::Chunked)])
|
||||
/// );
|
||||
/// ```
|
||||
/// ```
|
||||
/// use hyper::header::{Headers, AcceptEncoding, Encoding, qitem};
|
||||
///
|
||||
/// let mut headers = Headers::new();
|
||||
/// headers.set(
|
||||
/// AcceptEncoding(vec![
|
||||
/// qitem(Encoding::Chunked),
|
||||
/// qitem(Encoding::Gzip),
|
||||
/// qitem(Encoding::Deflate),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
/// ```
|
||||
/// use hyper::header::{Headers, AcceptEncoding, Encoding, QualityItem, q, qitem};
|
||||
///
|
||||
/// let mut headers = Headers::new();
|
||||
/// headers.set(
|
||||
/// AcceptEncoding(vec![
|
||||
/// qitem(Encoding::Chunked),
|
||||
/// QualityItem::new(Encoding::Gzip, q(600)),
|
||||
/// QualityItem::new(Encoding::EncodingExt("*".to_owned()), q(0)),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
(AcceptEncoding, "Accept-Encoding") => (QualityItem<Encoding>)*
|
||||
|
||||
test_accept_encoding {
|
||||
// From the RFC
|
||||
test_header!(test1, vec![b"compress, gzip"]);
|
||||
test_header!(test2, vec![b""], Some(AcceptEncoding(vec![])));
|
||||
test_header!(test3, vec![b"*"]);
|
||||
// Note: Removed quality 1 from gzip
|
||||
test_header!(test4, vec![b"compress;q=0.5, gzip"]);
|
||||
// Note: Removed quality 1 from gzip
|
||||
test_header!(test5, vec![b"gzip, identity; q=0.5, *;q=0"]);
|
||||
}
|
||||
}
|
76
src/header/common/accept_language.rs
Normal file
76
src/header/common/accept_language.rs
Normal file
@ -0,0 +1,76 @@
|
||||
use language_tags::LanguageTag;
|
||||
use header::{http, QualityItem};
|
||||
|
||||
|
||||
header! {
|
||||
/// `Accept-Language` header, defined in
|
||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-5.3.5)
|
||||
///
|
||||
/// The `Accept-Language` header field can be used by user agents to
|
||||
/// indicate the set of natural languages that are preferred in the
|
||||
/// response.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Accept-Language = 1#( language-range [ weight ] )
|
||||
/// language-range = <language-range, see [RFC4647], Section 2.1>
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `da, en-gb;q=0.8, en;q=0.7`
|
||||
/// * `en-us;q=1.0, en;q=0.5, fr`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # extern crate language_tags;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{AcceptLanguage, LanguageTag, qitem};
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// let mut langtag: LanguageTag = Default::default();
|
||||
/// langtag.language = Some("en".to_owned());
|
||||
/// langtag.region = Some("US".to_owned());
|
||||
/// builder.set(
|
||||
/// AcceptLanguage(vec![
|
||||
/// qitem(langtag),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # #[macro_use] extern crate language_tags;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{AcceptLanguage, QualityItem, q, qitem};
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// AcceptLanguage(vec![
|
||||
/// qitem(langtag!(da)),
|
||||
/// QualityItem::new(langtag!(en;;;GB), q(800)),
|
||||
/// QualityItem::new(langtag!(en), q(700)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(AcceptLanguage, http::ACCEPT_LANGUAGE) => (QualityItem<LanguageTag>)+
|
||||
|
||||
test_accept_language {
|
||||
// From the RFC
|
||||
test_header!(test1, vec![b"da, en-gb;q=0.8, en;q=0.7"]);
|
||||
// Own test
|
||||
test_header!(
|
||||
test2, vec![b"en-US, en; q=0.5, fr"],
|
||||
Some(AcceptLanguage(vec![
|
||||
qitem("en-US".parse().unwrap()),
|
||||
QualityItem::new("en".parse().unwrap(), q(500)),
|
||||
qitem("fr".parse().unwrap()),
|
||||
])));
|
||||
}
|
||||
}
|
85
src/header/common/allow.rs
Normal file
85
src/header/common/allow.rs
Normal file
@ -0,0 +1,85 @@
|
||||
use http::Method;
|
||||
use header::http;
|
||||
|
||||
header! {
|
||||
/// `Allow` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.4.1)
|
||||
///
|
||||
/// The `Allow` header field lists the set of methods advertised as
|
||||
/// supported by the target resource. The purpose of this field is
|
||||
/// strictly to inform the recipient of valid request methods associated
|
||||
/// with the resource.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Allow = #method
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `GET, HEAD, PUT`
|
||||
/// * `OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH, fOObAr`
|
||||
/// * ``
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate http;
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::Allow;
|
||||
/// use http::Method;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// Allow(vec![Method::GET])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate http;
|
||||
/// # extern crate actix_web;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::Allow;
|
||||
/// use http::Method;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// Allow(vec![
|
||||
/// Method::GET,
|
||||
/// Method::POST,
|
||||
/// Method::PATCH,
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(Allow, http::ALLOW) => (Method)*
|
||||
|
||||
test_allow {
|
||||
// From the RFC
|
||||
test_header!(
|
||||
test1,
|
||||
vec![b"GET, HEAD, PUT"],
|
||||
Some(HeaderField(vec![Method::GET, Method::HEAD, Method::PUT])));
|
||||
// Own tests
|
||||
test_header!(
|
||||
test2,
|
||||
vec![b"OPTIONS, GET, PUT, POST, DELETE, HEAD, TRACE, CONNECT, PATCH"],
|
||||
Some(HeaderField(vec![
|
||||
Method::OPTIONS,
|
||||
Method::GET,
|
||||
Method::PUT,
|
||||
Method::POST,
|
||||
Method::DELETE,
|
||||
Method::HEAD,
|
||||
Method::TRACE,
|
||||
Method::CONNECT,
|
||||
Method::PATCH])));
|
||||
test_header!(
|
||||
test3,
|
||||
vec![b""],
|
||||
Some(HeaderField(Vec::<Method>::new())));
|
||||
}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user