mirror of
https://github.com/fafhrd91/actix-web
synced 2025-07-21 00:29:56 +02:00
Compare commits
278 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
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 |
@@ -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
|
||||
|
21
.travis.yml
21
.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:
|
||||
@@ -42,12 +42,12 @@ before_script:
|
||||
|
||||
script:
|
||||
- |
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "1.20.0" ]]; then
|
||||
if [[ "$TRAVIS_RUST_VERSION" == "stable" ]]; then
|
||||
cargo clean
|
||||
USE_SKEPTIC=1 cargo test --features=alpn
|
||||
else
|
||||
cargo clean
|
||||
cargo test
|
||||
cargo test -- --nocapture
|
||||
# --features=alpn
|
||||
fi
|
||||
|
||||
@@ -55,13 +55,18 @@ script:
|
||||
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/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
|
||||
@@ -71,8 +76,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" --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 .. &&
|
||||
@@ -82,7 +87,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)
|
||||
|
88
CHANGES.md
88
CHANGES.md
@@ -1,5 +1,93 @@
|
||||
# Changes
|
||||
|
||||
## 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 http 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
|
||||
|
67
Cargo.toml
67
Cargo.toml
@@ -1,15 +1,17 @@
|
||||
[package]
|
||||
name = "actix-web"
|
||||
version = "0.3.3"
|
||||
version = "0.4.5"
|
||||
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", "/examples/**"]
|
||||
@@ -25,7 +27,7 @@ name = "actix_web"
|
||||
path = "src/lib.rs"
|
||||
|
||||
[features]
|
||||
default = []
|
||||
default = ["session"]
|
||||
|
||||
# tls
|
||||
tls = ["native-tls", "tokio-tls"]
|
||||
@@ -33,38 +35,49 @@ 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"] }
|
||||
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 }
|
||||
@@ -74,12 +87,8 @@ tokio-tls = { version="0.1", optional = true }
|
||||
openssl = { version="0.10", optional = true }
|
||||
tokio-openssl = { version="0.2", optional = true }
|
||||
|
||||
[dependencies.actix]
|
||||
version = "^0.4.5"
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger = "0.5"
|
||||
reqwest = "0.8"
|
||||
skeptic = "0.13"
|
||||
serde_derive = "1.0"
|
||||
|
||||
@@ -90,18 +99,26 @@ version_check = "0.1"
|
||||
[profile.release]
|
||||
lto = true
|
||||
opt-level = 3
|
||||
codegen-units = 1
|
||||
|
||||
[workspace]
|
||||
members = [
|
||||
"./",
|
||||
"examples/basics",
|
||||
"examples/juniper",
|
||||
"examples/diesel",
|
||||
"examples/r2d2",
|
||||
"examples/json",
|
||||
"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/",
|
||||
]
|
||||
|
72
README.md
72
README.md
@@ -1,6 +1,34 @@
|
||||
# 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
|
||||
* 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,40 +47,11 @@ 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/)
|
||||
* [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/)
|
||||
@@ -61,6 +60,15 @@ Some basic benchmarks could be found in this [respository](https://github.com/fa
|
||||
* [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
|
||||
|
@@ -7,5 +7,5 @@ workspace = "../.."
|
||||
[dependencies]
|
||||
futures = "*"
|
||||
env_logger = "0.5"
|
||||
actix = "0.4"
|
||||
actix = "0.5"
|
||||
actix-web = { path="../.." }
|
||||
|
@@ -22,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()));
|
||||
}
|
||||
@@ -139,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")
|
||||
|
@@ -6,7 +6,7 @@ workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.4"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
||||
futures = "0.1"
|
||||
|
@@ -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::*;
|
||||
|
@@ -31,14 +31,14 @@ 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()})
|
||||
req.state().db.send(CreateUser{name: name.to_owned()})
|
||||
.from_err()
|
||||
.and_then(|res| {
|
||||
match res {
|
||||
|
@@ -6,5 +6,5 @@ workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.4"
|
||||
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 = "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 = "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);
|
||||
|
||||
|
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();
|
||||
}
|
@@ -7,5 +7,5 @@ workspace = "../.."
|
||||
[dependencies]
|
||||
futures = "*"
|
||||
env_logger = "0.5"
|
||||
actix = "0.4"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
|
@@ -36,17 +36,16 @@ impl Actor for MyWebSocket {
|
||||
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) => 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();
|
||||
}
|
||||
_ => (),
|
||||
|
@@ -6,6 +6,6 @@ workspace = "../.."
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "0.4"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../" }
|
||||
tera = "*"
|
||||
|
@@ -10,6 +10,6 @@ path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
env_logger = "0.5"
|
||||
actix = "^0.4.2"
|
||||
actix = "0.5"
|
||||
actix-web = { path = "../../", features=["alpn"] }
|
||||
openssl = { version="0.10", features = ["v110"] }
|
||||
openssl = { version="0.10" }
|
||||
|
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 = "0.5"
|
||||
actix-web = { path="../../" }
|
||||
|
@@ -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.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,21 +123,17 @@ 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);
|
||||
}
|
||||
@@ -146,4 +151,3 @@ impl FramedActor for ChatClient {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@@ -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,11 +108,13 @@ 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| {
|
||||
ctx.state().addr.send(server::ListRooms)
|
||||
.into_actor(self)
|
||||
.then(|res, _, ctx| {
|
||||
match res {
|
||||
Ok(Ok(rooms)) => {
|
||||
Ok(rooms) => {
|
||||
for room in rooms {
|
||||
ctx.text(&room);
|
||||
ctx.text(room);
|
||||
}
|
||||
},
|
||||
_ => println!("Something is wrong"),
|
||||
@@ -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::*;
|
||||
@@ -21,17 +24,19 @@ 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 convenient wrapper around `Framed` object from `tokio_io`
|
||||
type Context = FramedContext<Self>;
|
||||
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,30 +58,30 @@ 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| {
|
||||
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"),
|
||||
}
|
||||
@@ -87,13 +93,13 @@ impl FramedActor for ChatSession {
|
||||
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));
|
||||
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.send(
|
||||
self.addr.do_send(
|
||||
server::Message{id: self.id,
|
||||
msg: message, room:
|
||||
self.room.clone()})
|
||||
@@ -104,29 +110,30 @@ impl FramedActor for ChatSession {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Handler for Message, chat server sends this message, we just send string to peer
|
||||
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,16 +141,15 @@ 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() {
|
||||
act.framed.write(ChatResponse::Ping);
|
||||
// if we can not send message to sink, sink is closed (disconnected)
|
||||
act.hb(ctx);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -152,11 +158,11 @@ impl ChatSession {
|
||||
/// 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 requires rust version 1.20 and up.
|
||||
Actix web framework requires rust version 1.21 and up.
|
||||
|
||||
## Running Examples
|
||||
|
||||
|
@@ -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();
|
||||
}
|
||||
@@ -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.
|
||||
|
@@ -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,7 +38,7 @@ 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>
|
||||
type Result = Result<User, Error>;
|
||||
|
||||
fn handle(&mut self, msg: CreateUser, _: &mut Self::Context) -> Self::Result
|
||||
{
|
||||
@@ -72,7 +74,7 @@ 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() {
|
||||
@@ -106,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()
|
||||
@@ -122,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,6 +79,9 @@ fn index(req: HttpRequest) -> &'static str {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
# // 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()
|
||||
|
@@ -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)),
|
||||
]);
|
||||
}
|
||||
```
|
||||
|
@@ -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();
|
||||
}
|
||||
```
|
||||
@@ -57,7 +57,7 @@ fn main() {
|
||||
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
|
||||
}
|
||||
```
|
||||
@@ -143,14 +141,12 @@ connection behavior is defined by server settings.
|
||||
```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)))
|
||||
.keep_alive(None); // <- Use `SO_KEEPALIVE` socket option.
|
||||
}
|
||||
```
|
||||
@@ -159,7 +155,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 +165,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();
|
||||
}
|
||||
```
|
||||
|
@@ -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
|
||||
|
@@ -1,9 +1,7 @@
|
||||
# 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 being 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 information is available in [handler section](../qs_4.html).
|
||||
@@ -34,7 +32,7 @@ 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();
|
||||
}
|
||||
```
|
||||
@@ -54,7 +52,7 @@ returns *NOT FOUND* http resources.
|
||||
Resource contains set of routes. Each route in turn has set of predicates and handler.
|
||||
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,7 +85,7 @@ 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
|
||||
@@ -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 being 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));
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
@@ -84,7 +84,7 @@ 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()
|
||||
}
|
||||
@@ -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() {}
|
||||
```
|
||||
@@ -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 several convenience 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()
|
||||
.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())));
|
||||
}
|
||||
```
|
||||
|
@@ -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,7 +44,8 @@ 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.starts_with(prefix) && (
|
||||
path.len() == prefix.len() ||
|
||||
path.split_at(prefix.len()).1.starts_with('/'))
|
||||
};
|
||||
if m {
|
||||
@@ -94,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>>>,
|
||||
}
|
||||
|
||||
@@ -116,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(),
|
||||
})
|
||||
}
|
||||
@@ -141,14 +150,15 @@ impl<S> Application<S> where S: 'static {
|
||||
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,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -174,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();
|
||||
/// }
|
||||
@@ -217,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);
|
||||
/// });
|
||||
/// }
|
||||
/// ```
|
||||
@@ -232,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
|
||||
}
|
||||
@@ -253,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
|
||||
@@ -266,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() {
|
||||
@@ -286,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
|
||||
}
|
||||
@@ -305,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,
|
||||
/// }});
|
||||
/// }
|
||||
/// ```
|
||||
@@ -335,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);
|
||||
@@ -344,18 +360,19 @@ 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,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -378,11 +395,11 @@ impl<S> Application<S> where S: 'static {
|
||||
/// HttpServer::new(|| { vec![
|
||||
/// Application::with_state(State1)
|
||||
/// .prefix("/app1")
|
||||
/// .resource("/", |r| r.h(httpcodes::HTTPOk))
|
||||
/// .resource("/", |r| r.h(httpcodes::HttpOk))
|
||||
/// .boxed(),
|
||||
/// Application::with_state(State2)
|
||||
/// .prefix("/app2")
|
||||
/// .resource("/", |r| r.h(httpcodes::HTTPOk))
|
||||
/// .resource("/", |r| r.h(httpcodes::HttpOk))
|
||||
/// .boxed() ]})
|
||||
/// .bind("127.0.0.1:8080").unwrap()
|
||||
/// .run()
|
||||
@@ -443,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();
|
||||
@@ -455,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);
|
||||
@@ -466,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());
|
||||
}
|
||||
@@ -474,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);
|
||||
@@ -485,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);
|
||||
@@ -507,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();
|
||||
@@ -535,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();
|
||||
|
31
src/body.rs
31
src/body.rs
@@ -36,6 +36,8 @@ pub enum Binary {
|
||||
/// 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(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -134,8 +137,9 @@ impl Clone for Binary {
|
||||
match *self {
|
||||
Binary::Bytes(ref bytes) => Binary::Bytes(bytes.clone()),
|
||||
Binary::Slice(slice) => Binary::Bytes(Bytes::from(slice)),
|
||||
Binary::SharedString(ref s) => Binary::Bytes(Bytes::from(s.as_str())),
|
||||
Binary::ArcSharedString(ref s) => Binary::Bytes(Bytes::from(s.as_str())),
|
||||
Binary::SharedString(ref s) => Binary::SharedString(s.clone()),
|
||||
Binary::ArcSharedString(ref s) => Binary::ArcSharedString(s.clone()),
|
||||
Binary::SharedVec(ref s) => Binary::SharedVec(s.clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -147,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())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -217,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 {
|
||||
@@ -224,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -304,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),
|
||||
|
||||
/// Connecting 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(),
|
||||
}
|
||||
}
|
||||
}
|
194
src/client/parser.rs
Normal file
194
src/client/parser.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
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
|
||||
{
|
||||
println!("PARSE payload, {:?}", self.decoder.is_some());
|
||||
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()),
|
||||
}
|
||||
}
|
||||
}
|
595
src/client/request.rs
Normal file
595
src/client/request.rs
Normal file
@@ -0,0 +1,595 @@
|
||||
use std::{fmt, mem};
|
||||
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 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: Option<(usize, 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: None,
|
||||
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
|
||||
}
|
||||
|
||||
pub fn buffer_capacity(&self) -> Option<(usize, 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
|
||||
pub fn buffer_capacity(&mut self,
|
||||
low_watermark: usize,
|
||||
high_watermark: usize) -> &mut Self
|
||||
{
|
||||
if let Some(parts) = parts(&mut self.request, &self.err) {
|
||||
parts.buffer_capacity = Some((low_watermark, high_watermark));
|
||||
}
|
||||
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 {
|
||||
for cookie in jar.delta() {
|
||||
request.headers.append(
|
||||
header::COOKIE, HeaderValue::from_str(&cookie.to_string()).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()
|
||||
}
|
131
src/client/response.rs
Normal file
131
src/client/response.rs
Normal file
@@ -0,0 +1,131 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
373
src/client/writer.rs
Normal file
373
src/client/writer.rs
Normal file
@@ -0,0 +1,373 @@
|
||||
#![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 LOW_WATERMARK: usize = 1024;
|
||||
const HIGH_WATERMARK: usize = 8 * LOW_WATERMARK;
|
||||
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,
|
||||
encoder: ContentEncoder,
|
||||
low: usize,
|
||||
high: usize,
|
||||
}
|
||||
|
||||
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,
|
||||
encoder,
|
||||
low: LOW_WATERMARK,
|
||||
high: HIGH_WATERMARK,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn disconnected(&mut self) {
|
||||
self.buffer.take();
|
||||
}
|
||||
|
||||
// pub fn keepalive(&self) -> bool {
|
||||
// self.flags.contains(Flags::KEEPALIVE) && !self.flags.contains(Flags::UPGRADE)
|
||||
// }
|
||||
|
||||
/// Set write buffer capacity
|
||||
pub fn set_buffer_capacity(&mut self, low_watermark: usize, high_watermark: usize) {
|
||||
self.low = low_watermark;
|
||||
self.high = high_watermark;
|
||||
}
|
||||
|
||||
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.high {
|
||||
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);
|
||||
if let Some(capacity) = msg.buffer_capacity() {
|
||||
self.set_buffer_capacity(capacity.0, capacity.1);
|
||||
}
|
||||
|
||||
// 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)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
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.high {
|
||||
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;
|
||||
}
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
use std;
|
||||
use std::mem;
|
||||
use std::marker::PhantomData;
|
||||
use futures::{Async, Future, Poll};
|
||||
use futures::sync::oneshot::Sender;
|
||||
@@ -6,13 +6,12 @@ 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;
|
||||
|
||||
|
||||
@@ -82,20 +81,14 @@ impl<A, S> AsyncContext<A> for HttpContext<A, S> where A: Actor<Context=Self>
|
||||
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> {
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
@@ -171,26 +164,14 @@ impl<A, S> HttpContext<A, S> where A: Actor<Context=Self> {
|
||||
self.stream = Some(SmallVec::new());
|
||||
}
|
||||
self.stream.as_mut().map(|s| s.push(frame));
|
||||
}
|
||||
self.inner.modify();
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
#[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()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -204,7 +185,7 @@ impl<A, S> ActorHttpContext for HttpContext<A, S> where A: Actor<Context=Self>,
|
||||
|
||||
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() {
|
||||
@@ -225,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,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 }
|
||||
}
|
||||
}
|
||||
|
||||
|
172
src/error.rs
172
src/error.rs
@@ -4,19 +4,18 @@ 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 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};
|
||||
@@ -25,7 +24,7 @@ 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
|
||||
@@ -91,21 +90,26 @@ impl<T: ResponseError> From<T> for Error {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Error { cause: Box::new(err), backtrace: backtrace }
|
||||
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 {}
|
||||
@@ -125,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 {
|
||||
@@ -219,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),
|
||||
@@ -229,7 +247,7 @@ pub enum PayloadError {
|
||||
|
||||
impl From<IoError> for PayloadError {
|
||||
fn from(err: IoError) -> PayloadError {
|
||||
PayloadError::ParseError(err)
|
||||
PayloadError::Io(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -285,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),
|
||||
@@ -325,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,
|
||||
/// Upgrade 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 websocket 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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -423,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(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -521,10 +521,10 @@ unsafe impl<T> Sync for InternalError<T> {}
|
||||
unsafe impl<T> Send for InternalError<T> {}
|
||||
|
||||
impl<T> InternalError<T> {
|
||||
pub fn new(err: T, status: StatusCode) -> Self {
|
||||
pub fn new(cause: T, status: StatusCode) -> Self {
|
||||
InternalError {
|
||||
cause: err,
|
||||
status: status,
|
||||
cause,
|
||||
status,
|
||||
backtrace: Backtrace::new(),
|
||||
}
|
||||
}
|
||||
@@ -641,11 +641,13 @@ pub fn ErrorInternalServerError<T>(err: T) -> InternalError<T> {
|
||||
|
||||
#[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]
|
||||
@@ -722,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) {
|
||||
@@ -774,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
317
src/fs.rs
317
src/fs.rs
@@ -1,26 +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 headers::ContentEncoding;
|
||||
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.
|
||||
@@ -30,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.
|
||||
@@ -52,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");
|
||||
@@ -61,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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -69,30 +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();
|
||||
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())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -105,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 {
|
||||
@@ -167,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())
|
||||
}
|
||||
@@ -177,6 +358,7 @@ impl Responder for Directory {
|
||||
pub enum FilesystemElement {
|
||||
File(NamedFile),
|
||||
Directory(Directory),
|
||||
Redirect(HttpResponse),
|
||||
}
|
||||
|
||||
impl Responder for FilesystemElement {
|
||||
@@ -187,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),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -210,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,
|
||||
}
|
||||
@@ -221,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() {
|
||||
@@ -242,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 {
|
||||
@@ -270,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())))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,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; }
|
||||
@@ -299,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);
|
||||
@@ -318,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");
|
||||
}
|
||||
}
|
||||
|
@@ -215,7 +215,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 +225,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 +266,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 +287,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 +309,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()
|
||||
@@ -344,10 +345,10 @@ impl NormalizePath {
|
||||
/// 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 +380,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,12 +443,13 @@ mod tests {
|
||||
.finish();
|
||||
|
||||
// trailing slashes
|
||||
let params = vec![("/resource1", "", StatusCode::OK),
|
||||
("/resource1/", "", StatusCode::NOT_FOUND),
|
||||
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", "", StatusCode::NOT_FOUND),
|
||||
("/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)
|
||||
@@ -450,11 +478,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 +505,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 +547,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())));
|
||||
}
|
||||
}
|
231
src/header/common/cache_control.rs
Normal file
231
src/header/common/cache_control.rs
Normal file
@@ -0,0 +1,231 @@
|
||||
use std::fmt::{self, Write};
|
||||
use std::str::FromStr;
|
||||
use header::{Header, IntoHeaderValue, Writer};
|
||||
use header::{http, from_comma_delimited, fmt_comma_delimited};
|
||||
|
||||
/// `Cache-Control` header, defined in [RFC7234](https://tools.ietf.org/html/rfc7234#section-5.2)
|
||||
///
|
||||
/// The `Cache-Control` header field is used to specify directives for
|
||||
/// caches along the request/response chain. Such cache directives are
|
||||
/// unidirectional in that the presence of a directive in a request does
|
||||
/// not imply that the same directive is to be given in the response.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Cache-Control = 1#cache-directive
|
||||
/// cache-directive = token [ "=" ( token / quoted-string ) ]
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `no-cache`
|
||||
/// * `private, community="UCI"`
|
||||
/// * `max-age=30`
|
||||
///
|
||||
/// # Examples
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{CacheControl, CacheDirective};
|
||||
///
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// CacheControl(vec![CacheDirective::MaxAge(86400u32)])
|
||||
/// );
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::{CacheControl, CacheDirective};
|
||||
///
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// CacheControl(vec![
|
||||
/// CacheDirective::NoCache,
|
||||
/// CacheDirective::Private,
|
||||
/// CacheDirective::MaxAge(360u32),
|
||||
/// CacheDirective::Extension("foo".to_owned(),
|
||||
/// Some("bar".to_owned())),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub struct CacheControl(pub Vec<CacheDirective>);
|
||||
|
||||
__hyper__deref!(CacheControl => Vec<CacheDirective>);
|
||||
|
||||
//TODO: this could just be the header! macro
|
||||
impl Header for CacheControl {
|
||||
fn name() -> http::HeaderName {
|
||||
http::CACHE_CONTROL
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn parse<T>(msg: &T) -> Result<Self, ::error::ParseError>
|
||||
where T: ::HttpMessage
|
||||
{
|
||||
let directives = from_comma_delimited(msg.headers().get_all(Self::name()))?;
|
||||
if !directives.is_empty() {
|
||||
Ok(CacheControl(directives))
|
||||
} else {
|
||||
Err(::error::ParseError::Header)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for CacheControl {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
fmt_comma_delimited(f, &self[..])
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for CacheControl {
|
||||
type Error = http::InvalidHeaderValueBytes;
|
||||
|
||||
fn try_into(self) -> Result<http::HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
http::HeaderValue::from_shared(writer.take())
|
||||
}
|
||||
}
|
||||
|
||||
/// `CacheControl` contains a list of these directives.
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum CacheDirective {
|
||||
/// "no-cache"
|
||||
NoCache,
|
||||
/// "no-store"
|
||||
NoStore,
|
||||
/// "no-transform"
|
||||
NoTransform,
|
||||
/// "only-if-cached"
|
||||
OnlyIfCached,
|
||||
|
||||
// request directives
|
||||
/// "max-age=delta"
|
||||
MaxAge(u32),
|
||||
/// "max-stale=delta"
|
||||
MaxStale(u32),
|
||||
/// "min-fresh=delta"
|
||||
MinFresh(u32),
|
||||
|
||||
// response directives
|
||||
/// "must-revalidate"
|
||||
MustRevalidate,
|
||||
/// "public"
|
||||
Public,
|
||||
/// "private"
|
||||
Private,
|
||||
/// "proxy-revalidate"
|
||||
ProxyRevalidate,
|
||||
/// "s-maxage=delta"
|
||||
SMaxAge(u32),
|
||||
|
||||
/// Extension directives. Optionally include an argument.
|
||||
Extension(String, Option<String>)
|
||||
}
|
||||
|
||||
impl fmt::Display for CacheDirective {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
use self::CacheDirective::*;
|
||||
fmt::Display::fmt(match *self {
|
||||
NoCache => "no-cache",
|
||||
NoStore => "no-store",
|
||||
NoTransform => "no-transform",
|
||||
OnlyIfCached => "only-if-cached",
|
||||
|
||||
MaxAge(secs) => return write!(f, "max-age={}", secs),
|
||||
MaxStale(secs) => return write!(f, "max-stale={}", secs),
|
||||
MinFresh(secs) => return write!(f, "min-fresh={}", secs),
|
||||
|
||||
MustRevalidate => "must-revalidate",
|
||||
Public => "public",
|
||||
Private => "private",
|
||||
ProxyRevalidate => "proxy-revalidate",
|
||||
SMaxAge(secs) => return write!(f, "s-maxage={}", secs),
|
||||
|
||||
Extension(ref name, None) => &name[..],
|
||||
Extension(ref name, Some(ref arg)) => return write!(f, "{}={}", name, arg),
|
||||
|
||||
}, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for CacheDirective {
|
||||
type Err = Option<<u32 as FromStr>::Err>;
|
||||
fn from_str(s: &str) -> Result<CacheDirective, Option<<u32 as FromStr>::Err>> {
|
||||
use self::CacheDirective::*;
|
||||
match s {
|
||||
"no-cache" => Ok(NoCache),
|
||||
"no-store" => Ok(NoStore),
|
||||
"no-transform" => Ok(NoTransform),
|
||||
"only-if-cached" => Ok(OnlyIfCached),
|
||||
"must-revalidate" => Ok(MustRevalidate),
|
||||
"public" => Ok(Public),
|
||||
"private" => Ok(Private),
|
||||
"proxy-revalidate" => Ok(ProxyRevalidate),
|
||||
"" => Err(None),
|
||||
_ => match s.find('=') {
|
||||
Some(idx) if idx+1 < s.len() => match (&s[..idx], (&s[idx+1..]).trim_matches('"')) {
|
||||
("max-age" , secs) => secs.parse().map(MaxAge).map_err(Some),
|
||||
("max-stale", secs) => secs.parse().map(MaxStale).map_err(Some),
|
||||
("min-fresh", secs) => secs.parse().map(MinFresh).map_err(Some),
|
||||
("s-maxage", secs) => secs.parse().map(SMaxAge).map_err(Some),
|
||||
(left, right) => Ok(Extension(left.to_owned(), Some(right.to_owned())))
|
||||
},
|
||||
Some(_) => Err(None),
|
||||
None => Ok(Extension(s.to_owned(), None))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use header::Header;
|
||||
use test::TestRequest;
|
||||
|
||||
#[test]
|
||||
fn test_parse_multiple_headers() {
|
||||
let req = TestRequest::with_header(
|
||||
http::CACHE_CONTROL, "no-cache, private").finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::NoCache,
|
||||
CacheDirective::Private])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_argument() {
|
||||
let req = TestRequest::with_header(
|
||||
http::CACHE_CONTROL, "max-age=100, private").finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::MaxAge(100),
|
||||
CacheDirective::Private])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_quote_form() {
|
||||
let req = TestRequest::with_header(
|
||||
http::CACHE_CONTROL, "max-age=\"200\"").finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(cache.ok(), Some(CacheControl(vec![CacheDirective::MaxAge(200)])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_extension() {
|
||||
let req = TestRequest::with_header(
|
||||
http::CACHE_CONTROL, "foo, bar=baz").finish();
|
||||
let cache = Header::parse(&req);
|
||||
assert_eq!(cache.ok(), Some(CacheControl(vec![
|
||||
CacheDirective::Extension("foo".to_owned(), None),
|
||||
CacheDirective::Extension("bar".to_owned(), Some("baz".to_owned()))])))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_parse_bad_syntax() {
|
||||
let req = TestRequest::with_header(http::CACHE_CONTROL, "foo=").finish();
|
||||
let cache: Result<CacheControl, _> = Header::parse(&req);
|
||||
assert_eq!(cache.ok(), None)
|
||||
}
|
||||
}
|
264
src/header/common/content_disposition.rs
Normal file
264
src/header/common/content_disposition.rs
Normal file
@@ -0,0 +1,264 @@
|
||||
// # References
|
||||
//
|
||||
// "The Content-Disposition Header Field" https://www.ietf.org/rfc/rfc2183.txt
|
||||
// "The Content-Disposition Header Field in the Hypertext Transfer Protocol (HTTP)" https://www.ietf.org/rfc/rfc6266.txt
|
||||
// "Returning Values from Forms: multipart/form-data" https://www.ietf.org/rfc/rfc2388.txt
|
||||
// Browser conformance tests at: http://greenbytes.de/tech/tc2231/
|
||||
// IANA assignment: http://www.iana.org/assignments/cont-disp/cont-disp.xhtml
|
||||
|
||||
use language_tags::LanguageTag;
|
||||
use std::fmt;
|
||||
use unicase;
|
||||
|
||||
use header::{Header, Raw, parsing};
|
||||
use header::parsing::{parse_extended_value, http_percent_encode};
|
||||
use header::shared::Charset;
|
||||
|
||||
/// The implied disposition of the content of the HTTP body.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DispositionType {
|
||||
/// Inline implies default processing
|
||||
Inline,
|
||||
/// Attachment implies that the recipient should prompt the user to save the response locally,
|
||||
/// rather than process it normally (as per its media type).
|
||||
Attachment,
|
||||
/// Extension type. Should be handled by recipients the same way as Attachment
|
||||
Ext(String)
|
||||
}
|
||||
|
||||
/// A parameter to the disposition type.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum DispositionParam {
|
||||
/// A Filename consisting of a Charset, an optional LanguageTag, and finally a sequence of
|
||||
/// bytes representing the filename
|
||||
Filename(Charset, Option<LanguageTag>, Vec<u8>),
|
||||
/// Extension type consisting of token and value. Recipients should ignore unrecognized
|
||||
/// parameters.
|
||||
Ext(String, String)
|
||||
}
|
||||
|
||||
/// A `Content-Disposition` header, (re)defined in [RFC6266](https://tools.ietf.org/html/rfc6266).
|
||||
///
|
||||
/// The Content-Disposition response header field is used to convey
|
||||
/// additional information about how to process the response payload, and
|
||||
/// also can be used to attach additional metadata, such as the filename
|
||||
/// to use when saving the response payload locally.
|
||||
///
|
||||
/// # ABNF
|
||||
|
||||
/// ```text
|
||||
/// content-disposition = "Content-Disposition" ":"
|
||||
/// disposition-type *( ";" disposition-parm )
|
||||
///
|
||||
/// disposition-type = "inline" | "attachment" | disp-ext-type
|
||||
/// ; case-insensitive
|
||||
///
|
||||
/// disp-ext-type = token
|
||||
///
|
||||
/// disposition-parm = filename-parm | disp-ext-parm
|
||||
///
|
||||
/// filename-parm = "filename" "=" value
|
||||
/// | "filename*" "=" ext-value
|
||||
///
|
||||
/// disp-ext-parm = token "=" value
|
||||
/// | ext-token "=" ext-value
|
||||
///
|
||||
/// ext-token = <the characters in token, followed by "*">
|
||||
/// ```
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```
|
||||
/// use hyper::header::{Headers, ContentDisposition, DispositionType, DispositionParam, Charset};
|
||||
///
|
||||
/// let mut headers = Headers::new();
|
||||
/// headers.set(ContentDisposition {
|
||||
/// disposition: DispositionType::Attachment,
|
||||
/// parameters: vec![DispositionParam::Filename(
|
||||
/// Charset::Iso_8859_1, // The character set for the bytes of the filename
|
||||
/// None, // The optional language tag (see `language-tag` crate)
|
||||
/// b"\xa9 Copyright 1989.txt".to_vec() // the actual bytes of the filename
|
||||
/// )]
|
||||
/// });
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct ContentDisposition {
|
||||
/// The disposition
|
||||
pub disposition: DispositionType,
|
||||
/// Disposition parameters
|
||||
pub parameters: Vec<DispositionParam>,
|
||||
}
|
||||
|
||||
impl Header for ContentDisposition {
|
||||
fn header_name() -> &'static str {
|
||||
static NAME: &'static str = "Content-Disposition";
|
||||
NAME
|
||||
}
|
||||
|
||||
fn parse_header(raw: &Raw) -> ::Result<ContentDisposition> {
|
||||
parsing::from_one_raw_str(raw).and_then(|s: String| {
|
||||
let mut sections = s.split(';');
|
||||
let disposition = match sections.next() {
|
||||
Some(s) => s.trim(),
|
||||
None => return Err(::Error::Header),
|
||||
};
|
||||
|
||||
let mut cd = ContentDisposition {
|
||||
disposition: if unicase::eq_ascii(&*disposition, "inline") {
|
||||
DispositionType::Inline
|
||||
} else if unicase::eq_ascii(&*disposition, "attachment") {
|
||||
DispositionType::Attachment
|
||||
} else {
|
||||
DispositionType::Ext(disposition.to_owned())
|
||||
},
|
||||
parameters: Vec::new(),
|
||||
};
|
||||
|
||||
for section in sections {
|
||||
let mut parts = section.splitn(2, '=');
|
||||
|
||||
let key = if let Some(key) = parts.next() {
|
||||
key.trim()
|
||||
} else {
|
||||
return Err(::Error::Header);
|
||||
};
|
||||
|
||||
let val = if let Some(val) = parts.next() {
|
||||
val.trim()
|
||||
} else {
|
||||
return Err(::Error::Header);
|
||||
};
|
||||
|
||||
cd.parameters.push(
|
||||
if unicase::eq_ascii(&*key, "filename") {
|
||||
DispositionParam::Filename(
|
||||
Charset::Ext("UTF-8".to_owned()), None,
|
||||
val.trim_matches('"').as_bytes().to_owned())
|
||||
} else if unicase::eq_ascii(&*key, "filename*") {
|
||||
let extended_value = try!(parse_extended_value(val));
|
||||
DispositionParam::Filename(extended_value.charset, extended_value.language_tag, extended_value.value)
|
||||
} else {
|
||||
DispositionParam::Ext(key.to_owned(), val.trim_matches('"').to_owned())
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
Ok(cd)
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn fmt_header(&self, f: &mut ::header::Formatter) -> fmt::Result {
|
||||
f.fmt_line(self)
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for ContentDisposition {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self.disposition {
|
||||
DispositionType::Inline => try!(write!(f, "inline")),
|
||||
DispositionType::Attachment => try!(write!(f, "attachment")),
|
||||
DispositionType::Ext(ref s) => try!(write!(f, "{}", s)),
|
||||
}
|
||||
for param in &self.parameters {
|
||||
match *param {
|
||||
DispositionParam::Filename(ref charset, ref opt_lang, ref bytes) => {
|
||||
let mut use_simple_format: bool = false;
|
||||
if opt_lang.is_none() {
|
||||
if let Charset::Ext(ref ext) = *charset {
|
||||
if unicase::eq_ascii(&**ext, "utf-8") {
|
||||
use_simple_format = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if use_simple_format {
|
||||
try!(write!(f, "; filename=\"{}\"",
|
||||
match String::from_utf8(bytes.clone()) {
|
||||
Ok(s) => s,
|
||||
Err(_) => return Err(fmt::Error),
|
||||
}));
|
||||
} else {
|
||||
try!(write!(f, "; filename*={}'", charset));
|
||||
if let Some(ref lang) = *opt_lang {
|
||||
try!(write!(f, "{}", lang));
|
||||
};
|
||||
try!(write!(f, "'"));
|
||||
try!(http_percent_encode(f, bytes))
|
||||
}
|
||||
},
|
||||
DispositionParam::Ext(ref k, ref v) => try!(write!(f, "; {}=\"{}\"", k, v)),
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{ContentDisposition,DispositionType,DispositionParam};
|
||||
use ::header::Header;
|
||||
use ::header::shared::Charset;
|
||||
|
||||
#[test]
|
||||
fn test_parse_header() {
|
||||
assert!(ContentDisposition::parse_header(&"".into()).is_err());
|
||||
|
||||
let a = "form-data; dummy=3; name=upload;\r\n filename=\"sample.png\"".into();
|
||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
||||
let b = ContentDisposition {
|
||||
disposition: DispositionType::Ext("form-data".to_owned()),
|
||||
parameters: vec![
|
||||
DispositionParam::Ext("dummy".to_owned(), "3".to_owned()),
|
||||
DispositionParam::Ext("name".to_owned(), "upload".to_owned()),
|
||||
DispositionParam::Filename(
|
||||
Charset::Ext("UTF-8".to_owned()),
|
||||
None,
|
||||
"sample.png".bytes().collect()) ]
|
||||
};
|
||||
assert_eq!(a, b);
|
||||
|
||||
let a = "attachment; filename=\"image.jpg\"".into();
|
||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
||||
let b = ContentDisposition {
|
||||
disposition: DispositionType::Attachment,
|
||||
parameters: vec![
|
||||
DispositionParam::Filename(
|
||||
Charset::Ext("UTF-8".to_owned()),
|
||||
None,
|
||||
"image.jpg".bytes().collect()) ]
|
||||
};
|
||||
assert_eq!(a, b);
|
||||
|
||||
let a = "attachment; filename*=UTF-8''%c2%a3%20and%20%e2%82%ac%20rates".into();
|
||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
||||
let b = ContentDisposition {
|
||||
disposition: DispositionType::Attachment,
|
||||
parameters: vec![
|
||||
DispositionParam::Filename(
|
||||
Charset::Ext("UTF-8".to_owned()),
|
||||
None,
|
||||
vec![0xc2, 0xa3, 0x20, b'a', b'n', b'd', 0x20,
|
||||
0xe2, 0x82, 0xac, 0x20, b'r', b'a', b't', b'e', b's']) ]
|
||||
};
|
||||
assert_eq!(a, b);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_display() {
|
||||
let as_string = "attachment; filename*=UTF-8'en'%C2%A3%20and%20%E2%82%AC%20rates";
|
||||
let a = as_string.into();
|
||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
||||
let display_rendered = format!("{}",a);
|
||||
assert_eq!(as_string, display_rendered);
|
||||
|
||||
let a = "attachment; filename*=UTF-8''black%20and%20white.csv".into();
|
||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
||||
let display_rendered = format!("{}",a);
|
||||
assert_eq!("attachment; filename=\"black and white.csv\"".to_owned(), display_rendered);
|
||||
|
||||
let a = "attachment; filename=colourful.csv".into();
|
||||
let a: ContentDisposition = ContentDisposition::parse_header(&a).unwrap();
|
||||
let display_rendered = format!("{}",a);
|
||||
assert_eq!("attachment; filename=\"colourful.csv\"".to_owned(), display_rendered);
|
||||
}
|
||||
}
|
66
src/header/common/content_language.rs
Normal file
66
src/header/common/content_language.rs
Normal file
@@ -0,0 +1,66 @@
|
||||
use language_tags::LanguageTag;
|
||||
use header::{http, QualityItem};
|
||||
|
||||
|
||||
header! {
|
||||
/// `Content-Language` header, defined in
|
||||
/// [RFC7231](https://tools.ietf.org/html/rfc7231#section-3.1.3.2)
|
||||
///
|
||||
/// The `Content-Language` header field describes the natural language(s)
|
||||
/// of the intended audience for the representation. Note that this
|
||||
/// might not be equivalent to all the languages used within the
|
||||
/// representation.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Content-Language = 1#language-tag
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `da`
|
||||
/// * `mi, en`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # #[macro_use] extern crate language_tags;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// # use actix_web::header::{ContentLanguage, qitem};
|
||||
/// #
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// ContentLanguage(vec![
|
||||
/// qitem(langtag!(en)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate actix_web;
|
||||
/// # #[macro_use] extern crate language_tags;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// # use actix_web::header::{ContentLanguage, qitem};
|
||||
/// #
|
||||
/// # fn main() {
|
||||
///
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// ContentLanguage(vec![
|
||||
/// qitem(langtag!(da)),
|
||||
/// qitem(langtag!(en;;;GB)),
|
||||
/// ])
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(ContentLanguage, http::CONTENT_LANGUAGE) => (QualityItem<LanguageTag>)+
|
||||
|
||||
test_content_language {
|
||||
test_header!(test1, vec![b"da"]);
|
||||
test_header!(test2, vec![b"mi, en"]);
|
||||
}
|
||||
}
|
205
src/header/common/content_range.rs
Normal file
205
src/header/common/content_range.rs
Normal file
@@ -0,0 +1,205 @@
|
||||
use std::fmt::{self, Display, Write};
|
||||
use std::str::FromStr;
|
||||
use header::{http, IntoHeaderValue, Writer};
|
||||
use error::ParseError;
|
||||
|
||||
|
||||
header! {
|
||||
/// `Content-Range` header, defined in
|
||||
/// [RFC7233](http://tools.ietf.org/html/rfc7233#section-4.2)
|
||||
(ContentRange, http::CONTENT_RANGE) => [ContentRangeSpec]
|
||||
|
||||
test_content_range {
|
||||
test_header!(test_bytes,
|
||||
vec![b"bytes 0-499/500"],
|
||||
Some(ContentRange(ContentRangeSpec::Bytes {
|
||||
range: Some((0, 499)),
|
||||
instance_length: Some(500)
|
||||
})));
|
||||
|
||||
test_header!(test_bytes_unknown_len,
|
||||
vec![b"bytes 0-499/*"],
|
||||
Some(ContentRange(ContentRangeSpec::Bytes {
|
||||
range: Some((0, 499)),
|
||||
instance_length: None
|
||||
})));
|
||||
|
||||
test_header!(test_bytes_unknown_range,
|
||||
vec![b"bytes */500"],
|
||||
Some(ContentRange(ContentRangeSpec::Bytes {
|
||||
range: None,
|
||||
instance_length: Some(500)
|
||||
})));
|
||||
|
||||
test_header!(test_unregistered,
|
||||
vec![b"seconds 1-2"],
|
||||
Some(ContentRange(ContentRangeSpec::Unregistered {
|
||||
unit: "seconds".to_owned(),
|
||||
resp: "1-2".to_owned()
|
||||
})));
|
||||
|
||||
test_header!(test_no_len,
|
||||
vec![b"bytes 0-499"],
|
||||
None::<ContentRange>);
|
||||
|
||||
test_header!(test_only_unit,
|
||||
vec![b"bytes"],
|
||||
None::<ContentRange>);
|
||||
|
||||
test_header!(test_end_less_than_start,
|
||||
vec![b"bytes 499-0/500"],
|
||||
None::<ContentRange>);
|
||||
|
||||
test_header!(test_blank,
|
||||
vec![b""],
|
||||
None::<ContentRange>);
|
||||
|
||||
test_header!(test_bytes_many_spaces,
|
||||
vec![b"bytes 1-2/500 3"],
|
||||
None::<ContentRange>);
|
||||
|
||||
test_header!(test_bytes_many_slashes,
|
||||
vec![b"bytes 1-2/500/600"],
|
||||
None::<ContentRange>);
|
||||
|
||||
test_header!(test_bytes_many_dashes,
|
||||
vec![b"bytes 1-2-3/500"],
|
||||
None::<ContentRange>);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/// Content-Range, described in [RFC7233](https://tools.ietf.org/html/rfc7233#section-4.2)
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Content-Range = byte-content-range
|
||||
/// / other-content-range
|
||||
///
|
||||
/// byte-content-range = bytes-unit SP
|
||||
/// ( byte-range-resp / unsatisfied-range )
|
||||
///
|
||||
/// byte-range-resp = byte-range "/" ( complete-length / "*" )
|
||||
/// byte-range = first-byte-pos "-" last-byte-pos
|
||||
/// unsatisfied-range = "*/" complete-length
|
||||
///
|
||||
/// complete-length = 1*DIGIT
|
||||
///
|
||||
/// other-content-range = other-range-unit SP other-range-resp
|
||||
/// other-range-resp = *CHAR
|
||||
/// ```
|
||||
#[derive(PartialEq, Clone, Debug)]
|
||||
pub enum ContentRangeSpec {
|
||||
/// Byte range
|
||||
Bytes {
|
||||
/// First and last bytes of the range, omitted if request could not be
|
||||
/// satisfied
|
||||
range: Option<(u64, u64)>,
|
||||
|
||||
/// Total length of the instance, can be omitted if unknown
|
||||
instance_length: Option<u64>
|
||||
},
|
||||
|
||||
/// Custom range, with unit not registered at IANA
|
||||
Unregistered {
|
||||
/// other-range-unit
|
||||
unit: String,
|
||||
|
||||
/// other-range-resp
|
||||
resp: String
|
||||
}
|
||||
}
|
||||
|
||||
fn split_in_two(s: &str, separator: char) -> Option<(&str, &str)> {
|
||||
let mut iter = s.splitn(2, separator);
|
||||
match (iter.next(), iter.next()) {
|
||||
(Some(a), Some(b)) => Some((a, b)),
|
||||
_ => None
|
||||
}
|
||||
}
|
||||
|
||||
impl FromStr for ContentRangeSpec {
|
||||
type Err = ParseError;
|
||||
|
||||
fn from_str(s: &str) -> Result<Self, ParseError> {
|
||||
let res = match split_in_two(s, ' ') {
|
||||
Some(("bytes", resp)) => {
|
||||
let (range, instance_length) = split_in_two(
|
||||
resp, '/').ok_or(ParseError::Header)?;
|
||||
|
||||
let instance_length = if instance_length == "*" {
|
||||
None
|
||||
} else {
|
||||
Some(instance_length.parse()
|
||||
.map_err(|_| ParseError::Header)?)
|
||||
};
|
||||
|
||||
let range = if range == "*" {
|
||||
None
|
||||
} else {
|
||||
let (first_byte, last_byte) = split_in_two(
|
||||
range, '-').ok_or(ParseError::Header)?;
|
||||
let first_byte = first_byte.parse()
|
||||
.map_err(|_| ParseError::Header)?;
|
||||
let last_byte = last_byte.parse()
|
||||
.map_err(|_| ParseError::Header)?;
|
||||
if last_byte < first_byte {
|
||||
return Err(ParseError::Header);
|
||||
}
|
||||
Some((first_byte, last_byte))
|
||||
};
|
||||
|
||||
ContentRangeSpec::Bytes {range, instance_length}
|
||||
}
|
||||
Some((unit, resp)) => {
|
||||
ContentRangeSpec::Unregistered {
|
||||
unit: unit.to_owned(),
|
||||
resp: resp.to_owned()
|
||||
}
|
||||
}
|
||||
_ => return Err(ParseError::Header)
|
||||
};
|
||||
Ok(res)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for ContentRangeSpec {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
ContentRangeSpec::Bytes { range, instance_length } => {
|
||||
try!(f.write_str("bytes "));
|
||||
match range {
|
||||
Some((first_byte, last_byte)) => {
|
||||
try!(write!(f, "{}-{}", first_byte, last_byte));
|
||||
},
|
||||
None => {
|
||||
try!(f.write_str("*"));
|
||||
}
|
||||
};
|
||||
try!(f.write_str("/"));
|
||||
if let Some(v) = instance_length {
|
||||
write!(f, "{}", v)
|
||||
} else {
|
||||
f.write_str("*")
|
||||
}
|
||||
}
|
||||
ContentRangeSpec::Unregistered { ref unit, ref resp } => {
|
||||
try!(f.write_str(unit));
|
||||
try!(f.write_str(" "));
|
||||
f.write_str(resp)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for ContentRangeSpec {
|
||||
type Error = http::InvalidHeaderValueBytes;
|
||||
|
||||
fn try_into(self) -> Result<http::HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
http::HeaderValue::from_shared(writer.take())
|
||||
}
|
||||
}
|
119
src/header/common/content_type.rs
Normal file
119
src/header/common/content_type.rs
Normal file
@@ -0,0 +1,119 @@
|
||||
use mime::{self, Mime};
|
||||
use header::http;
|
||||
|
||||
|
||||
header! {
|
||||
/// `Content-Type` header, defined in
|
||||
/// [RFC7231](http://tools.ietf.org/html/rfc7231#section-3.1.1.5)
|
||||
///
|
||||
/// The `Content-Type` header field indicates the media type of the
|
||||
/// associated representation: either the representation enclosed in the
|
||||
/// message payload or the selected representation, as determined by the
|
||||
/// message semantics. The indicated media type defines both the data
|
||||
/// format and how that data is intended to be processed by a recipient,
|
||||
/// within the scope of the received message semantics, after any content
|
||||
/// codings indicated by Content-Encoding are decoded.
|
||||
///
|
||||
/// Although the `mime` crate allows the mime options to be any slice, this crate
|
||||
/// forces the use of Vec. This is to make sure the same header can't have more than 1 type. If
|
||||
/// this is an issue, it's possible to implement `Header` on a custom struct.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Content-Type = media-type
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `text/html; charset=utf-8`
|
||||
/// * `application/json`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::ContentType;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// ContentType::json()
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// # extern crate mime;
|
||||
/// # extern crate actix_web;
|
||||
/// use mime::TEXT_HTML;
|
||||
/// use actix_web::httpcodes::HttpOk;
|
||||
/// use actix_web::header::ContentType;
|
||||
///
|
||||
/// # fn main() {
|
||||
/// let mut builder = HttpOk.build();
|
||||
/// builder.set(
|
||||
/// ContentType(TEXT_HTML)
|
||||
/// );
|
||||
/// # }
|
||||
/// ```
|
||||
(ContentType, http::CONTENT_TYPE) => [Mime]
|
||||
|
||||
test_content_type {
|
||||
test_header!(
|
||||
test1,
|
||||
vec![b"text/html"],
|
||||
Some(HeaderField(TEXT_HTML)));
|
||||
}
|
||||
}
|
||||
|
||||
impl ContentType {
|
||||
/// A constructor to easily create a `Content-Type: application/json` header.
|
||||
#[inline]
|
||||
pub fn json() -> ContentType {
|
||||
ContentType(mime::APPLICATION_JSON)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: text/plain; charset=utf-8` header.
|
||||
#[inline]
|
||||
pub fn plaintext() -> ContentType {
|
||||
ContentType(mime::TEXT_PLAIN_UTF_8)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: text/html` header.
|
||||
#[inline]
|
||||
pub fn html() -> ContentType {
|
||||
ContentType(mime::TEXT_HTML)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: text/xml` header.
|
||||
#[inline]
|
||||
pub fn xml() -> ContentType {
|
||||
ContentType(mime::TEXT_XML)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: application/www-form-url-encoded` header.
|
||||
#[inline]
|
||||
pub fn form_url_encoded() -> ContentType {
|
||||
ContentType(mime::APPLICATION_WWW_FORM_URLENCODED)
|
||||
}
|
||||
/// A constructor to easily create a `Content-Type: image/jpeg` header.
|
||||
#[inline]
|
||||
pub fn jpeg() -> ContentType {
|
||||
ContentType(mime::IMAGE_JPEG)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: image/png` header.
|
||||
#[inline]
|
||||
pub fn png() -> ContentType {
|
||||
ContentType(mime::IMAGE_PNG)
|
||||
}
|
||||
|
||||
/// A constructor to easily create a `Content-Type: application/octet-stream` header.
|
||||
#[inline]
|
||||
pub fn octet_stream() -> ContentType {
|
||||
ContentType(mime::APPLICATION_OCTET_STREAM)
|
||||
}
|
||||
}
|
||||
|
||||
impl Eq for ContentType {}
|
42
src/header/common/date.rs
Normal file
42
src/header/common/date.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use std::time::SystemTime;
|
||||
use header::{http, HttpDate};
|
||||
|
||||
|
||||
header! {
|
||||
/// `Date` header, defined in [RFC7231](http://tools.ietf.org/html/rfc7231#section-7.1.1.2)
|
||||
///
|
||||
/// The `Date` header field represents the date and time at which the
|
||||
/// message was originated.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Date = HTTP-date
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `Tue, 15 Nov 1994 08:12:31 GMT`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::Date;
|
||||
/// use std::time::SystemTime;
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(Date(SystemTime::now().into()));
|
||||
/// ```
|
||||
(Date, http::DATE) => [HttpDate]
|
||||
|
||||
test_date {
|
||||
test_header!(test1, vec![b"Tue, 15 Nov 1994 08:12:31 GMT"]);
|
||||
}
|
||||
}
|
||||
|
||||
impl Date {
|
||||
pub fn now() -> Date {
|
||||
Date(SystemTime::now().into())
|
||||
}
|
||||
}
|
96
src/header/common/etag.rs
Normal file
96
src/header/common/etag.rs
Normal file
@@ -0,0 +1,96 @@
|
||||
use header::{http, EntityTag};
|
||||
|
||||
header! {
|
||||
/// `ETag` header, defined in [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.3)
|
||||
///
|
||||
/// The `ETag` header field in a response provides the current entity-tag
|
||||
/// for the selected representation, as determined at the conclusion of
|
||||
/// handling the request. An entity-tag is an opaque validator for
|
||||
/// differentiating between multiple representations of the same
|
||||
/// resource, regardless of whether those multiple representations are
|
||||
/// due to resource state changes over time, content negotiation
|
||||
/// resulting in multiple representations being valid at the same time,
|
||||
/// or both. An entity-tag consists of an opaque quoted string, possibly
|
||||
/// prefixed by a weakness indicator.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// ETag = entity-tag
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `"xyzzy"`
|
||||
/// * `W/"xyzzy"`
|
||||
/// * `""`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::{ETag, EntityTag};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(ETag(EntityTag::new(false, "xyzzy".to_owned())));
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::{ETag, EntityTag};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(ETag(EntityTag::new(true, "xyzzy".to_owned())));
|
||||
/// ```
|
||||
(ETag, http::ETAG) => [EntityTag]
|
||||
|
||||
test_etag {
|
||||
// From the RFC
|
||||
test_header!(test1,
|
||||
vec![b"\"xyzzy\""],
|
||||
Some(ETag(EntityTag::new(false, "xyzzy".to_owned()))));
|
||||
test_header!(test2,
|
||||
vec![b"W/\"xyzzy\""],
|
||||
Some(ETag(EntityTag::new(true, "xyzzy".to_owned()))));
|
||||
test_header!(test3,
|
||||
vec![b"\"\""],
|
||||
Some(ETag(EntityTag::new(false, "".to_owned()))));
|
||||
// Own tests
|
||||
test_header!(test4,
|
||||
vec![b"\"foobar\""],
|
||||
Some(ETag(EntityTag::new(false, "foobar".to_owned()))));
|
||||
test_header!(test5,
|
||||
vec![b"\"\""],
|
||||
Some(ETag(EntityTag::new(false, "".to_owned()))));
|
||||
test_header!(test6,
|
||||
vec![b"W/\"weak-etag\""],
|
||||
Some(ETag(EntityTag::new(true, "weak-etag".to_owned()))));
|
||||
test_header!(test7,
|
||||
vec![b"W/\"\x65\x62\""],
|
||||
Some(ETag(EntityTag::new(true, "\u{0065}\u{0062}".to_owned()))));
|
||||
test_header!(test8,
|
||||
vec![b"W/\"\""],
|
||||
Some(ETag(EntityTag::new(true, "".to_owned()))));
|
||||
test_header!(test9,
|
||||
vec![b"no-dquotes"],
|
||||
None::<ETag>);
|
||||
test_header!(test10,
|
||||
vec![b"w/\"the-first-w-is-case-sensitive\""],
|
||||
None::<ETag>);
|
||||
test_header!(test11,
|
||||
vec![b""],
|
||||
None::<ETag>);
|
||||
test_header!(test12,
|
||||
vec![b"\"unmatched-dquotes1"],
|
||||
None::<ETag>);
|
||||
test_header!(test13,
|
||||
vec![b"unmatched-dquotes2\""],
|
||||
None::<ETag>);
|
||||
test_header!(test14,
|
||||
vec![b"matched-\"dquotes\""],
|
||||
None::<ETag>);
|
||||
test_header!(test15,
|
||||
vec![b"\""],
|
||||
None::<ETag>);
|
||||
}
|
||||
}
|
39
src/header/common/expires.rs
Normal file
39
src/header/common/expires.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use header::{http, HttpDate};
|
||||
|
||||
header! {
|
||||
/// `Expires` header, defined in [RFC7234](http://tools.ietf.org/html/rfc7234#section-5.3)
|
||||
///
|
||||
/// The `Expires` header field gives the date/time after which the
|
||||
/// response is considered stale.
|
||||
///
|
||||
/// The presence of an Expires field does not imply that the original
|
||||
/// resource will change or cease to exist at, before, or after that
|
||||
/// time.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Expires = HTTP-date
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `Thu, 01 Dec 1994 16:00:00 GMT`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::Expires;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// let expiration = SystemTime::now() + Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(Expires(expiration.into()));
|
||||
/// ```
|
||||
(Expires, http::EXPIRES) => [HttpDate]
|
||||
|
||||
test_expires {
|
||||
// Testcase from RFC
|
||||
test_header!(test1, vec![b"Thu, 01 Dec 1994 16:00:00 GMT"]);
|
||||
}
|
||||
}
|
70
src/header/common/if_match.rs
Normal file
70
src/header/common/if_match.rs
Normal file
@@ -0,0 +1,70 @@
|
||||
use header::{http, EntityTag};
|
||||
|
||||
header! {
|
||||
/// `If-Match` header, defined in
|
||||
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.1)
|
||||
///
|
||||
/// The `If-Match` header field makes the request method conditional on
|
||||
/// the recipient origin server either having at least one current
|
||||
/// representation of the target resource, when the field-value is "*",
|
||||
/// or having a current representation of the target resource that has an
|
||||
/// entity-tag matching a member of the list of entity-tags provided in
|
||||
/// the field-value.
|
||||
///
|
||||
/// An origin server MUST use the strong comparison function when
|
||||
/// comparing entity-tags for `If-Match`, since the client
|
||||
/// intends this precondition to prevent the method from being applied if
|
||||
/// there have been any changes to the representation data.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// If-Match = "*" / 1#entity-tag
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `"xyzzy"`
|
||||
/// * "xyzzy", "r2d2xxxx", "c3piozzzz"
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::IfMatch;
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(IfMatch::Any);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::{IfMatch, EntityTag};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(
|
||||
/// IfMatch::Items(vec![
|
||||
/// EntityTag::new(false, "xyzzy".to_owned()),
|
||||
/// EntityTag::new(false, "foobar".to_owned()),
|
||||
/// EntityTag::new(false, "bazquux".to_owned()),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
(IfMatch, http::IF_MATCH) => {Any / (EntityTag)+}
|
||||
|
||||
test_if_match {
|
||||
test_header!(
|
||||
test1,
|
||||
vec![b"\"xyzzy\""],
|
||||
Some(HeaderField::Items(
|
||||
vec![EntityTag::new(false, "xyzzy".to_owned())])));
|
||||
test_header!(
|
||||
test2,
|
||||
vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""],
|
||||
Some(HeaderField::Items(
|
||||
vec![EntityTag::new(false, "xyzzy".to_owned()),
|
||||
EntityTag::new(false, "r2d2xxxx".to_owned()),
|
||||
EntityTag::new(false, "c3piozzzz".to_owned())])));
|
||||
test_header!(test3, vec![b"*"], Some(IfMatch::Any));
|
||||
}
|
||||
}
|
39
src/header/common/if_modified_since.rs
Normal file
39
src/header/common/if_modified_since.rs
Normal file
@@ -0,0 +1,39 @@
|
||||
use header::{http, HttpDate};
|
||||
|
||||
header! {
|
||||
/// `If-Modified-Since` header, defined in
|
||||
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.3)
|
||||
///
|
||||
/// The `If-Modified-Since` header field makes a GET or HEAD request
|
||||
/// method conditional on the selected representation's modification date
|
||||
/// being more recent than the date provided in the field-value.
|
||||
/// Transfer of the selected representation's data is avoided if that
|
||||
/// data has not changed.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// If-Unmodified-Since = HTTP-date
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::IfModifiedSince;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(IfModifiedSince(modified.into()));
|
||||
/// ```
|
||||
(IfModifiedSince, http::IF_MODIFIED_SINCE) => [HttpDate]
|
||||
|
||||
test_if_modified_since {
|
||||
// Testcase from RFC
|
||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
|
||||
}
|
||||
}
|
91
src/header/common/if_none_match.rs
Normal file
91
src/header/common/if_none_match.rs
Normal file
@@ -0,0 +1,91 @@
|
||||
use header::{http, EntityTag};
|
||||
|
||||
header! {
|
||||
/// `If-None-Match` header, defined in
|
||||
/// [RFC7232](https://tools.ietf.org/html/rfc7232#section-3.2)
|
||||
///
|
||||
/// The `If-None-Match` header field makes the request method conditional
|
||||
/// on a recipient cache or origin server either not having any current
|
||||
/// representation of the target resource, when the field-value is "*",
|
||||
/// or having a selected representation with an entity-tag that does not
|
||||
/// match any of those listed in the field-value.
|
||||
///
|
||||
/// A recipient MUST use the weak comparison function when comparing
|
||||
/// entity-tags for If-None-Match (Section 2.3.2), since weak entity-tags
|
||||
/// can be used for cache validation even if there have been changes to
|
||||
/// the representation data.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// If-None-Match = "*" / 1#entity-tag
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `"xyzzy"`
|
||||
/// * `W/"xyzzy"`
|
||||
/// * `"xyzzy", "r2d2xxxx", "c3piozzzz"`
|
||||
/// * `W/"xyzzy", W/"r2d2xxxx", W/"c3piozzzz"`
|
||||
/// * `*`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::IfNoneMatch;
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(IfNoneMatch::Any);
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::{IfNoneMatch, EntityTag};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(
|
||||
/// IfNoneMatch::Items(vec![
|
||||
/// EntityTag::new(false, "xyzzy".to_owned()),
|
||||
/// EntityTag::new(false, "foobar".to_owned()),
|
||||
/// EntityTag::new(false, "bazquux".to_owned()),
|
||||
/// ])
|
||||
/// );
|
||||
/// ```
|
||||
(IfNoneMatch, http::IF_NONE_MATCH) => {Any / (EntityTag)+}
|
||||
|
||||
test_if_none_match {
|
||||
test_header!(test1, vec![b"\"xyzzy\""]);
|
||||
test_header!(test2, vec![b"W/\"xyzzy\""]);
|
||||
test_header!(test3, vec![b"\"xyzzy\", \"r2d2xxxx\", \"c3piozzzz\""]);
|
||||
test_header!(test4, vec![b"W/\"xyzzy\", W/\"r2d2xxxx\", W/\"c3piozzzz\""]);
|
||||
test_header!(test5, vec![b"*"]);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::IfNoneMatch;
|
||||
use test::TestRequest;
|
||||
use header::{http, Header, EntityTag};
|
||||
|
||||
#[test]
|
||||
fn test_if_none_match() {
|
||||
let mut if_none_match: Result<IfNoneMatch, _>;
|
||||
|
||||
let req = TestRequest::with_header(http::IF_NONE_MATCH, "*").finish();
|
||||
if_none_match = Header::parse(&req);
|
||||
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Any));
|
||||
|
||||
let req = TestRequest::with_header(
|
||||
http::IF_NONE_MATCH, &b"\"foobar\", W/\"weak-etag\""[..]).finish();
|
||||
|
||||
if_none_match = Header::parse(&req);
|
||||
let mut entities: Vec<EntityTag> = Vec::new();
|
||||
let foobar_etag = EntityTag::new(false, "foobar".to_owned());
|
||||
let weak_etag = EntityTag::new(true, "weak-etag".to_owned());
|
||||
entities.push(foobar_etag);
|
||||
entities.push(weak_etag);
|
||||
assert_eq!(if_none_match.ok(), Some(IfNoneMatch::Items(entities)));
|
||||
}
|
||||
}
|
107
src/header/common/if_range.rs
Normal file
107
src/header/common/if_range.rs
Normal file
@@ -0,0 +1,107 @@
|
||||
use std::fmt::{self, Display, Write};
|
||||
use error::ParseError;
|
||||
use httpmessage::HttpMessage;
|
||||
use header::{http, from_one_raw_str};
|
||||
use header::{IntoHeaderValue, Header, EntityTag, HttpDate, Writer};
|
||||
|
||||
/// `If-Range` header, defined in [RFC7233](http://tools.ietf.org/html/rfc7233#section-3.2)
|
||||
///
|
||||
/// If a client has a partial copy of a representation and wishes to have
|
||||
/// an up-to-date copy of the entire representation, it could use the
|
||||
/// Range header field with a conditional GET (using either or both of
|
||||
/// If-Unmodified-Since and If-Match.) However, if the precondition
|
||||
/// fails because the representation has been modified, the client would
|
||||
/// then have to make a second request to obtain the entire current
|
||||
/// representation.
|
||||
///
|
||||
/// The `If-Range` header field allows a client to \"short-circuit\" the
|
||||
/// second request. Informally, its meaning is as follows: if the
|
||||
/// representation is unchanged, send me the part(s) that I am requesting
|
||||
/// in Range; otherwise, send me the entire representation.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// If-Range = entity-tag / HTTP-date
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
|
||||
/// * `\"xyzzy\"`
|
||||
///
|
||||
/// # Examples
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::{IfRange, EntityTag};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// builder.set(IfRange::EntityTag(EntityTag::new(false, "xyzzy".to_owned())));
|
||||
/// ```
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::IfRange;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// let fetched = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(IfRange::Date(fetched.into()));
|
||||
/// ```
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum IfRange {
|
||||
/// The entity-tag the client has of the resource
|
||||
EntityTag(EntityTag),
|
||||
/// The date when the client retrieved the resource
|
||||
Date(HttpDate),
|
||||
}
|
||||
|
||||
impl Header for IfRange {
|
||||
fn name() -> http::HeaderName {
|
||||
http::IF_RANGE
|
||||
}
|
||||
#[inline]
|
||||
fn parse<T>(msg: &T) -> Result<Self, ParseError> where T: HttpMessage
|
||||
{
|
||||
let etag: Result<EntityTag, _> = from_one_raw_str(msg.headers().get(http::IF_RANGE));
|
||||
if let Ok(etag) = etag {
|
||||
return Ok(IfRange::EntityTag(etag));
|
||||
}
|
||||
let date: Result<HttpDate, _> = from_one_raw_str(msg.headers().get(http::IF_RANGE));
|
||||
if let Ok(date) = date {
|
||||
return Ok(IfRange::Date(date));
|
||||
}
|
||||
Err(ParseError::Header)
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for IfRange {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match *self {
|
||||
IfRange::EntityTag(ref x) => Display::fmt(x, f),
|
||||
IfRange::Date(ref x) => Display::fmt(x, f),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoHeaderValue for IfRange {
|
||||
type Error = http::InvalidHeaderValueBytes;
|
||||
|
||||
fn try_into(self) -> Result<http::HeaderValue, Self::Error> {
|
||||
let mut writer = Writer::new();
|
||||
let _ = write!(&mut writer, "{}", self);
|
||||
http::HeaderValue::from_shared(writer.take())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
#[cfg(test)]
|
||||
mod test_if_range {
|
||||
use std::str;
|
||||
use header::*;
|
||||
use super::IfRange as HeaderField;
|
||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
|
||||
test_header!(test2, vec![b"\"xyzzy\""]);
|
||||
test_header!(test3, vec![b"this-is-invalid"], None::<IfRange>);
|
||||
}
|
40
src/header/common/if_unmodified_since.rs
Normal file
40
src/header/common/if_unmodified_since.rs
Normal file
@@ -0,0 +1,40 @@
|
||||
use header::{http, HttpDate};
|
||||
|
||||
header! {
|
||||
/// `If-Unmodified-Since` header, defined in
|
||||
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-3.4)
|
||||
///
|
||||
/// The `If-Unmodified-Since` header field makes the request method
|
||||
/// conditional on the selected representation's last modification date
|
||||
/// being earlier than or equal to the date provided in the field-value.
|
||||
/// This field accomplishes the same purpose as If-Match for cases where
|
||||
/// the user agent does not have an entity-tag for the representation.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// If-Unmodified-Since = HTTP-date
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::IfUnmodifiedSince;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(IfUnmodifiedSince(modified.into()));
|
||||
/// ```
|
||||
(IfUnmodifiedSince, http::IF_UNMODIFIED_SINCE) => [HttpDate]
|
||||
|
||||
test_if_unmodified_since {
|
||||
// Testcase from RFC
|
||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);
|
||||
}
|
||||
}
|
38
src/header/common/last_modified.rs
Normal file
38
src/header/common/last_modified.rs
Normal file
@@ -0,0 +1,38 @@
|
||||
use header::{http, HttpDate};
|
||||
|
||||
header! {
|
||||
/// `Last-Modified` header, defined in
|
||||
/// [RFC7232](http://tools.ietf.org/html/rfc7232#section-2.2)
|
||||
///
|
||||
/// The `Last-Modified` header field in a response provides a timestamp
|
||||
/// indicating the date and time at which the origin server believes the
|
||||
/// selected representation was last modified, as determined at the
|
||||
/// conclusion of handling the request.
|
||||
///
|
||||
/// # ABNF
|
||||
///
|
||||
/// ```text
|
||||
/// Expires = HTTP-date
|
||||
/// ```
|
||||
///
|
||||
/// # Example values
|
||||
///
|
||||
/// * `Sat, 29 Oct 1994 19:43:31 GMT`
|
||||
///
|
||||
/// # Example
|
||||
///
|
||||
/// ```rust
|
||||
/// use actix_web::httpcodes;
|
||||
/// use actix_web::header::LastModified;
|
||||
/// use std::time::{SystemTime, Duration};
|
||||
///
|
||||
/// let mut builder = httpcodes::HttpOk.build();
|
||||
/// let modified = SystemTime::now() - Duration::from_secs(60 * 60 * 24);
|
||||
/// builder.set(LastModified(modified.into()));
|
||||
/// ```
|
||||
(LastModified, http::LAST_MODIFIED) => [HttpDate]
|
||||
|
||||
test_last_modified {
|
||||
// Testcase from RFC
|
||||
test_header!(test1, vec![b"Sat, 29 Oct 1994 19:43:31 GMT"]);}
|
||||
}
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user